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/tasks/dns.go b/core/installer/tasks/dns.go
index 25af486..e115b2c 100644
--- a/core/installer/tasks/dns.go
+++ b/core/installer/tasks/dns.go
@@ -15,12 +15,19 @@
 type Check func(ch Check) error
 
 func SetupZoneTask(env Env, ingressIP net.IP, st *state) Task {
-	return newSequentialParentTask(
+	ret := newSequentialParentTask(
 		"Configure DNS",
 		true,
 		CreateZoneRecords(env.Domain, st.publicIPs, ingressIP, env, st),
 		WaitToPropagate(env.Domain, st.publicIPs),
 	)
+	ret.beforeStart = func() {
+		st.infoListener(fmt.Sprintf("Generating DNS zone records for %s", env.Domain))
+	}
+	ret.afterDone = func() {
+		st.infoListener("DNS zone records have been propagated.")
+	}
+	return ret
 }
 
 func CreateZoneRecords(
diff --git a/core/installer/tasks/env.go b/core/installer/tasks/env.go
index 80289c4..1d1eaba 100644
--- a/core/installer/tasks/env.go
+++ b/core/installer/tasks/env.go
@@ -12,6 +12,7 @@
 )
 
 type state struct {
+	infoListener   EnvInfoListener
 	publicIPs      []net.IP
 	nsCreator      installer.NamespaceCreator
 	repo           installer.RepoIO
@@ -33,6 +34,8 @@
 	AdminPublicKey string
 }
 
+type EnvInfoListener func(string)
+
 type DNSZoneRef struct {
 	Name      string
 	Namespace string
@@ -44,11 +47,13 @@
 	startIP net.IP,
 	nsCreator installer.NamespaceCreator,
 	repo installer.RepoIO,
+	infoListener EnvInfoListener,
 ) (Task, DNSZoneRef) {
 	st := state{
-		publicIPs: publicIPs,
-		nsCreator: nsCreator,
-		repo:      repo,
+		infoListener: infoListener,
+		publicIPs:    publicIPs,
+		nsCreator:    nsCreator,
+		repo:         repo,
 	}
 	t := newSequentialParentTask(
 		"Create env",
@@ -57,6 +62,9 @@
 		SetupZoneTask(env, startIP, &st),
 		SetupInfra(env, startIP, &st),
 	)
+	t.afterDone = func() {
+		infoListener(fmt.Sprintf("dodo environment for %s has been provisioned successfully. Visit [https://welcome.%s](https://welcome.%s) to create administrative account and log into the system.", env.Domain, env.Domain, env.Domain))
+	}
 	rctx, done := context.WithCancel(context.Background())
 	t.OnDone(func(_ error) {
 		done()
diff --git a/core/installer/tasks/infra.go b/core/installer/tasks/infra.go
index 0685a6d..39a1cd5 100644
--- a/core/installer/tasks/infra.go
+++ b/core/installer/tasks/infra.go
@@ -30,6 +30,9 @@
 		st.emptySuffixGen = installer.NewEmptySuffixGenerator()
 		return nil
 	})
+	t.beforeStart = func() {
+		st.infoListener("Setting up core infrastructure services.")
+	}
 	return &t
 }
 
diff --git a/core/installer/tasks/init.go b/core/installer/tasks/init.go
index e6dd47e..cb546c1 100644
--- a/core/installer/tasks/init.go
+++ b/core/installer/tasks/init.go
@@ -10,8 +10,8 @@
 )
 
 func SetupConfigRepoTask(env Env, st *state) Task {
-	return newSequentialParentTask(
-		"Configure Git repository for new environment",
+	ret := newSequentialParentTask(
+		"Configure Git repository",
 		true,
 		newSequentialParentTask(
 			"Start up Git server",
@@ -29,6 +29,10 @@
 			ConfigureFirstAccount(env, st),
 		),
 	)
+	ret.beforeStart = func() {
+		st.infoListener("dodo is driven by GitOps, changes are committed to the repository before updating an environment. This unlocks functionalities such as: rolling back to old working state, migrating dodo to new infrastructure (for example from Cloud to on-prem).")
+	}
+	return ret
 }
 
 func NewCreateConfigRepoTask(env Env, st *state) Task {
diff --git a/core/installer/tasks/tasks.go b/core/installer/tasks/tasks.go
index 6d0ec01..c097af3 100644
--- a/core/installer/tasks/tasks.go
+++ b/core/installer/tasks/tasks.go
@@ -21,10 +21,12 @@
 }
 
 type basicTask struct {
-	title     string
-	status    Status
-	err       error
-	listeners []TaskDoneListener
+	title       string
+	status      Status
+	err         error
+	listeners   []TaskDoneListener
+	beforeStart func()
+	afterDone   func()
 }
 
 func newBasicTask(title string) basicTask {
@@ -81,7 +83,15 @@
 }
 
 func (b *leafTask) Start() {
-	b.callDoneListeners(b.start())
+	b.status = StatusRunning
+	if b.beforeStart != nil {
+		b.beforeStart()
+	}
+	err := b.start()
+	defer b.callDoneListeners(err)
+	if b.afterDone != nil {
+		b.afterDone()
+	}
 }
 
 type parentTask struct {