P1-6 fix: one freshness source — Slate 'UPDATED' reads pipeline refreshed_at

Phone audit: the same screen showed green 'SIGNAL LIVE · UPDATED 0s ago' AND
amber 'SYNC 46:03' — two components reading different fields. The Slate's
'UPDATED Xs ago' measured the CLIENT poll time (always ~0s, since it refetches
every 60s), while the app-bar clock honestly measured the pipeline's data age
(refreshed_at). '0s ago' claimed a freshness the data didn't have.

Now the Slate captures snap.refreshed_at from the snapshot it already fetches
(newest across sports) and 'UPDATED' shows THAT — the same source of truth as
the clock. The clock still owns the amber/STALE reaction; the strip just states
the honest data age. No more contradiction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-17 01:24:48 -04:00
parent 8b35fd06ec
commit ff0d3b199e
+18 -4
View File
@@ -175,7 +175,7 @@ interface StreaksResponse { streaks?: StreakApiRow[] }
// Session 45 — pre-graded snapshot response (snapshot:{sport}:latest). // 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 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 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. // Session 46 — MLB probable pitchers response.
interface PitcherSide { team?: string | null; pitcher?: string | null; era?: number | null } 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<SlateGame[]>([]); const [games, setGames] = useState<SlateGame[]>([]);
// Session 45 — merged pre-graded snapshot across the loaded sports. // Session 45 — merged pre-graded snapshot across the loaded sports.
const [snapGrades, setSnapGrades] = useState<SnapshotGrade[]>([]); const [snapGrades, setSnapGrades] = useState<SnapshotGrade[]>([]);
// 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<number | null>(null);
const [snapDeltas, setSnapDeltas] = useState<SnapshotDelta[]>([]); const [snapDeltas, setSnapDeltas] = useState<SnapshotDelta[]>([]);
const [pitcherGames, setPitcherGames] = useState<PitcherGame[]>([]); const [pitcherGames, setPitcherGames] = useState<PitcherGame[]>([]);
// Session 64 (A1-S5) — prop viability (lineup confirmation + injury wire). // 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 oddsGames = groupByGame(oddsProps, sport);
const scheduleGames = schedule?.games || []; const scheduleGames = schedule?.games || [];
const merged = mergeSlate(sport, scheduleGames, oddsGames, lines?.games, streaksRes?.streaks); 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); setGames(allGames);
setSnapGrades(allSnapGrades); 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); setSnapDeltas(allSnapDeltas);
setPitcherGames(allPitcherGames); setPitcherGames(allPitcherGames);
setViability({ lineups: mergedLineups || undefined, injuries: Object.keys(mergedInjuries).length ? mergedInjuries : undefined }); 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.some((g) => g.status === 'in') && (
<><span style={{ color: '#3A3A48' }}>·</span><span style={{ color: 'var(--g-a)', fontWeight: 700 }}>{games.filter((g) => g.status === 'in').length} LIVE</span></> <><span style={{ color: '#3A3A48' }}>·</span><span style={{ color: 'var(--g-a)', fontWeight: 700 }}>{games.filter((g) => g.status === 'in').length} LIVE</span></>
)} )}
{lastRefreshed && ( {/* P1-6 — DATA age from the pipeline's refreshed_at (the same field the
<><span style={{ color: '#3A3A48' }}>·</span><span title="The slate auto-refreshes every 60 seconds">UPDATED {freshLabel(lastRefreshed, nowTick)}</span></> 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 && (
<><span style={{ color: '#3A3A48' }}>·</span><span title="Data age — the pipeline's last refresh (one source of truth with the app-bar clock)">UPDATED {freshLabel(snapRefreshedAt, nowTick)}</span></>
)} )}
</div> </div>
<input <input