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
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:
@@ -75,7 +75,7 @@ impl TrayStatus {
|
||||
TrayStatus::Degraded => "punktfunk host — running (status unavailable)".into(),
|
||||
TrayStatus::Error(e) => format!("punktfunk host — failed ({e})"),
|
||||
TrayStatus::Running(s) => {
|
||||
let base = match (&s.session, s.video_streaming) {
|
||||
let base = match (&s.session, self.is_streaming()) {
|
||||
(Some(sess), true) => format!(
|
||||
"punktfunk host {} — streaming {}×{}@{}",
|
||||
s.version, sess.width, sess.height, sess.fps
|
||||
@@ -113,8 +113,23 @@ impl TrayStatus {
|
||||
matches!(self, TrayStatus::Running(s) if !s.conflicts.is_empty())
|
||||
}
|
||||
|
||||
/// A client is streaming: the host's flag, OR a live session in the summary.
|
||||
///
|
||||
/// The session is checked too because a host from before the `get_local_summary` fix raised
|
||||
/// `video_streaming` from the GameStream plane only — through a whole session on the native
|
||||
/// (default) plane it said false while still reporting that session's mode, which is what left
|
||||
/// the tray sitting at "idle" mid-stream. This keeps a newer tray honest against such a host.
|
||||
pub fn is_streaming(&self) -> bool {
|
||||
matches!(self, TrayStatus::Running(s) if s.video_streaming)
|
||||
matches!(self, TrayStatus::Running(s) if s.video_streaming || s.session.is_some())
|
||||
}
|
||||
|
||||
/// Virtual displays held with no live session (lingering/pinned) — offered as a one-click
|
||||
/// release in the menu, since holding one can also be keeping physical monitors dark.
|
||||
pub fn kept_displays(&self) -> u32 {
|
||||
match self {
|
||||
TrayStatus::Running(s) => s.kept_displays,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// A pairing attempt is waiting on the operator (shown as an extra menu entry).
|
||||
@@ -156,9 +171,11 @@ struct Shared {
|
||||
|
||||
impl Poller {
|
||||
/// Spawn the poll thread; `on_change(status, console_up)` fires (from that thread) whenever
|
||||
/// either changes. `console_up` is a live loopback probe of the web console on `web_port` —
|
||||
/// ground truth for the "Open web console" menu entry (a layout sniff would miss consoles run
|
||||
/// from a repo checkout, and shows a dead entry while an installed console is still starting).
|
||||
/// either changes. `console_up` is a live loopback probe of the web console on `web_port`. It
|
||||
/// annotates the "Open web console" entry ("not responding") rather than hiding it: the entry
|
||||
/// is the tray's most-wanted action, and a menu that silently drops it — because the console
|
||||
/// was still starting, or the probe timed out — is indistinguishable from a tray that never
|
||||
/// had one.
|
||||
pub fn spawn(
|
||||
mgmt_addr: String,
|
||||
mgmt_port: u16,
|
||||
@@ -203,6 +220,10 @@ fn poll_loop(
|
||||
// When the summary became unreachable while the service was running (grace anchor).
|
||||
// Runs for the process lifetime (the tray exits by process exit; nothing to unwind).
|
||||
let mut unreachable_since: Option<Instant> = None;
|
||||
// Consecutive failed console probes. One miss is not "down": the console is a bun/Nitro SSR
|
||||
// whose cold first render can outrun this agent's 2 s timeout, and a menu entry that changes
|
||||
// its label every few seconds reads as broken.
|
||||
let mut console_misses = 0u32;
|
||||
loop {
|
||||
let svc = probe_service();
|
||||
let summary = if svc == ServiceState::Running {
|
||||
@@ -219,7 +240,13 @@ fn poll_loop(
|
||||
};
|
||||
let grace_expired = unreachable_since.is_some_and(|t| t.elapsed() >= START_GRACE);
|
||||
let status = map_status(&svc, summary, grace_expired);
|
||||
let console_up = probe_console(&agent, &console_url);
|
||||
let console_up = if probe_console(&agent, &console_url) {
|
||||
console_misses = 0;
|
||||
true
|
||||
} else {
|
||||
console_misses += 1;
|
||||
console_misses < 2
|
||||
};
|
||||
if last.as_ref() != Some(&(status.clone(), console_up)) {
|
||||
on_change(status.clone(), console_up);
|
||||
last = Some((status, console_up));
|
||||
@@ -478,6 +505,32 @@ mod tests {
|
||||
.contains("status unavailable"));
|
||||
}
|
||||
|
||||
/// A live session means streaming even if the host's flag says otherwise — a host from before
|
||||
/// the `get_local_summary` fix only raised `video_streaming` for the GameStream plane, so a
|
||||
/// native session showed as "idle" with its own mode printed next to it.
|
||||
#[test]
|
||||
fn a_live_session_reads_as_streaming_without_the_flag() {
|
||||
let mut s = summary(true);
|
||||
s.video_streaming = false; // pre-fix host, native session
|
||||
let st = TrayStatus::Running(s);
|
||||
assert!(st.is_streaming());
|
||||
assert_eq!(
|
||||
st.headline(),
|
||||
"punktfunk host 0.5.1 — streaming 2560×1440@120"
|
||||
);
|
||||
// No session and no flag is still idle.
|
||||
assert!(!TrayStatus::Running(summary(false)).is_streaming());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kept_displays_are_reported_for_the_release_action() {
|
||||
assert_eq!(TrayStatus::Running(summary(false)).kept_displays(), 0);
|
||||
let mut s = summary(false);
|
||||
s.kept_displays = 2;
|
||||
assert_eq!(TrayStatus::Running(s).kept_displays(), 2);
|
||||
assert_eq!(TrayStatus::Degraded.kept_displays(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pairing_attention_flags() {
|
||||
let mut s = summary(false);
|
||||
|
||||
Reference in New Issue
Block a user