/** * /api/combat/:date + /api/fight/:id (Wave 6 — combat intelligence, honest v1). * * Fight-card discovery + tale-of-the-tape + best-effort moneyline / round-total * odds. FREE ESPN MMA feed for the cards; the paid odds-api feed for ML/round * totals is BEST-EFFORT + cached (never fails the card). Combat is NOT in the * snapshot/settle loop → NO settled grades here; method/round/KO are surfaced by * the frontend as honest "— data-limited", never fabricated. * * Response (cards): * { date, events: [ { id, name, shortName, date, venue, * bouts: [ { id, weightClass, rounds, status, fighters: [ …tale ], * odds: { moneyline, roundTotal } | null } ] } ], source } */ const express = require('express'); const combat = require('../services/adapters/combatAdapter'); const { createRateLimit } = require('../middleware/rateLimit'); const router = express.Router(); // Public throttle (60/min; ESPN is free, odds are cached — be respectful). router.use(createRateLimit({ windowMs: 60_000, max: 60 })); const MISSION_HEADER = { 'X-VYNDR-Mission': 'Styles make fights' }; // Attach cached/best-effort odds onto each bout of each event. function attachOdds(events, oddsMap) { for (const ev of events || []) { for (const bout of ev.bouts || []) { const o = combat.matchBoutOdds(bout, oddsMap); bout.odds = o ? { moneyline: o.moneyline, roundTotal: o.roundTotal } : null; } } return events; } // GET /api/combat/:date — fight cards for an ET date (defaults to today). router.get('/:date', async (req, res) => { const date = /^\d{4}-\d{2}-\d{2}$/.test(String(req.params.date || '')) ? req.params.date : combat.todayET(); try { const [cards, oddsMap] = await Promise.all([ combat.getFightCards(date), combat.getCombatOdds().catch(() => ({})), ]); attachOdds(cards.events, oddsMap); res.set('Cache-Control', 'public, max-age=300'); return res.set(MISSION_HEADER).json(cards); } catch (err) { console.error('[combat/:date]', err && err.message); // The board is never a crash — honest empty on failure. return res.set(MISSION_HEADER).json({ date, events: [], source: 'espn' }); } }); module.exports = router; // Separate router for /api/fight/:id (a single card by event id). const fightRouter = express.Router(); fightRouter.use(createRateLimit({ windowMs: 60_000, max: 60 })); fightRouter.get('/:id', async (req, res) => { const id = String(req.params.id || '').replace(/[^0-9]/g, ''); if (!id) return res.status(404).set(MISSION_HEADER).json({ error: 'not found' }); try { const [card, oddsMap] = await Promise.all([ combat.getFightCard(id), combat.getCombatOdds().catch(() => ({})), ]); if (!card) return res.status(404).set(MISSION_HEADER).json({ error: 'card not found' }); attachOdds([card], oddsMap); res.set('Cache-Control', 'public, max-age=300'); return res.set(MISSION_HEADER).json({ event: card, source: 'espn' }); } catch (err) { console.error('[fight/:id]', err && err.message); return res.status(404).set(MISSION_HEADER).json({ error: 'card not found' }); } }); module.exports.fightRouter = fightRouter;