Session G (night2): Phase 6 — landing first-paint + content engine + OG
6.1 FIRST-PAINT ROOT CAUSE: the landing blocked its ENTIRE render on
Supabase auth init ('loading || user') — anonymous visitors stared at
'LOADING THE SLATE' for the whole auth roundtrip (~3-4s). Now a
synchronous localStorage session check gates the suppression: only
visitors who actually hold a session (and will redirect) wait;
anonymous traffic paints the hero immediately. Full RSC conversion of
the hero is deferred and logged — the blocker itself is dead.
Proof Strip rules: top-3 by grade whatever they are; 'TONIGHT'S TOP
SIGNALS' only with >=1 A-tier, else 'TONIGHT'S BOARD'; nothing graded
yet → yesterday's SETTLED reads with outcome chips (misses included).
6.2 Content routes: /api/content/top-signals/:sport,
/streak-watch/:sport (the zero-grade daily format off the aggregator),
/daily-report/:sport — built but self-flagging do_not_post until the
record clears n>=20. Flag, don't fake.
6.3 Per-player OG images: app/player/[name]/opengraph-image.tsx (Node
runtime per the S53 rule) + server layout generateMetadata — every
shared player link unfurls as an intelligence card.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,90 @@ function guard(req, res) {
|
||||
return sport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session 60 (night2/G, spec §12) — the aggregator's content formats.
|
||||
*
|
||||
* GET /top-signals/:sport — tonight's top graded props (snapshot cache).
|
||||
* GET /streak-watch/:sport — the DAILY ZERO-GRADE FORMAT: real streaks
|
||||
* through the lens; postable even on a morning with no lines.
|
||||
* GET /daily-report/:sport — the settled-record report. BUILT but flagged
|
||||
* DO-NOT-POST until the record clears n≥20: the payload carries
|
||||
* `do_not_post: true` until then. Flag, don't fake.
|
||||
*/
|
||||
router.get('/top-signals/:sport', async (req, res) => {
|
||||
const sport = guard(req, res);
|
||||
if (!sport) return undefined;
|
||||
try {
|
||||
const snap = await cacheGet(`snapshot:${sport}:latest`);
|
||||
const grades = (snap && Array.isArray(snap.grades) ? snap.grades : [])
|
||||
.filter((g) => g.grade && !g.outcome)
|
||||
.sort((a, b) => (Number(b.confidence) || 0) - (Number(a.confidence) || 0))
|
||||
.slice(0, 5)
|
||||
.map((g) => ({
|
||||
player: g.player || g.player_name, team: g.team || null,
|
||||
stat: g.stat_type || g.stat, line: g.line,
|
||||
side: String(g.direction || 'over').toLowerCase(), grade: g.grade,
|
||||
archetype: g.archetype || null,
|
||||
locked_at: (g.gradedAt && g.gradedAt.timestamp) || null,
|
||||
}));
|
||||
res.set('Cache-Control', 'public, max-age=300');
|
||||
return res.set(MISSION_HEADER).json({
|
||||
sport, format: 'top-signals', updated_at: snap && snap.updated_at,
|
||||
dataLevel: grades.length > 0 ? 'full' : 'empty', signals: grades,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[content/top-signals/${sport}]`, err.message);
|
||||
return res.set(MISSION_HEADER).json({ sport, format: 'top-signals', dataLevel: 'empty', signals: [] });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/streak-watch/:sport', async (req, res) => {
|
||||
const sport = guard(req, res);
|
||||
if (!sport) return undefined;
|
||||
try {
|
||||
const { loadRosterLogs } = require('../services/rosterLogs');
|
||||
const streaksService = require('../services/streaksService');
|
||||
const { applyLens } = require('../services/streakLens');
|
||||
const roster = await loadRosterLogs(sport);
|
||||
const rows = [
|
||||
...streaksService.computeStreaks(roster, sport, { limit: 8 }),
|
||||
...streaksService.computeFormHeat(roster, sport, { limit: 4 }),
|
||||
];
|
||||
const todayET = new Intl.DateTimeFormat('en-CA', { timeZone: 'America/New_York' }).format(new Date());
|
||||
const sched = await cacheGet(`schedule:${sport}:${todayET}`);
|
||||
const withLens = applyLens(rows, { scheduleGames: Array.isArray(sched) ? sched : [], pitcherGames: [] });
|
||||
res.set('Cache-Control', 'public, max-age=600');
|
||||
return res.set(MISSION_HEADER).json({
|
||||
sport, format: 'streak-watch',
|
||||
dataLevel: withLens.length > 0 ? 'full' : 'empty',
|
||||
streaks: withLens.map((r) => ({ player: r.player, team: r.team, description: r.description, read: r.lens && r.lens.read, streak: r.currentStreak })),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[content/streak-watch/${sport}]`, err.message);
|
||||
return res.set(MISSION_HEADER).json({ sport, format: 'streak-watch', dataLevel: 'empty', streaks: [] });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/daily-report/:sport', async (req, res) => {
|
||||
const sport = guard(req, res);
|
||||
if (!sport) return undefined;
|
||||
try {
|
||||
const ledgerService = require('../services/ledgerService');
|
||||
const agg = await ledgerService.getModelAggregate({ sport });
|
||||
const ready = agg.settled >= (agg.min_sample || 20) && agg.hit_pct != null;
|
||||
res.set('Cache-Control', 'public, max-age=600');
|
||||
return res.set(MISSION_HEADER).json({
|
||||
sport, format: 'daily-report',
|
||||
do_not_post: !ready,
|
||||
reason: ready ? null : `record building — ${agg.settled}/${agg.min_sample || 20} settles`,
|
||||
aggregate: agg,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[content/daily-report/${sport}]`, err.message);
|
||||
return res.set(MISSION_HEADER).json({ sport, format: 'daily-report', do_not_post: true, reason: 'unavailable' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/slate/:sport', async (req, res) => {
|
||||
const sport = guard(req, res);
|
||||
if (!sport) return undefined;
|
||||
|
||||
Reference in New Issue
Block a user