Add optional web UI mirroring --monitor (LISTEN_UI, off by default)

A single self-contained page polls /api/status once a second and renders
the same lines as the terminal monitor (downstreams with models/VRAM and
busy markers, lock, queue, GPU stats); the u/r buttons and keys trigger
update-now and reload-env through the same rate-limited command handler
the control pipe uses. Unauthenticated by design — the sample config
tells users to keep it on localhost.
This commit is contained in:
mram
2026-09-22 18:48:28 +02:00
parent 897163042c
commit 0796092b80
7 changed files with 351 additions and 15 deletions
+34 -15
View File
@@ -567,6 +567,7 @@ func run(ctx context.Context, cfg config.Config, log *slog.Logger, logOut io.Wri
"version", version,
"listen_ollama", cfg.ListenOllama,
"listen_comfy", cfg.ListenComfy,
"listen_ui", orDisabled(cfg.ListenUI),
"ollama_url", orDisabled(cfg.OllamaURL),
"comfy_url", orDisabled(cfg.ComfyURL),
"unload_timeout", cfg.UnloadTimeout,
@@ -808,13 +809,24 @@ func run(ctx context.Context, cfg config.Config, log *slog.Logger, logOut io.Wri
}
}
// The control channel (status for --monitor, update-now and reload-env
// triggers) is served whenever running as a service, independent of
// AUTO_UPDATE.
// The command handler backs both the local control channel (served
// whenever running as a service) and the optional web UI (LISTEN_UI),
// independent of AUTO_UPDATE.
handler := controlHandler(ctx, u, exePath, applyStaged,
statusProvider(cfg, lk, comfySup, health, started, gw, ollamaClient),
reloadHandler(cfg, configPath, restartWhenIdle))
if isService {
serveControl(ctx, log, u, exePath, applyStaged,
statusProvider(cfg, lk, comfySup, health, started, gw, ollamaClient),
reloadHandler(cfg, configPath, restartWhenIdle))
serveControl(ctx, log, handler)
}
if cfg.ListenUI != "" {
ln, err := net.Listen("tcp", cfg.ListenUI)
if err != nil {
return fmt.Errorf("listen ui on %s: %w", cfg.ListenUI, err)
}
uiSrv := &http.Server{Addr: cfg.ListenUI, Handler: uiHandler(handler)}
servers = append(servers, uiSrv)
log.Warn("listening", "consumer", "ui", "addr", cfg.ListenUI)
go func() { errCh <- uiSrv.Serve(ln) }()
}
select {
@@ -1024,17 +1036,18 @@ func updateLoop(ctx context.Context, interval time.Duration, log *slog.Logger, u
}
}
// serveControl opens the local control channel (named pipe on Windows,
// unix socket on Linux) so unprivileged local users can query status
// (--monitor), trigger an update check (--force-update/--update-now) and
// poke a config reload (--reload-env) without admin rights. The update
// payload is signature-verified regardless of who asks; update triggers
// are rate-limited to one per minute so the channel cannot be used to spam
// restarts. u is nil when AUTO_UPDATE=false.
func serveControl(ctx context.Context, log *slog.Logger, u *update.Updater, exePath string, applyStaged func(to string), status func() string, reload func() string) {
// controlHandler builds the command handler shared by the control channel
// (named pipe / unix socket) and the web UI's action endpoints: status for
// --monitor / the UI, an update check (--force-update/--update-now) and a
// config reload (--reload-env), all safe for unprivileged local users. The
// update payload is signature-verified regardless of who asks; update
// triggers are rate-limited to one per minute and reloads to one per two
// seconds so neither can be used to spam restarts or disk churn. u is nil
// when AUTO_UPDATE=false.
func controlHandler(ctx context.Context, u *update.Updater, exePath string, applyStaged func(to string), status func() string, reload func() string) control.Handler {
var mu sync.Mutex
var lastTrigger, lastReload time.Time
h := func(cmd string) string {
return func(cmd string) string {
switch cmd {
case control.CmdStatus:
return "OK " + status()
@@ -1075,6 +1088,12 @@ func serveControl(ctx context.Context, log *slog.Logger, u *update.Updater, exeP
applyStaged(to)
return "OK updated from " + version + " to " + to + "; the service restarts once the GPU is idle"
}
}
// serveControl opens the local control channel (named pipe on Windows,
// unix socket on Linux) for unprivileged local users (--monitor,
// --force-update/--update-now, --reload-env).
func serveControl(ctx context.Context, log *slog.Logger, h control.Handler) {
if err := control.Serve(ctx, h, log); err != nil {
log.Warn("control channel disabled", "err", err)
}