Session 17: Audit response — checkout 401 fix, hero prop 404 fix, Slate parsing fix, ALL tab cascade isolation, cookie/nav/footer/autocomplete polish (1438 tests)

This commit is contained in:
Kev
2026-06-11 21:22:59 -04:00
parent 73b65a0248
commit beaf8b2a61
14 changed files with 681 additions and 25 deletions
+76 -9
View File
@@ -60,12 +60,26 @@ const FETCH_URLS: Record<Exclude<SlateTab, 'all'>, string[] | null> = {
soccer: ['/api/odds/soccer/wc'],
};
// Session 17 — Express `/api/odds/{sport}` returns props in the
// GROUPED shape produced by `src/routes/odds.js#groupProps`:
// { player, stat_type, home_team, away_team, game_time,
// lines: [{ book, line, over_odds, under_odds }] }
// not a flat `line`/`direction`/`book` per prop. Pre-Session 17 the
// Slate assumed flat — every prop got filtered out by the
// `Number.isFinite(r.line)` check, which is why WNBA (the only
// active sport at audit time) showed "No games published yet."
//
// RawProp now mirrors both shapes; the unwrapper below picks the
// best available line out of the `lines[]` array when present.
interface RawProp {
player?: string;
stat_type?: string;
// Flat-shape fields (pre-Session 17 contract — still tolerated)
line?: number;
direction?: 'over' | 'under';
book?: string;
// Grouped-shape fields (actual Express response since Session 7+)
lines?: Array<{ book?: string; line?: number; over_odds?: number; under_odds?: number }>;
game_time?: string;
home_team?: string;
away_team?: string;
@@ -77,6 +91,35 @@ interface OddsResponse {
error?: string;
}
// Pick the most useful single line out of a grouped prop. Preference:
// 1. A line marked `direction: over` (matches the default scan flow)
// 2. The first numeric line in the array
// 3. The flat-shape `line` field if present (legacy callers)
function pickLine(r: RawProp): { line: number; direction: 'over' | 'under'; book: string } | null {
// Flat shape wins when present — preserves the older test fixtures.
if (Number.isFinite(r.line)) {
return {
line: r.line as number,
direction: (r.direction as 'over' | 'under') || 'over',
book: r.book || 'draftkings',
};
}
if (Array.isArray(r.lines)) {
const first = r.lines.find((l) => Number.isFinite(l.line));
if (first && Number.isFinite(first.line)) {
// The grouped response doesn't carry a per-line direction —
// each line has both over/under odds. Default to `over` since
// that's the default scan direction.
return {
line: first.line as number,
direction: 'over',
book: first.book || 'draftkings',
};
}
}
return null;
}
interface SlateGame {
sport: SlateSport;
homeTeam: string;
@@ -90,7 +133,10 @@ interface SlateGame {
function groupByGame(rawProps: RawProp[], sport: SlateSport): SlateGame[] {
const games = new Map<string, SlateGame>();
for (const r of rawProps) {
if (!r.player || !r.stat_type || r.line == null) continue;
if (!r.player || !r.stat_type) continue;
// Session 17 — unwrap the grouped `lines[]` shape from Express.
const lineInfo = pickLine(r);
if (!lineInfo) continue;
const home = r.home_team || '?';
const away = r.away_team || '?';
const time = r.game_time || '';
@@ -107,9 +153,9 @@ function groupByGame(rawProps: RawProp[], sport: SlateSport): SlateGame[] {
games.get(key)!.props.push({
player: r.player,
stat_type: r.stat_type,
line: Number(r.line),
direction: (r.direction as PropRowProp['direction']) || 'over',
book: r.book,
line: lineInfo.line,
direction: lineInfo.direction,
book: lineInfo.book,
});
}
// Sort each game's props by player + stat for stable rendering.
@@ -183,24 +229,45 @@ export default function Slate({ initialTab = 'all', tier = 'free' }: SlateProps)
if (!r.ok) throw new Error(body?.error || `HTTP ${r.status}`);
return { sport, body };
})
.catch((err) => {
// Re-throw so allSettled catches it, but attach the
// sport so the per-sport error-tracking below can
// surface "Soccer odds unavailable" without blanking
// the rest of the slate.
const e = err instanceof Error ? err : new Error(String(err));
(e as Error & { _vyndrSport?: SlateSport })._vyndrSport = sport;
throw e;
})
),
),
);
const allGames: SlateGame[] = [];
let firstError: string | null = null;
const failedSports: SlateSport[] = [];
const sportsAttempted = new Set<SlateSport>(sportsToFetch.map((s) => s.sport));
const sportsThatSucceeded = new Set<SlateSport>();
for (const r of results) {
if (r.status === 'fulfilled') {
sportsThatSucceeded.add(r.value.sport);
const grouped = groupByGame(r.value.body.props || [], r.value.sport);
allGames.push(...grouped);
} else if (!firstError) {
firstError = r.reason instanceof Error ? r.reason.message : 'Odds fetch failed';
} else {
const failed = (r.reason as Error & { _vyndrSport?: SlateSport })._vyndrSport;
if (failed && !failedSports.includes(failed)) failedSports.push(failed);
}
}
setGames(allGames);
setUnsupportedSports(unsupported);
if (allGames.length === 0 && firstError) setFetchError(firstError);
setUnsupportedSports([...unsupported, ...failedSports.filter((s) => !sportsThatSucceeded.has(s))]);
// Session 17 — only surface a top-level error when EVERY sport
// attempted in this tab failed. Partial successes (NBA ok,
// soccer 503) silently drop the failed sport's row and surface
// it via the existing "endpoint not configured" footer note.
if (sportsAttempted.size > 0 && sportsThatSucceeded.size === 0) {
const firstError = results.find((r) => r.status === 'rejected') as PromiseRejectedResult | undefined;
setFetchError(firstError ? (firstError.reason as Error).message : 'Odds fetch failed');
}
setLoading(false);
}, []);