forked from unom/punktfunk
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.
61 lines
2.1 KiB
TypeScript
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}
|
|
/>
|
|
);
|
|
};
|