Skip to content

feat: admin web UI POC (dbdeployer admin ui)#56

Merged
renecannao merged 6 commits intomasterfrom
feature/admin-ui
Apr 3, 2026
Merged

feat: admin web UI POC (dbdeployer admin ui)#56
renecannao merged 6 commits intomasterfrom
feature/admin-ui

Conversation

@renecannao
Copy link
Copy Markdown

@renecannao renecannao commented Mar 24, 2026

Summary

POC for a localhost web dashboard that turns dbdeployer from a CLI tool into a platform.

  • dbdeployer admin ui starts HTTP server on 127.0.0.1:9090
  • OTP authentication — CLI prints one-time token, browser validates, 1-hour session
  • Dashboard — sandbox cards grouped by topology, status badges (running/stopped), auto-refreshes every 5s
  • Actions — start, stop, destroy via HTMX (no page reload, 2s delay for status to settle)
  • Dark theme — matches the website aesthetic
  • Zero JS build — Go templates + HTMX from CDN (with SRI hash), embedded via //go:embed

Architecture

admin/
  auth.go              # OTP generation, session management (1hr expiry)
  sandbox_status.go    # Read catalog, run status scripts to check state
  actions.go           # Execute start/stop/destroy scripts
  handlers.go          # HTTP handlers (login, dashboard, API, refresh)
  server.go            # HTTP server with timeouts, routes, browser opener
  templates/           # Go HTML templates (embedded)
  static/              # Static assets (embedded)
cmd/admin.go           # Cobra command wiring

Security

  • Localhost-only (127.0.0.1)
  • Single-use OTP for initial auth
  • Session cookie with HttpOnly + SameSiteStrict
  • 1-hour session expiry
  • Path traversal protection on sandbox names
  • HTTP server timeouts (read 15s, write 30s, idle 60s)
  • HTMX loaded with SRI integrity hash

Test Plan

  • Build succeeds
  • Dashboard shows deployed sandboxes with correct running/stopped status
  • Start/stop buttons work and status refreshes
  • Code review issues addressed (auth, security, error handling, timeouts)
  • Manual: destroy button removes sandbox
  • Manual: invalid OTP is rejected

- `dbdeployer admin ui` starts localhost dashboard on port 9090
- OTP authentication: CLI prints token, browser validates
- Dashboard shows sandbox cards grouped by topology
- HTMX-powered start/stop/destroy actions (no page reload)
- Dark theme, Go templates embedded via //go:embed
- Reads existing sandbox catalog, checks PID for status
Copilot AI review requested due to automatic review settings March 24, 2026 18:56
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 24, 2026

Warning

Rate limit exceeded

@renecannao has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 42 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d060efe2-ac3b-458d-ad03-8932669db830

📥 Commits

Reviewing files that changed from the base of the PR and between 57d81b3 and 0d2876d.

📒 Files selected for processing (4)
  • admin/handlers.go
  • admin/sandbox_status.go
  • admin/server.go
  • admin/templates/dashboard.html
📝 Walkthrough

Walkthrough

Adds a localhost admin web UI: an embedded HTTP server with OTP one-time login and in-memory sessions, sandbox discovery and running-state detection, HTMX-driven start/stop/destroy actions, sandbox lifecycle helpers, embedded templates/static assets, and a new dbdeployer admin ui CLI subcommand.

Changes

Cohort / File(s) Summary
Server core & routes
admin/server.go, admin/handlers.go
New admin HTTP server with embedded templates/static, route registration for /login, /, and /api/sandboxes*, handlers for login, dashboard, JSON list, and HTMX-backed sandbox actions (start/stop/destroy).
Authentication
admin/auth.go
New AuthManager: generated one-time OTP, single-use OTP validation, in-memory session creation/validation, session cookie dbdeployer_session, and an AuthMiddleware.
Sandbox lifecycle
admin/actions.go
Functions to resolve sandbox directory, locate and execute lifecycle scripts via bash (with <script>_all fallback), and DestroySandbox which attempts stop, removes directory, and deletes catalog entry with contextual errors.
Sandbox discovery & status
admin/sandbox_status.go
GetAllSandboxes(), SandboxStatus/SandboxNode types: reads catalog, resolves dirs/names, extracts metadata/ports, inspects node status via status script or PID files and sets running flags.
Templates & static
admin/templates/dashboard.html, admin/templates/login.html, admin/templates/components/sandbox-list.html, admin/static/placeholder.txt
New embedded HTML templates for login, dashboard, and sandbox list component (HTMX fragments + styles) and a static placeholder file.
CLI integration
cmd/admin.go
Adds dbdeployer admin ui subcommand with --port flag (default 9090), constructs and starts the admin server.
Documentation
docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md
New design spec describing UX, routes, auth flow, data source, runtime detection, and implementation notes for the admin web UI POC.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Client
    participant Server as AdminServer
    participant Auth as AuthManager
    participant FS as Catalog/FileSystem

    Browser->>Server: GET /login
    Server-->>Browser: Render login.html
    Browser->>Server: GET /login?token=...
    Server->>Auth: ValidateOTP(token)
    Auth-->>Server: true / false
    alt valid
        Server->>Auth: CreateSession()
        Auth-->>Server: sessionID
        Server-->>Browser: Set cookie + Redirect to /
    else invalid
        Server-->>Browser: Render login.html
    end
Loading
sequenceDiagram
    participant Browser as Client
    participant Server as AdminServer
    participant FS as Catalog/FileSystem
    participant Shell as ShellScript

    Browser->>Server: POST /api/sandboxes/:name/start
    Server->>Server: AuthMiddleware validates cookie
    Server->>FS: Resolve sandbox directory (catalog)
    Server->>Shell: ExecuteSandboxScript(sandbox, "start")
    Shell-->>Server: stdout/stderr + exit status
    Server->>FS: GetAllSandboxes() (refresh)
    Server-->>Browser: Render sandbox-list.html (HTMX swap)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I nibbled code and found a gate,

A single-use carrot—one-time fate,
I press start, I hush, I sweep away,
Sandboxes dance at end of day,
Hooray, the admin burrow's great!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: admin web UI POC (dbdeployer admin ui)' accurately describes the main change: introduction of a proof-of-concept admin web UI for dbdeployer accessed via the 'dbdeployer admin ui' command.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/admin-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces an experimental web-based administration interface for dbdeployer, transforming it from a purely command-line tool into a platform with a graphical dashboard. The UI allows users to view their deployed sandboxes, monitor their status, and perform lifecycle operations like starting, stopping, and destroying them through an intuitive web browser interface. This proof-of-concept aims to demonstrate the feasibility of a richer, interactive management experience for dbdeployer users.

Highlights

  • New Web-Based Dashboard: Introduced a new web-based dashboard for dbdeployer to manage sandboxes, transforming it from a purely command-line tool into a platform with a graphical interface.
  • Secure OTP Authentication: Implemented a secure One-Time Password (OTP) authentication mechanism for accessing the UI, ensuring that access is granted only with a token printed to the CLI.
  • Sandbox Management Actions: Provided core sandbox management actions (start, stop, destroy) directly from the web interface, utilizing HTMX for dynamic updates without full page reloads.
  • Zero Dependencies & Dark Theme: Designed the UI with a dark theme matching the project's aesthetic and ensured zero external dependencies by embedding Go templates and HTMX directly into the binary.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new dbdeployer admin ui command, which launches a local web server providing an OTP-authenticated dashboard for managing sandboxes. The UI allows users to view, start, stop, and destroy sandboxes through HTMX-powered interactions. Feedback from the review highlights several areas for improvement, including the need to implement session expiration and robust error handling for cryptographic operations (rand.Read), sandbox lifecycle scripts, and sandbox data retrieval. Additionally, the openBrowser function should handle potential errors and extend support to Windows, and API error messages should be more specific for enhanced usability.

Comment thread admin/auth.go Outdated
a.mu.Lock()
defer a.mu.Unlock()
sessionID := generateOTP()
a.sessions[sessionID] = time.Now()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The created session currently has no expiration. This is a security risk as a compromised session ID could remain valid indefinitely. It's crucial to implement a session timeout mechanism to invalidate sessions after a period of inactivity or a fixed duration. This could involve storing an expiration timestamp with the session and checking it during ValidateSession.

Suggested change
a.sessions[sessionID] = time.Now()
a.sessions[sessionID] = time.Now().Add(1 * time.Hour) // Example: session expires in 1 hour

Comment thread admin/auth.go Outdated

func generateOTP() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Ignoring the error return from rand.Read can lead to a security vulnerability. If rand.Read fails, the byte slice b might not be filled with cryptographically secure random bytes, potentially making the OTP predictable. Always check for errors when dealing with cryptographic functions.

    if _, err := rand.Read(b); err != nil {
        // Handle the error appropriately, e.g., log it and return an error or panic.
        // For now, we'll panic as a critical security failure.
        panic(fmt.Sprintf("failed to generate random bytes for OTP: %v", err))
    }

Comment thread admin/actions.go Outdated
sbDir := sandboxDirFor(sandboxName)

// Try stop (ignore errors — sandbox may already be stopped).
_ = ExecuteSandboxScript(sandboxName, "stop")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Ignoring the error from ExecuteSandboxScript can hide important issues during sandbox destruction. While a sandbox might already be stopped, other errors could occur (e.g., permission issues, script not found for unexpected reasons). It's better to log this error for debugging purposes, even if it doesn't prevent the DestroySandbox operation from continuing.

    if err := ExecuteSandboxScript(sandboxName, "stop"); err != nil {
        // Log the error, but continue with destruction as stopping might not be critical.
        fmt.Printf("Warning: failed to stop sandbox %s during destruction: %v\n", sandboxName, err)
    }

Comment thread admin/handlers.go

// Parse: /api/sandboxes/<name>/<action>
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/sandboxes/"), "/")
if len(parts) != 2 {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message "Bad request" is too generic. Providing a more specific message, such as "Invalid sandbox action path format. Expected /api/sandboxes//", would greatly improve API usability and debugging for clients.

Suggested change
if len(parts) != 2 {
if len(parts) != 2 {
http.Error(w, "Invalid sandbox action path format. Expected /api/sandboxes/<name>/<action>", http.StatusBadRequest)
return
}

Comment thread admin/handlers.go Outdated
}

// Return updated sandbox list fragment for HTMX.
sandboxes, _ := GetAllSandboxes()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Ignoring the error returned by GetAllSandboxes() can lead to unexpected behavior if the function fails. If an error occurs, sandboxes might be nil, which could cause issues when passed to the template. It's best to handle this error explicitly, perhaps by logging it and returning an http.Error to the client.

Suggested change
sandboxes, _ := GetAllSandboxes()
sandboxes, err := GetAllSandboxes()
if err != nil {
http.Error(w, "Failed to retrieve sandboxes: "+err.Error(), http.StatusInternalServerError)
return
}

Comment thread admin/server.go

openBrowser(url)

listener, err := net.Listen("tcp", addr)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The openBrowser function is called without checking for potential errors. If the browser fails to open (e.g., due to missing xdg-open or open commands, or permission issues), the user might not see the URL and OTP, leading to a poor user experience. It's advisable to log any errors from openBrowser to aid debugging.

    if err := openBrowser(url); err != nil {
        fmt.Printf("Warning: failed to open browser: %v\n", err)
    }

Comment thread admin/server.go Outdated
Comment on lines +74 to +83
var cmd *exec.Cmd
switch runtime.GOOS {
case "linux":
cmd = exec.Command("xdg-open", url)
case "darwin":
cmd = exec.Command("open", url)
}
if cmd != nil {
_ = cmd.Start()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The openBrowser function currently only supports Linux and macOS. For Windows users, the browser will not open automatically, which can be a significant usability issue. Consider adding support for Windows using cmd /c start or start command.

func openBrowser(url string) error {
    var cmd *exec.Cmd
    switch runtime.GOOS {
    case "linux":
        cmd = exec.Command("xdg-open", url)
    case "darwin":
        cmd = exec.Command("open", url)
    case "windows":
        cmd = exec.Command("cmd", "/c", "start", url)
    default:
        return fmt.Errorf("unsupported operating system to open browser: %s", runtime.GOOS)
    }
    if cmd != nil {
        return cmd.Start()
    }
    return nil
}

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

POC implementation of a localhost-only “admin web UI” for dbdeployer, adding a new dbdeployer admin ui command that runs an HTTP server with OTP-based login and a sandbox dashboard with start/stop/destroy actions.

Changes:

  • Add dbdeployer admin ui Cobra subcommand wiring.
  • Introduce new admin/ package (HTTP server, OTP/session auth, sandbox listing/status, lifecycle actions).
  • Add embedded Go HTML templates (login, dashboard, sandbox list component) and initial static directory.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md Adds a design/spec doc for the admin web UI POC.
cmd/admin.go Wires new admin ui subcommand and --port flag.
admin/server.go Implements embedded-template HTTP server, routes, and browser opener.
admin/auth.go Implements OTP + in-memory session cookie auth.
admin/handlers.go Implements login, dashboard rendering, JSON listing, and action endpoints.
admin/sandbox_status.go Reads catalog and infers running status from PID files.
admin/actions.go Executes sandbox lifecycle scripts and supports destroy (rm -rf + catalog delete).
admin/templates/login.html Adds login page template.
admin/templates/dashboard.html Adds dashboard template and HTMX wiring.
admin/templates/components/sandbox-list.html Adds sandbox card/list fragment used for HTMX swaps.
admin/static/placeholder.txt Placeholder for embedded static assets.
admin/static/.gitkeep Keeps static directory in git.
Comments suppressed due to low confidence (1)

cmd/admin.go:31

  • Import block is not gofmt/goimports formatted (stdlib imports are mixed with module imports and there’s no blank line separation). This tends to drift further over time and makes diffs noisier; running gofmt/goimports on this file should reorder/group the imports correctly.
import (
	"fmt"
	"github.com/ProxySQL/dbdeployer/admin"
	"github.com/ProxySQL/dbdeployer/defaults"
	"os"
	"path"
	"sort"

	"github.com/ProxySQL/dbdeployer/common"
	"github.com/ProxySQL/dbdeployer/globals"
	"github.com/ProxySQL/dbdeployer/sandbox"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread admin/actions.go
Comment on lines +14 to +16
func ExecuteSandboxScript(sandboxName string, script string) error {
sbDir := sandboxDirFor(sandboxName)

Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sandboxName comes from the URL and is directly joined into a filesystem path. As-is, values like ../... can escape SandboxHome and make this endpoint execute/remove arbitrary paths after auth (and auth here is only a session cookie). Resolve the target sandbox via the catalog and validate the identifier strictly (e.g., only allow exact catalog keys or a validated name set), and reject names containing path separators or ...

Copilot uses AI. Check for mistakes.
Comment thread admin/actions.go
Comment on lines +34 to +49
// DestroySandbox stops a sandbox and removes its directory and catalog entry.
func DestroySandbox(sandboxName string) error {
sbDir := sandboxDirFor(sandboxName)

// Try stop (ignore errors — sandbox may already be stopped).
_ = ExecuteSandboxScript(sandboxName, "stop")

// Remove directory.
if err := os.RemoveAll(sbDir); err != nil {
return fmt.Errorf("removing %s: %w", sbDir, err)
}

// Remove from catalog. The catalog key is the full destination path.
if err := defaults.DeleteFromCatalog(sbDir); err != nil {
return fmt.Errorf("removing %s from catalog: %w", sandboxName, err)
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sandbox directory resolution doesn’t match how the catalog is keyed. The catalog key is the full destination path (defaults.UpdateCatalog(sandboxDef.SandboxDir, ...)), but here actions always assume SandboxHome/<name>, and DeleteFromCatalog is called with that derived path. This will break for sandboxes deployed outside the default home and may fail to delete the correct catalog entry; use the catalog destination path consistently for actions and catalog deletes.

Copilot uses AI. Check for mistakes.
Comment thread admin/handlers.go
Comment on lines +94 to +99
// Return updated sandbox list fragment for HTMX.
sandboxes, _ := GetAllSandboxes()
data := map[string]interface{}{
"Sandboxes": sandboxes,
"Count": len(sandboxes),
}
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error from GetAllSandboxes() is ignored when returning the updated HTMX fragment. If catalog reading fails here, the handler will still try to render with a nil/partial list and hide the underlying failure. Handle the error and return an appropriate HTTP error instead of discarding it.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +47
hx-post="/api/sandboxes/{{.Name}}/start"
hx-target="#sandbox-list"
hx-swap="innerHTML">
Start
</button>
<button class="btn btn-stop"
hx-post="/api/sandboxes/{{.Name}}/stop"
hx-target="#sandbox-list"
hx-swap="innerHTML">
Stop
</button>
<button class="btn btn-destroy"
hx-post="/api/sandboxes/{{.Name}}/destroy"
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.Name is interpolated directly into the hx-post URL path. Go templates will HTML-escape, not URL-path-escape, so names containing spaces/special chars will generate broken endpoints (e.g., & becomes &amp;). Either ensure sandbox names are restricted to URL-safe characters, or apply proper URL path escaping / use a stable ID (e.g., encoded catalog key) for API routes.

Suggested change
hx-post="/api/sandboxes/{{.Name}}/start"
hx-target="#sandbox-list"
hx-swap="innerHTML">
Start
</button>
<button class="btn btn-stop"
hx-post="/api/sandboxes/{{.Name}}/stop"
hx-target="#sandbox-list"
hx-swap="innerHTML">
Stop
</button>
<button class="btn btn-destroy"
hx-post="/api/sandboxes/{{.Name}}/destroy"
hx-post="/api/sandboxes/{{.Name | urlquery}}/start"
hx-target="#sandbox-list"
hx-swap="innerHTML">
Start
</button>
<button class="btn btn-stop"
hx-post="/api/sandboxes/{{.Name | urlquery}}/stop"
hx-target="#sandbox-list"
hx-swap="innerHTML">
Stop
</button>
<button class="btn btn-destroy"
hx-post="/api/sandboxes/{{.Name | urlquery}}/destroy"

Copilot uses AI. Check for mistakes.
Comment thread admin/server.go Outdated
Comment on lines +56 to +70
addr := fmt.Sprintf("127.0.0.1:%d", s.port)
url := fmt.Sprintf("http://%s/login?token=%s", addr, s.auth.OTP())

fmt.Printf("\n dbdeployer admin\n")
fmt.Printf(" ────────────────────────────────\n")
fmt.Printf(" URL: %s\n", url)
fmt.Printf(" (opening browser...)\n\n")

openBrowser(url)

listener, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("listen on %s: %w", addr, err)
}
return http.Serve(listener, mux)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The browser is opened before the TCP listener is created. On a fast browser or slow startup, the initial navigation can race and fail to connect. Bind/listen first (or start the server in a goroutine and wait until it’s accepting connections) and only then open the browser.

Copilot uses AI. Check for mistakes.
Comment thread admin/sandbox_status.go Outdated
Comment on lines +144 to +150
// Check if the process exists by sending signal 0.
proc, err := os.FindProcess(pid)
if err != nil {
return false
}
err = proc.Signal(os.Signal(nil))
return err == nil
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Process existence check is broken: proc.Signal(os.Signal(nil)) will panic on Unix (nil doesn’t implement the expected signal type) and won’t reliably check liveness. Use a proper signal-0 check (e.g., syscall.Kill(pid, 0) / proc.Signal(syscall.Signal(0))) and handle platform differences accordingly.

Copilot uses AI. Check for mistakes.
Comment thread admin/templates/dashboard.html Outdated
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dbdeployer admin</title>
<script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js" crossorigin="anonymous"></script>
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This loads HTMX from a CDN, but the PR description calls out “HTMX from CDN, embedded via //go:embed” and the design doc suggests embedding static assets. If the intent is zero external runtime deps, switch this to /static/htmx.min.js (and embed that file). Otherwise, update the PR description/spec to match the CDN approach.

Copilot uses AI. Check for mistakes.
Comment thread admin/auth.go Outdated

func generateOTP() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateOTP ignores the error from rand.Read. If the system CSPRNG fails, this will silently generate an all-zero token, undermining auth/session IDs. Propagate the error (and fail server startup) or retry/exit on failure so token generation can’t silently degrade.

Suggested change
_, _ = rand.Read(b)
if _, err := rand.Read(b); err != nil {
// Fail fast if the CSPRNG is unavailable rather than generating weak tokens.
panic("crypto/rand.Read failed while generating OTP: " + err.Error())
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (5)
admin/actions.go (1)

26-31: Consider adding a timeout for script execution.

The script execution has no timeout, which could cause the HTTP request to hang indefinitely if the sandbox script doesn't terminate. For a web UI, this could lead to poor user experience.

♻️ Proposed fix using context with timeout
+import (
+	"context"
+	"time"
+	...
+)
+
 func ExecuteSandboxScript(sandboxName string, script string) error {
 	sbDir := sandboxDirFor(sandboxName)
 
 	scriptPath := filepath.Join(sbDir, script)
 	if _, err := os.Stat(scriptPath); err != nil {
 		// Try the _all variant for multi-node sandboxes.
 		scriptPath = filepath.Join(sbDir, script+"_all")
 		if _, err2 := os.Stat(scriptPath); err2 != nil {
 			return fmt.Errorf("script %q not found in %s", script, sbDir)
 		}
 	}
 
-	cmd := exec.Command("bash", scriptPath)
+	ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
+	defer cancel()
+	cmd := exec.CommandContext(ctx, "bash", scriptPath)
 	output, err := cmd.CombinedOutput()
 	if err != nil {
+		if ctx.Err() == context.DeadlineExceeded {
+			return fmt.Errorf("%s timed out after 60s", script)
+		}
 		return fmt.Errorf("%s failed: %s: %w", script, string(output), err)
 	}
 	return nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/actions.go` around lines 26 - 31, The script executor currently uses
exec.Command("bash", scriptPath) with no timeout; change it to use a context
with timeout (e.g., context.WithTimeout) and exec.CommandContext(ctx, "bash",
scriptPath) inside the same function so the process is cancelled when the
timeout elapses; ensure you capture cmd.CombinedOutput(), detect
context.DeadlineExceeded (or ctx.Err()) and return a clear timeout error that
includes any captured output, and cancel the context (or defer cancel()) to free
resources (refer to the cmd/exec invocation and the error-return block around
CombinedOutput).
docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md (2)

29-37: Optional: Add language specifier to fenced code blocks.

Per markdownlint, fenced code blocks should have a language specified. For ASCII diagrams, use text or plaintext.

📝 Suggested fix
-```
+```text
 dbdeployer admin
   └─ starts HTTP server on 127.0.0.1:<port>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md` around lines 29
- 37, The fenced ASCII diagram block (the one starting with "dbdeployer admin"
and listing behaviors like "starts HTTP server...", "generates OTP...", "opens
browser to http://127.0.0.1:<port>/login?token=<otp>", "serves embedded HTML
templates via Go's html/template", and "HTMX handles dynamic actions") needs a
language specifier; update the triple-backtick fence to use ```text (or
```plaintext) so markdownlint stops flagging it—i.e., replace the opening ```
with ```text for that code block in the
docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md file.

131-132: Spec mentions auto-port selection, but implementation doesn't include it.

The spec states "If busy, find next free port" but the current implementation in cmd/admin.go simply uses the provided port without fallback. This is fine for a POC, but consider updating either the spec or implementation to match.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md` around lines 131
- 132, The spec requires auto-port selection when the desired port is busy;
update cmd/admin.go (the routine that parses --port and starts the HTTP
server—e.g., startAdminServer/runAdminServer) to attempt binding starting at the
configured port (default 9090) and, on EADDRINUSE, increment the port and retry
until a free port is found (or a reasonable max attempts), then start the server
on the successful port and print the full URL to the terminal. Ensure the loop
only reacts to "address in use" errors, preserves any explicit --port value as
the starting point, and logs the final chosen port.
admin/auth.go (1)

40-46: Sessions accumulate without expiration.

The time.Time value stored for each session is never used—sessions remain valid indefinitely. For a localhost POC this is acceptable, but consider adding session expiration or a logout endpoint if this evolves beyond a proof-of-concept.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/auth.go` around lines 40 - 46, CreateSession currently stores
time.Now() so sessions never expire; update it to record an expiration time
(e.g., time.Now().Add(sessionTTL)) instead of the creation time and add logic to
enforce expiry: introduce a sessionTTL constant and either implement a
ValidateSession(sessionID string) bool that checks the stored expiry and deletes
expired entries from a.sessions, or run a background cleanup goroutine on
AuthManager that periodically removes expired sessions; reference AuthManager,
CreateSession, a.sessions, generateOTP and add ValidateSession or Logout as
appropriate.
admin/server.go (1)

64-70: Browser may open before server is ready.

openBrowser(url) is called before the listener is created. If the browser is fast, it could attempt to connect before the server is listening, showing a connection error. Consider opening the browser after the listener is established.

Proposed fix
 	listener, err := net.Listen("tcp", addr)
 	if err != nil {
 		return fmt.Errorf("listen on %s: %w", addr, err)
 	}
+
+	openBrowser(url)
+
 	return http.Serve(listener, mux)

And remove the earlier openBrowser(url) call on line 64.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/server.go` around lines 64 - 70, openBrowser(url) is currently invoked
before the server listener is created, which can cause the browser to try
connecting too early; remove the earlier openBrowser(url) call and instead call
openBrowser(url) after net.Listen succeeds (after listener, err :=
net.Listen("tcp", addr) returns without error) and before returning
http.Serve(listener, mux) — you may invoke it synchronously or in a goroutine to
avoid blocking; update code around net.Listen, listener, http.Serve, addr, mux
and url accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@admin/auth.go`:
- Around line 69-73: generateOTP currently ignores errors from rand.Read,
risking predictable tokens on failure; change generateOTP to return (string,
error) and return an error if rand.Read fails, then update callers
(NewAuthManager and CreateSession) to handle the returned error by propagating
it (or failing session creation) rather than using a zeroed token—ensure
function signatures and all call sites are adjusted to handle the error path and
log/return a clear error when OTP generation fails.

In `@admin/handlers.go`:
- Around line 94-102: GetAllSandboxes() error is being ignored which can lead to
rendering an empty HTMX fragment; update the handler to check the error returned
by GetAllSandboxes() and handle it before calling s.templates.ExecuteTemplate:
if GetAllSandboxes() returns an error, log or return an http.Error (with a clear
message and StatusInternalServerError) and do not call ExecuteTemplate;
otherwise build the data map ("Sandboxes" and "Count") and call
s.templates.ExecuteTemplate(w, "sandbox-list.html", data) as before.
- Around line 68-74: The sandbox name extracted from the path (name from
parts[0]) is not validated and can contain path traversal characters; update the
handler so that before calling ExecuteSandboxScript or DestroySandbox you reject
any invalid sandbox names: ensure name is non-empty, does not contain path
separators ('/' or '\\'), and does not include directory traversal segments like
".." (for example by comparing name to filepath.Base(name) or by using
path/filepath.Clean and ensuring it equals the original and contains no "..");
return http.StatusBadRequest on validation failure. Reference the handler that
sets name, and the downstream functions ExecuteSandboxScript, DestroySandbox and
sandboxDirFor when adding this check.

In `@admin/sandbox_status.go`:
- Around line 144-150: The process-existence check incorrectly calls
proc.Signal(os.Signal(nil)); replace that with sending signal 0 using the
syscall package (i.e. proc.Signal(syscall.Signal(0))) so the check uses the
standard Unix "signal 0" semantics; update imports to include "syscall" and
adjust the proc.Signal call (the code around os.FindProcess and proc.Signal)
accordingly (or add a platform-specific implementation if you need Windows
compatibility).

In `@admin/server.go`:
- Around line 64-70: Replace the direct call to http.Serve with an http.Server
instance that has sensible ReadTimeout, WriteTimeout and IdleTimeout values to
mitigate Slowloris/resource exhaustion; after creating listener via
net.Listen(addr) build server := &http.Server{Handler: mux, ReadTimeout:
<duration>, WriteTimeout: <duration>, IdleTimeout: <duration>} and call
server.Serve(listener); remember to add "time" to imports and keep
openBrowser(url) call as-is.

In `@admin/templates/dashboard.html`:
- Line 7: The HTMX CDN script tag in admin/templates/dashboard.html is missing a
correct Subresource Integrity attribute; update the <script> that loads
"https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js" to include the proper
integrity attribute value (use the actual SRI SHA256/SHA384/SHA512 hash computed
for that exact file) and keep crossorigin="anonymous" so the browser can
validate the resource; ensure the integrity hash matches the file served by the
CDN to avoid integrity mismatch errors.

---

Nitpick comments:
In `@admin/actions.go`:
- Around line 26-31: The script executor currently uses exec.Command("bash",
scriptPath) with no timeout; change it to use a context with timeout (e.g.,
context.WithTimeout) and exec.CommandContext(ctx, "bash", scriptPath) inside the
same function so the process is cancelled when the timeout elapses; ensure you
capture cmd.CombinedOutput(), detect context.DeadlineExceeded (or ctx.Err()) and
return a clear timeout error that includes any captured output, and cancel the
context (or defer cancel()) to free resources (refer to the cmd/exec invocation
and the error-return block around CombinedOutput).

In `@admin/auth.go`:
- Around line 40-46: CreateSession currently stores time.Now() so sessions never
expire; update it to record an expiration time (e.g.,
time.Now().Add(sessionTTL)) instead of the creation time and add logic to
enforce expiry: introduce a sessionTTL constant and either implement a
ValidateSession(sessionID string) bool that checks the stored expiry and deletes
expired entries from a.sessions, or run a background cleanup goroutine on
AuthManager that periodically removes expired sessions; reference AuthManager,
CreateSession, a.sessions, generateOTP and add ValidateSession or Logout as
appropriate.

In `@admin/server.go`:
- Around line 64-70: openBrowser(url) is currently invoked before the server
listener is created, which can cause the browser to try connecting too early;
remove the earlier openBrowser(url) call and instead call openBrowser(url) after
net.Listen succeeds (after listener, err := net.Listen("tcp", addr) returns
without error) and before returning http.Serve(listener, mux) — you may invoke
it synchronously or in a goroutine to avoid blocking; update code around
net.Listen, listener, http.Serve, addr, mux and url accordingly.

In `@docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md`:
- Around line 29-37: The fenced ASCII diagram block (the one starting with
"dbdeployer admin" and listing behaviors like "starts HTTP server...",
"generates OTP...", "opens browser to
http://127.0.0.1:<port>/login?token=<otp>", "serves embedded HTML templates via
Go's html/template", and "HTMX handles dynamic actions") needs a language
specifier; update the triple-backtick fence to use ```text (or ```plaintext) so
markdownlint stops flagging it—i.e., replace the opening ``` with ```text for
that code block in the
docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md file.
- Around line 131-132: The spec requires auto-port selection when the desired
port is busy; update cmd/admin.go (the routine that parses --port and starts the
HTTP server—e.g., startAdminServer/runAdminServer) to attempt binding starting
at the configured port (default 9090) and, on EADDRINUSE, increment the port and
retry until a free port is found (or a reasonable max attempts), then start the
server on the successful port and print the full URL to the terminal. Ensure the
loop only reacts to "address in use" errors, preserves any explicit --port value
as the starting point, and logs the final chosen port.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d0c9aac-cf9a-45a5-850b-f0cfddf3399a

📥 Commits

Reviewing files that changed from the base of the PR and between 8252ac7 and 87f1f59.

📒 Files selected for processing (12)
  • admin/actions.go
  • admin/auth.go
  • admin/handlers.go
  • admin/sandbox_status.go
  • admin/server.go
  • admin/static/.gitkeep
  • admin/static/placeholder.txt
  • admin/templates/components/sandbox-list.html
  • admin/templates/dashboard.html
  • admin/templates/login.html
  • cmd/admin.go
  • docs/superpowers/specs/2026-03-24-admin-webui-poc-design.md

Comment thread admin/auth.go
Comment on lines +69 to +73
func generateOTP() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle rand.Read error to avoid weak tokens on failure.

If rand.Read fails (rare but possible on systems with exhausted entropy), the buffer may contain zeros, producing a predictable token. For a security-sensitive value like an OTP or session ID, this error should be handled.

Proposed fix
-func generateOTP() string {
+func generateOTP() (string, error) {
 	b := make([]byte, 16)
-	_, _ = rand.Read(b)
-	return hex.EncodeToString(b)
+	if _, err := rand.Read(b); err != nil {
+		return "", fmt.Errorf("generating OTP: %w", err)
+	}
+	return hex.EncodeToString(b), nil
 }

This would require updating NewAuthManager and CreateSession to propagate or handle the error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/auth.go` around lines 69 - 73, generateOTP currently ignores errors
from rand.Read, risking predictable tokens on failure; change generateOTP to
return (string, error) and return an error if rand.Read fails, then update
callers (NewAuthManager and CreateSession) to handle the returned error by
propagating it (or failing session creation) rather than using a zeroed
token—ensure function signatures and all call sites are adjusted to handle the
error path and log/return a clear error when OTP generation fails.

Comment thread admin/handlers.go Outdated
Comment thread admin/handlers.go
Comment thread admin/sandbox_status.go Outdated
Comment thread admin/server.go Outdated
Comment thread admin/templates/dashboard.html Outdated
- auth.go: panic on crypto/rand failure; store session expiry and check it in ValidateSession
- handlers.go: path traversal guard on sandbox name; improved error message; handle GetAllSandboxes error after action
- actions.go: log warning instead of silently ignoring stop failure during destroy
- server.go: listen before opening browser to avoid race; add ReadTimeout/WriteTimeout/IdleTimeout; Windows support; openBrowser returns error
- sandbox_status.go: replace os.Signal(nil) with syscall.Signal(0) for portable process liveness check
- dashboard.html: add SRI integrity hash to HTMX CDN script tag
- sandbox-list.html: URL-encode sandbox names in hx-post attributes
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
admin/sandbox_status.go (1)

46-119: Sandbox list ordering is unstable due to map iteration.

At Line 46, iterating a map directly makes dashboard card order nondeterministic between refreshes. Sorting keys first will keep HTMX updates stable.

Proposed refactor
 import (
 	"fmt"
 	"os"
 	"path/filepath"
+	"sort"
 	"strconv"
 	"strings"
 	"syscall"
@@
-	for name, item := range catalog {
+	keys := make([]string, 0, len(catalog))
+	for name := range catalog {
+		keys = append(keys, name)
+	}
+	sort.Strings(keys)
+
+	for _, name := range keys {
+		item := catalog[name]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/sandbox_status.go` around lines 46 - 119, The loop iterates the map
variable catalog directly which yields nondeterministic ordering; to stabilize
sandbox list order, collect the map keys into a slice, sort them (e.g., using
sort.Strings), and then iterate over the sorted keys to build SandboxStatus
entries (the block that currently ranges "for name, item := range catalog").
Keep the rest of the logic intact (uses sandboxHome, SandboxStatus,
item.Destination, item.Nodes, common.ReadSandboxDescription, isRunning and
appends to result) but replace the direct map iteration with an iteration over
the sorted key slice so HTMX/dashboard card order is stable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@admin/sandbox_status.go`:
- Around line 54-56: The code only strips '/' before using filepath.Base, so
Windows backslashes can leave full paths in sbName; update the normalization to
always call filepath.Base(sbName) (remove the conditional) where sbName is set
in the sandbox status logic so the Name is consistently the base name on all
OSes.

---

Nitpick comments:
In `@admin/sandbox_status.go`:
- Around line 46-119: The loop iterates the map variable catalog directly which
yields nondeterministic ordering; to stabilize sandbox list order, collect the
map keys into a slice, sort them (e.g., using sort.Strings), and then iterate
over the sorted keys to build SandboxStatus entries (the block that currently
ranges "for name, item := range catalog"). Keep the rest of the logic intact
(uses sandboxHome, SandboxStatus, item.Destination, item.Nodes,
common.ReadSandboxDescription, isRunning and appends to result) but replace the
direct map iteration with an iteration over the sorted key slice so
HTMX/dashboard card order is stable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cee119e6-2fc1-45dc-a4a9-f36fafffebf7

📥 Commits

Reviewing files that changed from the base of the PR and between 87f1f59 and 57d81b3.

📒 Files selected for processing (7)
  • admin/actions.go
  • admin/auth.go
  • admin/handlers.go
  • admin/sandbox_status.go
  • admin/server.go
  • admin/templates/components/sandbox-list.html
  • admin/templates/dashboard.html
✅ Files skipped from review due to trivial changes (1)
  • admin/templates/dashboard.html
🚧 Files skipped from review as they are similar to previous changes (3)
  • admin/actions.go
  • admin/auth.go
  • admin/handlers.go

Comment thread admin/sandbox_status.go
Comment on lines +54 to +56
if strings.Contains(sbName, "/") {
sbName = filepath.Base(sbName)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Normalize sandbox name with filepath.Base unconditionally.

At Line 54, checking only "/" misses Windows-style separators (\), so full paths can leak into .Name on Windows. Use filepath.Base(sbName) directly.

Proposed fix
-		// If the name looks like a full path, extract just the base name.
-		if strings.Contains(sbName, "/") {
-			sbName = filepath.Base(sbName)
-		}
+		// Extract display name from catalog key/path in a cross-platform way.
+		sbName = filepath.Base(sbName)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if strings.Contains(sbName, "/") {
sbName = filepath.Base(sbName)
}
// Extract display name from catalog key/path in a cross-platform way.
sbName = filepath.Base(sbName)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@admin/sandbox_status.go` around lines 54 - 56, The code only strips '/'
before using filepath.Base, so Windows backslashes can leave full paths in
sbName; update the normalization to always call filepath.Base(sbName) (remove
the conditional) where sbName is set in the sandbox status logic so the Name is
consistently the base name on all OSes.

- isRunning now executes the sandbox's own status script instead of
  hardcoding PID file paths. Falls back to PID check if no script.
- URL-decode sandbox name in action handler (urlquery encoding in
  templates was causing mismatches).
- Add 2s delay after start/stop before reading status (scripts are async)
- Add /api/sandboxes/refresh endpoint returning sandbox list HTML fragment
- Dashboard auto-polls every 5s via HTMX hx-trigger="every 5s"
- Status badges now update without manual page refresh
The status script always exits 0 regardless of sandbox state.
It outputs '<name> on' or '<name> off'. Parse the text.
renecannao added a commit that referenced this pull request Mar 25, 2026
- Remove dbdeployer admin ui from README, quickstarts, and landing page
  (PR #56 not yet merged)
- PostgreSQL README example now uses safe apt-get download + unpack
  approach instead of destructive apt-get install
- Added "Why not just apt-get install?" section to PG quickstart
  explaining: no root needed, no conflict with running PG, multiple
  versions side by side
- Restored "No Root, No Docker" feature card on landing page
@renecannao renecannao merged commit 28953d8 into master Apr 3, 2026
8 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants