diff --git a/web/src/components/Slate.tsx b/web/src/components/Slate.tsx index 2123475..4930107 100644 --- a/web/src/components/Slate.tsx +++ b/web/src/components/Slate.tsx @@ -175,7 +175,7 @@ interface StreaksResponse { streaks?: StreakApiRow[] } // Session 45 — pre-graded snapshot response (snapshot:{sport}:latest). interface SnapshotGrade { player?: string; player_name?: string; stat_type?: string; line?: number; direction?: string; grade?: string; archetype?: string | null; projection?: number | null; model_value?: number | null; gradedAt?: { line: number; odds?: number | null; timestamp?: string } | null } interface SnapshotDelta { player?: string; stat?: string; side?: string; delta?: number; direction?: string; currentLine?: number } -interface SnapshotResponse { grades?: SnapshotGrade[]; deltas?: SnapshotDelta[] } +interface SnapshotResponse { grades?: SnapshotGrade[]; deltas?: SnapshotDelta[]; updated_at?: string | null; refreshed_at?: string | null } // Session 46 — MLB probable pitchers response. interface PitcherSide { team?: string | null; pitcher?: string | null; era?: number | null } @@ -583,6 +583,10 @@ export default function Slate({ initialTab = 'all', tier = 'free', preferredBook const [games, setGames] = useState([]); // Session 45 — merged pre-graded snapshot across the loaded sports. const [snapGrades, setSnapGrades] = useState([]); + // P1-6 — the PIPELINE's freshness (refreshed_at, the same field the app-bar + // clock reads). "UPDATED Xs ago" must show the DATA age, not the client's poll + // time — else the strip claims "0s ago" while the clock (honestly) says 46m. + const [snapRefreshedAt, setSnapRefreshedAt] = useState(null); const [snapDeltas, setSnapDeltas] = useState([]); const [pitcherGames, setPitcherGames] = useState([]); // Session 64 (A1-S5) — prop viability (lineup confirmation + injury wire). @@ -685,7 +689,7 @@ export default function Slate({ initialTab = 'all', tier = 'free', preferredBook const oddsGames = groupByGame(oddsProps, sport); const scheduleGames = schedule?.games || []; const merged = mergeSlate(sport, scheduleGames, oddsGames, lines?.games, streaksRes?.streaks); - return { sport, merged, oddsOk, hadSchedule: scheduleGames.length > 0, snapGrades: snap?.grades || [], snapDeltas: snap?.deltas || [], pitcherGames: pitchersRes?.games || [], lineups: lineupsRes || null, injuries: injuriesRes?.byPlayer || null }; + return { sport, merged, oddsOk, hadSchedule: scheduleGames.length > 0, snapGrades: snap?.grades || [], snapDeltas: snap?.deltas || [], snapRefreshed: snap?.refreshed_at || snap?.updated_at || null, pitcherGames: pitchersRes?.games || [], lineups: lineupsRes || null, injuries: injuriesRes?.byPlayer || null }; }), ); @@ -717,6 +721,12 @@ export default function Slate({ initialTab = 'all', tier = 'free', preferredBook setGames(allGames); setSnapGrades(allSnapGrades); + // P1-6 — newest pipeline refreshed_at across the loaded sports (the real + // data age, shared with the app-bar clock — one source of truth). + { + const stamps = perSport.map((s) => (s.snapRefreshed ? Date.parse(s.snapRefreshed) : NaN)).filter((t) => Number.isFinite(t)); + setSnapRefreshedAt(stamps.length ? Math.max(...stamps) : null); + } setSnapDeltas(allSnapDeltas); setPitcherGames(allPitcherGames); setViability({ lineups: mergedLineups || undefined, injuries: Object.keys(mergedInjuries).length ? mergedInjuries : undefined }); @@ -952,8 +962,12 @@ export default function Slate({ initialTab = 'all', tier = 'free', preferredBook {games.some((g) => g.status === 'in') && ( <>·{games.filter((g) => g.status === 'in').length} LIVE )} - {lastRefreshed && ( - <>·UPDATED {freshLabel(lastRefreshed, nowTick)} + {/* P1-6 — DATA age from the pipeline's refreshed_at (the same field the + app-bar clock reads), NOT the client poll time. "UPDATED 0s ago" + while the clock said "SYNC 46:03" was the contradiction; both now + read refreshed_at. The clock owns the amber/STALE reaction. */} + {snapRefreshedAt && ( + <>·UPDATED {freshLabel(snapRefreshedAt, nowTick)} )}