remove experimental web UI

The web UI shipped as "experimental" and was never finished — no nav
off the dashboard, no live updates, no settled design, never a
supported surface. It was opt-in by default already; leaving the code
in the tree for v0.1.0 only invited "does this work?" questions and
kept HostSummary/BangerSummary/SudoStatus types on the public RPC
surface that nothing else uses.

Removed:

  internal/webui/                         (all Go + templates + assets)
  internal/daemon/web.go                  (server start / Layout / Config / ListVMs / ListImages)
  internal/daemon/dashboard.go            (DashboardSummary aggregator)

Simplified:

  internal/api/types.go                   drop WebURL on PingResult, drop
                                          HostSummary / SudoStatus / BangerSummary /
                                          DashboardSummary / DashboardSummaryResult
  internal/model/types.go                 drop DaemonConfig.WebListenAddr
  internal/config/config.go               drop web_listen_addr from fileConfig + Load
  internal/daemon/daemon.go               drop webListener / webServer / webURL fields +
                                          startWebServer() call + ping WebURL population
  internal/cli/banger.go                  `daemon status` output no longer branches on web
  internal/daemon/{doc.go,ARCHITECTURE.md} drop web UI sections
  README.md                               drop web_listen_addr config bullet + security paragraph

Tests updated to reflect the new shape. Coverage 57.3 -> 58.9% (the
webui package was largely untested; its removal lifts the ratio
without moving the numerator). `banger daemon status` output and
--help are web-free. Lint + full suite green.
This commit is contained in:
Thales Maciel 2026-04-19 14:28:08 -03:00
parent 687fcf0b59
commit d1b9a8c102
No known key found for this signature in database
GPG key ID: 33112E6833C34679
24 changed files with 9 additions and 2752 deletions

View file

@ -34,7 +34,7 @@ owning types:
- `tapPool tapPool` — TAP interface pool; owns its own lock.
- `sessions sessionRegistry` — active guest session controllers; owns
its own lock.
- `listener`, `webListener`, `webServer`, `webURL`, `vmDNS` — networking.
- `listener`, `vmDNS` — networking.
- `vmCaps` — registered VM capability hooks.
- `pullAndFlatten`, `finalizePulledRootfs`, `bundleFetch`,
`requestHandler`, `guestWaitForSSH`, `guestDial`,
@ -98,9 +98,3 @@ Only `internal/cli` imports this package. The surface is:
All other `*Daemon` methods are reached only through the RPC `dispatch`
switch in `daemon.go` and are free to move/rename during refactoring.
## Web UI
The optional web UI served at `web_listen_addr` is experimental. It is
enabled by default for local observability but is not considered a stable
or supported interface. Set `web_listen_addr = ""` in config to disable.

View file

@ -9,7 +9,6 @@ import (
"fmt"
"log/slog"
"net"
"net/http"
"os"
"strings"
"sync"
@ -58,9 +57,6 @@ type Daemon struct {
once sync.Once
pid int
listener net.Listener
webListener net.Listener
webServer *http.Server
webURL string
vmDNS *vmdns.Server
vmCaps []vmCapability
pullAndFlatten func(ctx context.Context, ref, cacheDir, destDir string) (imagepull.Metadata, error)
@ -138,12 +134,6 @@ 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.clearVMDNSResolverRouting(context.Background()), d.stopVMDNS(), d.closeGuestSessionControllers(), d.store.Close())
})
return err
@ -167,10 +157,6 @@ 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()
for {
@ -274,7 +260,6 @@ func (d *Daemon) dispatch(ctx context.Context, req rpc.Request) rpc.Response {
result, _ := rpc.NewResult(api.PingResult{
Status: "ok",
PID: d.pid,
WebURL: d.webURL,
Version: info.Version,
Commit: info.Commit,
BuiltAt: info.BuiltAt,

View file

@ -33,7 +33,7 @@ func TestRegisterImageRequiresKernel(t *testing.T) {
}
func TestDispatchPingIncludesBuildInfo(t *testing.T) {
d := &Daemon{pid: 42, webURL: "http://127.0.0.1:7777"}
d := &Daemon{pid: 42}
resp := d.dispatch(context.Background(), rpc.Request{Version: rpc.Version, Method: "ping"})
if !resp.OK {
@ -46,8 +46,8 @@ func TestDispatchPingIncludesBuildInfo(t *testing.T) {
}
info := buildinfo.Current()
if got.Status != "ok" || got.PID != 42 || got.WebURL != "http://127.0.0.1:7777" {
t.Fatalf("PingResult = %+v, want status/pid/weburl populated", got)
if got.Status != "ok" || got.PID != 42 {
t.Fatalf("PingResult = %+v, want status/pid populated", got)
}
if got.Version != info.Version || got.Commit != info.Commit || got.BuiltAt != info.BuiltAt {
t.Fatalf("PingResult build info = %+v, want %+v", got, info)

View file

@ -1,63 +0,0 @@
package daemon
import (
"context"
"banger/internal/api"
"banger/internal/model"
"banger/internal/system"
)
func (d *Daemon) DashboardSummary(ctx context.Context) (api.DashboardSummary, error) {
summary := api.DashboardSummary{
GeneratedAt: model.Now(),
Sudo: api.SudoStatus{
Command: "sudo -v",
},
}
if err := system.CheckSudo(ctx); err != nil {
summary.Sudo.Error = err.Error()
} else {
summary.Sudo.Available = true
}
if host, err := system.ReadHostResources(); err == nil {
summary.Host.CPUCount = host.CPUCount
summary.Host.TotalMemoryBytes = host.TotalMemoryBytes
}
if usage, err := system.ReadFilesystemUsage(d.layout.StateDir); err == nil {
summary.Host.StateFilesystemTotalBytes = usage.TotalBytes
summary.Host.StateFilesystemFreeBytes = usage.FreeBytes
}
images, err := d.store.ListImages(ctx)
if err != nil {
return api.DashboardSummary{}, err
}
for _, image := range images {
summary.Banger.ImageCount++
if image.Managed {
summary.Banger.ManagedImageCount++
}
}
vms, err := d.store.ListVMs(ctx)
if err != nil {
return api.DashboardSummary{}, err
}
for _, vm := range vms {
summary.Banger.VMCount++
summary.Banger.ConfiguredVCPUCount += vm.Spec.VCPUCount
summary.Banger.ConfiguredMemoryBytes += int64(vm.Spec.MemoryMiB) * 1024 * 1024
summary.Banger.ConfiguredDiskBytes += vm.Spec.WorkDiskSizeBytes
summary.Banger.UsedSystemOverlayBytes += vm.Stats.SystemOverlayBytes
summary.Banger.UsedWorkDiskBytes += vm.Stats.WorkDiskBytes
if d.vmAlive(vm) {
summary.Banger.RunningVMCount++
summary.Banger.RunningCPUPercent += vm.Stats.CPUPercent
summary.Banger.RunningRSSBytes += vm.Stats.RSSBytes
summary.Banger.RunningVSZBytes += vm.Stats.VSZBytes
}
}
return summary, nil
}

View file

@ -1,9 +1,8 @@
// Package daemon hosts the Banger daemon process.
//
// The daemon exposes a JSON-RPC endpoint over a Unix socket and, optionally,
// an experimental local web UI. It owns VM lifecycle, image management,
// guest sessions, host networking bootstrap, and state persistence via
// internal/store.
// The daemon exposes a JSON-RPC endpoint over a Unix socket. It owns VM
// lifecycle, image management, guest sessions, host networking bootstrap,
// and state persistence via internal/store.
//
// The package is organised into cohesive groups. Pure stateless helpers for
// each group have been lifted into subpackages; orchestrator methods
@ -68,11 +67,9 @@
// Core (in this package):
//
// daemon.go Daemon struct, Open/Close/Serve, dispatch
// dashboard.go dashboard metrics aggregation
// doctor.go host diagnostics
// logger.go slog configuration
// runtime_assets.go paths to bundled companion binaries
// web.go experimental local web UI server
//
// Lock ordering:
//

View file

@ -1,65 +0,0 @@
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)
}