import { useEffect, useState } from 'react'; import { Link } from '@tanstack/react-router'; import { Scale } from 'lucide-react'; import { PageHeader } from '@/components/ui/PageHeader'; import { Card, CardBody, CardHeader } from '@/components/ui/Card'; import { Term } from '@/components/ui/Term'; import { teamFlag } from '@/lib/teams'; import { kickoffDay, kickoffTime } from '@/lib/format'; import { cn } from '@/lib/cn'; import type { MatchProbs, ScoreboardData, ScoreboardRow } from '@/lib/types'; const pct = (x: number) => `${Math.round(x * 100)}%`; function ProbTriple({ label, probs, tone }: { label: string; probs: MatchProbs; tone: 'model' | 'market' }) { const color = tone === 'model' ? 'bg-accent' : 'bg-gold'; return (
{label}
{pct(probs.home)} / {pct(probs.draw)} / {pct(probs.away)}
); } function Row({ r }: { r: ScoreboardRow }) { const closer = r.scored && r.scored.marketRps != null ? r.scored.modelRps < r.scored.marketRps ? 'model' : r.scored.marketRps < r.scored.modelRps ? 'market' : 'tie' : null; return (
{teamFlag(r.home)} {r.home} {r.status !== 'scheduled' && r.homeScore != null ? `${r.homeScore}–${r.awayScore}` : 'vs'} {r.away} {teamFlag(r.away)} {r.group ? `Group ${r.group} · ` : ''}{kickoffDay(r.kickoff)} {kickoffTime(r.kickoff)} {r.late && ' · snapshot late — excluded from totals'}
{r.market ? ( ) : (

no bookmaker line captured before kickoff

)} {r.scored && (
result: {r.scored.outcome} model RPS {r.scored.modelRps} {r.scored.marketRps != null && market RPS {r.scored.marketRps}} {closer && closer !== 'tie' && ( {closer === 'model' ? 'model closer' : 'market closer'} )}
)}
); } export function ScoreboardPage() { const [data, setData] = useState(null); useEffect(() => { let alive = true; const load = () => fetch('/api/scoreboard').then((r) => r.json()).then((d: ScoreboardData) => alive && setData(d)).catch(() => {}); void load(); const id = setInterval(load, 60_000); return () => { alive = false; clearInterval(id); }; }, []); const t = data?.totals ?? null; const lead = t ? (t.modelRps < t.marketRps ? 'model' : t.marketRps < t.modelRps ? 'market' : 'tie') : null; return (
Running head-to-head {t ? (
{t.modelRps.toFixed(4)}
our model · RPS
closer {t.modelCloser}×
{t.n} match{t.n === 1 ? '' : 'es'} scored
{lead === 'tie' ? 'dead level' : lead === 'model' ? 'model ahead' : 'market ahead'}
{t.marketRps.toFixed(4)}
bookmaker · RPS
closer {t.marketCloser}×
) : (

The head-to-head starts with the first final whistle — forecasts are frozen before each kickoff and scored when the match ends.

)}

Honest rules: both forecasts frozen pre-kickoff; bookmaker margin removed (); scored with the{' '} Ranked Probability Score; late snapshots excluded. Beating the market over a small sample is noise — watch the gap, not the lead.

{data ? (
{data.rows.length === 0 && (

No frozen forecasts yet — the first snapshots are taken 15 minutes before kickoff.

)} {data.rows.map((r) => )}
) : (
)}
); }