package main import ( "embed" "encoding/json" "flag" "fmt" "html/template" "net/http" "os" "sort" "strconv" "github.com/bdw/deploy/internal/state" "github.com/bdw/deploy/internal/templates" ) //go:embed templates/*.html var templatesFS embed.FS func runWebUI(args []string) { fs := flag.NewFlagSet("webui", flag.ExitOnError) port := fs.String("port", "8080", "Port to run the web UI on") help := fs.Bool("h", false, "Show help") fs.Parse(args) if *help { fmt.Fprintf(os.Stderr, `Usage: deploy webui [flags] Launch a web interface to view and manage deployments. FLAGS: -port string Port to run the web UI on (default "8080") -h Show this help message EXAMPLE: # Launch web UI on default port (8080) deploy webui # Launch on custom port deploy webui -port 3000 `) os.Exit(0) } // Parse template tmpl, err := template.ParseFS(templatesFS, "templates/webui.html") if err != nil { fmt.Fprintf(os.Stderr, "Error parsing template: %v\n", err) os.Exit(1) } // Handler for the main page http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Reload state on each request to show latest changes st, err := state.Load() if err != nil { http.Error(w, fmt.Sprintf("Error loading state: %v", err), http.StatusInternalServerError) return } // Prepare data for template 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 apps by name sort.Slice(apps, func(i, j int) bool { return apps[i].Name < apps[j].Name }) hosts = append(hosts, HostData{ Host: hostName, Apps: apps, }) } // Sort hosts by name 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 } }) // API endpoint to get state as JSON 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) }) // API endpoint to get rendered configs for an app 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) // Render environment file 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) } // Render configs based on app type if app.Type == "app" { // Render systemd service 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) // Render Caddy config 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" { // Render Caddy config for static site 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 { fmt.Fprintf(os.Stderr, "Error starting server: %v\n", err) os.Exit(1) } }