Dodo-app: fix sync user info bug
          disable form after running app installation

Change-Id: I28dec5f8a9ad1d586bc2d2cc56a6c1c66cf2fdbe
diff --git a/core/installer/soft/client.go b/core/installer/soft/client.go
index a285ffd..dd5bbfe 100644
--- a/core/installer/soft/client.go
+++ b/core/installer/soft/client.go
@@ -188,12 +188,23 @@
 			continue
 		}
 		if gettingKeys {
-			keys = append(keys, strings.TrimSpace(line))
+			if key := CleanKey(line); key != "" {
+				keys = append(keys, key)
+			}
 		}
 	}
 	return keys
 }
 
+func CleanKey(key string) string {
+	k := strings.TrimSpace(key)
+	fields := strings.Fields(k)
+	if len(fields) < 2 {
+		return k
+	}
+	return fields[0] + " " + fields[1]
+}
+
 func (ss *realClient) RunCommand(args ...string) (string, error) {
 	cmd := strings.Join(args, " ")
 	log.Printf("Running command %s", cmd)
diff --git a/core/installer/welcome/dodo-app-tmpl/base.html b/core/installer/welcome/dodo-app-tmpl/base.html
index 9cb4e07..d0e0693 100644
--- a/core/installer/welcome/dodo-app-tmpl/base.html
+++ b/core/installer/welcome/dodo-app-tmpl/base.html
@@ -5,7 +5,7 @@
     <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.4">
+    <link rel="stylesheet" href="/static/dodo_app.css?v=0.0.7">
 </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 9874d12..fa5ab48 100644
--- a/core/installer/welcome/dodo-app-tmpl/index.html
+++ b/core/installer/welcome/dodo-app-tmpl/index.html
@@ -2,7 +2,7 @@
 dodo app: status
 {{ end }}
 {{- define "content" -}}
-<form action="" method="POST">
+<form id="create-app" action="" method="POST">
 	<fieldset class="grid">
 		<select name="network">
 			{{- range .Networks -}}
@@ -15,7 +15,8 @@
 			<option value="{{ . }}">{{ . }}</option>
 			{{- end -}}
 		</select>
-		<button 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">
@@ -24,10 +25,11 @@
 		<ul>
 			{{- range .Apps -}}
 			<li>
-				<a href="/{{ . }}">{{ . }}</a>
+				<a class="app-info-link" href="/{{ . }}">{{ . }}</a>
 			</li>
 			{{- end -}}
 		</ul>
 	</nav>
 </aside>
+<script src="/static/dodo-app.js?v=0.0.7"></script>
 {{- end -}}
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 6ccbb6a..7a84d71 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -1075,6 +1075,9 @@
 	}
 	keyToUser := make(map[string]string)
 	for _, clientUser := range allClientUsers {
+		if clientUser == "admin" || clientUser == "fluxcd" {
+			continue
+		}
 		userData, ok := validUsernames[clientUser]
 		if !ok {
 			if err := s.client.RemoveUser(clientUser); err != nil {
@@ -1088,9 +1091,9 @@
 				return
 			}
 			for _, existingKey := range existingKeys {
-				cleanKey := CleanKey(existingKey)
+				cleanKey := soft.CleanKey(existingKey)
 				keyOk := slices.ContainsFunc(userData.SSHPublicKeys, func(key string) bool {
-					return cleanKey == CleanKey(key)
+					return cleanKey == soft.CleanKey(key)
 				})
 				if !keyOk {
 					if err := s.client.RemovePublicKey(clientUser, existingKey); err != nil {
@@ -1103,6 +1106,10 @@
 		}
 	}
 	for _, u := range users {
+		if err := s.st.CreateUser(u.Username, nil, ""); err != nil && !errors.Is(err, ErrorAlreadyExists) {
+			fmt.Println(err)
+			return
+		}
 		if len(u.SSHPublicKeys) == 0 {
 			continue
 		}
@@ -1118,11 +1125,14 @@
 			}
 		} else {
 			for _, key := range u.SSHPublicKeys {
-				cleanKey := CleanKey(key)
-				if user, ok := keyToUser[cleanKey]; ok && u.Username == user {
-					panic("MUST NOT REACH!")
+				cleanKey := soft.CleanKey(key)
+				if user, ok := keyToUser[cleanKey]; ok {
+					if u.Username != user {
+						panic("MUST NOT REACH! IMPOSSIBLE KEY USER RECORD")
+					}
+					continue
 				}
-				if err := s.client.AddPublicKey(u.Username, key); err != nil {
+				if err := s.client.AddPublicKey(u.Username, cleanKey); err != nil {
 					fmt.Println(err)
 					return
 				}
@@ -1140,16 +1150,8 @@
 		for _, u := range users {
 			if err := s.client.AddReadWriteCollaborator(r, u.Username); err != nil {
 				fmt.Println(err)
-				return
+				continue
 			}
 		}
 	}
 }
-
-func CleanKey(key string) string {
-	fields := strings.Fields(key)
-	if len(fields) < 2 {
-		return key
-	}
-	return fields[0] + " " + fields[1]
-}
diff --git a/core/installer/welcome/static/dodo-app.js b/core/installer/welcome/static/dodo-app.js
new file mode 100644
index 0000000..ccb5252
--- /dev/null
+++ b/core/installer/welcome/static/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/static/dodo_app.css b/core/installer/welcome/static/dodo_app.css
index 320f940..b66da00 100644
--- a/core/installer/welcome/static/dodo_app.css
+++ b/core/installer/welcome/static/dodo_app.css
@@ -73,11 +73,31 @@
 }
 
 body.container {
-	padding-top: 15px;
+  padding-top: 15px;
 }
 
 @media (min-width: 768px) {
-	fieldset.grid {
-		grid-template-columns: 1fr 1fr 1fr 200px;
-	}
-};
+  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;
+}