/**
* dashboard-client/src/tabs/GameTab.tsx — Game tab (NEW).
*
* Renders:
* (1) MEGA CACHE banner (animated gradient, shown when any mega_cache > 100)
* (2) Opie unlock tile (shown when mega_cache > 100)
* (3) High Scores header
* (4) 4 leaderboards in 2×2 grid: Cache %, Dedupe, Turns (LVL badge),
* MEGA CACHE trophies (firstSeen date meta)
* (5) Repos count badge via fetchGameScores({metric:'repos',limit:1})
* (6) Achievements sub-section (AchievementTiles shared component)
* (7) Game empty state
* Polls scores + achievements every 15s, game-state every 15s.
* Applies data-theme accent vars when game_mode_on.
*/
import type React from "react";
import { useState, useEffect, useCallback, useRef } from "react";
import type { GameScoreRow, AchievementRow } from "@contracts";
import { fetchGameScores, fetchAchievements, fetchGameState } from "../api/client";
import { AchievementTiles } from "../components/AchievementTiles";
/** Actual flat game state shape from the server (NOT the nested contract). */
interface FlatGameState {
game_mode_on: boolean;
theme: string;
tui_display_mode: "full" | "minimal";
}
/** Metadata object inside a mega_cache GameScoreRow. */
interface TrophyMeta {
firstSeenTs?: number;
firstSeen?: number;
}
/* ── Format helpers (matching html.ts logic) ─────────────────────────── */
function fmtPct(v: number): string {
return (Math.round(v * 10) / 10).toString() + "%";
}
function fmtDate(ts: number | null | undefined): string {
return ts != null ? new Date(ts).toLocaleString() : "\u2014";
}
function trophyMeta(m: unknown): TrophyMeta {
return m && typeof m === "object" ? (m as TrophyMeta) : {};
}
function repoBasename(path: string): string {
return path.split("/").pop() || path;
}
/* ── Leaderboard card ────────────────────────────────────────────────── */
interface LeaderboardCardProps {
title: string;
rows: GameScoreRow[];
badge?: React.ReactNode;
renderValue: (r: GameScoreRow) => React.ReactNode;
}
function LeaderboardCard({
title,
rows,
badge,
renderValue,
}: LeaderboardCardProps): React.ReactElement {
return (
{title}
{badge}
{rows.length === 0 ? (
—
) : (
{rows.map((r, i) => (
| {repoBasename(r.repo_root)} |
{renderValue(r)} |
))}
)}
);
}
/* ── Main GameTab component ──────────────────────────────────────────── */
export default function GameTab(): React.ReactElement {
const [scores, setScores] = useState<
Record
>({});
const [reposCount, setReposCount] = useState(0);
const [achievements, setAchievements] = useState([]);
const [gameModeOn, setGameModeOn] = useState(false);
const [loading, setLoading] = useState(true);
const [megaToast, setMegaToast] = useState(null);
const lastMegaTsRef = useRef(0);
const fetchAll = useCallback(async (): Promise => {
try {
const [cache, dedupe, turns, megaCache, repos, ach] =
await Promise.all([
fetchGameScores({ metric: "cache", limit: 25 }),
fetchGameScores({ metric: "dedupe", limit: 25 }),
fetchGameScores({ metric: "turns", limit: 25 }),
fetchGameScores({ metric: "mega_cache", limit: 25 }),
fetchGameScores({ metric: "repos", limit: 1 }),
fetchAchievements(),
]);
/* Mega-cache toast: new row with value > 100 since last poll */
const newRow = megaCache.find(
(r) => r.ts > lastMegaTsRef.current && r.value > 100,
);
if (lastMegaTsRef.current > 0 && newRow) {
setMegaToast(
`oopsie! cache went to ${Math.round(newRow.value)}% — MEGA CACHE \uD83E\uDD67`,
);
setTimeout(() => setMegaToast(null), 4000);
}
if (megaCache.length > 0) {
lastMegaTsRef.current = Math.max(
...megaCache.map((r) => r.ts),
);
}
setScores({ cache, dedupe, turns, mega_cache: megaCache });
setReposCount(repos[0]?.value ?? 0);
setAchievements(ach);
} catch {
/* non-fatal */
} finally {
setLoading(false);
}
}, []);
/* Poll scores + achievements every 15s. */
useEffect(() => {
let active = true;
const doFetch = async (): Promise => {
if (active) await fetchAll();
};
void doFetch();
const timer = setInterval(() => void doFetch(), 15000);
return () => {
active = false;
clearInterval(timer);
};
}, [fetchAll]);
/* Poll game state every 15s for styling. */
useEffect(() => {
let active = true;
const doFetch = async (): Promise => {
try {
const gs = (await fetchGameState()) as unknown as FlatGameState;
if (active) {
setGameModeOn(!!gs.game_mode_on);
if (gs.theme)
document.documentElement.dataset.theme = gs.theme;
}
} catch {
/* non-fatal */
}
};
void doFetch();
const timer = setInterval(() => void doFetch(), 15000);
return () => {
active = false;
clearInterval(timer);
};
}, []);
/* Compute mega-cache banner data */
const megaRows = scores.mega_cache ?? [];
let bestMega: number | null = null;
let firstSeen: number | null = null;
for (const r of megaRows) {
if (bestMega == null || r.value > bestMega) bestMega = r.value;
const m = trophyMeta(r.meta);
const fs = m.firstSeenTs ?? m.firstSeen ?? r.ts;
if (firstSeen == null || fs < firstSeen) firstSeen = fs;
}
const showMega = bestMega != null && bestMega > 100;
/* Turns level badge */
const turnsRows = scores.turns ?? [];
const maxTurns = turnsRows.reduce((mx, r) => Math.max(mx, r.value), 0);
const lvl = Math.floor(Math.log2(maxTurns + 1)) + 1;
/* Game empty state */
const hasData =
(scores.cache?.length ?? 0) > 0 ||
(scores.dedupe?.length ?? 0) > 0 ||
turnsRows.length > 0 ||
megaRows.length > 0 ||
reposCount > 0;
return (
{megaToast != null && (
{megaToast}
)}
{showMega && (
{"\uD83E\uDD67"} MEGA CACHE! peak {fmtPct(bestMega!)} — first
reached {fmtDate(firstSeen)}
)}
{showMega && (
{"\uD83C\uDFC6"} Opie's Wild Ride
best {fmtPct(bestMega!)} · first {fmtDate(firstSeen)}
)}
High Scores
{!hasData && !loading && (
No scores yet — run a session with game mode on.
)}
{hasData && (
fmtPct(r.value)}
/>
r.value.toLocaleString()}
/>
0 ? (
LVL {lvl}
) : undefined
}
renderValue={(r) => r.value.toLocaleString()}
/>
(
<>
{fmtPct(r.value)}
{fmtDate(
trophyMeta(r.meta).firstSeenTs ??
trophyMeta(r.meta).firstSeen ??
r.ts,
)}
>
)}
/>
)}
{reposCount > 0 && (
{reposCount} repos
)}
Achievements
);
}