installer: split up new env creation into chain of tasks
diff --git a/core/installer/welcome/env-created.html b/core/installer/welcome/env-created.html
index 0a64116..fc8be86 100644
--- a/core/installer/welcome/env-created.html
+++ b/core/installer/welcome/env-created.html
@@ -1,15 +1,29 @@
+{{ define "task" }}
+<li aria-busy="{{ eq .Status 0 }}">
+	{{ .Title }}{{ if .Err }} - {{ .Err.Error }} {{ end }}
+	<ul>
+		{{ range .Subtasks }}
+   		    {{ template "task" . }}
+		{{ end }}
+	</ul>
+</li>
+{{ end }}
+
 <!DOCTYPE html>
 <html lang="en" data-theme="light">
 	<head>
         <link rel="stylesheet" href="/static/pico.min.css">
 		<meta charset="utf-8" />
 		<meta name="viewport" content="width=device-width, initial-scale=1" />
+		{{ if not (or (eq .Root.Status 2) (eq .Root.Status 3))}}
+		<meta http-equiv="refresh" content="1">
+		{{ end }}
 	</head>
 	<body>
-		<div style="display: contents">
-            <main class="container">
-				Environment for {{ .Domain }} is being configured. In few minutes go to <a href="https://welcome.{{ .Domain }}">https://welcome.<b>{{ .Domain }}</b></a> to set up administrative account.
-            </main>
-        </div>
+        <main class="container">
+			<ul>
+				{{ template "task" .Root }}
+			</ul>
+        </main>
 	</body>
 </html>
diff --git a/core/installer/welcome/env-tmpl/config-kustomization.yaml b/core/installer/welcome/env-tmpl/config-kustomization.yaml
deleted file mode 100644
index 2bf55eb..0000000
--- a/core/installer/welcome/env-tmpl/config-kustomization.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: kustomize.toolkit.fluxcd.io/v1
-kind: Kustomization
-metadata:
-  name: {{ .Name }}
-  namespace: {{ .Name }}
-spec:
-  interval: 1m
-  path: "./"
-  sourceRef:
-    kind: GitRepository
-    name: {{ .Name }}
-    namespace: {{ .Name }}
-  prune: true
diff --git a/core/installer/welcome/env-tmpl/config-secret.yaml b/core/installer/welcome/env-tmpl/config-secret.yaml
deleted file mode 100644
index bba3de0..0000000
--- a/core/installer/welcome/env-tmpl/config-secret.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-apiVersion: v1
-kind: Secret
-type: Opaque
-metadata:
-  name: {{ .Name }}
-  namespace: {{ .Name }}
-data:
-  identity: {{ .PrivateKey }}
-  identity.pub: {{ .PublicKey }}
-  known_hosts: {{ .KnownHosts }}
diff --git a/core/installer/welcome/env-tmpl/config-source.yaml b/core/installer/welcome/env-tmpl/config-source.yaml
deleted file mode 100644
index d22ab03..0000000
--- a/core/installer/welcome/env-tmpl/config-source.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-apiVersion: source.toolkit.fluxcd.io/v1
-kind: GitRepository
-metadata:
-  name: {{ .Name }}
-  namespace: {{ .Name }}
-spec:
-  interval: 1m0s
-  ref:
-    branch: master
-  secretRef:
-    name: {{ .Name }}
-  timeout: 60s
-  url: ssh://{{ .RepoHost }}/{{ .RepoName }}
diff --git a/core/installer/welcome/env-tmpl/kustomization.yaml b/core/installer/welcome/env-tmpl/kustomization.yaml
deleted file mode 100644
index 070ae80..0000000
--- a/core/installer/welcome/env-tmpl/kustomization.yaml
+++ /dev/null
@@ -1,7 +0,0 @@
-apiVersion: kustomize.config.k8s.io/v1beta1
-kind: Kustomization
-resources:
-- config-repo
-- config-secret.yaml
-- config-source.yaml
-- config-kustomization.yaml
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index c0f2f01..bb8b6ff 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -1,9 +1,7 @@
 package welcome
 
 import (
-	"bytes"
-	"embed"
-	"encoding/base64"
+	_ "embed"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -11,27 +9,18 @@
 	"io"
 	"io/fs"
 	"log"
+	"net"
 	"net/http"
-	"net/netip"
-	"path"
-	"path/filepath"
-	"strings"
-	"text/template"
 
-	"github.com/Masterminds/sprig/v3"
-	"github.com/charmbracelet/keygen"
 	"github.com/gorilla/mux"
-	"github.com/miekg/dns"
 
 	"github.com/giolekva/pcloud/core/installer"
 	"github.com/giolekva/pcloud/core/installer/soft"
+	"github.com/giolekva/pcloud/core/installer/tasks"
 )
 
-//go:embed env-tmpl
-var filesTmpls embed.FS
-
 //go:embed create-env.html
-var createEnvFormHtml string
+var createEnvFormHtml []byte
 
 //go:embed env-created.html
 var envCreatedHtml string
@@ -55,6 +44,7 @@
 	repo          installer.RepoIO
 	nsCreator     installer.NamespaceCreator
 	nameGenerator installer.NameGenerator
+	tasks         map[string]tasks.Task
 }
 
 func NewEnvServer(port int, ss *soft.Client, repo installer.RepoIO, nsCreator installer.NamespaceCreator, nameGenerator installer.NameGenerator) *EnvServer {
@@ -64,12 +54,14 @@
 		repo,
 		nsCreator,
 		nameGenerator,
+		make(map[string]tasks.Task),
 	}
 }
 
 func (s *EnvServer) Start() {
 	r := mux.NewRouter()
 	r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticAssets)))
+	r.Path("/env/{key}").Methods("GET").HandlerFunc(s.monitorTask)
 	r.Path("/").Methods("GET").HandlerFunc(s.createEnvForm)
 	r.Path("/").Methods("POST").HandlerFunc(s.createEnv)
 	r.Path("/create-invitation").Methods("GET").HandlerFunc(s.createInvitation)
@@ -77,8 +69,29 @@
 	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
 }
 
+func (s *EnvServer) monitorTask(w http.ResponseWriter, r *http.Request) {
+	vars := mux.Vars(r)
+	fmt.Printf("%+v %+v\n", s.tasks, vars)
+	t, ok := s.tasks[vars["key"]]
+	if !ok {
+		http.Error(w, "Task not found", http.StatusBadRequest)
+		return
+	}
+	tmpl, err := htemplate.New("response").Parse(envCreatedHtml)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	if err := tmpl.Execute(w, map[string]any{
+		"Root": t,
+	}); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+}
+
 func (s *EnvServer) createEnvForm(w http.ResponseWriter, r *http.Request) {
-	if _, err := w.Write([]byte(createEnvFormHtml)); err != nil {
+	if _, err := w.Write(createEnvFormHtml); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 	}
 }
@@ -224,474 +237,22 @@
 	} else {
 		req.Name = name
 	}
-	appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
-	ssApp, err := appsRepo.Find("soft-serve")
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	ssAdminKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-admin-keys", req.Name))
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	ssKeys, err := installer.NewSSHKeyPair(fmt.Sprintf("%s-config-repo-keys", req.Name))
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	ssValues := map[string]any{
-		"ChartRepositoryNamespace": env.Name,
-		"ServiceType":              "ClusterIP",
-		"PrivateKey":               string(ssKeys.RawPrivateKey()),
-		"PublicKey":                string(ssKeys.RawAuthorizedKey()),
-		"AdminKey":                 string(ssAdminKeys.RawAuthorizedKey()),
-		"Ingress": map[string]any{
-			"Enabled": false,
+	t := tasks.NewCreateEnvTask(
+		tasks.Env{
+			PCloudEnvName:  env.Name,
+			Name:           req.Name,
+			ContactEmail:   req.ContactEmail,
+			Domain:         req.Domain,
+			AdminPublicKey: req.AdminPublicKey,
 		},
-	}
-	derived := installer.Derived{
-		Global: installer.Values{
-			Id:            req.Name,
-			PCloudEnvName: env.Name,
+		[]net.IP{
+			net.ParseIP("135.181.48.180"),
+			net.ParseIP("65.108.39.172"),
 		},
-		Release: installer.Release{
-			Namespace: req.Name,
-		},
-		Values: ssValues,
-	}
-	if err := s.nsCreator.Create(req.Name); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	if err := s.repo.InstallApp(*ssApp, filepath.Join("/environments", req.Name, "config-repo"), ssValues, derived); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	k := installer.NewKustomization()
-	k.AddResources("config-repo")
-	if err := s.repo.WriteKustomization(filepath.Join("/environments", req.Name, "kustomization.yaml"), k); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	ssClient, err := soft.WaitForClient(
-		fmt.Sprintf("soft-serve.%s.svc.cluster.local:%d", req.Name, 22),
-		ssAdminKeys.RawPrivateKey(),
-		log.Default())
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	if err := ssClient.AddPublicKey("admin", req.AdminPublicKey); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	defer func() {
-		if err := ssClient.RemovePublicKey("admin", string(ssAdminKeys.RawAuthorizedKey())); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-	}()
-	fluxUserName := fmt.Sprintf("flux-%s", req.Name)
-	keys, err := installer.NewSSHKeyPair(fluxUserName)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	{
-		if err := ssClient.AddRepository("config"); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		repo, err := ssClient.GetRepo("config")
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		repoIO := installer.NewRepoIO(repo, ssClient.Signer)
-		if err := repoIO.WriteCommitAndPush("README.md", fmt.Sprintf("# %s PCloud environment", req.Name), "readme"); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		if err := ssClient.AddUser(fluxUserName, keys.AuthorizedKey()); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		if err := ssClient.AddReadOnlyCollaborator("config", fluxUserName); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-	}
-	{
-		repo, err := ssClient.GetRepo("config")
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		if err := initNewEnv(ssClient, installer.NewRepoIO(repo, ssClient.Signer), s.nsCreator, req, env.Name, env.PublicIP); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-	}
-	{
-		ssPublicKeys, err := ssClient.GetPublicKeys()
-		if err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-		if err := addNewEnv(
-			s.repo,
-			req,
-			strings.Split(ssClient.Addr, ":")[0],
-			keys,
-			ssPublicKeys,
-		); err != nil {
-			http.Error(w, err.Error(), http.StatusInternalServerError)
-			return
-		}
-	}
-	tmpl, err := htemplate.New("response").Parse(envCreatedHtml)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-	if err := tmpl.Execute(w, map[string]any{
-		"Domain": req.Domain,
-	}); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
-}
-
-type DNSSecKey struct {
-	Basename string `json:"basename,omitempty"`
-	Key      []byte `json:"key,omitempty"`
-	Private  []byte `json:"private,omitempty"`
-	DS       []byte `json:"ds,omitempty"`
-}
-
-func newDNSSecKey(zone string) (DNSSecKey, error) {
-	key := &dns.DNSKEY{
-		Hdr:       dns.RR_Header{Name: dns.Fqdn(zone), Class: dns.ClassINET, Ttl: 3600, Rrtype: dns.TypeDNSKEY},
-		Algorithm: dns.ECDSAP256SHA256, Flags: 257, Protocol: 3,
-	}
-	priv, err := key.Generate(256)
-	if err != nil {
-		return DNSSecKey{}, err
-	}
-	return DNSSecKey{
-		Basename: fmt.Sprintf("K%s+%03d+%05d", key.Header().Name, key.Algorithm, key.KeyTag()),
-		Key:      []byte(key.String()),
-		Private:  []byte(key.PrivateKeyString(priv)),
-		DS:       []byte(key.ToDS(dns.SHA256).String()),
-	}, nil
-}
-
-func initNewEnv(
-	ss *soft.Client,
-	r installer.RepoIO,
-	nsCreator installer.NamespaceCreator,
-	req createEnvReq,
-	pcloudEnvName string,
-	pcloudPublicIP string,
-) error {
-	appManager, err := installer.NewAppManager(r, nsCreator)
-	if err != nil {
-		return err
-	}
-	appsRepo := installer.NewInMemoryAppRepository(installer.CreateAllApps())
-	// TODO(giolekva): private domain can be configurable as well
-	config := installer.Config{
-		Values: installer.Values{
-			PCloudEnvName:   pcloudEnvName,
-			Id:              req.Name,
-			ContactEmail:    req.ContactEmail,
-			Domain:          req.Domain,
-			PrivateDomain:   fmt.Sprintf("p.%s", req.Domain),
-			PublicIP:        pcloudPublicIP,
-			NamespacePrefix: fmt.Sprintf("%s-", req.Name),
-		},
-	}
-	if err := r.WriteYaml("config.yaml", config); err != nil {
-		return err
-	}
-	{
-		out, err := r.Writer("pcloud-charts.yaml")
-		if err != nil {
-			return err
-		}
-		defer out.Close()
-		_, err = fmt.Fprintf(out, `
-apiVersion: source.toolkit.fluxcd.io/v1
-kind: GitRepository
-metadata:
-  name: pcloud
-  namespace: %s
-spec:
-  interval: 1m0s
-  url: https://github.com/giolekva/pcloud
-  ref:
-    branch: main
-`, req.Name)
-		if err != nil {
-			return err
-		}
-	}
-	{
-		key, err := newDNSSecKey(req.Domain)
-		if err != nil {
-			return err
-		}
-		out, err := r.Writer("dns-zone.yaml")
-		if err != nil {
-			return err
-		}
-		defer out.Close()
-		dnsZoneTmpl, err := template.New("config").Funcs(sprig.TxtFuncMap()).Parse(`
-apiVersion: dodo.cloud.dodo.cloud/v1
-kind: DNSZone
-metadata:
-  name: dns-zone
-  namespace: {{ .namespace }}
-spec:
-  zone: {{ .zone }}
-  privateIP: 10.1.0.1
-  publicIPs:
-  - 135.181.48.180
-  - 65.108.39.172
-  nameservers:
-  - 135.181.48.180
-  - 65.108.39.172
-  dnssec:
-    enabled: true
-    secretName: dnssec-key
----
-apiVersion: v1
-kind: Secret
-metadata:
-  name: dnssec-key
-  namespace: {{ .namespace }}
-type: Opaque
-data:
-  basename: {{ .dnssec.Basename | b64enc }}
-  key: {{ .dnssec.Key | toString | b64enc }}
-  private: {{ .dnssec.Private | toString | b64enc }}
-  ds: {{ .dnssec.DS | toString | b64enc }}
-`)
-		if err != nil {
-			return err
-		}
-		if err := dnsZoneTmpl.Execute(out, map[string]any{
-			"namespace": req.Name,
-			"zone":      req.Domain,
-			"dnssec":    key,
-		}); err != nil {
-			return err
-		}
-	}
-	rootKust := installer.NewKustomization()
-	rootKust.AddResources("pcloud-charts.yaml", "dns-zone.yaml", "apps")
-	if err := r.WriteKustomization("kustomization.yaml", rootKust); err != nil {
-		return err
-	}
-	appsKust := installer.NewKustomization()
-	if err := r.WriteKustomization("apps/kustomization.yaml", appsKust); err != nil {
-		return err
-	}
-	r.CommitAndPush("initialize config")
-	nsGen := installer.NewPrefixGenerator(req.Name + "-")
-	emptySuffixGen := installer.NewEmptySuffixGenerator()
-	ingressPrivateIP, err := netip.ParseAddr("10.1.0.1")
-	if err != nil {
-		return err
-	}
-	{
-		headscaleIP := ingressPrivateIP.Next()
-		app, err := appsRepo.Find("metallb-ipaddresspool")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-ingress-private"), map[string]any{
-			"Name":       fmt.Sprintf("%s-ingress-private", req.Name),
-			"From":       ingressPrivateIP.String(),
-			"To":         ingressPrivateIP.String(),
-			"AutoAssign": false,
-			"Namespace":  "metallb-system",
-		}); err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, installer.NewSuffixGenerator("-headscale"), map[string]any{
-			"Name":       fmt.Sprintf("%s-headscale", req.Name),
-			"From":       headscaleIP.String(),
-			"To":         headscaleIP.String(),
-			"AutoAssign": false,
-			"Namespace":  "metallb-system",
-		}); err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"Name":       req.Name,
-			"From":       "10.1.0.100", // TODO(gio): auto-generate
-			"To":         "10.1.0.254",
-			"AutoAssign": false,
-			"Namespace":  "metallb-system",
-		}); err != nil {
-			return err
-		}
-	}
-	{
-		app, err := appsRepo.Find("private-network")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"PrivateNetwork": map[string]any{
-				"Hostname": "private-network-proxy",
-				"Username": "private-network-proxy",
-				"IPSubnet": "10.1.0.0/24",
-			},
-		}); err != nil {
-			return err
-		}
-	}
-	{
-		app, err := appsRepo.Find("certificate-issuer-public")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{}); err != nil {
-			return err
-		}
-	}
-	{
-		app, err := appsRepo.Find("certificate-issuer-private")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"APIConfigMap": map[string]any{
-				"Name":      "api-config", // TODO(gio): take from global pcloud config
-				"Namespace": fmt.Sprintf("%s-dns-zone-manager", pcloudEnvName),
-			},
-		}); err != nil {
-			return err
-		}
-	}
-	{
-		app, err := appsRepo.Find("core-auth")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"Subdomain": "test", // TODO(giolekva): make core-auth chart actually use this
-		}); err != nil {
-			return err
-		}
-	}
-	{
-		app, err := appsRepo.Find("headscale")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"Subdomain": "headscale",
-		}); err != nil {
-			return err
-		}
-	}
-	{
-		keys, err := installer.NewSSHKeyPair("welcome")
-		if err != nil {
-			return err
-		}
-		user := fmt.Sprintf("%s-welcome", req.Name)
-		if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
-			return err
-		}
-		if err := ss.AddReadWriteCollaborator("config", user); err != nil {
-			return err
-		}
-		app, err := appsRepo.Find("welcome")
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"RepoAddr":      ss.GetRepoAddress("config"),
-			"SSHPrivateKey": string(keys.RawPrivateKey()),
-		}); err != nil {
-			return err
-		}
-	}
-	{
-		user := fmt.Sprintf("%s-appmanager", req.Name)
-		keys, err := installer.NewSSHKeyPair(user)
-		if err != nil {
-			return err
-		}
-		if err := ss.AddUser(user, keys.AuthorizedKey()); err != nil {
-			return err
-		}
-		if err := ss.AddReadWriteCollaborator("config", user); err != nil {
-			return err
-		}
-		app, err := appsRepo.Find("app-manager") // TODO(giolekva): configure
-		if err != nil {
-			return err
-		}
-		if err := appManager.Install(*app, nsGen, emptySuffixGen, map[string]any{
-			"RepoAddr":      ss.GetRepoAddress("config"),
-			"SSHPrivateKey": string(keys.RawPrivateKey()),
-		}); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func addNewEnv(
-	repoIO installer.RepoIO,
-	req createEnvReq,
-	repoHost string,
-	keys *keygen.KeyPair,
-	configRepoPublicKeys []string,
-) error {
-	kust, err := repoIO.ReadKustomization("environments/kustomization.yaml")
-	if err != nil {
-		return err
-	}
-	kust.AddResources(req.Name)
-	tmpls, err := template.ParseFS(filesTmpls, "env-tmpl/*.yaml")
-	if err != nil {
-		return err
-	}
-	var knownHosts bytes.Buffer
-	for _, key := range configRepoPublicKeys {
-		fmt.Fprintf(&knownHosts, "%s %s\n", repoHost, key)
-	}
-	for _, tmpl := range tmpls.Templates() {
-		dstPath := path.Join("environments", req.Name, tmpl.Name())
-		dst, err := repoIO.Writer(dstPath)
-		if err != nil {
-			return err
-		}
-		defer dst.Close()
-
-		if err := tmpl.Execute(dst, map[string]string{
-			"Name":       req.Name,
-			"PrivateKey": base64.StdEncoding.EncodeToString(keys.RawPrivateKey()),
-			"PublicKey":  base64.StdEncoding.EncodeToString(keys.RawAuthorizedKey()),
-			"RepoHost":   repoHost,
-			"RepoName":   "config",
-			"KnownHosts": base64.StdEncoding.EncodeToString(knownHosts.Bytes()),
-		}); err != nil {
-			return err
-		}
-	}
-	if err := repoIO.WriteKustomization("environments/kustomization.yaml", *kust); err != nil {
-		return err
-	}
-	if err := repoIO.CommitAndPush(fmt.Sprintf("%s: initialize environment", req.Name)); err != nil {
-		return err
-	}
-	return nil
+		s.nsCreator,
+		s.repo,
+	)
+	s.tasks["foo"] = t
+	t.Start()
+	http.Redirect(w, r, "/env/foo", http.StatusSeeOther)
 }