bc8633466c
Step 3 — OUTLOOK MODE. The game grid no longer dead-ends in a "NO SLATE" CTA. When there are no live games (and it's not a network failure) it shows REAL, always-available data: yesterday's PROVEN A-tier receipts (/api/ledger/model) + tomorrow's date-pinned ESPN schedule preview (free/cached). A network fetchError stays a distinct ERROR state — never a fabricated outlook. - lib/outlook.js (new, CommonJS, unit-tested): buildOutlook selection + mapTomorrowPreview (upcoming-only, drops incomplete matchups, never invents). - Slate.tsx: OutlookSurface replaces the empty-grid CTA (dateOffset 0 only). - dashboard/page.tsx: DashboardOutlook replaces the "Today's games" NO-SLATE CTA. Step 4 — MARKET-BREADTH / CONSENSUS vs MODEL. Makes the DeskShowcase "consensus vs model" claim REAL. Consensus = median book line across a prop's per-book rows; the model's position is model_value vs consensus, signed by the graded side. <2 distinct books → null (never fabricate a consensus); a non-numeric line is ignored, never coerced to 0. - lib/marketBreadth.js (new, CommonJS, unit-tested): median/computeBreadth/ collectBreadth (strict null guards). - components/vyndr/MarketBreadth.tsx (new): mono/tabular strip, colored by sign via colorContract.edgeColor, self-hides when nothing has >=2 books. - Slate.tsx renders it above the grid (joins books + snapshot model_value). - slateAdapter.js exports gradeKey for the join. - DeskShowcase.tsx: the consensus claim is now backed by the shipped feature. Tests: tests/unit/outlook.test.js + tests/unit/marketBreadth.test.js (23 cases). Full suite 2984 passing (245 suites); next build exit 0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
3.2 KiB
JavaScript
79 lines
3.2 KiB
JavaScript
/* ============================================================
|
|
VYNDR — OUTLOOK MODE (Wave 4A, Step 3): the never-empty slate grid.
|
|
|
|
The game grid must NEVER dead-end in a "NO SLATE" CTA. When there are no
|
|
live games (and it's not a network failure) the grid falls back to REAL,
|
|
always-available data — the terminal is never dark, but it is never
|
|
fabricated either:
|
|
• yesterday's PROVEN A-tier receipts (settled ledger hits), and/or
|
|
• tomorrow's date-pinned ESPN schedule preview (free / cached).
|
|
|
|
A network `fetchError` is DISTINCT and stays an ERROR state — a fetch
|
|
failure is never dressed up as a fake outlook.
|
|
|
|
Plain CommonJS so the .tsx surfaces import it (allowJs) AND Jest exercises
|
|
the selection logic directly.
|
|
============================================================ */
|
|
|
|
const { buildHeroReceipts } = require('./slateAdapter');
|
|
|
|
/** Statuses that mean a game is no longer a PREVIEW (already underway/done). */
|
|
const NON_PREVIEW = new Set(['in', 'post', 'final', 'live', 'completed']);
|
|
|
|
/**
|
|
* mapTomorrowPreview(scheduleGames, limit) — map real schedule games (either
|
|
* the ESPN `{ awayTeam, homeTeam, gameTime, status }` shape OR the flattened
|
|
* `{ away, home, start_time }` shape) → compact preview rows. Upcoming only;
|
|
* a game missing either team is DROPPED (never fabricate a matchup).
|
|
*/
|
|
function mapTomorrowPreview(scheduleGames, limit = 8) {
|
|
const list = Array.isArray(scheduleGames) ? scheduleGames : [];
|
|
const out = [];
|
|
for (const g of list) {
|
|
if (!g) continue;
|
|
const status = String(g.status || g.state || '').toLowerCase();
|
|
if (NON_PREVIEW.has(status)) continue;
|
|
const away = g.away || g.awayTeam?.name || g.awayTeam?.abbreviation || null;
|
|
const home = g.home || g.homeTeam?.name || g.homeTeam?.abbreviation || null;
|
|
if (!away || !home) continue; // absent beats a fabricated fixture
|
|
out.push({
|
|
id: g.id || `${away}-${home}`,
|
|
away,
|
|
home,
|
|
time: g.gameTime || g.start_time || g.time || null,
|
|
sport: g.sport ? String(g.sport).toUpperCase() : null,
|
|
});
|
|
if (out.length >= Math.max(0, limit)) break;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* buildOutlook(opts) →
|
|
* { mode: 'error' } — a network failure
|
|
* { mode: 'live' } — real games on the board
|
|
* { mode: 'outlook', receipts, tomorrow, hasContent }
|
|
*
|
|
* The outlook surface is ALWAYS non-error / non-blank at the surface level:
|
|
* even with no receipts and no schedule, the caller still renders the
|
|
* month-aware empty-state header — the grid never dead-ends in a CTA.
|
|
*
|
|
* @param {{ gamesCount?: number, fetchError?: unknown, settledRows?: unknown[], tomorrow?: unknown[] }} [opts]
|
|
*/
|
|
function buildOutlook(opts = {}) {
|
|
const { gamesCount = 0, fetchError = null, settledRows = [], tomorrow = [] } = opts;
|
|
if (fetchError) return { mode: 'error' };
|
|
if (Number(gamesCount) > 0) return { mode: 'live' };
|
|
|
|
const receipts = buildHeroReceipts(settledRows, 6);
|
|
const preview = mapTomorrowPreview(tomorrow, 8);
|
|
return {
|
|
mode: 'outlook',
|
|
receipts,
|
|
tomorrow: preview,
|
|
hasContent: receipts.length > 0 || preview.length > 0,
|
|
};
|
|
}
|
|
|
|
module.exports = { buildOutlook, mapTomorrowPreview };
|