const express = require('express'); const { getSupabaseServiceClient } = require('../utils/supabase'); const { getStatFilters } = require('../config/statFilters'); const { createRateLimit } = require('../middleware/rateLimit'); const { getPlayerIntel, getLeaders, sanitizePlayerName } = require('../services/playerIntelService'); const { getPitcherArsenal } = require('../services/adapters/savantAdapter'); const depthChart = require('../services/depthChartService'); const router = express.Router(); const MISSION_HEADER = { 'X-VYNDR-Mission': 'Kill bad satisfieds before they satisfieds you' }; // Player-intelligence endpoints are public + cache-backed (Session 42). Cap // per-IP like the other public cached routers (60/min). const intelLimit = createRateLimit({ windowMs: 60_000, max: 60 }); // GET /filters/:sport — stat-filter categories for the StatFilterPills UI // (Session 23). Lets the frontend stay data-driven without re-declaring // the category list. NO auth / NO DB — pure config. router.get('/filters/:sport', (req, res) => { const sport = String(req.params.sport || '').toLowerCase(); res.set(MISSION_HEADER).json({ sport, filters: getStatFilters(sport) }); }); // GET /parlays-graded — total scan count router.get('/parlays-graded', async (req, res) => { try { const supabase = getSupabaseServiceClient(); const { count, error } = await supabase .from('scan_sessions') .select('*', { count: 'exact', head: true }); if (error) throw error; res.set(MISSION_HEADER).json({ count: count || 0 }); } catch (err) { console.error('[stats/parlays-graded]', err.message); res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' }); } }); // GET /public — public dashboard stats router.get('/public', async (req, res) => { try { const supabase = getSupabaseServiceClient(); // Total parlays graded const { count: parlaysGraded, error: countErr } = await supabase .from('scan_sessions') .select('*', { count: 'exact', head: true }); if (countErr) throw countErr; // Most common grade const { data: grades, error: gradesErr } = await supabase .from('scan_sessions') .select('final_grade'); if (gradesErr) throw gradesErr; let avg_grade = null; if (grades && grades.length > 0) { const freq = {}; for (const row of grades) { const g = row.final_grade; if (g) freq[g] = (freq[g] || 0) + 1; } let maxCount = 0; for (const [grade, c] of Object.entries(freq)) { if (c > maxCount) { maxCount = c; avg_grade = grade; } } } // Kill conditions caught const { data: picks, error: picksErr } = await supabase .from('picks') .select('kill_conditions') .not('kill_conditions', 'eq', '[]'); if (picksErr) throw picksErr; const kill_conditions_caught = picks ? picks.filter(p => p.kill_conditions && Array.isArray(p.kill_conditions) && p.kill_conditions.length > 0 ).length : 0; res.set(MISSION_HEADER).json({ parlays_graded: parlaysGraded || 0, avg_grade, kill_conditions_caught, sports_covered: ['NBA', 'MLB'], }); } catch (err) { console.error('[stats/public]', err.message); res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' }); } }); // GET /live — top 3 most recently graded props router.get('/live', async (req, res) => { try { const supabase = getSupabaseServiceClient(); const { data, error } = await supabase .from('picks') .select('player, stat_type, line, direction, grade, confidence, created_at') .order('created_at', { ascending: false }) .limit(3); if (error) throw error; const result = (data || []).map(row => ({ player: row.player, stat: row.stat_type, line: row.line, direction: row.direction, grade: row.grade, confidence: row.confidence, sport: 'NBA', graded_at: row.created_at, })); res.set(MISSION_HEADER).json(result); } catch (err) { console.error('[stats/live]', err.message); res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' }); } }); // GET /player/:name?sport=nba — full player intelligence payload (Session 42): // standard stats + archetype DNA + VYNDR intelligence + tonight's graded props. // Name is sanitized inside the service. Always 200 with a valid shape (degrades // gracefully on cold caches) — the profile page must never hard-fail. router.get('/player/:name', intelLimit, async (req, res) => { try { const sport = String(req.query.sport || 'nba').toLowerCase(); const data = await getPlayerIntel(req.params.name, sport); res.set(MISSION_HEADER).json(data); } catch (err) { console.error('[stats/player]', err.message); res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' }); } }); // GET /pitcher/:name/arsenal?sport=mlb — Baseball Savant pitch arsenal (Wave 5B). // Pitch mix % + velo + whiff% (the mockup's PITCHER IDENTITY lens). FREE Statcast // source; the adapter resolves name→MLBAM id + caches 24h. MLB only — any other // sport (or a miss) returns { found:false } and the card self-hides. Never fabricates. router.get('/pitcher/:name/arsenal', intelLimit, async (req, res) => { try { const sport = String(req.query.sport || 'mlb').toLowerCase(); if (sport !== 'mlb') { return res.set(MISSION_HEADER).json({ found: false, reason: 'arsenal is MLB-only' }); } const name = sanitizePlayerName(req.params.name); const arsenal = await getPitcherArsenal(name); res.set(MISSION_HEADER).json(arsenal || { found: false }); } catch (err) { console.error('[stats/pitcher/arsenal]', err.message); // Honesty: an error is an ABSENT arsenal, not a fabricated one. Card self-hides. res.set(MISSION_HEADER).json({ found: false }); } }); // GET /leaders?sport=mlb&stat=hits&limit=10 — tonight's stat leaders (top // graded props by confidence) for the Terminal / Stats Explorer. router.get('/leaders', intelLimit, async (req, res) => { try { const sport = String(req.query.sport || 'nba').toLowerCase(); const leaders = await getLeaders(sport, { stat: req.query.stat, limit: req.query.limit }); res.set(MISSION_HEADER).json({ sport, stat: req.query.stat || null, leaders }); } catch (err) { console.error('[stats/leaders]', err.message); res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' }); } }); // GET /game/:id?sport=nba — game-level enrichment (pitchers, injuries, leaders, // team records) via the ESPN summary (Session 30). Best-effort. router.get('/game/:id', intelLimit, async (req, res) => { try { const sport = String(req.query.sport || 'nba').toLowerCase(); const { getGameSummary } = require('../services/scheduleService'); const summary = await getGameSummary(sport, req.params.id); res.set(MISSION_HEADER).json(summary || { error: 'not_found' }); } catch (err) { console.error('[stats/game]', err.message); res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' }); } }); // Depth chart / lineup / cascade (Session 43). Graceful — always 200 with a // valid (possibly empty) shape so the UI never hard-fails. // GET /lineup/:team?sport=mlb router.get('/lineup/:team', intelLimit, async (req, res) => { try { const sport = String(req.query.sport || 'nba').toLowerCase(); const lineup = await depthChart.getLineup(sport, req.params.team); res.set(MISSION_HEADER).json({ sport, team: req.params.team, lineup }); } catch (err) { console.error('[stats/lineup]', err.message); res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' }); } }); // GET /depth/:team?sport=nba router.get('/depth/:team', intelLimit, async (req, res) => { try { const sport = String(req.query.sport || 'nba').toLowerCase(); const chart = await depthChart.getDepthChart(sport, req.params.team); res.set(MISSION_HEADER).json(chart); } catch (err) { console.error('[stats/depth]', err.message); res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' }); } }); // GET /cascade/:player?sport=nba&team=SA router.get('/cascade/:player', intelLimit, async (req, res) => { try { const sport = String(req.query.sport || 'nba').toLowerCase(); const cascade = await depthChart.getCascadeProjection(sport, req.params.player, req.query.team); res.set(MISSION_HEADER).json({ sport, player: req.params.player, cascade }); } catch (err) { console.error('[stats/cascade]', err.message); res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' }); } }); module.exports = router;