env-manager: ui polish (#127)

* env-manager: migrate to pico 2.0.6

* env: option to hide children from ui

* introduce template hierarchy

* style: improve menu styling

* env: reorganize tasks, pull before install

---------

Co-authored-by: Giorgi Lekveishvili <lekva@gl-mbp-m1-max.local>
diff --git a/core/installer/welcome/create-env.html b/core/installer/welcome/create-env.html
deleted file mode 100644
index f3d0f65..0000000
--- a/core/installer/welcome/create-env.html
+++ /dev/null
@@ -1,86 +0,0 @@
-<!DOCTYPE html>
-<html lang="en" data-theme="light">
-	<head>
-		<link rel="stylesheet" href="/static/pico.min.css">
-		<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css">
-		<link rel="stylesheet" href="/static/main.css">
-		<meta charset="utf-8" />
-		<meta name="viewport" content="width=device-width, initial-scale=1" />
-	</head>
-	<body>
-		<nav id="menu" class="container-fluid">
-			<ul>
-				<li class="dodo"><span style="color: #ffffff;">do</span><span class="highlight">do:</span></li>
-			</ul>
-			<ul>
-				<li><a href="#">register</a></li>
-				<li><a href="#">apps</a></li>
-				<li><a href="#" style="border-right: none;">about</a></li>
-			</ul>
-		</nav>
-		<main class="container">
-			<div class="grid contents-header">
-				<div style="border-width: 1px; border-right-style: solid;">
-					attention
-				</div>
-				<div>
-					take <span class="highlight">control</span>
-				</div>
-			</div>
-			<div id="contents" class="grid">
-				<div style="border-width: 1px; border-right-style: solid;">
-					As part of provisioning new dodo instance you will have to update DNS records at your domain registrar, so that it points to the nameservers running on your newly created dodo. Please first get familiar with your domain registrar documentation, and only then proceed with provisioning.
-					<label for="accept" style="padding-top: 1rem;">
-						<input type="checkbox" name="accept" id="accept" form="create-form" required tabindex="5">
-						<strong>I understand</strong>
-					</label>
-				</div>
-				<div id="create-instance-form">
-					<form action="" method="POST" id="create-form">
-						<label for="domain">
-							domain
-							<input
-								type="text"
-								id="domain"
-								name="domain"
-								required
-								autofocus
-								tabindex="1"
-							/>
-						</label>
-						<label for="contact-email">
-							contact email
-							<input
-								type="email"
-								id="contact-email"
-								name="contact-email"
-								required
-								tabindex="2"
-							/>
-						</label>
-						<label for="admin-public-key">
-							admin ssh public key
-							<input
-								type="string"
-								id="admin-public-key"
-								name="admin-public-key"
-								required
-								tabindex="3"
-							/> <!-- TODO(gio): remove-->
-						</label>
-						<label for="secret-token">
-							invitation code
-							<textarea
-								id="secret-token"
-								name="secret-token"
-								required
-								tabindex="4"
-							></textarea>
-						</label>
-						<button type="submit" tabindex="6">provision</button>
-					</form>
-				</div>
-			</div>
-		</main>
-	</body>
-</html>
diff --git a/core/installer/welcome/env-created.html b/core/installer/welcome/env-created.html
deleted file mode 100644
index c50457e..0000000
--- a/core/installer/welcome/env-created.html
+++ /dev/null
@@ -1,69 +0,0 @@
-{{ define "task" }}
-{{ range . }}
-<li aria-busy="{{ eq .Status 0 }}">
-	{{ .Title }}{{ if .Err }} - {{ .Err.Error }} {{ end }}
-	{{ if .Subtasks }}
-	<ul>
-   		{{ template "task" .Subtasks }}
-	</ul>
-	{{ end }}
-</li>
-{{ end }}
-{{ end }}
-
-<!DOCTYPE html>
-<html lang="en" data-theme="light">
-	<head>
-		<link rel="stylesheet" href="/static/pico.min.css">
-		<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css">
-		<link rel="stylesheet" href="/static/main.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="60">
-		{{ end }}
-	</head>
-	<body>
-		<nav id="menu" class="container-fluid">
-			<ul>
-				<li class="dodo">do<span class="highlight">do:</span></li>
-			</ul>
-			<ul>
-				<li><a href="#">register</a></li>
-				<li><a href="#">apps</a></li>
-				<li><a href="#" style="border-right: none;">about</a></li>
-			</ul>
-		</nav>
-		<main class="container">
-			<article>
-			  <ul>
-				  {{ template "task" .Root.Subtasks }}
-			  </ul>
-			</article>
-			{{ if .DNSRecords }}
-			<div>
-				<form action="" method="POST">
-					You will have to publish following DNS records via your domain registrar.
-					<textarea rows="7">{{ .DNSRecords }}</textarea>
-					<label for="domain-registrar">Domain Registrar</label>
-					<select id="domain-registrar" required tabindex="1">
-						<option value="" selected>Select registrar</option>
-						<option value="gandi">Gandi</option>
-						<option value="namecheap">Namecheap</option>
-					</select>
-					<label for="api-token">API Token</label>
-					<input
-						type="text"
-						id="api-token"
-						name="api-token"
-						required
-						autofocus
-						tabindex="2"
-					/>
-					<button type="submit" tabindex="3">Update</button>
-				</form>
-			</div>
-			{{ end }}
-        </main>
-	</body>
-</html>
diff --git a/core/installer/welcome/env-manager-tmpl/base.html b/core/installer/welcome/env-manager-tmpl/base.html
new file mode 100644
index 0000000..a9ca048
--- /dev/null
+++ b/core/installer/welcome/env-manager-tmpl/base.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html lang="en" data-theme="light">
+	<head>
+		<link rel="stylesheet" href="/static/pico.2.0.6.min.css">
+		<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/hack-font@3/build/web/hack.css">
+		<link rel="stylesheet" href="/static/main.css">
+		<meta charset="utf-8" />
+		<meta name="viewport" content="width=device-width, initial-scale=1" />
+		<title>dodo:</title>
+	</head>
+	<body>
+		<nav id="menu" class="container-fluid">
+			<ul>
+				<li class="dodo"><span style="color: #ffffff;">do</span><span class="highlight">do:</span></li>
+			</ul>
+			<ul id="links">
+				<li><a href="#">register</a></li>
+				<li><a href="#">apps</a></li>
+				<li><a href="#" style="border-right: none;">about</a></li>
+			</ul>
+		</nav>
+		<main class="container">
+  		    {{ block "main" . }}{{ end }}
+		</main>
+	</body>
+</html>
diff --git a/core/installer/welcome/env-manager-tmpl/form.html b/core/installer/welcome/env-manager-tmpl/form.html
new file mode 100644
index 0000000..c0f0d4a
--- /dev/null
+++ b/core/installer/welcome/env-manager-tmpl/form.html
@@ -0,0 +1,64 @@
+{{ define "main" }}
+<div class="grid contents-header">
+	<div style="border-width: 1px; border-right-style: solid;">
+		attention
+	</div>
+	<div>
+		take <span class="highlight">control</span>
+	</div>
+</div>
+<div id="contents" class="grid">
+	<div style="border-width: 1px; border-right-style: solid;">
+		As part of provisioning new dodo instance you will have to update DNS records at your domain registrar, so that it points to the nameservers running on your newly created dodo. Please first get familiar with your domain registrar documentation, and only then proceed with provisioning.
+		<label for="accept" style="padding-top: 1rem;">
+			<input type="checkbox" name="accept" id="accept" form="create-form" required tabindex="5">
+			<strong>I understand</strong>
+		</label>
+	</div>
+	<div id="create-instance-form">
+		<form action="" method="POST" id="create-form">
+			<label for="domain">
+				domain
+				<input
+					type="text"
+					id="domain"
+					name="domain"
+					required
+					autofocus
+					tabindex="1"
+				/>
+			</label>
+			<label for="contact-email">
+				contact email
+				<input
+					type="email"
+					id="contact-email"
+					name="contact-email"
+					required
+					tabindex="2"
+				/>
+			</label>
+			<label for="admin-public-key">
+				admin ssh public key
+				<input
+					type="string"
+					id="admin-public-key"
+					name="admin-public-key"
+					required
+					tabindex="3"
+				/> <!-- TODO(gio): remove-->
+			</label>
+			<label for="secret-token">
+				invitation code
+				<textarea
+					id="secret-token"
+					name="secret-token"
+					required
+					tabindex="4"
+				></textarea>
+			</label>
+			<button type="submit" tabindex="6">provision</button>
+		</form>
+	</div>
+</div>
+{{ end }}
diff --git a/core/installer/welcome/env-manager-tmpl/status.html b/core/installer/welcome/env-manager-tmpl/status.html
new file mode 100644
index 0000000..3a54edf
--- /dev/null
+++ b/core/installer/welcome/env-manager-tmpl/status.html
@@ -0,0 +1,44 @@
+{{ define "task" }}
+{{ range . }}
+<li aria-busy="{{ eq .Status 0 }}">
+	{{ .Title }}{{ if .Err }} - {{ .Err.Error }} {{ end }}
+	{{ if .Subtasks }}
+	<ul>
+   		{{ template "task" .Subtasks }}
+	</ul>
+	{{ end }}
+</li>
+{{ end }}
+{{ end }}
+
+{{ define "main" }}
+<article>
+  <ul>
+	  {{ template "task" .Root.Subtasks }}
+  </ul>
+</article>
+{{ if .DNSRecords }}
+<div>
+	<form action="" method="POST">
+		You will have to publish following DNS records via your domain registrar.
+		<textarea rows="7">{{ .DNSRecords }}</textarea>
+		<label for="domain-registrar">Domain Registrar</label>
+		<select id="domain-registrar" required tabindex="1">
+			<option value="" selected>Select registrar</option>
+			<option value="gandi">Gandi</option>
+			<option value="namecheap">Namecheap</option>
+		</select>
+		<label for="api-token">API Token</label>
+		<input
+			type="text"
+			id="api-token"
+			name="api-token"
+			required
+			autofocus
+			tabindex="2"
+		/>
+		<button type="submit" tabindex="3">Update</button>
+	</form>
+</div>
+{{ end }}
+{{ end }}
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 5436702..f8ee70d 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -1,11 +1,11 @@
 package welcome
 
 import (
-	_ "embed"
+	"embed"
 	"encoding/json"
 	"errors"
 	"fmt"
-	htemplate "html/template"
+	"html/template"
 	"io"
 	"io/fs"
 	"log"
@@ -21,11 +21,46 @@
 	"github.com/giolekva/pcloud/core/installer/tasks"
 )
 
-//go:embed create-env.html
-var createEnvFormHtml []byte
+//go:embed env-manager-tmpl/*
+var tmpls embed.FS
 
-//go:embed env-created.html
-var envCreatedHtml string
+var tmplsParsed templates
+
+func init() {
+	if t, err := parseTemplates(tmpls); err != nil {
+		panic(err)
+	} else {
+		tmplsParsed = t
+	}
+}
+
+type templates struct {
+	form   *template.Template
+	status *template.Template
+}
+
+func parseTemplates(fs embed.FS) (templates, error) {
+	base, err := template.ParseFS(fs, "env-manager-tmpl/base.html")
+	if err != nil {
+		return templates{}, err
+	}
+	parse := func(path string) (*template.Template, error) {
+		if b, err := base.Clone(); err != nil {
+			return nil, err
+		} else {
+			return b.ParseFS(fs, path)
+		}
+	}
+	form, err := parse("env-manager-tmpl/form.html")
+	if err != nil {
+		return templates{}, err
+	}
+	status, err := parse("env-manager-tmpl/status.html")
+	if err != nil {
+		return templates{}, err
+	}
+	return templates{form, status}, nil
+}
 
 type Status string
 
@@ -108,12 +143,7 @@
 	if !ready && len(info.Records) > 0 {
 		panic("!! SHOULD NOT REACH !!")
 	}
-	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{
+	if err := tmplsParsed.status.Execute(w, map[string]any{
 		"Root":       t,
 		"DNSRecords": info.Records,
 	}); err != nil {
@@ -158,7 +188,7 @@
 }
 
 func (s *EnvServer) createEnvForm(w http.ResponseWriter, r *http.Request) {
-	if _, err := w.Write(createEnvFormHtml); err != nil {
+	if err := tmplsParsed.form.Execute(w, nil); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 	}
 }
@@ -294,10 +324,6 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if err := s.repo.CommitAndPush(fmt.Sprintf("Allocate CIDR for %s", req.Name)); err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		return
-	}
 	// if err := s.acceptInvitation(req.SecretToken); err != nil {
 	// 	http.Error(w, err.Error(), http.StatusInternalServerError)
 	// 	return
@@ -329,6 +355,10 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
+	if err := s.repo.CommitAndPush(fmt.Sprintf("Allocate CIDR for %s", req.Name)); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
 	t, dns := tasks.NewCreateEnvTask(
 		tasks.Env{
 			PCloudEnvName:  env.Name,
diff --git a/core/installer/welcome/static/main.css b/core/installer/welcome/static/main.css
index 1b6ec68..2a01ae8 100644
--- a/core/installer/welcome/static/main.css
+++ b/core/installer/welcome/static/main.css
@@ -1,26 +1,29 @@
 [data-theme="light"],
 :root:not([data-theme="dark"]) {
-	--font-family: Hack, monospace;
-	--font-size: 14px;
-	--background-color: #d6d6d6;
-	--border-radius: 0;
-	--form-element-border-color: #ffffff;
-	--form-element-active-border-color: #7f9f7f;
-	--primary: #7f9f7f;
-	--primary-hover: #d4888d;
-	--grid-spacing-horizontal: 0;
+	--pico-font-family: Hack, monospace;
+	--pico-font-size: 14px;
+	--pico-background-color: #d6d6d6;
+	--pico-border-radius: 0;
+	--pico-form-element-border-color: #ffffff;
+	--pico-form-element-active-border-color: #7f9f7f;
+	--pico-primary: #7f9f7f;
+	--pico-primary-background: #7f9f7f;
+	--pico-primary-hover: #d4888d;
+	--pico-primary-hover-background: #d4888d;
+	--pico-grid-spacing-horizontal: 0;
 }
 
 input[type="checkbox"] {
-	--form-element-border-color: #3a3a3a;
+	--pico-form-element-border-color: #3a3a3a;
 }
 
 main.container {
 	max-width: 850px;
+	margin-top: 13rem;
 }
 
 .dodo {
-	font-size: 2.5rem;
+	font-size: 1.6rem;
 	font-weight: bold;
 	background-color: #3a3a3a;
 	border-left: 1px dashed #3a3a3a;
@@ -32,12 +35,13 @@
 
 #menu {
 	border-bottom: 1px dashed #3a3a3a;
+	padding-left: 5rem;
+	padding-right: 5rem;
 }
 
 #menu a {
-	font-size: 1.4rem;
+	/* font-size: 1.2rem; */
 	color: #3a3a3a;
-	border-right: 1px dashed #3a3a3a;
 }
 
 #menu li {
@@ -45,6 +49,14 @@
 	padding-bottom: 0;
 }
 
+#links {
+	--pico-nav-element-spacing-horizontal: 3rem;
+}
+
+#menu li {
+	border-right: 1px dashed #3a3a3a;
+}
+
 div.contents-header {
 	font-size: 1.2rem;
 	border-width: 1px;
@@ -56,6 +68,10 @@
 	border-style: none solid solid solid;
 }
 
+main > div.grid {
+	--pico-grid-column-gap: 0;
+}
+
 div.contents-header > div {
 	padding: 1rem 3rem;
 }
@@ -65,7 +81,8 @@
 }
 
 #create-instance-form {
-	--spacing: 10px;
+	--pico-spacing: 10px;
+	/* TODO(gio): figure out why overriding --pico-background-color does not work */
 	background-color: #3a3a3a;
 }
 
@@ -75,6 +92,7 @@
 
 #create-instance-form input, textarea, button {
 	border-width: 0.5px !important;
+	background-color: #3a3a3a;
 }
 
 #create-instance-form input {