From f98547c41fa6aa94a5e75519c284d30f513ee98c Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Tue, 28 Jul 2026 17:51:52 +0200 Subject: [PATCH] fix(web-console): the paired-clients card counts both pairing planes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GameStream and native (punktfunk/1) clients pair into separate stores, and `/status` only ever reported the GameStream certificate count. Native is the DEFAULT plane, so a host whose clients had all paired normally — and which Moonlight had never touched — showed "0 paired" on the dashboard. Report the native count alongside it, the way the tray's own summary route already does, and sum the two in the card. Closes #10 Co-Authored-By: Claude Opus 5 (1M context) --- api/openapi.json | 9 ++++++++- crates/punktfunk-host/src/mgmt/host.rs | 8 +++++++- web/src/sections/Dashboard/view.tsx | 6 +++++- web/src/stories/lib/fixtures.ts | 2 ++ 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/api/openapi.json b/api/openapi.json index aa14d7e2..323af334 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -6355,6 +6355,7 @@ "audio_streaming", "pin_pending", "paired_clients", + "native_paired_clients", "active_sessions", "games" ], @@ -6376,10 +6377,16 @@ }, "description": "Every launched game the host is tracking: one row per live session that launched a title, plus\nany game whose session has ended and which is waiting out its reconnect window before being\nended (`state: \"grace\"`). Empty when nothing was launched — a plain desktop stream has no game." }, + "native_paired_clients": { + "type": "integer", + "format": "int32", + "description": "Number of paired native (punktfunk/1) devices — the default plane, so on a host that has\nnever been touched by Moonlight this is the only non-zero one of the pair.", + "minimum": 0 + }, "paired_clients": { "type": "integer", "format": "int32", - "description": "Number of pinned (paired) client certificates.", + "description": "Number of pinned (paired) GameStream client certificates. Native (punktfunk/1) devices pair\nagainst a separate store and are counted in `native_paired_clients` — sum the two for\n\"how many clients are paired with this host\".", "minimum": 0 }, "pin_pending": { diff --git a/crates/punktfunk-host/src/mgmt/host.rs b/crates/punktfunk-host/src/mgmt/host.rs index 4af1dbe9..ae0cb91d 100644 --- a/crates/punktfunk-host/src/mgmt/host.rs +++ b/crates/punktfunk-host/src/mgmt/host.rs @@ -100,8 +100,13 @@ pub(crate) struct RuntimeStatus { /// True while a pairing handshake is parked waiting for the user's PIN /// (submit it via `POST /api/v1/pair/pin`). pin_pending: bool, - /// Number of pinned (paired) client certificates. + /// Number of pinned (paired) GameStream client certificates. Native (punktfunk/1) devices pair + /// against a separate store and are counted in `native_paired_clients` — sum the two for + /// "how many clients are paired with this host". paired_clients: u32, + /// Number of paired native (punktfunk/1) devices — the default plane, so on a host that has + /// never been touched by Moonlight this is the only non-zero one of the pair. + native_paired_clients: u32, /// Number of live streaming sessions across BOTH planes (GameStream + native punktfunk/1). The /// native server admits concurrent sessions, so this can exceed 1; `session`/`stream` below /// describe a single representative session for the detail card. @@ -420,6 +425,7 @@ pub(crate) async fn get_status(State(st): State>) -> Json + {/* Both planes. GameStream and native (punktfunk/1) devices pair + into SEPARATE stores, and native is the DEFAULT one — counting + only the GameStream certs read as "0 paired" on a host every + one of whose clients was in fact paired. */} {m.status_paired_count()} - {s.paired_clients} + {s.paired_clients + s.native_paired_clients} diff --git a/web/src/stories/lib/fixtures.ts b/web/src/stories/lib/fixtures.ts index 3ae67109..c52454a2 100644 --- a/web/src/stories/lib/fixtures.ts +++ b/web/src/stories/lib/fixtures.ts @@ -46,6 +46,7 @@ export const statusActive: RuntimeStatus = { video_streaming: true, audio_streaming: true, paired_clients: 3, + native_paired_clients: 2, pin_pending: false, active_sessions: 1, session: { width: 5120, height: 1440, fps: 240 }, @@ -75,6 +76,7 @@ export const statusIdle: RuntimeStatus = { video_streaming: false, audio_streaming: false, paired_clients: 1, + native_paired_clients: 0, pin_pending: true, active_sessions: 0, session: null,