Session 23: All-day intelligence layer — schedule, game lines, streaks, hot lists, stat filtering, ParlayAPI dead (1567 tests)

This commit is contained in:
Kev
2026-06-12 11:16:58 -04:00
parent 6ab49d4c37
commit 0538205fab
32 changed files with 2276 additions and 2 deletions
+15 -1
View File
@@ -61,9 +61,16 @@ const PROVIDERS = {
// /historical/player_props → hit rate enrichment
// /historical/closing_lines → CLV reference
// Base URL: https://api.parlayapi.io/v1. Auth: X-Api-Key header.
// Session 23 — DEAD. Chrome Claude confirmed on 2026-06-12 that
// `api.parlayapi.io` no longer resolves (domain unreachable). We keep
// the entry so the adapter code + its (network-mocked) tests still
// resolve a config, but `status: 'dead'` removes it from every
// fallback chain and the configured-providers list, so the gateway
// never routes a live call to a host that doesn't exist.
'parlayapi': {
name: 'ParlayAPI (historical)',
envKey: 'PARLAYAPI_KEY',
status: 'dead',
quotaType: 'monthly',
quotaLimit: 1000,
resetDay: 1,
@@ -131,10 +138,15 @@ function listProviderIds() {
*/
function getConfiguredProviders() {
return Object.entries(PROVIDERS)
.filter(([, cfg]) => !!process.env[cfg.envKey])
.filter(([, cfg]) => !!process.env[cfg.envKey] && cfg.status !== 'dead')
.map(([id, cfg]) => ({ id, ...cfg }));
}
/** True when a provider is retired (e.g. its host no longer resolves). */
function isDeadProvider(providerId) {
return PROVIDERS[providerId]?.status === 'dead';
}
/**
* Fallback chain for a capability + sport, in priority order,
* excluding `excludeId`. Used by the gateway to walk down to the
@@ -144,6 +156,7 @@ function getFallbackChain(capability, sport, excludeId) {
return Object.entries(PROVIDERS)
.filter(([id, cfg]) =>
id !== excludeId &&
cfg.status !== 'dead' && // Session 23 — skip retired providers
cfg.capabilities.includes(capability) &&
(!sport || cfg.sports.includes(sport)) &&
!!process.env[cfg.envKey],
@@ -159,4 +172,5 @@ module.exports = {
listProviderIds,
getConfiguredProviders,
getFallbackChain,
isDeadProvider,
};
+35
View File
@@ -0,0 +1,35 @@
'use strict';
/**
* Stat-filter categories per sport (Session 23).
*
* The stat filter is VYNDR's navigation system — users browse by what
* they care about ("show me everyone on a 3-point streak"), not by sport
* alone. These categories drive:
* - the StatFilterPills UI
* - the `?stat=` param on /api/streaks and /api/hotlist
*
* Category strings here MUST match the `category` field the streaks &
* hot-list engines emit, or the filter silently returns nothing. Mirror
* any change in `web/src/config/statFilters.ts`.
*/
const STAT_FILTERS = Object.freeze({
nba: ['all', 'points', 'rebounds', 'assists', 'threes', 'blocks', 'steals', 'pra'],
wnba: ['all', 'points', 'rebounds', 'assists', 'threes', 'blocks', 'steals'],
mlb: ['all', 'hits', 'home_runs', 'stolen_bases', 'rbis', 'strikeouts', 'total_bases', 'on_base'],
soccer: ['all', 'goals', 'assists', 'shots', 'tackles', 'saves'],
nfl: ['all', 'passing_yards', 'rushing_yards', 'receiving_yards', 'touchdowns', 'interceptions'],
});
function getStatFilters(sport) {
return STAT_FILTERS[String(sport || '').toLowerCase()] || ['all'];
}
/** Is `stat` a valid category for `sport`? 'all' is always valid. */
function isValidStat(sport, stat) {
if (!stat || stat === 'all') return true;
return getStatFilters(sport).includes(String(stat).toLowerCase());
}
module.exports = { STAT_FILTERS, getStatFilters, isValidStat };