From 55061634931cd4bf85c026e379421deea57962ef Mon Sep 17 00:00:00 2001 From: mram Date: Tue, 22 Sep 2026 18:54:52 +0200 Subject: [PATCH] Monitor: show the web UI URL when LISTEN_UI is active MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status snapshot carries the UI address; the monitor renders it as a dim 'UI: http://…' line below the GPU line. --- cmd/gpu-turnstile/main.go | 3 +++ cmd/gpu-turnstile/monitor.go | 3 +++ cmd/gpu-turnstile/monitor_test.go | 6 ++++++ cmd/gpu-turnstile/webui.go | 13 +++++++++++++ cmd/gpu-turnstile/webui_test.go | 13 +++++++++++++ 5 files changed, 38 insertions(+) diff --git a/cmd/gpu-turnstile/main.go b/cmd/gpu-turnstile/main.go index c4a984a..4000d17 100644 --- a/cmd/gpu-turnstile/main.go +++ b/cmd/gpu-turnstile/main.go @@ -1164,6 +1164,8 @@ type statusSnapshot struct { // false when game detection (and with it nvidia-smi polling) is not // configured. GPU statusGPU `json:"gpu"` + // UI is the web UI's URL when LISTEN_UI is active, empty otherwise. + UI string `json:"ui,omitempty"` // MonitorNote is set client-side (never over the wire) when the // monitor's own binary differs from the service's version. MonitorNote string `json:"-"` @@ -1232,6 +1234,7 @@ func statusProvider(cfg config.Config, lk *lock.Lock, comfySup *supervise.Proces snap := statusSnapshot{ Version: version, UptimeS: int64(time.Since(started).Seconds()), + UI: uiURL(cfg.ListenUI), } st, known, foreign, ageS := gw.get() snap.GPU = statusGPU{ diff --git a/cmd/gpu-turnstile/monitor.go b/cmd/gpu-turnstile/monitor.go index 65873a2..d476ff5 100644 --- a/cmd/gpu-turnstile/monitor.go +++ b/cmd/gpu-turnstile/monitor.go @@ -211,6 +211,9 @@ func renderMonitor(snap statusSnapshot, width int) string { if snap.GPU.Enabled { b.WriteString(renderGPU(snap.GPU) + "\x1b[K\n") } + if snap.UI != "" { + b.WriteString(" " + cDim + "UI: " + snap.UI + cReset + "\x1b[K\n") + } if snap.MonitorNote != "" { b.WriteString(" " + cYellow + snap.MonitorNote + cReset + "\x1b[K\n") } diff --git a/cmd/gpu-turnstile/monitor_test.go b/cmd/gpu-turnstile/monitor_test.go index 9e1e071..39078b4 100644 --- a/cmd/gpu-turnstile/monitor_test.go +++ b/cmd/gpu-turnstile/monitor_test.go @@ -66,6 +66,12 @@ func TestRenderMonitor(t *testing.T) { if strings.Contains(frame, "busy") { t.Errorf("idle lock still shows busy:\n%s", frame) } + + snap.UI = "http://127.0.0.1:7860" + frame = renderMonitor(snap, 80) + if !strings.Contains(frame, "UI: http://127.0.0.1:7860") { + t.Errorf("frame missing UI line:\n%s", frame) + } } func TestFmtDur(t *testing.T) { diff --git a/cmd/gpu-turnstile/webui.go b/cmd/gpu-turnstile/webui.go index d8e2800..6be4e30 100644 --- a/cmd/gpu-turnstile/webui.go +++ b/cmd/gpu-turnstile/webui.go @@ -79,6 +79,19 @@ func isLoopbackHost(host string) bool { return ip != nil && ip.IsLoopback() } +// uiURL renders a LISTEN_UI address as a browsable URL ("" when disabled). +// A wildcard host reads as 127.0.0.1 — that's where the browser is usually +// running. +func uiURL(addr string) string { + if addr == "" { + return "" + } + if strings.HasPrefix(addr, ":") { + addr = "127.0.0.1" + addr + } + return "http://" + addr +} + // uiPage is the whole web UI: a terminal-styled page that polls /api/status // once a second and renders the same lines as --monitor. The buttons (and // the u/r keys) hit the action endpoints, like the monitor's hotkeys. diff --git a/cmd/gpu-turnstile/webui_test.go b/cmd/gpu-turnstile/webui_test.go index f41920e..d8585ec 100644 --- a/cmd/gpu-turnstile/webui_test.go +++ b/cmd/gpu-turnstile/webui_test.go @@ -170,3 +170,16 @@ func TestIsLoopbackHost(t *testing.T) { } } } + +func TestUIURL(t *testing.T) { + for addr, want := range map[string]string{ + "": "", + "127.0.0.1:7860": "http://127.0.0.1:7860", + ":7860": "http://127.0.0.1:7860", + "192.168.1.5:9000": "http://192.168.1.5:9000", + } { + if got := uiURL(addr); got != want { + t.Errorf("uiURL(%q) = %q, want %q", addr, got, want) + } + } +}