fix(tray): probe /login without redirects, and open the address the probe checked
ci / docs-site (push) Successful in 1m17s
apple / swift (push) Successful in 1m17s
ci / rust-arm64 (push) Successful in 1m48s
ci / web (push) Successful in 1m56s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 8s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 6s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 9s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
deb / build-publish-client-arm64 (push) Successful in 2m6s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
deb / build-publish (push) Successful in 5m3s
docker / builders-arm64cross (push) Successful in 6s
deb / build-publish-host (push) Successful in 4m14s
docker / deploy-docs (push) Successful in 27s
ci / rust (push) Successful in 6m52s
android / android (push) Successful in 7m11s
arch / build-publish (push) Failing after 7m24s
apple / screenshots (push) Successful in 14m27s
windows-host / package (push) Successful in 18m20s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m38s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 17m10s

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),
);
}