Session 59: Addendum + work-order 1.6 + Phase 2 + Phase 3 (2352 tests)

Overnight sprint for the Saturday 10 AM ET deploy gate — day one of the
public ledger record locks against freshly posted lines.

Task A — ledger team/opponent (migration 020, applied at 0 rows):
  populated in both write paths from the real feed; opponent only when the
  player's team matches a game participant (never guessed). Roadmap: Phase
  4.5 WNBA ESPN-boxscore settlement (due ~Jul 24) + Phase 5 per-tier
  calibration logged.

Task B — work-order 1.6 CLOSED (canonical player keys):
  - searchPlayer resolves via nameKey; the old matcher deleted accents
    ("Sanchez" with acute -> "snchez") and substring-guessed onto the WRONG
    player (the mismatched last-10 bug). Ambiguous -> null, never guess.
  - Slate JOIN INVARIANT: a graded prop whose player's real team isn't in
    the game is dropped (TB player can't render under MIL@PIT) — locked by
    tests that fail the suite on regression.
  - grades:{sport} TTL 2h -> 6h (expired between 5h cron gaps — the real
    cause of /team "No active props" for slate players).

Task C — Phase 2 slate UX: tabs are THE filter (URL ?sport=, deep-linkable,
  duplicate legacy tablist removed); cards cap at 6 graded props sorted
  A+->F with ALL N READS in-place expander; waiting states show the real
  next pipeline run ("Grades post ~6:00 PM ET").

Task D — Phase 3 mobile P0: root cause of vanished 390px nav was HIDE_ON
  including '/' (landing had zero navigation) — fixed; html/body overflow-x
  contained; GAME LINES collapses to best-line summary + "N BOOKS" expander
  below 640px; venue drops before time/pitchers ever truncate.

Live verification: raw ESPN today STILL returns the Jun 13 NYK@SA Finals
game without a date pin; the pinned fetch returns 0 games, 0 off-date.

Backend 2327 -> 2352 tests (202 suites), web build exit 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-10 22:24:47 -04:00
parent c96e74c54b
commit d10bb4cce2
20 changed files with 648 additions and 80 deletions
+22 -46
View File
@@ -1,6 +1,6 @@
'use client';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import { useParlay } from '@/contexts/ParlayContext';
@@ -13,6 +13,8 @@ import Slate from '@/components/Slate';
import { AccuracyBadge } from '@/components/vyndr';
// Session 57 (Phase 0) — honest per-sport empty-slate copy.
import { emptyStateCopy } from '@/lib/emptyState';
// Session 59 (work-order 2.3) — the real pipeline schedule for waiting states.
import { nextRunLabelET } from '@/lib/pipelineSchedule';
type Sport = 'NBA' | 'MLB' | 'WNBA';
@@ -69,8 +71,6 @@ interface RecentScan {
created_at: string;
}
const SPORT_TABS: Sport[] = ['NBA', 'MLB', 'WNBA'];
const SPORT_COLOR: Record<Sport, string> = {
NBA: '#E94B3C',
MLB: '#1E90FF',
@@ -180,12 +180,6 @@ export default function DashboardPage() {
}
}, [user]);
const gameCountsBySport = useMemo(() => {
// Updated whenever the sport's slate refreshes. We only know counts for
// the current sport — others show a dash until clicked.
return { [sport]: games?.length ?? 0 } as Partial<Record<Sport, number>>;
}, [sport, games]);
if (authLoading || !user) {
return (
<section style={{ minHeight: '80vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
@@ -234,42 +228,23 @@ export default function DashboardPage() {
{/* Session 13 — Browse-first slate. Owns its own sport-tab UI,
search, and inline grading. Renders ABOVE the existing
intelligence sections (Top Graded / Most Parlayed / Recent
Reads) which serve as supplementary surfaces. */}
<Slate tier={tier} initialTab={primaryTab} preferredBooks={prefBooks} key={primaryTab} />
{/* Legacy sport tabs — supplementary, kept for the existing
Top Graded / Most Parlayed flows below. */}
<div role="tablist" aria-label="Sport" style={{ display: 'flex', gap: 4, marginTop: 40, marginBottom: 32, borderBottom: '1px solid var(--border)' }}>
{SPORT_TABS.map((s) => {
const active = s === sport;
const count = gameCountsBySport[s];
return (
<button
key={s}
role="tab"
aria-selected={active}
onClick={() => setSport(s)}
style={{
padding: '12px 20px',
background: 'transparent',
border: 'none',
borderBottom: `2px solid ${active ? SPORT_COLOR[s] : 'transparent'}`,
color: active ? 'var(--text-primary)' : 'var(--text-secondary)',
fontFamily: 'inherit',
fontWeight: active ? 600 : 500,
fontSize: 14,
cursor: 'pointer',
marginBottom: -1,
}}
>
{s}{' '}
<span className="mono" style={{ fontSize: 11, color: 'var(--text-tertiary)', marginLeft: 4 }}>
{active && count != null ? `(${count})` : '·'}
</span>
</button>
);
})}
</div>
Reads) which serve as supplementary surfaces.
Session 59 (work-order 2.1) — the Slate's tabs are THE filter
(URL-driven, ?sport=). The legacy duplicate tab row below is GONE;
the supplementary sections follow the same selection via
onTabChange. Don't re-add a second tablist. */}
<Slate
tier={tier}
initialTab={primaryTab}
preferredBooks={prefBooks}
key={primaryTab}
onTabChange={(t) => {
if (t === 'nba') setSport('NBA');
else if (t === 'mlb') setSport('MLB');
else if (t === 'wnba') setSport('WNBA');
// 'all' / 'soccer' → the legacy sections keep their last sport.
}}
/>
{/* Top grades horizontal scroll */}
<Section
@@ -279,7 +254,8 @@ export default function DashboardPage() {
{topGrades === null ? (
<SkeletonRow />
) : topGrades.length === 0 ? (
<p style={emptyCopy}>No grades yet. The model is waiting on lines.</p>
// Session 59 (work-order 2.3) — the real schedule, not passive waiting.
<p style={emptyCopy}>No grades yet. Grades post {nextRunLabelET() || 'on the next pipeline run'}.</p>
) : (
<div
style={{