02c17a65c3
- lineupService: statsapi hydrate=lineups (live shape verified) → CONFIRMED (batting slot) / NOT_IN (team posted without the player) / PROJECTED (not posted). 10-min cache, pure parser, injectable. - NOT_IN visibly KILLS the grade on the slate: struck through + NOT IN LINEUP chip, parlay/book actions suppressed. The locked ledger read is untouched — honesty is showing the read is dead, not deleting it. - injuryService: ESPN injuries feed → OUT/GTD/PROB chips (unknown status → no chip, never invented). Chips on slate strips via ViabilityChips. - Date navigation on the Slate: YESTERDAY (results surface — finals + THE SETTLE panel of that date's settled reads w/ outcome + CLV chips, via new ?date= filter on /api/ledger/model) / TODAY / TOMORROW (schedule until lines post). Odds/grades/pitcher layers are TODAY's and never fake other dates; 60s poll only refreshes today. - Routes /api/schedule/:sport/lineups + /injuries + Next proxies. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
111 lines
4.4 KiB
JavaScript
111 lines
4.4 KiB
JavaScript
/**
|
|
* /api/schedule/:sport (Session 23)
|
|
*
|
|
* Returns today's game schedule from cached/free ESPN data. NO odds-api
|
|
* credits burned. The PM2 pollers warm this cache every 60s; on a miss
|
|
* the schedule service self-heals by fetching the free ESPN scoreboard.
|
|
*
|
|
* Each game carries two boolean flags read from OTHER caches (no fetch):
|
|
* hasOdds — odds-api player props exist for this slate
|
|
* hasGameLines — Tank01 game-level lines exist for this slate
|
|
*
|
|
* Response shape:
|
|
* {
|
|
* sport: 'nba',
|
|
* date: '2026-06-12',
|
|
* games: [ { id, homeTeam, awayTeam, gameTime, status, score,
|
|
* venue, broadcast, hasOdds, hasGameLines } ],
|
|
* source: 'espn',
|
|
* }
|
|
*/
|
|
|
|
const express = require('express');
|
|
const scheduleService = require('../services/scheduleService');
|
|
const { SPORT_CONFIG } = require('../config/sports');
|
|
const { createRateLimit } = require('../middleware/rateLimit');
|
|
|
|
const router = express.Router();
|
|
// Session 32 — public throttle (60/min; ESPN is free but be respectful).
|
|
router.use(createRateLimit({ windowMs: 60_000, max: 60 }));
|
|
|
|
const MISSION_HEADER = { 'X-VYNDR-Mission': 'The slate is never empty' };
|
|
|
|
// GET /api/schedule/:sport/pitchers (Session 46) — today's MLB probable starters
|
|
// (statsapi.mlb.com; the ESPN schedule above doesn't carry them). MLB only.
|
|
router.get('/:sport/pitchers', async (req, res) => {
|
|
const sport = String(req.params.sport || '').toLowerCase();
|
|
if (sport !== 'mlb') {
|
|
return res.set(MISSION_HEADER).json({ sport, date: null, games: [] });
|
|
}
|
|
const date = req.query.date || scheduleService.todayET();
|
|
try {
|
|
const { getProbablePitchers } = require('../services/probablePitchers');
|
|
const games = await getProbablePitchers(date);
|
|
res.set('Cache-Control', 'public, max-age=300');
|
|
return res.set(MISSION_HEADER).json({ sport, date, games });
|
|
} catch (err) {
|
|
console.error('[schedule/pitchers]', err.message);
|
|
return res.set(MISSION_HEADER).json({ sport, date, games: [] });
|
|
}
|
|
});
|
|
|
|
// Session 64 (A1-S5) — MLB lineup confirmation. { byPlayer, postedTeams };
|
|
// consumers resolve CONFIRMED / NOT_IN / PROJECTED via the posted-team rule.
|
|
router.get('/:sport/lineups', async (req, res) => {
|
|
const sport = String(req.params.sport || '').toLowerCase();
|
|
if (sport !== 'mlb') return res.set(MISSION_HEADER).json({ sport, byPlayer: {}, postedTeams: [] });
|
|
const date = req.query.date || scheduleService.todayET();
|
|
try {
|
|
const { fetchLineups } = require('../services/lineupService');
|
|
const parsed = await fetchLineups(date);
|
|
res.set('Cache-Control', 'public, max-age=300');
|
|
return res.set(MISSION_HEADER).json({ sport, date, ...parsed });
|
|
} catch (err) {
|
|
console.error('[schedule/lineups]', err.message);
|
|
return res.set(MISSION_HEADER).json({ sport, date, byPlayer: {}, postedTeams: [] });
|
|
}
|
|
});
|
|
|
|
// Session 64 (A1-S5) — the injury wire (ESPN feed): { byPlayer: { key:
|
|
// { status: OUT|GTD|PROB, detail, team } } }. Accurate chips, no cascades yet.
|
|
router.get('/:sport/injuries', async (req, res) => {
|
|
const sport = String(req.params.sport || '').toLowerCase();
|
|
try {
|
|
const { fetchInjuries } = require('../services/injuryService');
|
|
const byPlayer = await fetchInjuries(sport);
|
|
res.set('Cache-Control', 'public, max-age=600');
|
|
return res.set(MISSION_HEADER).json({ sport, byPlayer });
|
|
} catch (err) {
|
|
console.error('[schedule/injuries]', err.message);
|
|
return res.set(MISSION_HEADER).json({ sport, byPlayer: {} });
|
|
}
|
|
});
|
|
|
|
router.get('/:sport', async (req, res) => {
|
|
const sport = String(req.params.sport || '').toLowerCase();
|
|
const date = req.query.date || scheduleService.todayET();
|
|
|
|
if (!SPORT_CONFIG[sport]) {
|
|
return res.status(404).set(MISSION_HEADER).json({ error: `Unknown sport: ${sport}` });
|
|
}
|
|
|
|
try {
|
|
const raw = await scheduleService.getSchedule(sport, date);
|
|
// null → unsupported sport (already guarded above); treat defensively.
|
|
const games = await scheduleService.enrichFlags(sport, date, raw || []);
|
|
return res.set(MISSION_HEADER).json({
|
|
sport,
|
|
date,
|
|
games,
|
|
source: 'espn',
|
|
});
|
|
} catch (err) {
|
|
console.error(`[schedule/${sport}]`, err.message);
|
|
// Even on error we return an empty slate, not a 5xx — the platform
|
|
// is NEVER down. Other layers (game lines, props) keep it alive.
|
|
return res.set(MISSION_HEADER).json({ sport, date, games: [], source: 'espn' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|