fix(tray): count native sessions as streaming, and stop hiding "Open web console"
apple / swift (push) Successful in 1m30s
ci / web (push) Successful in 1m1s
windows-host / package (push) Failing after 4m40s
ci / docs-site (push) Successful in 1m8s
ci / bench (push) Successful in 5m31s
decky / build-publish (push) Successful in 29s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
arch / build-publish (push) Successful in 11m30s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 41s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
android / android (push) Successful in 15m55s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 5m36s
deb / build-publish (push) Successful in 9m54s
deb / build-publish-host (push) Successful in 9m42s
ci / rust (push) Failing after 21m48s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 7m45s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 9m57s
apple / screenshots (push) Successful in 22m35s
docker / deploy-docs (push) Successful in 25s

The tray sat at "Idle" through an entire stream, and its Windows context menu
could come up without its main action. Two separate causes.

`GET /api/v1/local/summary` reported `video_streaming`/`audio_streaming` straight
from `AppState.streaming` — flags only the GameStream media pipeline raises. The
native punktfunk/1 plane is the DEFAULT (GameStream is opt-in) and never touches
them, so every native session read as idle: idle icon, and a tooltip that printed
that session's own resolution next to the word "idle" (the `session` field was
already native-aware). This is the blind spot `/status` was fixed for — see the
`session_status` module doc — which the summary kept. Both flags now OR in a live
native session, and the tray additionally treats a reported session as streaming,
so a new tray reads an older host correctly too.

The menu built "Open web console" (and the pairing entry) only while a live
loopback probe of the console had just succeeded. A console still starting, or an
SSR render slower than the 2 s probe timeout, therefore deleted the tray's
most-wanted action outright — indistinguishable from a tray that never had one,
with left-clicking the icon as the only, undiscoverable, way in. The entry is now
always present and the default item; a failed probe changes its label to
"(not responding)" instead of hiding it, and takes two consecutive misses to say
so, since one timeout is not "down".

While here: a "Release kept display…" entry when displays are held (the summary
field's own doc promised a one-click release that did not exist), and entries
deep-link to /pairing and /displays instead of all landing on the dashboard. The
Linux SNI menu mirrors all of it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 00:46:30 +02:00
co-authored by Claude Opus 5
parent 09aa2db37c
commit d49f1bba49
7 changed files with 204 additions and 44 deletions
+27 -9
View File
@@ -15,8 +15,8 @@ use crate::status::{self, Poller, TrayStatus};
struct HostTray {
status: TrayStatus,
web_port: u16,
/// The console answered the poller's live loopback probe — the "Open web console" entry is
/// shown iff opening it would actually work (repo-run consoles included, stopped ones not).
/// The console answered the poller's live loopback probe — labels the (always present) "Open
/// web console" entry; it never hides it.
web_console: bool,
/// Filled right after `spawn` (the poller needs the tray handle first) — lets menu actions
/// force an immediate re-poll instead of waiting out the cadence.
@@ -33,8 +33,10 @@ impl HostTray {
}
}
fn open_console(&self) {
let url = format!("https://127.0.0.1:{}", self.web_port);
/// 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.
fn open_console(&self, path: &str) {
let url = format!("https://127.0.0.1:{}/{path}", self.web_port);
let _ = std::process::Command::new("xdg-open").arg(url).spawn();
}
}
@@ -107,17 +109,33 @@ impl ksni::Tray for HostTray {
}
.into(),
MenuItem::Separator,
// Always present — it is the reason most people open this menu. When the loopback
// probe says the console isn't answering, the label says so rather than the entry
// disappearing (a menu missing its main action reads as a broken tray).
StandardItem {
label: "Open web console".into(),
visible: self.web_console,
activate: Box::new(|t: &mut Self| t.open_console()),
label: if self.web_console {
"Open web console".to_string()
} else {
"Open web console (not responding)".to_string()
},
activate: Box::new(|t: &mut Self| t.open_console("")),
..Default::default()
}
.into(),
StandardItem {
label: "Approve pairing request…".into(),
visible: self.web_console && self.status.pairing_attention(),
activate: Box::new(|t: &mut Self| t.open_console()),
visible: self.status.pairing_attention(),
activate: Box::new(|t: &mut Self| t.open_console("pairing")),
..Default::default()
}
.into(),
StandardItem {
label: match self.status.kept_displays() {
1 => "Release kept display…".to_string(),
n => format!("Release {n} kept displays…"),
},
visible: self.status.kept_displays() > 0,
activate: Box::new(|t: &mut Self| t.open_console("displays")),
..Default::default()
}
.into(),