env: status page

Updates page asynchronously every 5 seconds.
Introduces beforeStart and afterStart trigger points to update setup status information.

Change-Id: Ic2f6a9bb7a0fefeefc4d6a1a7338d506a4f99e80
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index f8ee70d..e0d5a3f 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -14,6 +14,7 @@
 	"net/netip"
 	"strings"
 
+	"github.com/gomarkdown/markdown"
 	"github.com/gorilla/mux"
 
 	"github.com/giolekva/pcloud/core/installer"
@@ -83,7 +84,9 @@
 	dnsFetcher    installer.ZoneStatusFetcher
 	nameGenerator installer.NameGenerator
 	tasks         map[string]tasks.Task
+	envInfo       map[string]template.HTML
 	dns           map[string]tasks.DNSZoneRef
+	dnsPublished  map[string]struct{}
 }
 
 func NewEnvServer(
@@ -102,7 +105,9 @@
 		dnsFetcher,
 		nameGenerator,
 		make(map[string]tasks.Task),
+		make(map[string]template.HTML),
 		make(map[string]tasks.DNSZoneRef),
+		make(map[string]struct{}),
 	}
 }
 
@@ -130,23 +135,29 @@
 		http.Error(w, "Task not found", http.StatusBadRequest)
 		return
 	}
-	dnsRef, ok := s.dns[key]
-	if !ok {
-		http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
-		return
+	dnsRecords := ""
+	if _, ok := s.dnsPublished[key]; !ok {
+		dnsRef, ok := s.dns[key]
+		if !ok {
+			http.Error(w, "Task dns configuration not found", http.StatusInternalServerError)
+			return
+		}
+		err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
+		// TODO(gio): check error type
+		if err != nil && (ready || len(info.Records) > 0) {
+			panic("!! SHOULD NOT REACH !!")
+		}
+		if !ready && len(info.Records) > 0 {
+			panic("!! SHOULD NOT REACH !!")
+		}
+		dnsRecords = info.Records
 	}
-	err, ready, info := s.dnsFetcher.Fetch(dnsRef.Namespace, dnsRef.Name)
-	// TODO(gio): check error type
-	if err != nil && (ready || len(info.Records) > 0) {
-		panic("!! SHOULD NOT REACH !!")
-	}
-	if !ready && len(info.Records) > 0 {
-		panic("!! SHOULD NOT REACH !!")
-	}
-	if err := tmplsParsed.status.Execute(w, map[string]any{
+	data := map[string]any{
 		"Root":       t,
-		"DNSRecords": info.Records,
-	}); err != nil {
+		"EnvInfo":    s.envInfo[key],
+		"DNSRecords": dnsRecords,
+	}
+	if err := tmplsParsed.status.Execute(w, data); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -184,6 +195,8 @@
 			return
 		}
 	}
+	s.envInfo[key] = "Successfully published DNS records, waiting to propagate."
+	s.dnsPublished[key] = struct{}{}
 	http.Redirect(w, r, fmt.Sprintf("/env/%s", key), http.StatusSeeOther)
 }
 
@@ -359,6 +372,17 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
+	key := func() string {
+		for {
+			key, err := s.nameGenerator.Generate()
+			if err == nil {
+				return key
+			}
+		}
+	}()
+	infoUpdater := func(info string) {
+		s.envInfo[key] = template.HTML(markdown.ToHTML([]byte(info), nil, nil))
+	}
 	t, dns := tasks.NewCreateEnvTask(
 		tasks.Env{
 			PCloudEnvName:  env.Name,
@@ -374,15 +398,8 @@
 		startIP,
 		s.nsCreator,
 		s.repo,
+		infoUpdater,
 	)
-	key := func() string {
-		for {
-			key, err := s.nameGenerator.Generate()
-			if err == nil {
-				return key
-			}
-		}
-	}()
 	s.tasks[key] = t
 	s.dns[key] = dns
 	go t.Start()