listGpus gains encoder_pin (PUNKTFUNK_ENCODER when it actually pins something —
unset/auto stay null), and the console's GPU card shows it: a muted note when
the pin is compatible, an amber warning naming the pinned vendor and the next
session's GPU when it contradicts the selection (the host overrides such a pin
at session open — without this field the selection just looked broken). Docs
updated to say the adapter wins.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`<CardContent className="p-0">` never removed the padding it looked like it
removed: tailwind-merge resolves conflicts within a variant, so `p-0` cancels
`p-4` but leaves `sm:p-6` standing. Every call site that used it nests a
CardHeader inside, which brings its own `sm:p-6` — so at >=640px the title sat
24px further in than its neighbours'. Visible on the Plugins page as 'Installed
plugins' not lining up with 'Plugin runner'.
Give CardContent a `flush` prop that omits the padding outright, and use it at
the three call sites (Store, Pairing, Stats) rather than fixing the symptom
three times and leaving the footgun in place.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two things looked wrong in the store:
The cards had no space above the icon. `CardContent` zeroes its own top padding
(`pt-0`/`sm:pt-0`) because it normally sits under a `CardHeader` that supplies
it — but these cards have no header, so the top padding simply vanished while
the other three sides kept `p-card`. `p-card` does not undo it: `card` is a
custom `--spacing-*` token, which tailwind-merge does not recognise as a
spacing value and so never dedupes against `pt-0`, leaving the longhand to win.
Both header-less cards in this section now restore it explicitly, at both
breakpoints.
Platform chips read "windows" / "linux". Those are the catalog's platform
IDENTIFIERS (the index validator pins `linux | windows | macos`), which is right
for the wire and wrong on screen; the card now maps them to display names —
Linux, Windows, macOS. Proper nouns, so deliberately not routed through i18n.
Adds Store/StoreCard stories covering the padding, the platform labels, and the
installed / update / incompatible / blocked / external states, so the card can
be eyeballed without a host or a catalog.
Console: a Plugins section (Browse / Installed / Sources) on a static nav
entry, with install friction proportional to trust — a plain confirm for a
verified entry, a warning naming the curator for an external one, and a
danger dialog that makes you retype the spec for a raw package. Tier badges
are permanent and follow the plugin onto its own UI page.
Index: unom/punktfunk-plugin-index published and served from Gitea's raw
endpoint (real HTTPS, byte-exact, no vhost to stand up) — merge to main is
publish, which resolves the design's open hosting question.
Four things only running it could find:
- runner discovery matched @punktfunk/plugin-* only, so a third-party
scoped plugin (which D8 requires) would install and never run
- ...and that convention also matches @punktfunk/plugin-kit, a plugin's
own framework: it listed as installed and would have been imported as a
unit. Both now key off the plugins dir's top-level dependencies, with an
emptied dependency list meaning 'nothing installed' rather than falling
back to the naming convention
- the store must not pass new flags to the runner: the scripting package
ships separately and an older one reads an unknown flag's value as a
package name. The host writes the bunfig scope mapping itself
- ureq reports only >= 400 as Err, so a conditional request's 304 arrives
as Ok with an empty body — handled as an error it made every refresh
after the first verify a signature over zero bytes and sit stale
Also: the console's first Tabs use exposed an @unom/ui theme gap that
rendered inactive tabs invisible (caught in a browser pass, not by types).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CardContent is shadcn's header-adjacent variant (`p-4 pt-0 sm:p-6 sm:pt-0`),
but the four Live-status tiles use it with no CardHeader. tailwind-merge lets
the call site's unprefixed `p-4` cancel the base `pt-0`, yet nothing cancels
`sm:pt-0` — so the tiles lost their top padding at >=640px only, and the
content sat against the top edge. Restores the top inset at the sm breakpoint
and lets the content fill the stretched grid cell so it stays centred when a
sibling tile is taller.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every installed-store scanner (Steam; Lutris+Heroic on Linux; Epic/GOG/
Xbox on Windows) was hardwired on. New library-scanners.json persists the
operator's disabled set (default all on; absent/malformed = all on);
all_games() gates each provider, so disabling one hides its titles from
every surface (console grid, native clients, GameStream app list, launch
resolve). GET /library/scanners lists this platform's scanners + state;
PUT /library/scanners/{id} toggles and emits library.changed — admin lane
only (the cert allowlist's exact-path /library match keeps both off the
LAN surface). The console's Library page grows a "Game sources" card with
one chip per scanner (platform-shaped by the API), EN+DE strings, story.
The scanners are slated to become plugins; the stable per-scanner ids are
the migration seam.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pairing:
- Refresh the paired-devices list after a native PIN pairing (the happy path never
invalidated it, so a newly paired device stayed hidden until remount).
- Moonlight PIN: a 204 means "PIN delivered to the waiting handshake", NOT paired, so
it now reads "PIN sent" instead of a false "Paired successfully".
- Hide the Moonlight pairing card on native-only hosts (HostInfo.gamestream) — it could
never receive a PIN there.
- Per-row pending on unpair/approve/deny; PIN input maxLength 16 (was 8).
Displays / Library:
- "Arrange displays" save refreshes the settings card (it rewrites the policy), without
clobbering unsaved Custom edits (re-seed only when the draft still matches the server).
- Live-display list wrapped in QueryState so errors don't read as "no displays".
- "Forever" keep-alive option in the custom editor; edit-game form round-trips the logo
artwork (was dropped on save); per-card delete pending.
Stats:
- Distinct colour for the native "queue" latency stage (it collided with "capture").
- "Not measured on this path" note on the GameStream health chart; configured-bitrate
target line on throughput; host-authoritative elapsed timer; LiveCard surfaces
non-404 errors.
Shell / auth / i18n:
- SSR-stable locale: first client render matches the base-locale SSR (no hydration
mismatch), then adopts the persisted/browser locale post-hydration.
- BFF proxy maps an upstream (mgmt-token) 401 to 502 so a logged-in user isn't bounced
into a post-login redirect loop.
- Logout checks the POST result before navigating; logs dedup by seq (StrictMode);
login "next" keeps query/hash; Dashboard shows the active-session count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second experiment against the connected-but-dark-head stutter (field-proven
on the reporter's box: unplugging his standby HDMI TV removes a metronomic
~4 s double-jolt; DDC/CI is a dead end for TVs — measured on the lab LG, VCP
0xD6 gets no I2C ACK). An Exclusive isolate only removes physical monitors
from the CCD topology; their PnP devnodes stay live, so every standby wake
(auto input scan, Instant-On HPD cycling) still triggers the full Windows
reaction: PnP arrival/removal, CCD re-evaluation, DWM invalidation — the
suspected hiccup mechanism (Apollo #368's Device-Manager-refresh signature).
- New `pnp_disable_monitors` display-policy axis (default off): orthogonal
to presets like game_session/ddc_power_off, surfaced in GET/PUT
/display/settings + the enforced list, carried through the layout
transform.
- windows/monitor_devnode.rs: after the isolate takes, disable exactly the
deactivated monitors' devnodes — CCD target → monitor device path
(DISPLAYCONFIG_TARGET_DEVICE_NAME) → PnP instance id → CM_Disable_DevNode
with CM_DISABLE_PERSIST, so a hot-plug RE-ARRIVAL stays disabled (that
persistence is the whole point). Teardown re-enables BEFORE the CCD
restore (+300 ms re-arrival settle) so restored paths have their monitors
back. Precise selection — co-installed third-party virtual displays are
never touched.
- Crash safety: instance ids journal to <config>/pnp-disabled-monitors.json
before disabling; serve startup re-enables leftovers from a crashed host.
Worst case is documented in the console help (Device Manager re-enable).
- Web console: second Experimental-badged toggle (the DDC block refactored
into a shared ExperimentalToggle), EN/DE strings, preset-switch carry.
Verified: Linux 263 tests + clippy + fmt clean; Windows (RTX box) 220/220 +
clippy clean; web tsc + production build clean; openapi.json regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The sole-virtual-display stutter investigation's active experiment: when the
Exclusive isolate deactivates a physical monitor, the dark-but-connected head
keeps getting serviced (monitor standby auto-input-scan / DP link churn) at a
seconds-scale cadence — the leading suspect for the periodic double-jolt. A
panel commanded off over DDC/CI (the VESA monitor-control channel in the video
cable) believes it has an owner and, on cooperating firmware, stops probing.
- New `ddc_power_off` display-policy axis (default off): orthogonal to presets
like game_session, stored in display-settings.json, surfaced in GET/PUT
/display/settings + the enforced list, carried through the layout transform.
- windows/ddc.rs: VCP 0xD6 power-mode control via the dxva2 Physical Monitor
API. Deliberately DPMS-off (0x04, DDC stays responsive, signal return wakes)
and never power-button-off (0x05, bricks-until-button on many monitors).
Probe-before-write; every failure is skip-and-log — monitors without DDC/CI,
OSD-disabled, or behind docks/KVMs degrade to a logged no-op.
- Manager wiring: panels commanded off immediately BEFORE the Exclusive CCD
isolate (an HMONITOR — and with it the DDC channel — only exists while the
display is active); teardown wakes them right after the CCD restore, where
returning signal alone already wakes most firmware.
- Web console: an Experimental-badged on/off control on the display card,
applied immediately like the game-session axis and preserved across preset
switches; EN/DE strings incl. the wake-failure escape hatch (press the
monitor's power button once, turn the option off).
Diagnostic value on top of the fix: if this kills a reporter's stutter, the
churn is monitor-firmware-initiated; if only topology=primary/extend does, the
driver services dark heads regardless — the two remaining root-cause classes.
Verified: Linux 258 tests + clippy + fmt clean; Windows (RTX box) 220/220 +
clippy clean; web tsc + production build clean; openapi.json regenerated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address findings from a repo security review:
- core: add a sliding-window anti-replay filter over the AEAD-authenticated
sequence in Session (poll_input/poll_frame), closing the input-replay gap the
data plane previously left to the LAN/VPN trust assumption. 4096-deep window,
unit-tested; the encrypted loopback suite confirms no false drops.
- clients: write the mTLS client private key 0600 and lock the config dir 0700
on Unix (it was world-readable at the umask default), re-locking existing
stores on load. pf-client-core::trust plus the probe's own identity writer.
Windows keeps the %APPDATA% ACL; Android/Apple already wrap the key.
- web: fix a post-login open redirect — resolve `next` via URL and require it to
stay same-origin, rejecting `/\evil.com` and tab/encoding variants the old
`!startsWith("//")` guard missed. Also fixes the dead safeNextPath helper.
- ci: SHA-256-pin the BtbN FFmpeg DLLs bundled into the signed Windows installer
(were fetched from the rolling `latest` tag unverified); fails closed on a
re-roll, matching the VB-CABLE gate.
- ci: fail-open fork-guard on the Windows/Apple host-mode PR build jobs that
share runner labels with the signing jobs. Definitive fix stays server-side
(Gitea outside-collaborator approval / isolated PR runners) — see the notes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Favicon: the brand-mark lens SVG at /favicon.svg, linked from the root head.
- Branding: user-facing "punktfunk" → "Punktfunk" (tab title, app_name,
pairing prose, the logo aria-labels/SVG titles) — protocol identifiers
(punktfunk/1, _punktfunk._udp, binary/module names) stay lowercase.
- The display policy auto-saves on every preset pick / field edit with no
signal; mount @unom/ui's sonner Toaster in the root and fire a success toast
from the save funnel's onSuccess (new i18n key, en + de). First toast usage
in the console — errors stay inline as before.
- The stop-session button rendered destructive-red even with nothing to stop:
variant switches to the muted `secondary` while no session is active.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Save named bundles of the display-management policy (the six behavior axes
plus the game-session axis) as custom presets, alongside the built-ins. A
custom preset is data — stored in <config>/display-presets.json — not a Preset
enum variant, so DisplayPolicy::effective() stays pure and the built-in set is
untouched; applying one writes a Custom policy via the existing PUT
/display/settings.
- policy.rs: CustomPreset/CustomPresetInput + load/add/update/delete store
- mgmt.rs: GET/POST /display/presets + PUT/DELETE /display/presets/{id},
surfaced on GET /display/settings
- web console: custom-preset cards with save-as / edit / delete + i18n
- regenerated api/openapi.json; docs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Console polish on the Virtual displays card + shell:
- Topology help now leads with the streamed display's role (Extend/Primary/Exclusive) instead of
the confusing physical-monitor-only framing; notes the headless case. Identity help spells out the
actual behavior (stable per-client identity → the desktop reapplies that client's scaling/resolution
on reconnect) + what Shared / Per-client / Per-client+resolution each do.
- Cap description/help width at max-w-prose so long help text isn't a full-viewport line on large screens.
- Mobile bottom nav: 8 flat tabs were too cramped → 4 pinned tabs + a "More" tab whose sheet holds the
rest (Performance/Logs/Pairing/Settings), "More" highlighted when the active route is in the overflow.
- More breathing room under the "Preset" heading.
web tsc + biome + vite build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the last §6A-era preset. The Linux registry already resolved forever→Pinned (pure
lifecycle machine); the blockers were the Windows manager, the mgmt reject, and the console tag:
- Windows manager: new `MgrState::Pinned { mon }` — the last-released monitor under keep_alive=forever
is kept indefinitely (like Lingering but the linger timer never fires). A reconnect preempts +
recreates it (same as Lingering — a reused IddCx swap-chain is dead), snapshot reports "pinned",
and `force_release` (POST /display/release, the §8 escape hatch) frees a pinned monitor. release()
branches on the new `keep_alive_forever()`; all MgrState matches made exhaustive over Pinned.
- mgmt PUT /display/settings: stop rejecting keep_alive=forever (now honored on both platforms with a
release path). OpenAPI regenerated.
- web: un-disable the gaming-rig preset (DISABLED_PRESETS now empty) — one-click applies.
Linux paths + web/tsc/openapi green; 47 vdisplay tests pass. The Windows manager.rs is #[cfg(windows)]
(not compilable on the Linux dev box) — build-verified + on-glass validation on .173 to follow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The preset options were raw <button>s — flat, no motion/material — unlike the rest of
the console. They now render as the `interactive` AnimatedCard (motion hover + specular
material, consistent with every other card), keyboard-accessible (role=button + Enter/
Space), with a 2px primary ring for the active one and a proper disabled state for
gaming-rig.
web tsc + vite build + biome-lint green; deployed on .21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Every option in the custom form now renders through one `Field` wrapper (label →
control → help at a consistent `space-y-3`), so the label→input gap is roomier and
identical across keep-alive, the button groups, and max-displays — the first field no
longer spaces differently from the rest.
- Reworded the multi-monitor layout help: it now says Auto is side-by-side and Manual
gives a per-display X/Y editor "in the Live displays section below once two or more are
streaming" — instead of pointing at an "arrangement table" that isn't visible until
clients connect.
web tsc + vite build + biome-lint green; deployed on .21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two UX fixes on the Virtual displays Configuration card:
- Keep-alive is no longer implicitly "on" by typing in the seconds field. It's an
explicit two-button toggle — **Off** (tear down at disconnect) vs. **Keep for** [N]
seconds — and the seconds input only appears when "Keep for" is selected. The
duration is remembered across toggles, and the help text explains both modes.
- Opened up the cramped custom form: the fields container is `space-y-6` with more
padding (`p-5`, rounded-lg), each option group is `space-y-2.5`, and the Save button
sits below a divider — so it reads as sections with room instead of a pressed stack.
web tsc + vite build + biome-lint green; deployed on .21.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Host page was crowded (identity, codecs, ports, GPU, displays, compositors) and the
virtual-display config surface is large enough to warrant its own home.
- New **Virtual displays** nav section: `/displays` route + `sections/Displays` (moved
DisplayCard out of `sections/Host`), a `MonitorPlay` sidebar entry after Host, and
`nav_displays` i18n. Removed the displays card from the Host page/view.
- On its own page the card splits into two: **Configuration** (presets + custom axes) and
**Live displays** (the live list + arrangement table) — room to breathe.
- Presets now render in a max-2-column grid (`sm:grid-cols-2`) with larger padding, a bigger
section heading + preset titles (text-base semibold), roomier spacing, and bottom-aligned
"what it sets" badges so the cards line up.
web tsc + vite build + biome-lint green; deployed + verified on the Mutter box (.21).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Virtual displays card previously only exposed keep_alive/topology/max_displays as
editable custom fields; conflict/identity/layout (enforced since Stages 3-5) had no
controls, and the presets weren't surfaced as one-click options. Rework the card so the
whole policy is configurable WITHOUT any client connected:
- Presets front-and-center: each of the five (default/shared-desktop/hotdesk/workstation/
gaming-rig) is a one-click row showing its story AND what it sets (keep-alive · topology ·
conflict · identity badges), highlighting the active one. A click applies it immediately.
gaming-rig stays disabled + "coming soon" (keep_alive: forever isn't cross-platform yet).
- Custom mode reveals EVERY axis editably — keep-alive, topology, conflict, identity, layout,
max-displays — seeded from the current effective behavior, with a Save button. A reusable
`Choice` button-group + a tolerant `tr()` label lookup keep it tidy.
- The live-display list + multi-monitor arrangement table stay below (they need a live
session); the settings above work standalone.
- en+de i18n for the new controls; refreshed the effective-preview row to show all axes.
web tsc + vite build + biome-lint green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes Stage 5's web piece (design/display-management.md §6.2): a `DisplayArrangement`
editor in the Virtual displays card. For a ≥2-display group, it renders an x/y table over
the live displays that carry a stable identity slot (the manual-layout key), seeded from
the current computed positions; Save writes `PUT /display/layout` (via the generated
`useSetDisplayLayout`), which switches the host to a manual layout applied from the next
connect. Shared/anonymous displays (no identity slot) are omitted (they can't be pinned).
Also refreshes the now-stale `display_pending_note` copy (conflict/identity/layout ARE
enforced as of Stages 3-5) in en + de.
web tsc + vite build green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stage 1 of design/display-management.md — the lifecycle core + the display
management surface:
- vdisplay/lifecycle.rs: pure per-slot state machine (Idle/Active{refs}/
Lingering{until}/Pinned) with acquire/release/expiry/force-release
transitions. No I/O, no OS types — the platform-neutral distillation of the
Windows manager's model. Unit + a 200k-iteration seeded property walk
(no leaks / double-frees / refcount underflow across arbitrary interleavings).
- vdisplay/registry.rs: neutral snapshot/release facade over the per-OS
lifecycle owners. Windows reads/controls the VirtualDisplayManager; Linux
keep-alive (a per-session pool) lands in a following increment (needs GPU-box
validation).
- windows/manager.rs: additive snapshot() + force_release() (no behavior change
to the on-glass-validated path).
- mgmt: GET /api/v1/display/state (live/kept displays) + POST /api/v1/display/release
(tear down lingering/pinned now; refuses active). OpenAPI regenerated.
- web console: Virtual displays card gains a live-display list (polled) with
per-row + release-all buttons and a linger countdown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A user-configurable policy layer above the per-compositor VirtualDisplay
backends: keep-alive, topology, conflict, identity, layout, max-displays —
persisted to display-settings.json, editable from the web console, applied
per connect. Design: design/display-management.md.
Stage 0 stands up the surface and wires the two behaviors the existing code
can already express — the Windows monitor linger duration and the
"make the streamed output the sole desktop" topology — through it; every
other option is stored + echoed but not yet enforced (later stages). An
unconfigured host (no display-settings.json) keeps today's exact behavior.
- vdisplay/policy.rs: pure DisplayPolicy + 5 presets + JSON store (gpu-settings
pattern) + EffectivePolicy; 9 unit tests.
- vdisplay.rs: resolve_topology(Auto); apply_session_env drives *_VIRTUAL_PRIMARY
from the policy only when a settings file exists.
- windows/manager.rs: linger_ms() + should_isolate() read the policy when configured.
- mgmt: GET/PUT /api/v1/display/settings (bearer-only); PUT rejects keep_alive
forever until the lifecycle stage. OpenAPI regenerated.
- web console: Host → Virtual displays card (preset picker + custom fields); en+de.
- docs-site: virtual-displays.md + configuration.md cross-links.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remote debugging without shell access: a tracing layer tees every
event at DEBUG-and-up — independent of the RUST_LOG filter gating
stderr/host.log, so console-side debugging never needs a restart —
into a bounded in-memory ring (log_capture.rs, 4096 newest entries,
OnceLock singleton like config()), installed at both init sites
(stderr path in main, the Windows service file path). The mgmt API
serves it cursor-paged at GET /api/v1/logs?after=&limit= — bearer-only
and deliberately NOT on the mTLS cert allowlist (log lines can name
client identities and host paths). The web console grows a Logs page
(follow/pause · min-level filter · text search · eviction-gap badge);
polling self-paces: a non-empty page advances the after-cursor (new
query key → immediate refetch, drains backlogs), an empty page idles
at the 2s interval. OpenAPI regenerated; ring pagination/eviction,
layer wiring, and the authed route are unit-tested; Storybook story
included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new crate::gpu (compiled on all platforms so the OpenAPI doc stays
platform-independent): DXGI / sysfs GPU inventory with reboot-stable ids
(PCI vendor:device + occurrence — LUIDs are per-boot), persisted auto/manual
preference (<config>/gpu-settings.json, atomic temp+rename with in-memory
rollback), one selection with precedence console preference >
PUNKTFUNK_RENDER_ADAPTER > max VRAM and graceful fallback when the preferred
GPU is absent, plus a live "in use" record (RAII session guard wrapped around
every encoder open_video returns)
- fix: windows_gpu_vendor derived the encoder backend from DXGI adapter 0
instead of the selected render adapter — on a hybrid box (e.g. Intel iGPU at
index 0 + NVIDIA dGPU) the backend could disagree with the GPU the capture
ring / IddCx render pin sit on. The NVENC 4:4:4 probe now also runs on the
selected adapter (was: OS default), the codec/4:4:4 probe caches are keyed
per selected GPU (were process-lifetime OnceLocks), and an explicit
PUNKTFUNK_ENCODER conflicting with the selected GPU's vendor warns up front
- mgmt API: GET /api/v1/gpus (inventory + mode + preferred + next-session
selection with reason + in-use GPU/backend/session-count) and
PUT /api/v1/gpus/preference (validates mode/gpu_id before writing);
openapi.json regenerated; the vdisplay render pin now also engages for a
console preference (not just the env pin)
- web console: GPU card on the Host page — list with vendor + VRAM,
Automatic / Prefer controls, Preferred / Next session / "In use · backend"
badges, missing-preferred-GPU warning and env-pin note; en + de messages
- Linux: a matched manual preference picks the VAAPI render node and the
NVENC-vs-VAAPI auto choice; auto mode is exactly the previous behavior
Validated live on the hybrid laptop (RTX 3500 Ada + Intel Arc Pro, which
enumerates twice — the occurrence ids disambiguate): enumerate, prefer,
bad-id 400, restart persistence, auto-restore keeping the stored pick.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Web console
- Pairing/Library/Stats refactored into self-contained subsections that each own
their own queries + mutations; a shared slot-based layout (view.tsx) is filled by
the live page (containers) and Storybook (pure cards + fixtures) so the layout can't
drift.
- All paired devices in one list on Pairing with a protocol column (punktfunk/1 +
Moonlight), routing each unpair to the right endpoint; the redundant Clients page is
removed.
- Library: overview grid split from the add/edit form into separate files.
- Login screen links out to the docs.
Docs
- "Console login password" section on every host page (apt/RPM/Bazzite/SteamOS/Windows)
plus a new "Forgot your Password?" troubleshooting page, linked from the login screen.
- Console served as HTTP/1.1 over TLS (drop the unusable HTTP/3 advertising) across the
Bun entry, launchers, systemd units, and packaging.
Tooling
- Biome now respects .gitignore (stops linting generated code), config migrated to
2.5.1; all lint issues fixed cleanly.
Also includes this branch's in-progress host, Apple client, packaging, and CI changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Marketing/store screenshots of the console, captured from the built Storybook
with headless Chromium (web/tools/screenshots.mjs) — every Pages/* + Shell/*
story rendered at 1440x900@2x. The page stories render from fixtures, so no live
mgmt API, login, or GPU is needed (the web analogue of apple.yml's screenshots
job). Gated to stable release tags in a standalone best-effort workflow; PNGs
upload as a 30-day artifact, not committed.
- Add Stats + Pairing stories (the two pages that lacked them) with stats/pairing
fixtures typed against the generated models.
- Extract a pure PairingView (index.tsx -> view.tsx), matching the
Dashboard/Clients/Stats split, so the page renders host-free from mock state
instead of racing its polling queries. Container wiring is behaviour-identical.
- Playwright driver + a chromium-capable tag-gated job.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Arm streaming-perf-stats capture from the web console, play, stop, and review the
run as graphs; finished captures are saved to disk as browsable/exportable
recordings. Covers both the native punktfunk/1 path and GameStream.
- stats_recorder.rs: one shared Arc<StatsRecorder> ring (created in gamestream::serve,
shared with the mgmt API + both streaming loops, mirroring NativePairing). The
hot-path gate is a runtime AtomicBool that replaces the startup-only PUNKTFUNK_PERF
for *recording* (PERF stdout logging unchanged); bounded ring (~3 h); atomic
temp+rename writes to ~/.config/punktfunk/captures/*.json; path-traversal-safe ids;
poison-resilient locks.
- native (punktfunk1.rs) + GameStream (stream.rs) emit a StatsSample at their existing
~2 s / ~1 s aggregation boundary — per-stage latency p50/p99, fps new/repeat, goodput,
loss/FEC deltas — with no new per-frame work beyond the cheap atomic check.
FrameMsg.was_measured keeps pre-arm in-flight frames out of the first window's
percentiles (without zeroing the Windows-relay path's fps/encode).
- mgmt.rs: 7 bearer-only /api/v1/stats/* endpoints (capture start/stop/status/live;
recordings list/get/delete); api/openapi.json regenerated, in sync.
- web: new "Performance" page (recharts, rendered SSR-safe) — capture control, live
graphs while armed, recordings table (view / download-JSON / delete), and a detail
view with the latency stacked-area bottleneck breakdown (p50/p99 toggle) + throughput
+ health. Charts adapt to either path's stage set.
Design: design/stats-capture-plan.md. Built and adversarially reviewed via a multi-agent
workflow; workspace build/clippy(-D warnings)/fmt/tests green, OpenAPI no-drift. Not yet
on-glass validated against a live session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GameCard badge hard-coded steam-vs-custom, so any non-Steam non-custom store
rendered with the "Steam" label. Add storeLabel(store): steam/custom keep their
localized strings, every other store is shown as a capitalized proper noun — so the
new Lutris/Heroic providers (and future ones) surface correctly with no per-store
translation. tsc --noEmit clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>