DodoApp: Cache static resources.

Change-Id: Ib1f800ea052c5f3b7929a1d325b351dfa0a6633e
diff --git a/core/auth/memberships/main.go b/core/auth/memberships/main.go
index 0e3f2da..ba5db7c 100644
--- a/core/auth/memberships/main.go
+++ b/core/auth/memberships/main.go
@@ -28,7 +28,7 @@
 //go:embed memberships-tmpl/*
 var tmpls embed.FS
 
-//go:embed static
+//go:embed stat
 var staticResources embed.FS
 
 type Store interface {
@@ -673,7 +673,7 @@
 	e := make(chan error)
 	go func() {
 		r := mux.NewRouter()
-		r.PathPrefix("/static/").Handler(http.FileServer(http.FS(staticResources)))
+		r.PathPrefix("/stat/").Handler(http.FileServer(http.FS(staticResources)))
 		r.HandleFunc("/group/{group-name}/add-user/", s.addUserToGroupHandler).Methods(http.MethodPost)
 		r.HandleFunc("/group/{parent-group}/add-child-group", s.addChildGroupHandler).Methods(http.MethodPost)
 		r.HandleFunc("/group/{owned-group}/add-owner-group", s.addOwnerGroupHandler).Methods(http.MethodPost)
diff --git a/core/auth/memberships/memberships-tmpl/base.html b/core/auth/memberships/memberships-tmpl/base.html
index 9755c08..e824597 100644
--- a/core/auth/memberships/memberships-tmpl/base.html
+++ b/core/auth/memberships/memberships-tmpl/base.html
@@ -4,8 +4,8 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ block "title" . }}{{ end }}</title>
-    <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
-    <link rel="stylesheet" href="/static/main.css?v=0.0.3">
+    <link rel="stylesheet" href="/stat/pico.2.0.6.min.css">
+    <link rel="stylesheet" href="/stat/main.css?v=0.0.3">
 </head>
 <body class="container">
     {{ define "svgIcon" }}
@@ -29,6 +29,6 @@
         </article>
     </dialog>
     {{ end }}
-    <script src="/static/main.js"></script>
+    <script src="/stat/main.js"></script>
 </body>
 </html>
diff --git a/core/auth/memberships/static/main.css b/core/auth/memberships/stat/main.css
similarity index 100%
rename from core/auth/memberships/static/main.css
rename to core/auth/memberships/stat/main.css
diff --git a/core/auth/memberships/static/main.js b/core/auth/memberships/stat/main.js
similarity index 100%
rename from core/auth/memberships/static/main.js
rename to core/auth/memberships/stat/main.js
diff --git a/core/auth/memberships/static/pico.2.0.6.min.css b/core/auth/memberships/stat/pico.2.0.6.min.css
similarity index 100%
rename from core/auth/memberships/static/pico.2.0.6.min.css
rename to core/auth/memberships/stat/pico.2.0.6.min.css
diff --git a/core/installer/welcome/appmanager-tmpl/base.html b/core/installer/welcome/appmanager-tmpl/base.html
index dd0549a..7944783 100644
--- a/core/installer/welcome/appmanager-tmpl/base.html
+++ b/core/installer/welcome/appmanager-tmpl/base.html
@@ -2,8 +2,8 @@
 <html lang="en" data-theme="light">
 	<head>
 		<meta charset="utf-8" />
-        <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
-        <link rel="stylesheet" type="text/css" href="/static/appmanager.css?v=0.0.14">
+        <link rel="stylesheet" href="/stat/pico.2.0.6.min.css">
+        <link rel="stylesheet" type="text/css" href="/stat/appmanager.css?v=0.0.14">
 		<meta name="viewport" content="width=device-width, initial-scale=1" />
 	</head>
 	<body>
@@ -26,6 +26,6 @@
 			  {{ block "content" . }}{{ end }}
 		  </div>
       </main>
-    <script src="/static/app-manager.js?v=0.0.10"></script>
+    <script src="/stat/app-manager.js?v=0.0.10"></script>
 	</body>
 </html>
diff --git a/core/installer/welcome/appmanager.go b/core/installer/welcome/appmanager.go
index 345cbab..4fecf36 100644
--- a/core/installer/welcome/appmanager.go
+++ b/core/installer/welcome/appmanager.go
@@ -94,7 +94,7 @@
 
 func (s *AppManagerServer) Start() error {
 	r := mux.NewRouter()
-	r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
+	r.PathPrefix("/stat/").Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
 	r.HandleFunc("/api/networks", s.handleNetworks).Methods(http.MethodGet)
 	r.HandleFunc("/api/app-repo", s.handleAppRepo)
 	r.HandleFunc("/api/app/{slug}/install", s.handleAppInstall).Methods(http.MethodPost)
diff --git a/core/installer/welcome/create-account-success.html b/core/installer/welcome/create-account-success.html
index 9c647ac..86563c6 100644
--- a/core/installer/welcome/create-account-success.html
+++ b/core/installer/welcome/create-account-success.html
@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <html lang="en" data-theme="light">
     <head>
-        <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
+        <link rel="stylesheet" href="/stat/pico.2.0.6.min.css">
         <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css">
-        <link rel="stylesheet" href="/static/welcome.css?v=0.0.1">
+        <link rel="stylesheet" href="/stat/welcome.css?v=0.0.1">
         <meta charset="UTF-8">
         <meta name="viewport" content="width=device-width, initial-scale=1" >
         <title>Successful Registration</title>
diff --git a/core/installer/welcome/create-account.html b/core/installer/welcome/create-account.html
index 05429a8..cb9351e 100644
--- a/core/installer/welcome/create-account.html
+++ b/core/installer/welcome/create-account.html
@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <html lang="en" data-theme="light">
 <head>
-    <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
+    <link rel="stylesheet" href="/stat/pico.2.0.6.min.css">
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css">
-    <link rel="stylesheet" href="/static/welcome.css?v=0.0.1">
+    <link rel="stylesheet" href="/stat/welcome.css?v=0.0.1">
     <meta charset="utf-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
 </head>
diff --git a/core/installer/welcome/dodo-app-tmpl/base.html b/core/installer/welcome/dodo-app-tmpl/base.html
index d0e0693..b0251cd 100644
--- a/core/installer/welcome/dodo-app-tmpl/base.html
+++ b/core/installer/welcome/dodo-app-tmpl/base.html
@@ -4,8 +4,8 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>{{ block "title" . }}{{ end }}</title>
-    <link rel="stylesheet" href="/static/pico.2.0.6.min.css">
-    <link rel="stylesheet" href="/static/dodo_app.css?v=0.0.7">
+    <link rel="stylesheet" href="/stat/pico.2.0.6.min.css">
+    <link rel="stylesheet" href="/stat/dodo_app.css?v=0.0.8">
 </head>
 <body class="container">
     {{- block "content" . }}
diff --git a/core/installer/welcome/dodo-app-tmpl/index.html b/core/installer/welcome/dodo-app-tmpl/index.html
index fa5ab48..865f0cf 100644
--- a/core/installer/welcome/dodo-app-tmpl/index.html
+++ b/core/installer/welcome/dodo-app-tmpl/index.html
@@ -9,14 +9,13 @@
 			<option value="{{ .Name }}">{{ .Name }} - {{ .Domain }}</option>
 			{{- end -}}
 		</select>
-		<input type="text" name="subdomain" placeholder="Subdomain" />
+		<input type="text" name="subdomain" placeholder="subdomain" />
 		<select name="type">
 			{{- range .Types -}}
 			<option value="{{ . }}">{{ . }}</option>
 			{{- end -}}
 		</select>
-		<button id="create-app-button" aria-busy="false" type="submit" name="create-app">
-			create app</button>
+		<button id="create-app-button" aria-busy="false" type="submit" name="create-app">create app</button>
 	</fieldset>
 </form>
 <hr class="divider">
@@ -31,5 +30,5 @@
 		</ul>
 	</nav>
 </aside>
-<script src="/static/dodo-app.js?v=0.0.7"></script>
+<script src="/stat/dodo-app.js?v=0.0.8"></script>
 {{- end -}}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 32fb0de..b11e3a7 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -32,15 +32,12 @@
 //go:embed all:app-tmpl
 var appTmplsFS embed.FS
 
-//go:embed static
-var staticResources embed.FS
-
 const (
 	ConfigRepoName = "config"
 	appConfigsFile = "/apps.json"
 	loginPath      = "/login"
 	logoutPath     = "/logout"
-	staticPath     = "/static"
+	staticPath     = "/stat/"
 	apiPublicData  = "/api/public-data"
 	apiCreateApp   = "/api/apps"
 	sessionCookie  = "dodo-app-session"
@@ -182,7 +179,7 @@
 	go func() {
 		r := mux.NewRouter()
 		r.Use(s.mwAuth)
-		r.PathPrefix(staticPath).Handler(http.FileServer(http.FS(staticResources)))
+		r.PathPrefix(staticPath).Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
 		r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
 		r.HandleFunc(apiPublicData, s.handleAPIPublicData)
 		r.HandleFunc(apiCreateApp, s.handleAPICreateApp).Methods(http.MethodPost)
@@ -835,23 +832,28 @@
 }
 
 func (s *DodoAppServer) updateDodoApp(appStatus installer.EnvApp, name, namespace string, networks []installer.Network) error {
+	fmt.Println("111")
 	repo, err := s.client.GetRepo(name)
 	if err != nil {
 		return err
 	}
+	fmt.Println("111")
 	hf := installer.NewGitHelmFetcher()
 	m, err := installer.NewAppManager(repo, s.nsc, s.jc, hf, "/.dodo")
 	if err != nil {
 		return err
 	}
+	fmt.Println("111")
 	appCfg, err := soft.ReadFile(repo, "app.cue")
 	if err != nil {
 		return err
 	}
+	fmt.Println("111")
 	app, err := installer.NewDodoApp(appCfg)
 	if err != nil {
 		return err
 	}
+	fmt.Println("111")
 	lg := installer.GitRepositoryLocalChartGenerator{"app", namespace}
 	return repo.Do(func(r soft.RepoFS) (string, error) {
 		res, err := m.Install(
@@ -872,13 +874,16 @@
 			installer.WithLocalChartGenerator(lg),
 			installer.WithNoLock(),
 		)
+		fmt.Println("111")
 		if err != nil {
 			return "", err
 		}
+		fmt.Println("111")
 		var rendered dodoAppRendered
 		if err := json.NewDecoder(bytes.NewReader(res.RenderedRaw)).Decode(&rendered); err != nil {
 			return "", nil
 		}
+		fmt.Println("111")
 		if _, err := m.Install(
 			appStatus,
 			"status",
@@ -898,6 +903,7 @@
 		); err != nil {
 			return "", err
 		}
+		fmt.Println("111")
 		return "install app", nil
 	},
 		soft.WithCommitToBranch("dodo"),
diff --git a/core/installer/welcome/env-manager-tmpl/base.html b/core/installer/welcome/env-manager-tmpl/base.html
index 689ca79..3e4b6b6 100644
--- a/core/installer/welcome/env-manager-tmpl/base.html
+++ b/core/installer/welcome/env-manager-tmpl/base.html
@@ -1,9 +1,9 @@
 <!DOCTYPE html>
 <html lang="en" data-theme="light">
 	<head>
-		<link rel="stylesheet" href="/static/pico.2.0.6.min.css">
+		<link rel="stylesheet" href="/stat/pico.2.0.6.min.css">
 		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css">
-		<link rel="stylesheet" href="/static/main.css">
+		<link rel="stylesheet" href="/stat/main.css">
 		<meta charset="utf-8" />
 		<meta name="viewport" content="width=device-width, initial-scale=1" />
 		<title>dodo:</title>
diff --git a/core/installer/welcome/env.go b/core/installer/welcome/env.go
index 4c084cd..356e47c 100644
--- a/core/installer/welcome/env.go
+++ b/core/installer/welcome/env.go
@@ -131,7 +131,7 @@
 
 func (s *EnvServer) Start() {
 	r := mux.NewRouter()
-	r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
+	r.PathPrefix("/stat/").Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
 	r.Path("/env/{key}").Methods("GET").HandlerFunc(s.monitorTask)
 	r.Path("/env/{key}").Methods("POST").HandlerFunc(s.publishDNSRecords)
 	r.Path("/").Methods("GET").HandlerFunc(s.createEnvForm)
diff --git a/core/installer/welcome/launcher-tmpl/launcher.html b/core/installer/welcome/launcher-tmpl/launcher.html
index 6032869..00e67f7 100644
--- a/core/installer/welcome/launcher-tmpl/launcher.html
+++ b/core/installer/welcome/launcher-tmpl/launcher.html
@@ -4,8 +4,8 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>dodo: Launcher</title>
-    <link rel="stylesheet" type="text/css" href="/static/pico.2.0.6.min.css">
-    <link rel="stylesheet" type="text/css" href="/static/launcher.css?v=0.0.19">
+    <link rel="stylesheet" type="text/css" href="/stat/pico.2.0.6.min.css">
+    <link rel="stylesheet" type="text/css" href="/stat/launcher.css?v=0.0.19">
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css">
 </head>
 <body class="container-fluid">
@@ -82,6 +82,6 @@
             {{ template "help-content-template" (dict "Help" $h.Children "First" false) }}
         {{ end }}
     {{ end }}
-    <script src="/static/launcher.js?v=0.0.19"></script>
+    <script src="/stat/launcher.js?v=0.0.19"></script>
 </body>
 </html>
diff --git a/core/installer/welcome/launcher.go b/core/installer/welcome/launcher.go
index 3dd70b9..a828f1a 100644
--- a/core/installer/welcome/launcher.go
+++ b/core/installer/welcome/launcher.go
@@ -18,9 +18,6 @@
 //go:embed launcher-tmpl/launcher.html
 var indexHTML embed.FS
 
-//go:embed static/*
-var files embed.FS
-
 type AppLauncherInfo struct {
 	Id         string
 	Name       string
@@ -140,7 +137,7 @@
 }
 
 func (s *LauncherServer) Start() {
-	http.Handle("/static/", cachingHandler{http.FileServer(http.FS(files))})
+	http.Handle("/stat/", cachingHandler{http.FileServer(http.FS(statAssets))})
 	http.HandleFunc("/", s.homeHandler)
 	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", s.port), nil))
 }
diff --git a/core/installer/welcome/stat/app-manager.js b/core/installer/welcome/stat/app-manager.js
new file mode 100644
index 0000000..55357f3
--- /dev/null
+++ b/core/installer/welcome/stat/app-manager.js
@@ -0,0 +1,66 @@
+function delaySearch(func, wait) {
+    let timeout;
+    return function (...args) {
+        clearTimeout(timeout);
+        timeout = setTimeout(() => func.apply(this, args), wait);
+    };
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+    let searchRequestCount = 0;
+    const page = document.documentElement;
+    const headerHeight = parseFloat(getComputedStyle(page).getPropertyValue('--pico-header-height').replace("px", ""));
+    const nav = document.getElementById("menu");
+    const windowHeight = window.innerHeight - headerHeight;
+    nav.style.setProperty("--max-height", `${windowHeight}px`);
+    const menu = document.getElementById("menu-nav");
+    const menuHeight = parseFloat(getComputedStyle(document.getElementById('menu-nav')).height.replace("px", "")) + 15;
+    menu.style.setProperty("height", `${menuHeight}px`);
+    const searchForm = document.getElementById('search-form');
+    const searchInput = document.getElementById('search-input');
+    function fetchAndUpdateAppList() {
+        searchRequestCount++;
+        const currentRequest = searchRequestCount;
+        const formData = new FormData(searchForm);
+        const query = formData.get('query');
+        const pageType = document.getElementById('page-type').value;
+        const url = `/${pageType}?query=${encodeURIComponent(query)}`;
+        fetch(url, {
+            method: 'GET'
+        })
+            .then(response => response.text())
+            .then(html => {
+                if (currentRequest !== searchRequestCount) {
+                    return;
+                }
+                const tempDiv = document.createElement('div');
+                tempDiv.innerHTML = html;
+                const newAppListHTML = tempDiv.querySelector('#app-list').innerHTML;
+                const appListContainer = document.getElementById("app-list");
+                appListContainer.innerHTML = newAppListHTML;
+            })
+            .catch(error => console.error('Error fetching app list:', error));
+    }
+    const delayedFetchAndUpdateAppList = delaySearch(fetchAndUpdateAppList, 300);
+    searchForm.addEventListener('submit', (event) => {
+        event.preventDefault();
+        fetchAndUpdateAppList();
+    });
+    searchInput.addEventListener('input', () => {
+        delayedFetchAndUpdateAppList();
+    });
+});
+
+let prevWindowHeight = window.innerHeight;
+
+window.addEventListener("resize", function () {
+    const nav = document.getElementById("menu");
+    const windowHeight = window.innerHeight;
+    const heightDiff = prevWindowHeight - windowHeight;
+    const currentMaxHeight = parseFloat(nav.style.getPropertyValue("--max-height").replace("px", ""));
+    if (!isNaN(currentMaxHeight)) {
+        const newMaxHeight = currentMaxHeight - heightDiff;
+        nav.style.setProperty("--max-height", `${newMaxHeight}px`);
+    }
+    prevWindowHeight = windowHeight;
+});
diff --git a/core/installer/welcome/stat/appmanager.css b/core/installer/welcome/stat/appmanager.css
new file mode 100644
index 0000000..cde49ab
--- /dev/null
+++ b/core/installer/welcome/stat/appmanager.css
@@ -0,0 +1,329 @@
+[data-theme="light"],
+:root:not([data-theme="dark"]) {
+  --pico-font-family: Hack, monospace;
+  --pico-font-size: 14px;
+  --pico-header-height: 56px;
+  --pico-border-radius: 0;
+  --pico-background-color: #d6d6d6;
+  --pico-form-element-border-color: #3a3a3a;
+  --pico-form-element-active-border-color: #7f9f7f;
+  --pico-form-element-focus-color: #7f9f7f;
+  --pico-form-element-background-color: #d6d6d6;
+  --pico-form-element-active-background-color: #d6d6d6;
+  --pico-form-element-selected-background-color: #d6d6d6;
+  --pico-dropdown-color: #3a3a3a;
+  --pico-dropdown-background-color: #d6d6d6;
+  --pico-dropdown-border-color: #7f9f7f;
+  --pico-dropdown-hover-background-color: #7f9f7f;
+  --pico-primary: #7f9f7f;
+  --pico-primary-background: #7f9f7f;
+  --pico-primary-hover: #d4888d;
+  --pico-primary-hover-background: #d4888d;
+  --pico-grid-spacing-horizontal: 0;
+  --search-background-color: #d6d6d6;
+  --pico-color: #3a3a3a;
+  --pico-form-element-color: #3a3a3a;
+  --pico-primary-inverse: #3a3a3a;
+  --pico-tooltip-background-color: #3a3a3a;
+  --pico-tooltip-color: #d6d6d6;
+  --pico-icon-color: #3a3a3a;
+  --icon-width: 50px;
+  --icon-height: 50px;
+  --icon-margin-left: 6px;
+  --icon-margin-right: 6px;
+  --app-details-padding-right: calc(
+    var(--icon-margin-right) + var(--icon-margin-left) + var(--icon-width)
+  );
+  h3,
+  p {
+    --pico-color: #3a3a3a;
+  }
+  label {
+    color: var(--pico-color);
+  }
+  input:is([type="checkbox"]) {
+    --pico-form-element-focus-color: none;
+    --pico-border-color: var(--pico-color);
+  }
+  [data-tooltip]:not(a, button, input) {
+    text-decoration: none;
+    cursor: pointer;
+  }
+  #menu-nav nav ul li a {
+    --pico-primary: #3a3a3a;
+  }
+  .icon {
+    color: var(--pico-icon-color);
+  }
+}
+
+@media (max-width: 768px) {
+  body > main {
+    grid-template-columns: 9rem 1fr !important;
+    column-gap: 0 !important;
+  }
+
+  .container-fluid {
+    padding-left: 1px;
+    padding-right: 1px;
+    margin-left: 0;
+    margin-right: 0;
+  }
+
+  #content {
+    width: 100% !important;
+  }
+
+  .app-details {
+    padding-right: 22px !important;
+  }
+}
+
+body > header {
+  z-index: 4;
+  position: relative;
+}
+
+html {
+  scroll-behavior: smooth;
+  overflow-x: hidden;
+}
+
+body > header.is-fixed-above-lg + main {
+  --pico-main-top-offset: var(--pico-header-height);
+}
+
+body > header.is-fixed-above-lg {
+  height: var(--pico-header-height);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 2;
+  position: sticky;
+  top: 0;
+  -webkit-backdrop-filter: blur(1rem);
+  backdrop-filter: blur(1rem);
+  background-color: var(--pico-form-element-border-color);
+  transition: border-top-color 0.4s ease-in-out, box-shadow 0.4s ease-in-out;
+}
+
+body > main > aside > nav.is-sticky-above-lg {
+  position: sticky;
+  top: calc(
+    var(--pico-main-top-offset) + var(--pico-block-spacing-vertical) / 2
+  );
+  max-height: calc(var(--max-height) - var(--pico-spacing));
+  overflow: auto;
+  transition: top var(--pico-transition);
+  transition-delay: 50ms;
+}
+
+body > main {
+  display: grid;
+  grid-template-rows: auto auto 1fr;
+  grid-template-columns: 11rem calc(100% - 11rem);
+  grid-template-areas: "menu content";
+  column-gap: 2rem;
+  margin-top: 1rem;
+  padding: 0;
+}
+
+header > h1,
+header > svg {
+  margin-bottom: 2.5px;
+  color: white;
+}
+
+header > svg {
+  margin-right: var(--pico-spacing);
+}
+
+.search-bar {
+  max-width: 616px;
+  width: 100%;
+}
+
+article {
+  margin: 0.3em;
+  margin-bottom: 0.3em;
+  display: flex;
+  padding: 6px !important;
+  position: relative;
+  align-items: flex-start;
+}
+
+.icon {
+  margin: 0 var(--icon-margin-right) 0 var(--icon-margin-left);
+  flex-shrink: 0;
+  /* --pico-primary: #3a3a3a;
+  color: var(--pico-color); */
+}
+
+.app-details {
+  display: flex;
+  flex-direction: column;
+  flex-grow: 1;
+  position: relative;
+  padding-right: var(--app-details-padding-right);
+}
+
+.app-name-container {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.app {
+  margin-bottom: 2px;
+  margin-top: 0px;
+  margin-left: 5px;
+  font-weight: bold;
+  font-size: 16px;
+}
+
+.app-link:hover h3.app,
+.app-link:hover .icon {
+  color: var(--pico-primary-hover);
+}
+
+.app-link:hover .app {
+  text-decoration: underline;
+}
+.primary:hover {
+  text-decoration: underline;
+  color: var(--pico-primary-hover);
+}
+
+.description {
+  margin: 0 0 3px 5px;
+}
+
+.instance-count {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  font-weight: bold;
+  border: 2px solid var(--pico-color) !important;
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  transform: translate(50%, -50%);
+}
+
+pre {
+  white-space: pre-wrap;
+  /* Since CSS 2.1 */
+  white-space: -moz-pre-wrap;
+  /* Mozilla, since 1999 */
+  white-space: -pre-wrap;
+  /* Opera 4-6 */
+  white-space: -o-pre-wrap;
+  /* Opera 7 */
+  word-wrap: break-word;
+  /* Internet Explorer 5.5+ */
+  background-color: transparent;
+}
+
+.hidden {
+  visibility: hidden;
+}
+
+.toast {
+  position: fixed;
+  z-index: 999;
+  bottom: 10px;
+}
+
+.app-link {
+  padding-top: 0px;
+  padding-bottom: 2px;
+  text-decoration: none;
+  width: 100%;
+  padding-right: 0;
+}
+
+nav li {
+  padding-top: 0;
+  padding-bottom: 0;
+}
+
+nav hr {
+  border-color: var(--pico-color);
+}
+
+input[type="search"] {
+  margin-bottom: 0;
+  height: 100%;
+}
+
+.page {
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+}
+
+.card-content {
+  width: 100%;
+}
+
+.app-card {
+  margin-bottom: 6px;
+}
+
+nav {
+  height: 100%;
+}
+
+#config-form label {
+  width: auto !important;
+  padding: 0 5px 0 5px;
+}
+
+#config-form > label:nth-of-type(2) label {
+  padding-left: 0;
+  padding-right: 0;
+}
+
+input[type="checkbox"]:checked {
+  border-color: var(--pico-form-element-focus-color) !important;
+}
+
+#menu-nav {
+  grid-area: menu;
+}
+
+#content {
+  grid-area: content;
+  width: calc(100% - 11rem);
+}
+
+main > aside#menu-nav nav {
+  margin-bottom: var(--pico-spacing);
+  margin-block: calc(var(--pico-outline-width) * -1);
+  padding-block: var(--pico-outline-width);
+  overflow: auto;
+}
+
+#menu-nav nav ul:first-of-type {
+  margin: 0;
+  padding: 0;
+}
+
+.progress {
+  padding-left: 0;
+}
+
+.progress ul {
+  padding-left: 15px;
+}
+
+.progress li {
+  list-style-type: none;
+}
+
+.primary {
+  color: #7f9f7f;
+}
diff --git a/core/installer/welcome/stat/dodo-app.js b/core/installer/welcome/stat/dodo-app.js
new file mode 100644
index 0000000..ccb5252
--- /dev/null
+++ b/core/installer/welcome/stat/dodo-app.js
@@ -0,0 +1,20 @@
+function triggerForm(status, buttonTxt) {
+    const form = document.getElementById("create-app");
+    const elements = form.querySelectorAll("input, select, textarea, button");
+    const button = document.getElementById("create-app-button");
+    button.textContent = buttonTxt;
+    button.setAttribute("aria-busy", status);
+    elements.forEach(element => {
+        element.disabled = status;
+    });
+}
+
+document.addEventListener("DOMContentLoaded", () => {
+    const form = document.getElementById("create-app");
+    form.addEventListener("submit", (event) => {
+        setTimeout(() => {
+            triggerForm(true, "creating app ...");
+        }, 0);
+    });
+    triggerForm(false, "create app");
+});
diff --git a/core/installer/welcome/stat/dodo_app.css b/core/installer/welcome/stat/dodo_app.css
new file mode 100644
index 0000000..5a66768
--- /dev/null
+++ b/core/installer/welcome/stat/dodo_app.css
@@ -0,0 +1,104 @@
+[data-theme="light"],
+:root:not([data-theme="dark"]) {
+  --pico-font-family: Hack, monospace;
+  --pico-font-size: 14px;
+  --pico-header-height: 56px;
+  --pico-border-radius: 0;
+  --pico-background-color: #d6d6d6;
+  --pico-form-element-border-color: #3a3a3a;
+  --pico-form-element-active-border-color: #7f9f7f;
+  --pico-form-element-focus-color: #7f9f7f;
+  --pico-form-element-background-color: #d6d6d6;
+  --pico-form-element-active-background-color: #d6d6d6;
+  --pico-form-element-selected-background-color: #d6d6d6;
+  --pico-primary: #7f9f7f;
+  --pico-primary-background: #7f9f7f;
+  --pico-primary-hover: #d4888d;
+  --pico-primary-hover-background: #d4888d;
+  --pico-grid-spacing-horizontal: 0;
+  --search-background-color: #d6d6d6;
+  --pico-color: #3a3a3a;
+  --pico-form-element-color: #3a3a3a;
+  --pico-primary-inverse: #3a3a3a;
+  --pico-tooltip-background-color: #3a3a3a;
+  --pico-tooltip-color: #d6d6d6;
+  --pico-icon-color: #3a3a3a;
+  --pico-group-box-shadow-focus-with-button: 0 0 0 0;
+  --pico-card-background-color: var(--pico-card-sectioning-background-color);
+  p {
+    --pico-color: #3a3a3a;
+  }
+  h1 {
+    font-size: 20px;
+    --pico-color: #3a3a3a;
+  }
+  h2 {
+    font-size: 18px;
+    --pico-color: #3a3a3a;
+  }
+  h3 {
+    font-size: 16px;
+    --pico-color: #3a3a3a;
+  }
+  h4 {
+    font-size: 14px;
+    --pico-color: #3a3a3a;
+  }
+  label {
+    color: var(--pico-color);
+  }
+  input:is([type="checkbox"]) {
+    --pico-form-element-focus-color: none;
+    --pico-border-color: var(--pico-color);
+  }
+  [data-tooltip]:not(a, button, input) {
+    text-decoration: none;
+    cursor: pointer;
+  }
+  hr {
+    border-top: 1px solid var(--pico-color);
+  }
+  :is(button, [type="submit"], [type="button"], [role="button"]).secondary,
+  [type="file"]::file-selector-button,
+  [type="reset"] {
+    --pico-background-color: var(--pico-primary-hover-background);
+    --pico-border-color: var(--pico-primary-hover);
+    --pico-color: var(--pico-color);
+    cursor: pointer;
+  }
+  #confirm-button:hover {
+    background-color: var(--pico-primary);
+    border-color: var(--pico-primary);
+  }
+}
+
+body.container {
+  padding-top: 15px;
+}
+
+@media (min-width: 768px) {
+  fieldset.grid {
+    grid-template-columns: 1fr 1fr 1fr 200px;
+  }
+}
+
+[role="button"][aria-busy="true"],
+[type="button"][aria-busy="true"],
+[type="reset"][aria-busy="true"],
+[type="submit"][aria-busy="true"],
+a[aria-busy="true"],
+button[aria-busy="true"] {
+  pointer-events: auto;
+}
+
+input:disabled,
+select:disabled,
+textarea:disabled,
+button:disabled {
+  cursor: not-allowed;
+}
+
+.app-info-link {
+  width: fit-content;
+}
+
diff --git a/core/installer/welcome/stat/hi.txt b/core/installer/welcome/stat/hi.txt
new file mode 100644
index 0000000..45b983b
--- /dev/null
+++ b/core/installer/welcome/stat/hi.txt
@@ -0,0 +1 @@
+hi
diff --git a/core/installer/welcome/stat/launcher.css b/core/installer/welcome/stat/launcher.css
new file mode 100644
index 0000000..af8b293
--- /dev/null
+++ b/core/installer/welcome/stat/launcher.css
@@ -0,0 +1,348 @@
+:root:not([data-theme]) {
+  --pico-background-color: unset;
+  --pico-color: unset;
+}
+
+:root {
+  --bg: #d6d6d6;
+  --bodyBg: #3a3a3a;
+  --text: #3a3a3a;
+  --formText: #d6d6d6;
+  --button: #7f9f7f;
+  --logo: #d4888d;
+  --fontSize: 14px;
+}
+
+body {
+  margin: 0;
+  padding: 0;
+  font-family: Hack, monospace;
+  display: flex;
+  height: 100vh;
+  padding-left: 0 !important;
+  padding-right: 0 !important;
+  background-color: var(--bodyBg);
+  overflow-x: hidden;
+  overflow-y: hidden;
+}
+
+#left-panel {
+  width: 80px;
+  background-color: var(--bg);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.app-list {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  overflow-y: auto;
+  overflow-x: hidden;
+  padding-top: 3px;
+  width: 95% !important;
+}
+
+.scrollbar-custom {
+  scrollbar-width: thin;
+  scrollbar-color: var(--bodyBg) var(--bg);
+}
+
+.scrollbar-custom::-webkit-scrollbar {
+  width: 6px;
+}
+
+.scrollbar-custom::-webkit-scrollbar-track {
+  background-color: var(--bg) !important;
+}
+
+.scrollbar-custom::-webkit-scrollbar-thumb {
+  background-color: var(--bodyBg) !important;
+  border-radius: 4px !important;
+}
+
+.scrollbar-custom::-webkit-scrollbar-thumb:hover {
+  background-color: var(--bodyBg);
+}
+
+#right-panel {
+  flex: 1;
+  background-color: none !important;
+  padding: 0 0 0 2px;
+}
+
+.appFrame {
+  border-radius: 0;
+  width: 100%;
+  height: 100%;
+  border: 0;
+}
+
+.app-icon {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  width: 80px !important;
+  height: 50px !important;
+  margin-bottom: 10px !important;
+  cursor: pointer !important;
+}
+
+@keyframes pulsate {
+  from { opacity: 1; }
+  10% { opacity: 0; }
+  20% { opacity: 1; }
+  30% { opacity: 0; }
+  40% { opacity: 1; }
+  50% { opacity: 0; }
+  60% { opacity: 1; }
+  70% { opacity: 0; }
+  80% { opacity: 1; }
+  90% { opacity: 0; }
+  to { opacity: 1; }
+}
+
+@keyframes fadeout {
+  /* TODO(gio): figure out why animating from 1 does not work */
+  from { opacity: 0.999; }
+  to { opacity: 0; }
+}
+
+.pulsate {
+  animation: pulsate 5s linear;
+}
+
+.fadeout {
+  animation: fadeout 2s ease-in;
+}
+
+.tooltip {
+  position: absolute;
+  width: 200px;
+  left: 80px;
+  transform: translateY(-50%);
+  background-color: var(--bodyBg);
+  padding: 5px;
+  z-index: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  visibility: hidden;
+  opacity: 0;
+  cursor: auto;
+  font-size: 16px;
+  box-shadow: 2px 2px 5px var(--bodyBg);
+}
+
+.help-button {
+  margin-top: 5px !important;
+  padding: 0 !important;
+  border: 0 !important;
+  margin-bottom: 1px !important;
+  width: 100% !important;
+  background-color: var(--button) !important;
+  color: var(--bodyBg) !important;
+  border-radius: 0 !important;
+  cursor: pointer !important;
+  font-size: 16px !important;
+}
+
+.tooltip p {
+  color: var(--formText);
+  margin: 0;
+  cursor: auto;
+  font-size: var(--fontSize);
+}
+
+.app-icon:hover {
+  transform: scale(1.15);
+}
+
+.modal-left {
+  overflow-y: auto;
+  float: left;
+  margin-left: 0px;
+  padding-right: 10px;
+  background-color: #fbfcfc;
+  border-radius: 2px;
+}
+
+.modal-right {
+  flex: 1;
+  overflow-y: auto;
+  float: right;
+  margin-left: 2px;
+  color: var(--bg);
+  padding-left: 10px;
+  padding-right: 10px;
+  font-size: 16px !important;
+}
+
+.app-help-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.app-help-modal-article {
+  display: flex;
+  flex-direction: row;
+  width: 100%;
+  max-width: 100%;
+  min-height: 97%;
+  max-height: 97%;
+  overflow: hidden;
+}
+
+.app-info-modal-article header {
+  flex: 0 0 auto;
+}
+
+header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  position: relative;
+  margin-bottom: 2px !important;
+  background-color: var(--bodyBg) !important;
+}
+
+header h4 {
+  color: var(--formText) !important;
+  padding-left: 10px;
+}
+
+.close-button {
+  padding: 0;
+  border: none;
+  background: none;
+  cursor: pointer;
+  outline: none;
+  width: 1.5em;
+  height: 1.5em;
+  position: absolute;
+  top: 11px;
+  right: 28px;
+}
+
+.modal-article {
+  min-width: 80% !important;
+  max-width: 80% !important;
+  min-height: 90% !important;
+  max-height: 90% !important;
+  overflow: hidden;
+  padding-left: 5px !important;
+  padding-right: 5px !important;
+}
+
+.help-content {
+  display: none;
+}
+
+.circle {
+  width: 50px;
+  height: 50px;
+  border-radius: 50%;
+  background-color: var(--bodyBg);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  margin-top: 2px;
+}
+
+#user-initial {
+  font-size: 24px;
+  text-align: center;
+  line-height: 50px;
+  margin: 0;
+  position: relative;
+  display: inline-block;
+  color: var(--logo);
+}
+
+.user-circle {
+  min-width: 80px !important;
+  max-width: 80px !important;
+  cursor: pointer;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+
+.separator {
+  margin-top: 2px !important;
+  margin-bottom: 4px !important;
+  border-width: 2px !important;
+  border-color: var(--bodyBg) !important;
+  width: 100% !important;
+}
+
+.modal-left ul {
+  padding-inline-start: 0px !important;
+  margin-bottom: 0px;
+  list-style: none;
+  font-size: 14px;
+}
+
+.modal-left ul li {
+  list-style: none !important;
+  padding-inline-start: 10px !important;
+  margin-bottom: 0px;
+  font-size: 16px !important;
+}
+
+.modal-left ul li a {
+  --pico-text-decoration: none;
+  cursor: pointer;
+}
+.modal-left ul li a[aria-current] {
+  color: var(--pico-primary);
+}
+
+.tooltip-user {
+  position: absolute;
+  top: 38.7px;
+  left: 80px;
+  transform: translateY(-50%);
+  width: 234px;
+  background-color: var(--bodyBg);
+  padding: 5px;
+  z-index: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  visibility: hidden;
+  opacity: 0;
+  cursor: auto;
+  box-shadow: 2px 2px 5px var(--bodyBg);
+}
+
+#logout-button {
+  margin-top: 5px !important;
+  padding: 0 !important;
+  border: 0 !important;
+  margin-bottom: 5px !important;
+  width: 100% !important;
+  cursor: pointer !important;
+  font-size: 19px !important;
+  border-radius: 0;
+  background-color: var(--button);
+  color: var(--text) !important;
+}
+
+.tooltip-user p {
+  color: white;
+  margin: 0;
+  cursor: auto;
+  font-size: 19px;
+  color: var(--logo);
+}
diff --git a/core/installer/welcome/stat/launcher.js b/core/installer/welcome/stat/launcher.js
new file mode 100644
index 0000000..ab34e4c
--- /dev/null
+++ b/core/installer/welcome/stat/launcher.js
@@ -0,0 +1,259 @@
+function showTooltip(obj) {
+  obj.style.visibility = 'visible';
+  obj.style.opacity = '1';
+}
+function hideTooltip(obj) {
+  obj.style.visibility = 'hidden';
+  obj.style.opacity = '0';
+}
+
+document.addEventListener("DOMContentLoaded", function () {
+  document.getElementById('appFrame-default').contentDocument.write("Welcome to the dodo: application launcher, think of it as your desktop environment. You can launch applications from left-hand side dock. You should setup VPN clients on your devices, so you can install applications from Application Manager and access your private network. Instructions on how to do that can be viewed by clicking <b>Help</b> button after hovering over <b>Headscale</b> icon in the dock.");
+  document.getElementById('appFrame-default').style.backgroundColor = '#d6d6d6';
+  initDock();
+  setTimeout(reloadDock, 5000);
+});
+
+function copyToClipboard(elem, text) {
+  navigator.clipboard.writeText(text);
+  elem.setAttribute("data-tooltip", "Copied");
+  elem.setAttribute("data-placement", "bottom");
+  setTimeout(() => {
+    elem.removeAttribute("data-tooltip");
+    elem.removeAttribute("data-placement");
+  }, 500);
+};
+
+function reloadDock() {
+  fetch("/")
+	.then((resp) => {
+	  if (resp.ok) {
+		return resp.text();
+	  } else {
+		return undefined;
+	  }
+	})
+	.then((resp) => {
+	  if (!resp) {
+		return;
+	  }
+	  const tmp = document.createElement("div");
+	  tmp.innerHTML = resp;
+	  const apps = document.querySelector(".app-list");
+	  let existing = [...document.querySelectorAll(".app-container")];
+	  let current = [...tmp.querySelectorAll(".app-container")];
+	  const getId = (e) => e.getAttribute("id");
+	  const existingIds = existing.map(getId);
+	  const currentIds = current.map(getId);
+	  existing.forEach((e) => {
+		const id = getId(e);
+		if (!currentIds.includes(id)) {
+		  e.classList.add("fadeout");
+		  setTimeout(() => apps.removeChild(e), 1900);
+		}
+	  });
+	  let prevId = undefined;
+	  current.forEach((c) => {
+		const id = getId(c);
+		if (existingIds.includes(id)) {
+		  prevId = id;
+		  return;
+		}
+		c.classList.add("pulsate");
+		if (prevId) {
+		  apps.insertBefore(c, document.getElementById(prevId).nextSibling);
+		} else {
+		  apps.insertBefore(c, apps.firstChild);
+		}
+		prevId = id;
+	  });
+	  initDock();
+	})
+	.finally(() => setTimeout(reloadDock, 5000));
+}
+
+function initDock() {
+  const icons = document.querySelectorAll(".app-icon");
+  const circle = document.querySelector(".user-circle");
+  const tooltipUser = document.querySelector("#tooltip-user");
+  const initial = document.getElementById('user-initial');
+
+  circle.addEventListener('mouseenter', () => {
+    icons.forEach(icon => {
+      const tooltip = icon.nextElementSibling;
+      hideTooltip(tooltip);
+    });
+    showTooltip(tooltipUser);
+    initial.style.color = "#7f9f7f";
+  });
+
+  circle.addEventListener('mouseleave', () => {
+    hideTooltip(tooltipUser);
+    initial.style.color = "#d4888d";
+  });
+
+  let hideTimeout;
+  let activeTooltip;
+
+  icons.forEach(function (icon) {
+	if (activeAppId && icon.getAttribute("data-app-id") === activeAppId) {
+	  icon.style.color = "var(--button)";
+	}
+    icon.addEventListener("click", function (event) {
+      event.stopPropagation();
+      const appUrl = this.getAttribute("data-app-url");
+      const appId = this.getAttribute("data-app-id");
+      const modalId = this.getAttribute("data-modal-id");
+
+      if (!appUrl && modalId) {
+        openModal(document.getElementById(modalId));
+      } else {
+        if (!iframes[appId]) {
+		  createIframe(appId, appUrl);
+		}
+		activeAppId = appId;
+        showIframe(appId);
+        document.querySelectorAll(".app-icon").forEach((icon) => {
+          icon.style.color = "var(--bodyBg)";
+        });
+        this.style.color = "var(--button)";
+      };
+    });
+
+    const tooltip = icon.nextElementSibling;
+    [
+      ['mouseenter', () => {
+        clearTimeout(hideTimeout);
+        if (activeTooltip && activeTooltip !== tooltip) {
+          hideTooltip(activeTooltip);
+        };
+        const rect = icon.getBoundingClientRect();
+        tooltip.style.top = `${rect.top + 26}px`;
+        showTooltip(tooltip);
+        activeTooltip = tooltip;
+      }],
+      ['mouseleave', () => {
+        hideTimeout = setTimeout(() => {
+          hideTooltip(tooltip);
+          if (activeTooltip === tooltip) {
+            activeTooltip = null;
+          };
+        }, 200);
+      }],
+    ].forEach(([event, listener]) => {
+      icon.addEventListener(event, listener);
+    });
+
+    tooltip.addEventListener('mouseenter', () => {
+      clearTimeout(hideTimeout);
+    });
+
+    tooltip.addEventListener('mouseleave', () => {
+      hideTimeout = setTimeout(() => {
+        hideTooltip(tooltip);
+        if (activeTooltip === tooltip) {
+          activeTooltip = null;
+        };
+      }, 200);
+    });
+  });
+
+  let visibleModal = undefined;
+  const openModal = function (modal) {
+    modal.removeAttribute("close");
+    modal.setAttribute("open", true);
+    visibleModal = modal;
+  };
+
+  const closeModal = function (modal) {
+    modal.removeAttribute("open");
+    modal.setAttribute("close", true);
+    visibleModal = undefined;
+  };
+
+  const helpButtons = document.querySelectorAll('.help-button');
+
+  helpButtons.forEach(function (button) {
+    button.addEventListener('click', function (event) {
+      event.stopPropagation();
+      const buttonId = button.getAttribute('id');
+      const modalId = 'modal-' + buttonId.substring("help-button-".length);
+      const closeHelpId = "close-help-" + buttonId.substring("help-button-".length);
+      const modal = document.getElementById(modalId);
+      openModal(modal);
+      const closeHelpButton = document.getElementById(closeHelpId);
+      closeHelpButton.addEventListener('click', function (event) {
+        event.stopPropagation();
+        closeModal(modal);
+      });
+    });
+  });
+
+  const modalHelpButtons = document.querySelectorAll('.title-menu');
+
+  modalHelpButtons.forEach(function (button) {
+    button.addEventListener('click', function (event) {
+      event.stopPropagation();
+      const helpTitle = button.getAttribute('id');
+      const helpTitleId = helpTitle.substring('title-'.length);
+      const helpContentId = 'help-content-' + helpTitleId;
+      let clDiv = document.getElementById(helpContentId).parentNode;
+      const allContentElements = clDiv.querySelectorAll('.help-content');
+
+      allContentElements.forEach(function (contentElement) {
+        contentElement.style.display = "none";
+      });
+
+      let currentHelpTitle = button;
+      while (currentHelpTitle && !currentHelpTitle.classList.contains('modal-left')) {
+        currentHelpTitle = currentHelpTitle.parentNode;
+        if (currentHelpTitle === document.body) {
+          currentHelpTitle = null;
+          break;
+        }
+      }
+
+      currentHelpTitle.querySelectorAll('.title-menu').forEach(function (button) {
+        button.removeAttribute("aria-current");
+      });
+
+      document.getElementById(helpContentId).style.display = 'block';
+      button.setAttribute("aria-current", "page");
+    });
+  });
+
+  document.addEventListener("keydown", (event) => {
+    if (event.key === "Escape" && visibleModal) {
+      closeModal(visibleModal);
+    }
+  });
+
+  document.addEventListener("click", (event) => {
+    if (visibleModal === null || visibleModal === undefined) return;
+    const modalContent = visibleModal.querySelector("article");
+    const closeButton = visibleModal.querySelector(".close-button");
+    if (!modalContent.contains(event.target) || closeButton.contains(event.target)) {
+      closeModal(visibleModal);
+    }
+  });
+}
+
+let activeAppId = undefined;
+const iframes = {};
+const rightPanel = document.getElementById('right-panel');
+
+function showIframe(appId) {
+  document.querySelectorAll('.appFrame').forEach(iframe => {
+    iframe.style.display = iframe.id === `appFrame-${appId}` ? 'block' : 'none';
+  });
+};
+
+function createIframe(appId, appUrl) {
+  const iframe = document.createElement('iframe');
+  iframe.id = `appFrame-${appId}`;
+  iframe.className = 'appFrame';
+  iframe.src = appUrl;
+  iframe.style.display = 'none';
+  rightPanel.appendChild(iframe);
+  iframes[appId] = iframe;
+};
diff --git a/core/installer/welcome/stat/main.css b/core/installer/welcome/stat/main.css
new file mode 100644
index 0000000..7c9f8f5
--- /dev/null
+++ b/core/installer/welcome/stat/main.css
@@ -0,0 +1,127 @@
+[data-theme="light"],
+:root:not([data-theme="dark"]) {
+	--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"] {
+	--pico-form-element-border-color: #3a3a3a;
+}
+
+main.container {
+	max-width: 850px;
+	margin-top: 13rem;
+}
+
+.dodo {
+	font-size: 1.6rem;
+	font-weight: bold;
+	background-color: #3a3a3a;
+	border-left: 1px dashed #3a3a3a;
+}
+
+.highlight{
+    color: #d4888d;
+}
+
+#menu {
+	border-bottom: 1px dashed #3a3a3a;
+	padding-left: 5rem;
+	padding-right: 5rem;
+}
+
+#menu a {
+	/* font-size: 1.2rem; */
+	color: #3a3a3a;
+}
+
+#menu li {
+	padding-top: 0;
+	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;
+	border-style: solid;
+}
+
+#contents {
+	border-width: 1px;
+	border-style: none solid solid solid;
+}
+
+main > div.grid {
+	--pico-grid-column-gap: 0;
+}
+
+div.contents-header > div {
+	padding: 1rem 3rem;
+}
+
+#contents > div {
+	padding: 3rem;
+}
+
+#create-instance-form {
+	--pico-spacing: 10px;
+	/* TODO(gio): figure out why overriding --pico-background-color does not work */
+	background-color: #3a3a3a;
+}
+
+#create-instance-form label {
+	color: #ffffff;
+}
+
+#create-instance-form input, textarea, button, select {
+	border-width: 0.5px !important;
+	background-color: #3a3a3a;
+}
+
+#create-instance-form p {
+	color: #ffffff;
+}
+
+#create-instance-form input {
+	height: 3rem;
+	color: #ffffff;
+}
+
+#create-instance-form textarea {
+	color: #ffffff;
+}
+
+#create-instance-form button {
+	width: fit-content;
+	font-weight: bold;
+	color: #3a3a3a;
+}
+
+.progress {
+	padding-left: 0;
+}
+
+.progress ul {
+	padding-left: 15px;
+}
+
+.progress li {
+	list-style-type: none;
+}
diff --git a/core/auth/memberships/static/pico.2.0.6.min.css b/core/installer/welcome/stat/pico.2.0.6.min.css
similarity index 100%
copy from core/auth/memberships/static/pico.2.0.6.min.css
copy to core/installer/welcome/stat/pico.2.0.6.min.css
diff --git a/core/installer/welcome/stat/welcome.css b/core/installer/welcome/stat/welcome.css
new file mode 100644
index 0000000..6098d88
--- /dev/null
+++ b/core/installer/welcome/stat/welcome.css
@@ -0,0 +1,92 @@
+[data-theme="light"],
+:root:not([data-theme="dark"]) {
+  --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-form-element-focus-color: #7f9f7f;
+  --pico-form-element-background-color: #3a3a3a;
+  --pico-form-element-active-background-color: #3a3a3a;
+  --pico-form-element-selected-background-color: #3a3a3a;
+  --pico-primary: #7f9f7f;
+  --pico-primary-background: #7f9f7f;
+  --pico-primary-hover: #d4888d;
+  --pico-primary-hover-background: #d4888d;
+  --pico-grid-spacing-horizontal: 0;
+}
+
+body {
+  width: 100%;
+  height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.container {
+  max-width: 500px !important;
+  width: 100%;
+}
+
+form {
+  padding: 10px;
+  background-color: var(--pico-form-element-background-color);
+  width: 100%;
+}
+
+input {
+  background: var(--pico-form-element-background-color);
+  color: white;
+  padding: 10px;
+  text-align: left;
+  font-family: var(--pico-font-family) !important;
+  margin-bottom: 0 !important;
+}
+
+input:-webkit-autofill,
+input:-webkit-autofill:hover,
+input:-webkit-autofill:focus,
+input:-webkit-autofill:active {
+  -webkit-text-fill-color: white !important;
+  transition: background-color 5000s ease-in-out 0s;
+  background-color: var(--pico-form-element-background-color) !important;
+}
+
+&:-webkit-autofill::first-line {
+  font-family: var(--pico-font-family) !important;
+}
+
+.logo span:first-child {
+  color: white;
+  font-size: 24px;
+  padding-left: 10px;
+}
+
+.logo span:nth-child(2) {
+  color: var(--pico-primary-hover);
+  font-size: 24px;
+}
+
+.logo {
+  padding-top: var(--pico-spacing);
+  background-color: var(--pico-form-element-background-color);
+}
+
+label {
+  color: white;
+  margin-top: 14px;
+}
+
+label:first-of-type {
+  margin-top: 0;
+}
+
+label:last-of-type {
+  margin-bottom: 14px;
+}
+
+.error-message {
+  color: var(--pico-primary-hover);
+}
diff --git a/core/installer/welcome/static/dodo_app.css b/core/installer/welcome/static/dodo_app.css
index b66da00..5a66768 100644
--- a/core/installer/welcome/static/dodo_app.css
+++ b/core/installer/welcome/static/dodo_app.css
@@ -101,3 +101,4 @@
 .app-info-link {
   width: fit-content;
 }
+
diff --git a/core/installer/welcome/static/hi.txt b/core/installer/welcome/static/hi.txt
new file mode 100644
index 0000000..45b983b
--- /dev/null
+++ b/core/installer/welcome/static/hi.txt
@@ -0,0 +1 @@
+hi
diff --git a/core/installer/welcome/static/launcher.js b/core/installer/welcome/static/launcher.js
index be5f216..ab34e4c 100644
--- a/core/installer/welcome/static/launcher.js
+++ b/core/installer/welcome/static/launcher.js
@@ -25,40 +25,51 @@
 };
 
 function reloadDock() {
-  fetch("/").then(resp => resp.text()).then(resp => {
-	const tmp = document.createElement("div");
-	tmp.innerHTML = resp;
-	const apps = document.querySelector(".app-list");
-	let existing = [...document.querySelectorAll(".app-container")];
-	let current = [...tmp.querySelectorAll(".app-container")];
-	const getId = (e) => e.getAttribute("id");
-	const existingIds = existing.map(getId);
-	const currentIds = current.map(getId);
-	existing.forEach((e) => {
-	  const id = getId(e);
-	  if (!currentIds.includes(id)) {
-		e.classList.add("fadeout");
-		setTimeout(() => apps.removeChild(e), 1900);
+  fetch("/")
+	.then((resp) => {
+	  if (resp.ok) {
+		return resp.text();
+	  } else {
+		return undefined;
 	  }
-	});
-	let prevId = undefined;
-	current.forEach((c) => {
-	  const id = getId(c);
-	  if (existingIds.includes(id)) {
-		prevId = id;
+	})
+	.then((resp) => {
+	  if (!resp) {
 		return;
 	  }
-	  c.classList.add("pulsate");
-	  if (prevId) {
-		apps.insertBefore(c, document.getElementById(prevId).nextSibling);
-	  } else {
-		apps.insertBefore(c, apps.firstChild);
-	  }
-	  prevId = id;
-	});
-	initDock();
-	setTimeout(reloadDock, 5000);
-  });
+	  const tmp = document.createElement("div");
+	  tmp.innerHTML = resp;
+	  const apps = document.querySelector(".app-list");
+	  let existing = [...document.querySelectorAll(".app-container")];
+	  let current = [...tmp.querySelectorAll(".app-container")];
+	  const getId = (e) => e.getAttribute("id");
+	  const existingIds = existing.map(getId);
+	  const currentIds = current.map(getId);
+	  existing.forEach((e) => {
+		const id = getId(e);
+		if (!currentIds.includes(id)) {
+		  e.classList.add("fadeout");
+		  setTimeout(() => apps.removeChild(e), 1900);
+		}
+	  });
+	  let prevId = undefined;
+	  current.forEach((c) => {
+		const id = getId(c);
+		if (existingIds.includes(id)) {
+		  prevId = id;
+		  return;
+		}
+		c.classList.add("pulsate");
+		if (prevId) {
+		  apps.insertBefore(c, document.getElementById(prevId).nextSibling);
+		} else {
+		  apps.insertBefore(c, apps.firstChild);
+		}
+		prevId = id;
+	  });
+	  initDock();
+	})
+	.finally(() => setTimeout(reloadDock, 5000));
 }
 
 function initDock() {
diff --git a/core/installer/welcome/welcome.go b/core/installer/welcome/welcome.go
index 85d7f61..234a53b 100644
--- a/core/installer/welcome/welcome.go
+++ b/core/installer/welcome/welcome.go
@@ -27,6 +27,9 @@
 //go:embed static/*
 var staticAssets embed.FS
 
+//go:embed stat/*
+var statAssets embed.FS
+
 type Server struct {
 	port              int
 	repo              soft.RepoIO
@@ -59,7 +62,7 @@
 
 func (s *Server) Start() {
 	r := mux.NewRouter()
-	r.PathPrefix("/static/").Handler(cachingHandler{http.FileServer(http.FS(staticAssets))})
+	r.PathPrefix("/stat/").Handler(cachingHandler{http.FileServer(http.FS(statAssets))})
 	r.Path("/").Methods("POST").HandlerFunc(s.createAccount)
 	r.Path("/").Methods("GET").HandlerFunc(s.createAccountForm)
 	http.Handle("/", r)