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 }