diff --git a/admin/actions.go b/admin/actions.go new file mode 100644 index 0000000..00408ec --- /dev/null +++ b/admin/actions.go @@ -0,0 +1,58 @@ +package admin + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + + "github.com/ProxySQL/dbdeployer/defaults" +) + +// ExecuteSandboxScript runs a lifecycle script (start, stop, restart) in a sandbox directory. +// It tries + + + +
+

dbdeployer admin

+ {{.Count}} sandbox{{if ne .Count 1}}es{{end}} +
+
+
+ {{template "sandbox-list.html" .}} +
+
+ + diff --git a/admin/templates/login.html b/admin/templates/login.html new file mode 100644 index 0000000..f1898e8 --- /dev/null +++ b/admin/templates/login.html @@ -0,0 +1,87 @@ + + + + + + dbdeployer admin — login + + + +
+

dbdeployer admin

+

Enter your one-time access token

+
+ + + +
+

The token was printed to the terminal when you started dbdeployer admin ui.

+
+ + diff --git a/cmd/admin.go b/cmd/admin.go index 03a2569..131829a 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -17,6 +17,7 @@ package cmd import ( "fmt" + "github.com/ProxySQL/dbdeployer/admin" "github.com/ProxySQL/dbdeployer/defaults" "os" "path" @@ -518,6 +519,23 @@ func SandboxNames(n int) cobra.PositionalArgs { } } +var adminUiCmd = &cobra.Command{ + Use: "ui", + Short: "Start the admin web UI", + Long: `Starts a local web server with a dashboard for managing deployed sandboxes. +Opens a browser with a one-time authentication token.`, + Run: func(cmd *cobra.Command, args []string) { + port, _ := cmd.Flags().GetInt("port") + server, err := admin.NewServer(port) + if err != nil { + common.Exitf(1, "Failed to start admin server: %s", err) + } + if err := server.Start(); err != nil { + common.Exitf(1, "Server error: %s", err) + } + }, +} + func init() { rootCmd.AddCommand(adminCmd) adminCmd.AddCommand(adminLockCmd) @@ -526,6 +544,7 @@ func init() { adminCmd.AddCommand(adminCapabilitiesCmd) adminCmd.AddCommand(adminSetDefaultCmd) adminCmd.AddCommand(adminRemoveDefaultCmd) + adminCmd.AddCommand(adminUiCmd) adminUpgradeCmd.Flags().BoolP(globals.VerboseLabel, "", false, "Shows upgrade operations") adminUpgradeCmd.Flags().BoolP(globals.DryRunLabel, "", false, "Shows upgrade operations, but don't execute them") @@ -533,4 +552,6 @@ func init() { defaults.Defaults().DefaultSandboxExecutable, "Name of the executable to run commands in the default sandbox") adminRemoveDefaultCmd.PersistentFlags().StringP(globals.DefaultSandboxExecutable, "", defaults.Defaults().DefaultSandboxExecutable, "Name of the executable to run commands in the default sandbox") + + adminUiCmd.Flags().Int("port", 9090, "Port for the admin web UI") } diff --git a/docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md b/docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md new file mode 100644 index 0000000..3fa195f --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md @@ -0,0 +1,132 @@ +# Admin Web UI POC Design + +**Date:** 2026-03-24 +**Author:** Rene (ProxySQL) +**Status:** POC + +## Goal + +Prove that dbdeployer can be a platform, not just a CLI. A `dbdeployer admin` command launches a localhost web dashboard showing all deployed sandboxes with start/stop/destroy controls. + +## Scope (POC only) + +- Dashboard showing all sandboxes as cards grouped by topology +- Start/stop/destroy actions via the UI +- OTP authentication (CLI generates token, browser validates) +- Localhost only (127.0.0.1) +- Go templates + HTMX, embedded in binary + +## NOT in scope (future) + +- Deploy new sandboxes via UI +- Real-time log streaming +- Topology graph visualization +- Multi-user / remote access +- Persistent sessions + +## Architecture + +``` +dbdeployer admin + └─ starts HTTP server on 127.0.0.1: + └─ generates OTP, prints to terminal + └─ opens browser to http://127.0.0.1:/login?token= + └─ serves embedded HTML templates via Go's html/template + └─ HTMX handles dynamic actions (no page reload for start/stop/destroy) + └─ API endpoints read sandbox catalog + execute lifecycle commands +``` + +### Authentication Flow + +1. `dbdeployer admin` generates a random OTP (32-char hex) +2. Prints: `Admin UI: http://127.0.0.1:9090/login?token=` +3. Browser hits `/login?token=` → server validates → sets session cookie +4. Session cookie used for all subsequent requests +5. OTP is single-use (invalidated after first login) +6. Session expires when server stops (in-memory) + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/login` | Validate OTP, set session cookie, redirect to dashboard | +| GET | `/` | Dashboard (HTML) | +| GET | `/api/sandboxes` | JSON list of all sandboxes | +| POST | `/api/sandboxes/:name/start` | Start a sandbox | +| POST | `/api/sandboxes/:name/stop` | Stop a sandbox | +| POST | `/api/sandboxes/:name/destroy` | Destroy a sandbox (requires confirmation) | + +### Dashboard Layout + +**Header:** "dbdeployer admin" + sandbox count + server uptime + +**Sandbox cards grouped by topology:** + +``` +┌─ Replication: rsandbox_8_4_4 ────────────────────────┐ +│ │ +│ ┌─ master ─────────┐ ┌─ node1 ──────────┐ │ +│ │ Port: 8404 │ │ Port: 8405 │ │ +│ │ ● Running │ │ ● Running │ │ +│ │ [Stop] │ │ [Stop] │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ │ +│ ┌─ node2 ──────────┐ ┌─ proxysql ───────┐ │ +│ │ Port: 8406 │ │ Port: 6032/6033 │ │ +│ │ ● Running │ │ ● Running │ │ +│ │ [Stop] │ │ [Stop] │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ │ +│ [Stop All] [Destroy] ──────────────────────────────│ +└────────────────────────────────────────────────────────┘ + +┌─ Single: msb_8_4_4 ──────────────────────────────────┐ +│ Port: 8404 │ ● Running │ [Stop] [Destroy] │ +└────────────────────────────────────────────────────────┘ +``` + +### Sandbox Data Source + +Read from `~/.dbdeployer/sandboxes.json` (the existing sandbox catalog). Each entry has: +- Sandbox name and directory +- Type (single, multiple, replication, group, etc.) +- Ports +- Nodes (for multi-node topologies) + +Status is determined by checking if the sandbox's PID file exists / process is running. + +### Technology + +- **Server:** Go `net/http` (stdlib, no framework) +- **Templates:** Go `html/template` with `//go:embed` +- **Interactivity:** HTMX (loaded from CDN or embedded) +- **Styling:** Inline CSS in the template (single file, dark theme matching the website) +- **Session:** In-memory map, cookie-based + +## File Structure + +``` +cmd/admin.go # Cobra command: dbdeployer admin +admin/ + server.go # HTTP server, routes, middleware + auth.go # OTP generation, session management + handlers.go # API handlers (list, start, stop, destroy) + sandbox_status.go # Read catalog, check process status + templates/ + layout.html # Base layout (head, nav, footer) + dashboard.html # Dashboard with sandbox cards + login.html # Login page (auto-submits with OTP) + components/ + sandbox-card.html # Single sandbox card partial + topology-group.html # Topology group wrapper partial + static/ + htmx.min.js # HTMX library (embedded) + style.css # Dashboard styles +``` + +All templates and static files embedded via `//go:embed admin/templates/* admin/static/*`. + +## Port Selection + +Default: 9090. If busy, find next free port. Print the URL to terminal. +Flag: `--port` to override.