Files
punktfunk/web/src/sections/Dashboard/index.tsx
T
enricobuehler 6a2c127ce9 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.
2026-07-26 18:28:13 +02:00

61 lines
2.1 KiB
TypeScript

import { useQueryClient } from "@tanstack/react-query";
import type { FC } from "react";
import { getGetStatusQueryKey, useGetStatus } from "@/api/gen/host/host";
import { useGetLibrary } from "@/api/gen/library/library";
import type { ActiveGame } from "@/api/gen/model/activeGame";
import {
useEndGame,
useRequestIdr,
useStopSession,
} from "@/api/gen/session/session";
import { useLocale } from "@/lib/i18n";
import { DashboardView } from "./view";
export const SectionDashboard: FC = () => {
useLocale();
const qc = useQueryClient();
// Poll live status every 2s so the console tracks an active session.
const status = useGetStatus({ query: { refetchInterval: 2_000 } });
// The catalog, for the running-game card's box art. Fetched once and held: a library scan touches
// every installed store's on-disk metadata, so it must not ride the 2 s status poll.
const library = useGetLibrary(undefined, {
query: { staleTime: 5 * 60_000 },
});
const stop = useStopSession();
const idr = useRequestIdr();
const endGame = useEndGame();
const invalidate = () =>
qc.invalidateQueries({ queryKey: getGetStatusQueryKey() });
/**
* "End now" means two different things, and which one is right follows from the row's state: a
* game whose session is still live ends by stopping that session (what then happens to the game
* follows the operator's policy — stopping a session is not licence to close a game), while a
* game already waiting out its reconnect window has no session left to stop and is ended directly.
*/
const onEndGame = (game: ActiveGame) => {
if (game.state === "grace") {
endGame.mutate(
{ data: { app_id: game.app_id ?? null } },
{ onSuccess: invalidate },
);
} else {
stop.mutate(undefined, { onSuccess: invalidate });
}
};
return (
<DashboardView
status={status}
library={library.data}
onStopSession={() => stop.mutate(undefined, { onSuccess: invalidate })}
onRequestIdr={() => idr.mutate(undefined)}
onEndGame={onEndGame}
isStopping={stop.isPending}
isRequestingIdr={idr.isPending}
isEndingGame={endGame.isPending || stop.isPending}
/>
);
};