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>
83 lines
3.1 KiB
JavaScript
83 lines
3.1 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* lineupService — MLB lineup confirmation (Session 64 / A1-S5).
|
|
*
|
|
* FREE statsapi `schedule?hydrate=lineups`: once a team posts its lineup the
|
|
* feed carries homePlayers/awayPlayers in batting order. From that, per
|
|
* player (nameKey):
|
|
* CONFIRMED (with batting slot) — the player is in a posted lineup.
|
|
* NOT_IN — his team's lineup IS posted and he isn't in it. This visibly
|
|
* kills the grade on the slate (struck through + chip). The
|
|
* locked ledger read is untouched — honesty means SHOWING the
|
|
* read is dead, not deleting it.
|
|
* (absent) — his team hasn't posted yet → the UI renders PROJECTED.
|
|
*
|
|
* Cache 10 min (lineups post in waves pre-game). Pure parser + injectable
|
|
* fetch → unit-tested on the real feed shape with zero network.
|
|
*/
|
|
|
|
const { nameKey } = require('../utils/playerName');
|
|
|
|
const TTL = 600; // 10 min
|
|
const BASE = 'https://statsapi.mlb.com/api/v1';
|
|
const HTTP_TIMEOUT_MS = 10_000;
|
|
|
|
/**
|
|
* Pure: statsapi schedule JSON → {
|
|
* byPlayer: { nameKey: { status:'confirmed', slot, team } },
|
|
* postedTeams: [team names whose lineup is up],
|
|
* }
|
|
* Players NOT in byPlayer whose team IS in postedTeams are NOT_IN —
|
|
* resolved by statusFor().
|
|
*/
|
|
function parseLineups(scheduleJson) {
|
|
const byPlayer = {};
|
|
const postedTeams = [];
|
|
const games = ((scheduleJson || {}).dates || [])[0]?.games || [];
|
|
for (const g of games) {
|
|
const lu = g.lineups || {};
|
|
for (const side of ['home', 'away']) {
|
|
const players = lu[`${side}Players`];
|
|
if (!Array.isArray(players) || players.length === 0) continue;
|
|
const team = g.teams?.[side]?.team?.name || null;
|
|
if (team) postedTeams.push(team);
|
|
players.forEach((p, i) => {
|
|
if (!p || !p.fullName) return;
|
|
byPlayer[nameKey(p.fullName)] = { status: 'confirmed', slot: i + 1, team };
|
|
});
|
|
}
|
|
}
|
|
return { byPlayer, postedTeams };
|
|
}
|
|
|
|
/** Resolve one player's viability given parsed lineups + his team. */
|
|
function statusFor(player, team, parsed) {
|
|
if (!parsed) return { status: 'projected' };
|
|
const hit = parsed.byPlayer[nameKey(player)];
|
|
if (hit) return hit;
|
|
const token = (n) => String(n || '').toLowerCase().split(/\s+/).pop();
|
|
const posted = team && parsed.postedTeams.some((t) => t === team || token(t) === token(team));
|
|
return posted ? { status: 'not_in', team } : { status: 'projected' };
|
|
}
|
|
|
|
async function fetchLineups(date, opts = {}) {
|
|
const cacheGet = opts.cacheGet || require('../utils/redis').cacheGet;
|
|
const cacheSet = opts.cacheSet || require('../utils/redis').cacheSet;
|
|
const key = `lineups:mlb:${date}`;
|
|
const cached = await cacheGet(key);
|
|
if (cached) return cached;
|
|
const axios = opts.axios || require('axios');
|
|
try {
|
|
const res = await axios.get(`${BASE}/schedule?sportId=1&date=${date}&hydrate=lineups`, { timeout: HTTP_TIMEOUT_MS });
|
|
const parsed = parseLineups(res.data);
|
|
await cacheSet(key, parsed, TTL);
|
|
return parsed;
|
|
} catch (e) {
|
|
console.warn('[lineups] fetch failed:', e.message);
|
|
return { byPlayer: {}, postedTeams: [] };
|
|
}
|
|
}
|
|
|
|
module.exports = { fetchLineups, parseLineups, statusFor, __internals: { TTL, BASE } };
|