TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1"use client";23import Link from "next/link";4import { Activity, Database, ShieldAlert, Timer } from "lucide-react";5import { useAdminQuery } from "@/components/admin/use-query";6import type { DashboardResponse } from "@/components/admin/types";7import { PageHeader, RefreshButton, StatTile, StatGrid, Panel, DataTable, ErrorState, TileSkeleton, type Column, Mono } from "@/components/admin/primitives";8import { compact, dateTime, duration, int, ms, multiplier, pct, sc } from "@/components/admin/format";9import { cn } from "@/lib/utils";1011type TopGame = DashboardResponse["topGames"][number];12type HighWin = DashboardResponse["highestWins"][number];1314const topGameCols: Column<TopGame>[] = [15 { key: "slug", header: "Game", render: (r) => <Link href={`/admin/games/${r.slug}`} className="font-medium text-fg hover:text-accent-2">{r.slug}</Link> },16 { key: "spins", header: "Spins", align: "right", render: (r) => int(r.spins) },17 { key: "wagered", header: "Wagered", align: "right", render: (r) => sc(r.wagered) },18 { key: "won", header: "Won", align: "right", render: (r) => sc(r.won) },19 { key: "rtp", header: "RTP", align: "right", render: (r) => <span className={cn(r.rtp !== null && (r.rtp > 1.02 || r.rtp < 0.9) ? "text-[#ffc46b]" : "")}>{pct(r.rtp)}</span> },20];2122const highWinCols: Column<HighWin>[] = [23 { key: "at", header: "When", render: (r) => <span className="text-fg-3">{dateTime(r.at)}</span> },24 { key: "username", header: "Player", render: (r) => r.username },25 { key: "game", header: "Game", render: (r) => <Link href={`/admin/games/${r.game}`} className="hover:text-accent-2">{r.game}</Link> },26 { key: "bet", header: "Bet", align: "right", render: (r) => sc(r.bet) },27 { key: "win", header: "Win", align: "right", render: (r) => <span className="text-credit">{sc(r.win)}</span> },28 { key: "multiplier", header: "×", align: "right", render: (r) => multiplier(r.multiplier) },29 { key: "roundId", header: "Round", mono: true, render: (r) => <Mono>{r.roundId}</Mono> },30];3132export default function AdminDashboardPage() {33 const q = useAdminQuery<DashboardResponse>("/api/admin/dashboard", { refreshMs: 30_000 });34 const d = q.data;3536 return (37 <>38 <PageHeader title="Dashboard" description="Live platform overview. Figures refresh every 30 seconds." actions={<RefreshButton onClick={() => void q.refresh()} loading={q.refreshing} />} />3940 {q.error && !d ? (41 <ErrorState error={q.error} onRetry={() => void q.refresh()} />42 ) : !d ? (43 <div className="space-y-4">44 <TileSkeleton count={6} cols={6} />45 <TileSkeleton count={6} cols={6} />46 </div>47 ) : (48 <div className={cn("space-y-5", q.stale && "opacity-70")}>49 <StatGrid cols={6}>50 <StatTile label="Registered users" value={int(d.users.registered)} />51 <StatTile label="DAU / WAU / MAU" value={<span>{compact(d.users.dau)} <span className="text-fg-4">/</span> {compact(d.users.wau)} <span className="text-fg-4">/</span> {compact(d.users.mau)}</span>} sub="Distinct players with rounds" />52 <StatTile label="Active sessions" value={int(d.users.activeSessions)} sub="Seen in the last 30 min" />53 <StatTile label="Live players" value={int(d.users.livePlayers)} tone="accent" sub="Spinning in the last 5 min" />54 <StatTile label="Spins today" value={int(d.today.spins)} sub="UTC day" />55 <StatTile label="Avg session" value={duration(d.averageSessionSec)} sub="Sessions started this week" />56 </StatGrid>57 <StatGrid cols={6}>58 <StatTile label="Credits wagered today" value={compact(d.today.wagered)} sub={sc(d.today.wagered)} />59 <StatTile label="Credits won today" value={compact(d.today.won)} sub={sc(d.today.won)} />60 <StatTile label="Effective RTP today" value={pct(d.today.effectiveRtp)} tone={d.today.effectiveRtp === null ? "neutral" : d.today.effectiveRtp > 1.02 || d.today.effectiveRtp < 0.9 ? "danger" : "success"} sub="won ÷ wagered" />61 <StatTile label="Avg spin latency" value={ms(d.today.avgSpinMs)} icon={<Timer className="h-3.5 w-3.5" />} />62 <StatTile label="p95 spin latency" value={ms(d.today.p95SpinMs)} tone={d.today.p95SpinMs > 250 ? "danger" : "neutral"} />63 <StatTile label="High-severity events" value={int(d.health.highSeverityEvents24h)} tone={d.health.highSeverityEvents24h > 0 ? "danger" : "success"} sub="Last 24 h" icon={<ShieldAlert className="h-3.5 w-3.5" />} />64 </StatGrid>6566 <div className="grid gap-4 xl:grid-cols-2">67 <Panel title="Top games" description="Last 7 days, by spins" padded={false} actions={<Link href="/admin/analytics/games" className="text-[12px] text-fg-3 hover:text-fg">All analytics →</Link>}>68 <DataTable columns={topGameCols} rows={d.topGames} rowKey={(r) => r.slug} dense empty="No rounds recorded in the last 7 days." />69 </Panel>70 <Panel title="Highest wins" description="All time, top 10 rounds" padded={false}>71 <DataTable columns={highWinCols} rows={d.highestWins} rowKey={(r) => r.roundId} dense empty="No rounds yet." />72 </Panel>73 </div>7475 <Panel title="Health" description="API process and database" padded={false}>76 <div className="grid grid-cols-2 divide-y divide-line text-[13px] sm:grid-cols-5 sm:divide-x sm:divide-y-0">77 <HealthCell icon={<Database className="h-3.5 w-3.5" />} label="DB latency" value={d.health.dbLatencyMs < 0 ? "down" : ms(d.health.dbLatencyMs)} tone={d.health.dbLatencyMs < 0 ? "danger" : d.health.dbLatencyMs > 50 ? "warn" : "ok"} />78 <HealthCell icon={<ShieldAlert className="h-3.5 w-3.5" />} label="High-severity 24 h" value={int(d.health.highSeverityEvents24h)} tone={d.health.highSeverityEvents24h > 0 ? "danger" : "ok"} />79 <HealthCell icon={<Activity className="h-3.5 w-3.5" />} label="Uptime" value={duration(d.health.uptimeSec)} tone="ok" />80 <HealthCell label="Version" value={<Mono>v{d.health.version}</Mono>} />81 <HealthCell label="Node" value={<Mono>{d.health.node}</Mono>} />82 </div>83 </Panel>84 </div>85 )}86 </>87 );88}8990function HealthCell({ icon, label, value, tone }: { icon?: React.ReactNode; label: string; value: React.ReactNode; tone?: "ok" | "warn" | "danger" }) {91 return (92 <div className="flex items-center gap-3 px-4 py-3">93 {tone ? <span className={cn("h-2 w-2 shrink-0 rounded-full", tone === "ok" ? "bg-success" : tone === "warn" ? "bg-[#ffb454]" : "bg-danger")} /> : <span className="h-2 w-2 shrink-0" />}94 <div className="min-w-0">95 <div className="flex items-center gap-1 text-[11px] uppercase tracking-wider text-fg-4">96 {icon}97 {label}98 </div>99 <div className="truncate font-medium">{value}</div>100 </div>101 </div>102 );103}104