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:
@@ -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);
|
||||
}, []);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user