/** * /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;