From ef2bb56251aeee36fb8b7d88cf07a6f3da858803 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 30 Jul 2026 20:42:42 +0200 Subject: [PATCH] fix(tray): probe /login without redirects, and open the address the probe checked MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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: 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) --- crates/punktfunk-tray/src/status.rs | 11 ++++++++++- crates/punktfunk-tray/src/win.rs | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/punktfunk-tray/src/status.rs b/crates/punktfunk-tray/src/status.rs index a08b48c3..331af3ed 100644 --- a/crates/punktfunk-tray/src/status.rs +++ b/crates/punktfunk-tray/src/status.rs @@ -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() } diff --git a/crates/punktfunk-tray/src/win.rs b/crates/punktfunk-tray/src/win.rs index f8c9e485..c45d7aed 100644 --- a/crates/punktfunk-tray/src/win.rs +++ b/crates/punktfunk-tray/src/win.rs @@ -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), ); }