From 87752492d0dc7df3cf78011d5ce315a3eb0cad51 Mon Sep 17 00:00:00 2001 From: bndw Date: Fri, 23 Jan 2026 21:52:50 -0800 Subject: Restructure CLI with Cobra Replace custom switch-based routing with Cobra for cleaner command hierarchy. Reorganize commands into logical groups: - Root command handles deployment (--binary, --static, --domain, etc.) - App management at top level: list, logs, status, restart, remove - env subcommand group: list, set, unset - host subcommand group: init, status, update, ssh - Standalone: ui (renamed from webui), version Add version command with ldflags support for build info. --- cmd/deploy/ui.go | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 cmd/deploy/ui.go (limited to 'cmd/deploy/ui.go') diff --git a/cmd/deploy/ui.go b/cmd/deploy/ui.go new file mode 100644 index 0000000..2ca88e0 --- /dev/null +++ b/cmd/deploy/ui.go @@ -0,0 +1,199 @@ +package main + +import ( + "embed" + "encoding/json" + "fmt" + "html/template" + "net/http" + "sort" + "strconv" + + "github.com/bdw/deploy/internal/state" + "github.com/bdw/deploy/internal/templates" + "github.com/spf13/cobra" +) + +//go:embed templates/*.html +var templatesFS embed.FS + +var uiCmd = &cobra.Command{ + Use: "ui", + Short: "Launch web management UI", + RunE: runUI, +} + +func init() { + uiCmd.Flags().StringP("port", "p", "8080", "Port to run the web UI on") +} + +func runUI(cmd *cobra.Command, args []string) error { + port, _ := cmd.Flags().GetString("port") + + tmpl, err := template.ParseFS(templatesFS, "templates/webui.html") + if err != nil { + return fmt.Errorf("error parsing template: %w", err) + } + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + st, err := state.Load() + if err != nil { + http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) + return + } + + type AppData struct { + Name string + Type string + Domain string + Port int + Env map[string]string + Host string + } + + type HostData struct { + Host string + Apps []AppData + } + + var hosts []HostData + for hostName, host := range st.Hosts { + var apps []AppData + for appName, app := range host.Apps { + apps = append(apps, AppData{ + Name: appName, + Type: app.Type, + Domain: app.Domain, + Port: app.Port, + Env: app.Env, + Host: hostName, + }) + } + + sort.Slice(apps, func(i, j int) bool { + return apps[i].Name < apps[j].Name + }) + + hosts = append(hosts, HostData{ + Host: hostName, + Apps: apps, + }) + } + + sort.Slice(hosts, func(i, j int) bool { + return hosts[i].Host < hosts[j].Host + }) + + data := struct { + Hosts []HostData + }{ + Hosts: hosts, + } + + if err := tmpl.Execute(w, data); err != nil { + http.Error(w, fmt.Sprintf("Error rendering template: %v", err), http.StatusInternalServerError) + return + } + }) + + http.HandleFunc("/api/state", func(w http.ResponseWriter, r *http.Request) { + st, err := state.Load() + if err != nil { + http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(st) + }) + + http.HandleFunc("/api/configs", func(w http.ResponseWriter, r *http.Request) { + host := r.URL.Query().Get("host") + appName := r.URL.Query().Get("app") + + if host == "" || appName == "" { + http.Error(w, "Missing host or app parameter", http.StatusBadRequest) + return + } + + st, err := state.Load() + if err != nil { + http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) + return + } + + app, err := st.GetApp(host, appName) + if err != nil { + http.Error(w, fmt.Sprintf("App not found: %v", err), http.StatusNotFound) + return + } + + configs := make(map[string]string) + + if app.Env != nil && len(app.Env) > 0 { + envContent := "" + for k, v := range app.Env { + envContent += fmt.Sprintf("%s=%s\n", k, v) + } + configs["env"] = envContent + configs["envPath"] = fmt.Sprintf("/etc/deploy/env/%s.env", appName) + } + + if app.Type == "app" { + workDir := fmt.Sprintf("/var/lib/%s", appName) + binaryPath := fmt.Sprintf("/usr/local/bin/%s", appName) + envFilePath := fmt.Sprintf("/etc/deploy/env/%s.env", appName) + + serviceContent, err := templates.SystemdService(map[string]string{ + "Name": appName, + "User": appName, + "WorkDir": workDir, + "BinaryPath": binaryPath, + "Port": strconv.Itoa(app.Port), + "EnvFile": envFilePath, + "Args": app.Args, + }) + if err != nil { + http.Error(w, fmt.Sprintf("Error rendering systemd service: %v", err), http.StatusInternalServerError) + return + } + configs["systemd"] = serviceContent + configs["systemdPath"] = fmt.Sprintf("/etc/systemd/system/%s.service", appName) + + caddyContent, err := templates.AppCaddy(map[string]string{ + "Domain": app.Domain, + "Port": strconv.Itoa(app.Port), + }) + if err != nil { + http.Error(w, fmt.Sprintf("Error rendering Caddy config: %v", err), http.StatusInternalServerError) + return + } + configs["caddy"] = caddyContent + configs["caddyPath"] = fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", appName) + } else if app.Type == "static" { + remoteDir := fmt.Sprintf("/var/www/%s", appName) + caddyContent, err := templates.StaticCaddy(map[string]string{ + "Domain": app.Domain, + "RootDir": remoteDir, + }) + if err != nil { + http.Error(w, fmt.Sprintf("Error rendering Caddy config: %v", err), http.StatusInternalServerError) + return + } + configs["caddy"] = caddyContent + configs["caddyPath"] = fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", appName) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(configs) + }) + + addr := fmt.Sprintf("localhost:%s", port) + fmt.Printf("Starting web UI on http://%s\n", addr) + fmt.Printf("Press Ctrl+C to stop\n") + + if err := http.ListenAndServe(addr, nil); err != nil { + return fmt.Errorf("error starting server: %w", err) + } + return nil +} -- cgit v1.2.3