feat(mgmt/web): show the running game, and who decides when it ends

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.
This commit is contained in:
2026-07-26 18:28:13 +02:00
parent 110eabf281
commit 6a2c127ce9
9 changed files with 495 additions and 5 deletions
+6
View File
@@ -3,11 +3,16 @@ import type { FC } from "react";
import { useLocale } from "@/lib/i18n";
import { m } from "@/paraglide/messages";
import { DisplaySection } from "./DisplayCard";
import { SessionGameCard } from "./SessionGameCard";
/**
* The **Virtual displays** page (design/display-management.md): the host's virtual-display policy
* (presets + every axis) plus the live-display list + multi-monitor arrangement. Its own nav
* section — the config surface is large enough to warrant the room, and it kept the Host page busy.
*
* The session⇄game lifetime card sits here rather than on its own page because it is the same
* question one step further out: keep-alive decides how long a *display* outlives a disconnect, and
* this decides whether the *game* does.
*/
export const SectionDisplays: FC = () => {
useLocale();
@@ -16,6 +21,7 @@ export const SectionDisplays: FC = () => {
<div className="flex flex-col gap-card">
<h1 className="text-2xl font-semibold">{m.nav_displays()}</h1>
<DisplaySection />
<SessionGameCard />
</div>
</Section>
);