'use strict'; /** * /api/content (Session 29) * * Structured content objects generated from live VYNDR data, degrading * gracefully by data level. Read-only, zero-credit (reads cached data). * `?format=text` additionally returns post-ready plain text. * * GET /api/content/slate/:sport → daily slate thread * GET /api/content/potd/:sport → prop (or game) of the day * GET /api/content/recap/:sport → results recap (needs resolved grades) * GET /api/content/preview/:sport/:gameId → matchup preview for one game */ const express = require('express'); const template = require('../services/contentTemplateService'); const formatter = require('../services/contentFormatter'); const { cacheGet } = require('../utils/redis'); const router = express.Router(); const MISSION_HEADER = { 'X-VYNDR-Mission': 'Every post is a free ad' }; const SUPPORTED = new Set(['nba', 'wnba', 'mlb', 'soccer', 'nfl', 'nhl']); function guard(req, res) { const sport = String(req.params.sport || '').toLowerCase(); if (!SUPPORTED.has(sport)) { res.status(404).set(MISSION_HEADER).json({ error: `No content for sport: ${sport}` }); return null; } return sport; } router.get('/slate/:sport', async (req, res) => { const sport = guard(req, res); if (!sport) return undefined; try { const data = await template.collectSlateData(sport); const thread = template.generateSlateThread(sport, data); const body = { ...thread }; if (req.query.format === 'text') body.text = formatter.formatSlateThread(thread); return res.set(MISSION_HEADER).json(body); } catch (err) { console.error(`[content/slate/${sport}]`, err.message); return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' }); } }); router.get('/potd/:sport', async (req, res) => { const sport = guard(req, res); if (!sport) return undefined; try { const data = await template.collectSlateData(sport); const potd = template.generatePOTD(sport, data); const body = { ...potd }; if (req.query.format === 'text') body.text = formatter.formatPOTD(potd); return res.set(MISSION_HEADER).json(body); } catch (err) { console.error(`[content/potd/${sport}]`, err.message); return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' }); } }); router.get('/recap/:sport', async (req, res) => { const sport = guard(req, res); if (!sport) return undefined; try { // Resolved grades land in a daily cache when the grading flow settles. const utc = new Date().toISOString().split('T')[0]; const cache = (await cacheGet(`results:${sport}:${utc}`)) ?? (await cacheGet(`results:${sport}`)); const resolved = Array.isArray(cache) ? cache : Array.isArray(cache?.grades) ? cache.grades : []; const recap = template.generateResultsRecap(sport, resolved); const body = { ...recap }; if (req.query.format === 'text') body.text = formatter.formatRecap(recap); return res.set(MISSION_HEADER).json(body); } catch (err) { console.error(`[content/recap/${sport}]`, err.message); return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' }); } }); router.get('/preview/:sport/:gameId', async (req, res) => { const sport = guard(req, res); if (!sport) return undefined; const { gameId } = req.params; try { const data = await template.collectSlateData(sport); const game = (data.schedule || []).find((g) => String(g.id) === String(gameId)); if (!game) { return res.status(404).set(MISSION_HEADER).json({ error: 'Game not on today\'s schedule.' }); } const lines = template.__internals.findGameLinesFor(game, data.gameLines); const preview = template.generateMatchupPreview(game, lines, data.streaks); return res.set(MISSION_HEADER).json(preview); } catch (err) { console.error(`[content/preview/${sport}]`, err.message); return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' }); } }); module.exports = router;