banger/internal/daemon/web.go
Thales Maciel 2362d0ae39
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 ./...
2026-03-21 16:47:47 -03:00

65 lines
1.4 KiB
Go

package daemon
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"strings"
"time"
"banger/internal/model"
"banger/internal/paths"
"banger/internal/webui"
)
func (d *Daemon) startWebServer() error {
listenAddr := strings.TrimSpace(d.config.WebListenAddr)
if listenAddr == "" {
d.webURL = ""
return nil
}
listener, err := net.Listen("tcp", listenAddr)
if err != nil {
if d.logger != nil {
d.logger.Error("web ui listen failed", "addr", listenAddr, "error", err.Error())
}
return fmt.Errorf("web ui listen on %s: %w", listenAddr, err)
}
d.webListener = listener
d.webURL = "http://" + listener.Addr().String()
d.webServer = &http.Server{
Handler: webui.NewHandler(d),
ReadHeaderTimeout: 5 * time.Second,
}
if d.logger != nil {
d.logger.Info("web ui serving", "addr", listener.Addr().String(), "url", d.webURL)
}
go func() {
err := d.webServer.Serve(listener)
if err == nil || errors.Is(err, http.ErrServerClosed) {
return
}
if d.logger != nil {
d.logger.Error("web ui serve failed", "addr", listener.Addr().String(), "error", err.Error())
}
}()
return nil
}
func (d *Daemon) Layout() paths.Layout {
return d.layout
}
func (d *Daemon) Config() model.DaemonConfig {
return d.config
}
func (d *Daemon) ListVMs(ctx context.Context) ([]model.VMRecord, error) {
return d.store.ListVMs(ctx)
}
func (d *Daemon) ListImages(ctx context.Context) ([]model.Image, error) {
return d.store.ListImages(ctx)
}