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
+18 -13
View File
@@ -150,11 +150,13 @@ pub(crate) struct StreamInfo {
pub(crate) struct LocalSummary {
/// Host version (mirrors `/health`).
version: String,
/// True while the video stream thread is running.
/// True while video is streaming on EITHER plane: the GameStream media pipeline, or a live
/// native (punktfunk/1) session — the default plane, invisible in the GameStream flag alone.
video_streaming: bool,
/// True while the audio stream thread is running.
/// True while audio is streaming on either plane (same rule as `video_streaming`).
audio_streaming: bool,
/// The active launch session (set by Moonlight's `/launch`, cleared on cancel/stop).
/// The active session: GameStream's launch (Moonlight `/launch`) when present, else the first
/// live native session. `null` when nothing is streaming.
session: Option<SessionInfo>,
/// Number of pinned (paired) GameStream client certificates.
paired_clients: u32,
@@ -391,26 +393,27 @@ pub(crate) async fn get_status(State(st): State<Arc<MgmtState>>) -> Json<Runtime
)
)]
pub(crate) async fn get_local_summary(State(st): State<Arc<MgmtState>>) -> Json<LocalSummary> {
// Native punktfunk/1 plane (the DEFAULT plane; GameStream is opt-in) — read ONCE and used for
// both the session card and the streaming flags below.
let native = crate::session_status::snapshot();
// GameStream launch, else the first live native session — so the tray reflects a native session
// too (same GameStream-only blind spot the Dashboard `/status` had; see `session_status`).
let session = st
.app
.launch
.lock()
.unwrap()
.unwrap_or_else(|e| e.into_inner())
.map(|l| SessionInfo {
width: l.width,
height: l.height,
fps: l.fps,
})
.or_else(|| {
crate::session_status::snapshot()
.first()
.map(|s| SessionInfo {
width: s.width,
height: s.height,
fps: s.fps,
})
native.first().map(|s| SessionInfo {
width: s.width,
height: s.height,
fps: s.fps,
})
});
let (native_paired_clients, pending_approvals) = st
.native
@@ -419,8 +422,10 @@ pub(crate) async fn get_local_summary(State(st): State<Arc<MgmtState>>) -> Json<
.unwrap_or((0, 0));
Json(LocalSummary {
version: env!("PUNKTFUNK_VERSION").into(),
video_streaming: st.app.streaming.load(Ordering::SeqCst),
audio_streaming: st.app.audio_streaming.load(Ordering::SeqCst),
// Either plane counts, like `/status`: reading only the GameStream flags made the tray say
// "idle" (and wear the idle icon) through an entire native session.
video_streaming: st.app.streaming.load(Ordering::SeqCst) || !native.is_empty(),
audio_streaming: st.app.audio_streaming.load(Ordering::SeqCst) || !native.is_empty(),
session,
paired_clients: st
.app