fix(tray): probe /login without redirects, and open the address the probe checked

Two ways the tray could misreport a healthy console.

The probe hit `/`, which is auth-gated and 302s to /login, and ureq follows
redirects by default — so TLS, `/`, and a full cold Nitro SSR of /login all had
to fit inside one 2s budget, and a console that was merely warming up read as
down. It now probes /login with redirects(0) on the agent: one round trip, and a
302 already counted as up. Neither user of that agent wants redirects; the
summary is a terminal JSON route.

The menu then opened https://localhost:<port> while the poller probed 127.0.0.1.
web-run.cmd binds HOST=0.0.0.0 — IPv4 only — and Windows resolves localhost to
::1 first, so the tray could call the console healthy and hand the browser a URL
that fails. Both are 127.0.0.1 now, which is what the Linux tray already did, so
the menu can no longer disagree with the status printed above it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-30 20:42:50 +02:00
co-authored by Claude Opus 5
parent d9912aa795
commit ef2bb56251
2 changed files with 16 additions and 2 deletions
+10 -1
View File
@@ -194,7 +194,12 @@ fn poll_loop(
} else {
format!("https://{mgmt_addr}:{mgmt_port}/api/v1/local/summary")
};
let console_url = format!("https://127.0.0.1:{web_port}/");
// `/login`, not `/`: `/` is auth-gated and 302s to `/login`, and ureq follows redirects by
// default — so probing `/` spent TLS + `/` + a full cold `/login` SSR render inside one 2 s
// budget, and a console that was merely warming up read as down. `/login` is the cheapest page
// that proves the server is answering, and the agent below refuses redirects so the probe is
// exactly one round trip. (A 302 still counts as up via the `Status` arm in `probe_console`.)
let console_url = format!("https://127.0.0.1:{web_port}/login");
let agent = agent(load_pin());
let mut last: Option<(TrayStatus, bool)> = None;
// When the summary became unreachable while the service was running (grace anchor).
@@ -313,6 +318,10 @@ fn agent(pin: Option<[u8; 32]>) -> ureq::Agent {
.tls_config(Arc::new(cfg))
.timeout_connect(Duration::from_secs(2))
.timeout(Duration::from_secs(2))
// No redirect-following. Neither user of this agent wants it: the summary is a terminal
// JSON route, and the console probe treats any HTTP answer (302 included) as "up", so
// chasing the hop only spends the 2 s budget re-rendering a page nobody reads.
.redirects(0)
.build()
}
+6 -1
View File
@@ -559,9 +559,14 @@ fn elevate_service(hwnd: HWND, verb: &str) {
/// Open the web console at `path` ("" = dashboard). Deep links land the operator on the page the
/// menu entry promised — the pairing queue, the virtual displays — instead of the dashboard.
fn open_web_console(hwnd: HWND, path: &str) {
// 127.0.0.1, not `localhost`: the console binds HOST=0.0.0.0 (scripts/windows/web-run.cmd),
// which is IPv4-ONLY, while Windows resolves `localhost` to ::1 first. A browser that does not
// fall back cleanly got connection-refused on a perfectly healthy console — and because the
// poller probes 127.0.0.1, the tray would call it up while handing over a URL that fails. Same
// literal in both places, so the menu can never disagree with the status next to it.
shell_open(
hwnd,
&format!("https://localhost:{}/{path}", app().web_port),
&format!("https://127.0.0.1:{}/{path}", app().web_port),
);
}