DodoApp: Use picocss for UI

Change-Id: I2d610c4f57e4dfbbe566a7c7f82147443e0106f1
diff --git a/core/installer/welcome/dodo_app.go b/core/installer/welcome/dodo_app.go
index 1315266..4535be2 100644
--- a/core/installer/welcome/dodo_app.go
+++ b/core/installer/welcome/dodo_app.go
@@ -25,25 +25,50 @@
 //go:embed dodo-app-tmpl/*
 var dodoAppTmplFS embed.FS
 
+//go:embed all:app-tmpl
+var appTmplsFS embed.FS
+
+//go:embed static
+var staticResources embed.FS
+
 const (
 	ConfigRepoName = "config"
 	namespacesFile = "/namespaces.json"
 	loginPath      = "/login"
 	logoutPath     = "/logout"
+	staticPath     = "/static"
 	sessionCookie  = "dodo-app-session"
 	userCtx        = "user"
 )
 
+var types = []string{"golang:1.22.0", "golang:1.20.0", "hugo:latest"}
+
 type dodoAppTmplts struct {
-	index *template.Template
+	index     *template.Template
+	appStatus *template.Template
 }
 
 func parseTemplatesDodoApp(fs embed.FS) (dodoAppTmplts, error) {
-	index, err := template.New("index.html").ParseFS(fs, "dodo-app-tmpl/index.html")
+	base, err := template.ParseFS(fs, "dodo-app-tmpl/base.html")
 	if err != nil {
 		return dodoAppTmplts{}, err
 	}
-	return dodoAppTmplts{index}, nil
+	parse := func(path string) (*template.Template, error) {
+		if b, err := base.Clone(); err != nil {
+			return nil, err
+		} else {
+			return b.ParseFS(fs, path)
+		}
+	}
+	index, err := parse("dodo-app-tmpl/index.html")
+	if err != nil {
+		return dodoAppTmplts{}, err
+	}
+	appStatus, err := parse("dodo-app-tmpl/app_status.html")
+	if err != nil {
+		return dodoAppTmplts{}, err
+	}
+	return dodoAppTmplts{index, appStatus}, nil
 }
 
 type DodoAppServer struct {
@@ -67,6 +92,7 @@
 	appNs             map[string]string
 	sc                *securecookie.SecureCookie
 	tmplts            dodoAppTmplts
+	appTmpls          AppTmplStore
 }
 
 // TODO(gio): Initialize appNs on startup
@@ -95,6 +121,14 @@
 		securecookie.GenerateRandomKey(64),
 		securecookie.GenerateRandomKey(32),
 	)
+	apps, err := fs.Sub(appTmplsFS, "app-tmpl")
+	if err != nil {
+		return nil, err
+	}
+	appTmpls, err := NewAppTmplStoreFS(apps)
+	if err != nil {
+		return nil, err
+	}
 	s := &DodoAppServer{
 		&sync.Mutex{},
 		st,
@@ -116,6 +150,7 @@
 		map[string]string{},
 		sc,
 		tmplts,
+		appTmpls,
 	}
 	config, err := client.GetRepo(ConfigRepoName)
 	if err != nil {
@@ -138,6 +173,7 @@
 	go func() {
 		r := mux.NewRouter()
 		r.Use(s.mwAuth)
+		r.PathPrefix(staticPath).Handler(http.FileServer(http.FS(staticResources)))
 		r.HandleFunc(logoutPath, s.handleLogout).Methods(http.MethodGet)
 		r.HandleFunc("/{app-name}"+loginPath, s.handleLoginForm).Methods(http.MethodGet)
 		r.HandleFunc("/{app-name}"+loginPath, s.handleLogin).Methods(http.MethodPost)
@@ -193,7 +229,7 @@
 
 func (s *DodoAppServer) mwAuth(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		if strings.HasSuffix(r.URL.Path, loginPath) || strings.HasPrefix(r.URL.Path, logoutPath) {
+		if strings.HasSuffix(r.URL.Path, loginPath) || strings.HasPrefix(r.URL.Path, logoutPath) || strings.HasPrefix(r.URL.Path, staticPath) {
 			next.ServeHTTP(w, r)
 			return
 		}
@@ -289,6 +325,7 @@
 type statusData struct {
 	Apps     []string
 	Networks []installer.Network
+	Types    []string
 }
 
 func (s *DodoAppServer) handleStatus(w http.ResponseWriter, r *http.Request) {
@@ -307,13 +344,19 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	data := statusData{apps, networks}
+	data := statusData{apps, networks, types}
 	if err := s.tmplts.index.Execute(w, data); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
 }
 
+type appStatusData struct {
+	Name            string
+	GitCloneCommand string
+	Commits         []Commit
+}
+
 func (s *DodoAppServer) handleAppStatus(w http.ResponseWriter, r *http.Request) {
 	vars := mux.Vars(r)
 	appName, ok := vars["app-name"]
@@ -321,14 +364,19 @@
 		http.Error(w, "missing app-name", http.StatusBadRequest)
 		return
 	}
-	fmt.Fprintf(w, "git clone %s/%s\n\n\n", s.repoPublicAddr, appName)
 	commits, err := s.st.GetCommitHistory(appName)
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	for _, c := range commits {
-		fmt.Fprintf(w, "%s %s\n", c.Hash, c.Message)
+	data := appStatusData{
+		Name:            appName,
+		GitCloneCommand: fmt.Sprintf("git clone %s/%s\n\n\n", s.repoPublicAddr, appName),
+		Commits:         commits,
+	}
+	if err := s.tmplts.appStatus.Execute(w, data); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
 	}
 }
 
@@ -420,8 +468,18 @@
 		http.Error(w, "missing network", http.StatusBadRequest)
 		return
 	}
+	subdomain := r.FormValue("subdomain")
+	if subdomain == "" {
+		http.Error(w, "missing subdomain", http.StatusBadRequest)
+		return
+	}
+	appType := r.FormValue("type")
+	if appType == "" {
+		http.Error(w, "missing type", http.StatusBadRequest)
+		return
+	}
 	adminPublicKey := r.FormValue("admin-public-key")
-	if network == "" {
+	if adminPublicKey == "" {
 		http.Error(w, "missing admin public key", http.StatusBadRequest)
 		return
 	}
@@ -448,7 +506,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if err := s.CreateApp(user, appName, adminPublicKey, network); err != nil {
+	if err := s.CreateApp(user, appName, appType, adminPublicKey, network, subdomain); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -456,8 +514,10 @@
 }
 
 type apiCreateAppReq struct {
+	AppType        string `json:"type"`
 	AdminPublicKey string `json:"adminPublicKey"`
 	Network        string `json:"network"`
+	Subdomain      string `json:"subdomain"`
 }
 
 type apiCreateAppResp struct {
@@ -505,7 +565,7 @@
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	if err := s.CreateApp(user, appName, req.AdminPublicKey, req.Network); err != nil {
+	if err := s.CreateApp(user, appName, req.AppType, req.AdminPublicKey, req.Network, req.Subdomain); err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
@@ -519,7 +579,7 @@
 	}
 }
 
-func (s *DodoAppServer) CreateApp(user, appName, adminPublicKey, network string) error {
+func (s *DodoAppServer) CreateApp(user, appName, appType, adminPublicKey, network, subdomain string) error {
 	s.l.Lock()
 	defer s.l.Unlock()
 	fmt.Printf("Creating app: %s\n", appName)
@@ -528,6 +588,14 @@
 	} else if ok {
 		return nil
 	}
+	networks, err := s.getNetworks(user)
+	if err != nil {
+		return err
+	}
+	n, ok := installer.NetworkMap(networks)[strings.ToLower(network)]
+	if !ok {
+		return fmt.Errorf("network not found: %s\n", network)
+	}
 	if err := s.client.AddRepository(appName); err != nil {
 		return err
 	}
@@ -535,7 +603,7 @@
 	if err != nil {
 		return err
 	}
-	if err := InitRepo(appRepo, network); err != nil {
+	if err := s.initRepo(appRepo, appType, n, subdomain); err != nil {
 		return err
 	}
 	apps := installer.NewInMemoryAppRepository(installer.CreateAllApps())
@@ -550,10 +618,6 @@
 	}
 	namespace := fmt.Sprintf("%s%s%s", s.env.NamespacePrefix, app.Namespace(), suffix)
 	s.appNs[appName] = namespace
-	networks, err := s.getNetworks(user)
-	if err != nil {
-		return err
-	}
 	if err := s.updateDodoApp(appName, namespace, networks); err != nil {
 		return err
 	}
@@ -688,71 +752,17 @@
 	return nil
 }
 
-const goMod = `module dodo.app
-
-go 1.18
-`
-
-const mainGo = `package main
-
-import (
-	"flag"
-	"fmt"
-	"log"
-	"net/http"
-)
-
-var port = flag.Int("port", 8080, "Port to listen on")
-
-func handler(w http.ResponseWriter, r *http.Request) {
-	fmt.Fprintln(w, "Hello from Dodo App!")
-}
-
-func main() {
-	flag.Parse()
-	http.HandleFunc("/", handler)
-	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
-}
-`
-
-const appCue = `app: {
-	type: "golang:1.22.0"
-	run: "main.go"
-	ingress: {
-		network: "%s"
-		subdomain: "testapp"
-		auth: enabled: false
+func (s *DodoAppServer) initRepo(repo soft.RepoIO, appType string, network installer.Network, subdomain string) error {
+	appType = strings.ReplaceAll(appType, ":", "-")
+	appTmpl, err := s.appTmpls.Find(appType)
+	if err != nil {
+		return err
 	}
-}
-`
-
-func InitRepo(repo soft.RepoIO, network string) error {
 	return repo.Do(func(fs soft.RepoFS) (string, error) {
-		{
-			w, err := fs.Writer("go.mod")
-			if err != nil {
-				return "", err
-			}
-			defer w.Close()
-			fmt.Fprint(w, goMod)
+		if err := appTmpl.Render(network, subdomain, repo); err != nil {
+			return "", err
 		}
-		{
-			w, err := fs.Writer("main.go")
-			if err != nil {
-				return "", err
-			}
-			defer w.Close()
-			fmt.Fprintf(w, "%s", mainGo)
-		}
-		{
-			w, err := fs.Writer("app.cue")
-			if err != nil {
-				return "", err
-			}
-			defer w.Close()
-			fmt.Fprintf(w, appCue, network)
-		}
-		return "go web app template", nil
+		return "init", nil
 	})
 }