Serve a local web UI from bangerd

Add a localhost-only web console so VM and image management no longer depends on the CLI for every inspection and lifecycle action.

Wire bangerd up to a configurable web listener, expose dashboard and async image-build state through the daemon, and serve CSRF-protected HTML pages with host-path picking, VM/image detail views, logs, ports, and progress polling for long-running operations.

Keep the browser path aligned with the existing sudo and host-owned artifact model: surface sudo readiness, print the web URL in daemon status, and document the new workflow. Polish the UI with resource usage cards, clearer clickable affordances, cancel paths, confirmation prompts, image-name links, and HTTP port links.

Validation: GOCACHE=/tmp/banger-gocache go test ./...
This commit is contained in:
Thales Maciel 2026-03-21 16:47:47 -03:00
parent 30f0c0b54a
commit 2362d0ae39
No known key found for this signature in database
GPG key ID: 33112E6833C34679
24 changed files with 3308 additions and 52 deletions

View file

@ -9,6 +9,7 @@ import (
"fmt"
"log/slog"
"net"
"net/http"
"os"
"path/filepath"
"strings"
@ -26,27 +27,32 @@ import (
)
type Daemon struct {
layout paths.Layout
config model.DaemonConfig
store *store.Store
runner system.CommandRunner
logger *slog.Logger
mu sync.Mutex
createOpsMu sync.Mutex
createOps map[string]*vmCreateOperationState
vmLocksMu sync.Mutex
vmLocks map[string]*sync.Mutex
tapPoolMu sync.Mutex
tapPool []string
tapPoolNext int
closing chan struct{}
once sync.Once
pid int
listener net.Listener
vmDNS *vmdns.Server
vmCaps []vmCapability
imageBuild func(context.Context, imageBuildSpec) error
requestHandler func(context.Context, rpc.Request) rpc.Response
layout paths.Layout
config model.DaemonConfig
store *store.Store
runner system.CommandRunner
logger *slog.Logger
mu sync.Mutex
createOpsMu sync.Mutex
createOps map[string]*vmCreateOperationState
imageBuildOpsMu sync.Mutex
imageBuildOps map[string]*imageBuildOperationState
vmLocksMu sync.Mutex
vmLocks map[string]*sync.Mutex
tapPoolMu sync.Mutex
tapPool []string
tapPoolNext int
closing chan struct{}
once sync.Once
pid int
listener net.Listener
webListener net.Listener
webServer *http.Server
webURL string
vmDNS *vmdns.Server
vmCaps []vmCapability
imageBuild func(context.Context, imageBuildSpec) error
requestHandler func(context.Context, rpc.Request) rpc.Response
}
func Open(ctx context.Context) (d *Daemon, err error) {
@ -115,6 +121,12 @@ func (d *Daemon) Close() error {
if d.listener != nil {
_ = d.listener.Close()
}
if d.webServer != nil {
_ = d.webServer.Close()
}
if d.webListener != nil {
_ = d.webListener.Close()
}
err = errors.Join(d.stopVMDNS(), d.store.Close())
})
return err
@ -138,6 +150,9 @@ func (d *Daemon) Serve(ctx context.Context) error {
if d.logger != nil {
d.logger.Info("daemon serving", "socket", d.layout.SocketPath, "pid", d.pid)
}
if err := d.startWebServer(); err != nil {
return err
}
go d.backgroundLoop()
@ -238,7 +253,7 @@ func (d *Daemon) dispatch(ctx context.Context, req rpc.Request) rpc.Response {
}
switch req.Method {
case "ping":
result, _ := rpc.NewResult(api.PingResult{Status: "ok", PID: d.pid})
result, _ := rpc.NewResult(api.PingResult{Status: "ok", PID: d.pid, WebURL: d.webURL})
return result
case "shutdown":
go d.Close()
@ -392,6 +407,27 @@ func (d *Daemon) dispatch(ctx context.Context, req rpc.Request) rpc.Response {
}
image, err := d.BuildImage(ctx, params)
return marshalResultOrError(api.ImageShowResult{Image: image}, err)
case "image.build.begin":
params, err := rpc.DecodeParams[api.ImageBuildParams](req)
if err != nil {
return rpc.NewError("bad_request", err.Error())
}
op, err := d.BeginImageBuild(ctx, params)
return marshalResultOrError(api.ImageBuildBeginResult{Operation: op}, err)
case "image.build.status":
params, err := rpc.DecodeParams[api.ImageBuildStatusParams](req)
if err != nil {
return rpc.NewError("bad_request", err.Error())
}
op, err := d.ImageBuildStatus(ctx, params.ID)
return marshalResultOrError(api.ImageBuildStatusResult{Operation: op}, err)
case "image.build.cancel":
params, err := rpc.DecodeParams[api.ImageBuildStatusParams](req)
if err != nil {
return rpc.NewError("bad_request", err.Error())
}
err = d.CancelImageBuild(ctx, params.ID)
return marshalResultOrError(api.Empty{}, err)
case "image.register":
params, err := rpc.DecodeParams[api.ImageRegisterParams](req)
if err != nil {
@ -436,6 +472,7 @@ func (d *Daemon) backgroundLoop() {
d.logger.Error("background stale sweep failed", "error", err.Error())
}
d.pruneVMCreateOperations(time.Now().Add(-10 * time.Minute))
d.pruneImageBuildOperations(time.Now().Add(-10 * time.Minute))
}
}
}