**The streamed-screen pin could still be clobbered by three other write paths.**
Deferring it to the server's value on Save fixed the reported sequence but not
the general case: the draft is only re-seeded while it is CLEAN, so once there is
an unsaved edit its `capture_monitor` is frozen at whatever it was before the
operator used the picker — and `applyAxis` (which spreads the last saved policy),
the built-in preset switch and the custom-preset apply all put that stale value
back. Every write path reads `serverCaptureMonitor()` now; no path spreads the
draft's copy.
**The session⇄game grace input had no accessible name.** That card has its own
`Field` and only DisplayCard's was fixed, so the number input was still announced
as an unnamed spin button. Same treatment: `htmlFor`/`id` for the single control,
`fieldset`/`legend` for the two button groups. Verified in a browser — zero
inputs without an accessible name across the Displays page.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A verification pass re-read every finding from the original sweep against the
code on this branch rather than against the commit messages. It found that four
of them were still broken, two because the edit I made was inert. Commit
messages claim; code decides.
- **The Storybook typecheck was never on.** `tsconfig.json` listed `.storybook`
as a bare directory name, and tsc silently skips dot-prefixed directories in
that form — so the entry typechecked nothing at all. Proved it by planting
`export const __probe: string = 1` in `.storybook/preview.tsx` and watching
`bun run lint` pass. `.storybook/**/*` is what actually pulls it in; the same
probe now fails as it should.
- **The Moonlight stale-PIN reset was a no-op.** `submit.reset()` sat at the top
of `onSubmit`, immediately before `submit.mutate(...)` — which moves the status
to pending in the same update, so it cleared a flag that was already changing.
The green "PIN sent" note therefore still greeted the next pairing attempt over
an empty PIN box. It now resets on the transition that actually matters:
`pin_pending` going false → true.
- **The session⇄game controls had the enforcement flag inverted**, and I never
touched it. `enforced.length === 0 || …` reads an EMPTY list as "this build
enforces everything", when the contract says the opposite in as many words:
"Empty on a platform with no launch path (macOS), so the console can say so
instead of offering a switch that does nothing". On exactly the platform the
flag exists for, every control stayed live and reported success for an axis the
host would never act on. Absent still means "assume it acts" — that is the
compatible reading for an older host, and a different case from present-empty.
- **Logout stopped revoking after a restart.** The epoch was a module-level
counter starting at 1, so it revoked within one process run and then reset —
and since the seal key derives from the stable mgmt token, a cookie captured
before a restart unsealed fine and was accepted again for the rest of its
7-day TTL. One service restart undid the whole fix. It persists next to the
host's config now. Verified: log out, restart the console, the captured cookie
still 401s, a fresh login still works.
Two more the pass rated as partial, both worth closing:
- The plugin-UI response filter was a denylist of four header names, so
`Clear-Site-Data` sailed through — a plugin error page could wipe `pf_session`
and sign the operator out of the console, on our own origin, because the iframe
is same-origin by design. It is an allowlist now; a plugin-supplied CSP,
`X-Frame-Options` or CORS header no longer speaks for us either.
- A half-configured TLS setup now refuses to start instead of logging a warning
and serving anyway. Neither shape can work — one path missing puts the login
password on the LAN in the clear, and PUNKTFUNK_UI_SECURE without TLS marks the
cookie Secure so the browser drops it and login can never stick. Exiting with a
reason beats a console that looks fine and is not.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
On-glass on .41: with `game_on_session_end: keep` — "leave it running, nothing
is ever closed" — a deliberate stop ended the game, and a drop ended it ~14 s
later. Both times the LEASE did the right thing (no `ending the launched game`
line); the game died because the virtual display was torn down, and a nested
launch runs inside that gamescope.
So on a dedicated gamescope session the game's lifetime IS the display's, and
keep-alive decides it — not this setting:
* a deliberate stop skips the linger by design, so the game goes at once,
* a drop lingers the keep-alive window, then takes the game with it,
* only keep-alive Forever actually keeps it — and `always` still ends it at
the reconnect window, which is the documented precedence and was verified
(a `forever`-pinned display released at grace expiry, 60.006 s).
That makes the console's promise untrue on the most common Linux setup, so the
card now says so for every option rather than only warning about `always`, and
the docs get the three cases as a table. Worded so a non-gamescope host reads it
and moves on — a desktop-session launch is an ordinary process and none of it
applies.
Not changing behavior here: making `keep` pin the display would pit it against
"a stop must not leave a ghost display", and which wins is a product call rather
than something to decide silently mid-test.
web: codegen + tsc + biome clean, en/de complete. docs-site: build + tsc clean.
The host knows what it launched and, after a disconnect, that it is counting
down to closing it. None of that was visible: the console showed a stream with
no game, and a game on its way to being closed was something you found out about
afterwards.
The Dashboard gains a running-game card above the session card — box art matched
against the catalog it already fetches, so no new endpoint and no request on the
2 s status poll. "End now" means the two different things the row's state
implies: a live game ends by stopping its session (what then happens to the game
follows the policy — stopping a session is not licence to close a game), while
one already waiting out its reconnect window has no session left to stop and is
ended directly.
The settings card sits next to the display keep-alive policy because that is the
same question one step out: keep-alive decides how long a *display* outlives a
disconnect, this decides whether the *game* does. The copy says plainly what
`always` costs, that a drop is not someone pressing Stop, and that a display kept
forever is unaffected either way — the precedence rule that would otherwise
surprise someone. Where a build enforces nothing (macOS, no launch path) the
controls are shown disabled rather than hidden: "does nothing here" is
information.
Also fixes the story fixtures, which had gone stale against the `games[]` field
Phase 1 added, and adds a story for the state the card exists for — a game whose
client walked away.
web: build + tsc clean, biome-formatted; en/de messages complete.