Session 44: Make it visible — VYNDR archetype names, grade intel, schedule fix, landing page (2061 tests)
Frontend + wiring only. Wires existing backend into the pages users see. - VYNDR Original archetype rename (41) across archetypeService.js + lib/ archetypes.js + ArchetypeBadge, each keeping legacyName (resolves stale data). Judge -> BOMBER. Old POWER PULL slot -> WHIFF strikeout-artist pitcher. - BACKEND_HANDOFF.md: canonical frontend<->backend data contract. - Grade card intel: scan/page.tsx now forwards the engine's intel fields (season_avg/form/usage/matchup_grade/archetype/...) into mapScanToGradeResult -> STAT CONTEXT + VYNDR INTELLIGENCE sections populate. The chain already preserved them (tierGating + /api/scan spread); the page was dropping them. - Schedule freshness: slateAdapter.isRelevantGame drops completed games >24h old; Slate.filteredGames applies it. (TTL already 60s.) - Landing: Features.tsx rewritten to user-facing copy (no Point-biserial/Zone 14/ABS/Phi-coefficient). - Depth chart Next proxies added (/api/stats/lineup|depth|cascade) - were 404. - GameCard swap DEFERRED (Kev): legacy on-demand card stays as a bridge until the snapshot pipeline populates the grades cache; vyndr/GameCard swaps in then. Backend 2045 -> 2061 tests (+16), 167 suites. Web build clean (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
|
||||
|
||||
/** Cascade proxy (Session 44) — forwards GET /api/stats/cascade/:player to Express. */
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ player: string }> }) {
|
||||
const { player } = await ctx.params;
|
||||
const qs = req.nextUrl.search;
|
||||
try {
|
||||
const upstream = await fetch(`${BACKEND_URL}/api/stats/cascade/${encodeURIComponent(player)}${qs}`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const data = await upstream.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: upstream.ok ? 200 : upstream.status });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Cascade service is unreachable. Try again in a moment.' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
|
||||
|
||||
/** Depth chart proxy (Session 44) — forwards GET /api/stats/depth/:team to Express. */
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ team: string }> }) {
|
||||
const { team } = await ctx.params;
|
||||
const qs = req.nextUrl.search;
|
||||
try {
|
||||
const upstream = await fetch(`${BACKEND_URL}/api/stats/depth/${encodeURIComponent(team)}${qs}`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const data = await upstream.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: upstream.ok ? 200 : upstream.status });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Depth chart service is unreachable. Try again in a moment.' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
|
||||
|
||||
/** Lineup proxy (Session 44) — forwards GET /api/stats/lineup/:team to Express. */
|
||||
export async function GET(req: NextRequest, ctx: { params: Promise<{ team: string }> }) {
|
||||
const { team } = await ctx.params;
|
||||
const qs = req.nextUrl.search;
|
||||
try {
|
||||
const upstream = await fetch(`${BACKEND_URL}/api/stats/lineup/${encodeURIComponent(team)}${qs}`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const data = await upstream.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: upstream.ok ? 200 : upstream.status });
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Lineup service is unreachable. Try again in a moment.' }, { status: 502 });
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,18 @@ interface ScanResponse {
|
||||
tier: 'free' | 'analyst' | 'desk';
|
||||
error?: string;
|
||||
upgrade?: { tier: string; price: number };
|
||||
// Session 43/44 — Player-Intelligence fields the engine attaches; the grade
|
||||
// card's STAT CONTEXT + VYNDR INTELLIGENCE sections read these.
|
||||
season_avg?: number;
|
||||
last10_avg?: number;
|
||||
vs_opp_avg?: number;
|
||||
form?: number;
|
||||
usage?: string;
|
||||
matchup_grade?: string;
|
||||
rest?: string;
|
||||
archetype?: string;
|
||||
archetype_blend?: { archetype: string; weight: number }[];
|
||||
prop_dna?: { reliable: string[]; volatile: string[] };
|
||||
}
|
||||
|
||||
const NBA_STATS = [
|
||||
@@ -689,6 +701,18 @@ export default function ScanPage() {
|
||||
alt_lines: result.alt_lines,
|
||||
kill_conditions: result.kill_conditions,
|
||||
tier,
|
||||
// Session 44 — forward the engine's intel fields so the grade
|
||||
// card's STAT CONTEXT + VYNDR INTELLIGENCE sections populate.
|
||||
season_avg: result.season_avg,
|
||||
last10_avg: result.last10_avg,
|
||||
vs_opp_avg: result.vs_opp_avg,
|
||||
form: result.form,
|
||||
usage: result.usage,
|
||||
matchup_grade: result.matchup_grade,
|
||||
rest: result.rest,
|
||||
archetype: result.archetype,
|
||||
archetype_blend: result.archetype_blend,
|
||||
prop_dna: result.prop_dna,
|
||||
}) as GradeResultData}
|
||||
onAddToParlay={() => {
|
||||
addLeg({
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
const FEATURES = [
|
||||
{
|
||||
icon: '◆',
|
||||
title: 'Multi-dimensional player archetypes',
|
||||
body: 'Players aren\'t one thing. Our model scores every dimension — pitcher discipline, batter approach, NBA usage shape — and blends them per matchup.',
|
||||
title: 'Player DNA archetypes',
|
||||
body: 'Every player has a prop fingerprint. We classify it, show you which props are reliable vs volatile, and grade accordingly.',
|
||||
},
|
||||
{
|
||||
icon: '↻',
|
||||
title: 'Auto-calibrating engine',
|
||||
body: 'Every resolved grade trains the next one. Point-biserial weight tuning, per-stat calibration, blind-spot detection. The model improves itself.',
|
||||
title: 'Self-improving model',
|
||||
body: 'Every resolved grade makes the next one sharper. The engine learns what works and corrects what doesn\'t.',
|
||||
},
|
||||
{
|
||||
icon: '⚡',
|
||||
title: 'Beat reporter intelligence',
|
||||
body: 'Lineup intel from the people closest to the team — 30 minutes before tip. Trust-tiered, redistribution-aware, line-correlated.',
|
||||
title: 'Lineup intel before tip-off',
|
||||
body: 'Real-time lineup and injury data from trusted sources, giving you the edge before the books adjust.',
|
||||
},
|
||||
{
|
||||
icon: '⊘',
|
||||
title: 'Kill conditions',
|
||||
body: 'We don\'t just grade the prop. We tell you what kills it. Six hard checks per read: minutes, sample, fatigue, blowout risk, splits, line conflict.',
|
||||
body: 'Six hard checks on every prop. If minutes, fatigue, blowout risk, or splits say no, we flag it — even on an A grade.',
|
||||
},
|
||||
{
|
||||
icon: '∿',
|
||||
title: 'Parlay correlation math',
|
||||
body: 'Phi-coefficient analysis catches the legs that secretly fight each other. The books love correlated unders. We surface them.',
|
||||
body: 'We catch the legs that secretly fight each other. The books love correlated unders — we surface them before you tap.',
|
||||
},
|
||||
{
|
||||
icon: '⌧',
|
||||
title: 'ABS intelligence (MLB)',
|
||||
body: 'The automated strike zone changes everything. Per-pitcher, per-batter discipline scoring. Zone 14 framing loss. Challenge math.',
|
||||
title: 'Deep pitcher-batter matchups',
|
||||
body: 'We score discipline, contact quality, and zone tendencies for every MLB matchup. Not just ERA.',
|
||||
},
|
||||
{
|
||||
icon: '◯',
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import GameCard, { SlateSport, GameLines, GameStreak } from '@/components/GameCard';
|
||||
import { PropRowProp, PropRowResult, propRowKey, Tier } from '@/components/PropRow';
|
||||
import { isRelevantGame } from '@/lib/slateAdapter';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
// Session 23 — all-day intelligence layer. The stat filter is the
|
||||
// navigation system; streaks + hot lists layer ON TOP of the odds the
|
||||
@@ -507,9 +508,12 @@ export default function Slate({ initialTab = 'all', tier = 'free' }: SlateProps)
|
||||
|
||||
// Filter pipeline — searchQuery applied to games + props.
|
||||
const filteredGames = useMemo(() => {
|
||||
if (!searchQuery.trim()) return games;
|
||||
// Session 44 — drop completed games >24h old so a 5-day-old FINAL never
|
||||
// lingers on the dashboard. Upcoming + live always show.
|
||||
const fresh = games.filter((g) => isRelevantGame(g));
|
||||
if (!searchQuery.trim()) return fresh;
|
||||
const q = searchQuery.toLowerCase();
|
||||
return games
|
||||
return fresh
|
||||
.map((g) => {
|
||||
const homeMatch = g.homeTeam.toLowerCase().includes(q);
|
||||
const awayMatch = g.awayTeam.toLowerCase().includes(q);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { badgeStyle, glyphSvg } from '@/lib/archetypes';
|
||||
|
||||
interface ArchetypeBadgeProps {
|
||||
archetype: string; // archetype name, e.g. "POWER PULL" (case-insensitive)
|
||||
archetype: string; // VYNDR archetype name, e.g. "BOMBER" (case-insensitive; legacy names resolve too)
|
||||
sport?: string; // nba/mlb/wnba/soccer — informational; styling is per-archetype
|
||||
variant?: 'full' | 'ghost' | 'tint';
|
||||
size?: 'sm' | 'md';
|
||||
|
||||
+58
-44
@@ -33,60 +33,71 @@ const GLYPHS = {
|
||||
postup: '<rect x="6.4" y="2.6" width="3.2" height="11" rx="1.6" fill="currentColor"/><circle cx="12" cy="5.2" r="1.7" fill="currentColor"/>',
|
||||
};
|
||||
|
||||
/* name → { color, glyph, desc } — the design's MAP. */
|
||||
/* name → { color, glyph, desc, legacy } — VYNDR Originals (Session 44).
|
||||
Keys/colors/glyphs MUST match src/services/archetypeService.js ARCHETYPES.
|
||||
`legacy` is the old descriptive label, kept for reference (never displayed). */
|
||||
const ARCHETYPE_MAP = {
|
||||
// NBA
|
||||
'VOLUME SCORER': { c: '#FF6B4A', d: 'High usage, shot-dependent scorer', g: 'triangle' },
|
||||
'FLOOR GENERAL': { c: '#4A9EFF', d: 'Assist-heavy playmaker', g: 'node' },
|
||||
'TWO-WAY ANCHOR': { c: '#A78BFA', d: 'Defense, rebounds, blocks', g: 'shield' },
|
||||
'STRETCH BIG': { c: '#2DD4BF', d: 'Floor-spacing shooting big', g: 'target' },
|
||||
'USAGE SPONGE': { c: '#FFB347', d: 'Usage spikes when stars sit', g: 'uparrow' },
|
||||
'COMBO GUARD': { c: '#00D4A0', d: 'Scoring + playmaking hybrid', g: 'twin' },
|
||||
'ROLE GLUE': { c: '#9499A8', d: 'Low-usage specialist', g: 'chain' },
|
||||
'TRANSITION ENGINE': { c: '#22D3EE', d: 'Pace-pushing fast-break threat', g: 'chevrons' },
|
||||
'POST SCORER': { c: '#FF5C5C', d: 'Back-to-basket interior scorer', g: 'postup' },
|
||||
'DEFENSIVE SPECIALIST': { c: '#6366F1', d: 'Perimeter stopper, low usage', g: 'shieldCheck' },
|
||||
'POINT FORWARD': { c: '#38BDF8', d: 'Oversized primary creator', g: 'half' },
|
||||
SLASHER: { c: '#FB923C', d: 'Rim-attacking, foul-drawing driver', g: 'slash' },
|
||||
'RIM RUNNER': { c: '#F472B6', d: 'Lob and putback finisher', g: 'arc' },
|
||||
'3-AND-D': { c: '#818CF8', d: 'Catch-and-shoot plus defense', g: 'crosshair' },
|
||||
'SIXTH MAN': { c: '#FACC15', d: 'Bench scoring spark', g: 'bolt' },
|
||||
TORCH: { c: '#FF6B4A', d: 'High usage, shot-dependent scorer', g: 'triangle', legacy: 'VOLUME SCORER' },
|
||||
CONDUCTOR: { c: '#4A9EFF', d: 'Assist-heavy playmaker', g: 'node', legacy: 'FLOOR GENERAL' },
|
||||
FORTRESS: { c: '#A78BFA', d: 'Defense, rebounds, blocks', g: 'shield', legacy: 'TWO-WAY ANCHOR' },
|
||||
ARTILLERY: { c: '#2DD4BF', d: 'Floor-spacing shooting big', g: 'target', legacy: 'STRETCH BIG' },
|
||||
SURGE: { c: '#FFB347', d: 'Usage spikes when stars sit', g: 'uparrow', legacy: 'USAGE SPONGE' },
|
||||
'DUAL THREAT': { c: '#00D4A0', d: 'Scoring + playmaking hybrid', g: 'twin', legacy: 'COMBO GUARD' },
|
||||
CONNECTOR: { c: '#9499A8', d: 'Low-usage specialist', g: 'chain', legacy: 'ROLE GLUE' },
|
||||
FASTBREAK: { c: '#22D3EE', d: 'Pace-pushing fast-break threat', g: 'chevrons', legacy: 'TRANSITION ENGINE' },
|
||||
'PAINT BOSS': { c: '#FF5C5C', d: 'Back-to-basket interior scorer', g: 'postup', legacy: 'POST SCORER' },
|
||||
LOCKDOWN: { c: '#6366F1', d: 'Perimeter stopper, low usage', g: 'shieldCheck', legacy: 'DEFENSIVE SPECIALIST' },
|
||||
SWITCHBOARD: { c: '#38BDF8', d: 'Oversized primary creator', g: 'half', legacy: 'POINT FORWARD' },
|
||||
ARCHITECT: { c: '#FB923C', d: 'Self-created shot maker, rim-attacking', g: 'slash', legacy: 'SLASHER' },
|
||||
PISTON: { c: '#F472B6', d: 'Lob and putback finisher', g: 'arc', legacy: 'RIM RUNNER' },
|
||||
SENTINEL: { c: '#818CF8', d: 'Catch-and-shoot plus defense', g: 'crosshair', legacy: '3-AND-D' },
|
||||
IGNITER: { c: '#FACC15', d: 'Bench scoring spark', g: 'bolt', legacy: 'SIXTH MAN' },
|
||||
// WNBA-unique
|
||||
'POST FACILITATOR': { c: '#C084FC', d: 'Playmaking hub from the post', g: 'node' },
|
||||
'TWO-WAY WING': { c: '#A78BFA', d: 'Two-way perimeter wing', g: 'shieldCheck' },
|
||||
'STRETCH FORWARD': { c: '#2DD4BF', d: 'Floor-spacing forward', g: 'target' },
|
||||
'SLASHING GUARD': { c: '#FB923C', d: 'Downhill driving guard', g: 'slash' },
|
||||
'INTERIOR ANCHOR': { c: '#6366F1', d: 'Paint defender and rebounder', g: 'shield' },
|
||||
DISTRIBUTOR: { c: '#C084FC', d: 'Playmaking hub from the post', g: 'node', legacy: 'POST FACILITATOR' },
|
||||
SHIELD: { c: '#A78BFA', d: 'Two-way perimeter forward', g: 'shieldCheck', legacy: 'TWO-WAY WING' },
|
||||
RANGE: { c: '#2DD4BF', d: 'Floor-spacing forward', g: 'target', legacy: 'STRETCH FORWARD' },
|
||||
SPARK: { c: '#FB923C', d: 'Downhill scoring guard', g: 'slash', legacy: 'SLASHING GUARD' },
|
||||
ANCHOR: { c: '#6366F1', d: 'Dominant paint defender and rebounder', g: 'shield', legacy: 'INTERIOR ANCHOR' },
|
||||
// MLB
|
||||
'POWER PULL': { c: '#FF5C5C', d: 'HR-dependent, high strikeout power', g: 'batball' },
|
||||
CONTACT: { c: '#3DDC84', d: 'High average, low strikeout', g: 'crosshair' },
|
||||
'RUN PRODUCER': { c: '#4A9EFF', d: 'RBI-dependent, lineup context', g: 'diamond' },
|
||||
ACE: { c: '#A78BFA', d: 'High K/9, low WHIP, deep games', g: 'star' },
|
||||
'BULLPEN ARM': { c: '#FFB347', d: 'Short outings, high leverage', g: 'bolt' },
|
||||
'SPEED THREAT': { c: '#2DD4BF', d: 'Stolen bases, speed score', g: 'chevrons' },
|
||||
'TWO-WAY PLAYER': { c: '#F472B6', d: 'Bats and pitches at elite level', g: 'half' },
|
||||
'UTILITY PLAYER': { c: '#22D3EE', d: 'Multi-position lineup flex', g: 'plus' },
|
||||
'INNINGS EATER': { c: '#818CF8', d: 'Durable, deep-start workhorse', g: 'clock' },
|
||||
'POWER SLUGGER': { c: '#FF6B4A', d: 'All-fields power producer', g: 'triangle' },
|
||||
'TABLE SETTER': { c: '#38BDF8', d: 'On-base leadoff catalyst', g: 'diamondLine' },
|
||||
'GAP HITTER': { c: '#34D399', d: 'Doubles and extra-base gaps', g: 'uparrow' },
|
||||
CLOSER: { c: '#FB7185', d: 'Ninth-inning save specialist', g: 'lock' },
|
||||
SWINGMAN: { c: '#FBBF24', d: 'Spot starter and long relief', g: 'swap' },
|
||||
'DEFENSIVE WIZARD': { c: '#6366F1', d: 'Glove-first defensive value', g: 'shieldCheck' },
|
||||
BOMBER: { c: '#FF6B4A', d: 'Middle-of-the-order power producer', g: 'triangle', legacy: 'POWER SLUGGER' },
|
||||
BRUSH: { c: '#3DDC84', d: 'High average, low strikeout', g: 'crosshair', legacy: 'CONTACT' },
|
||||
DRIVER: { c: '#4A9EFF', d: 'RBI-dependent, lineup context', g: 'diamond', legacy: 'RUN PRODUCER' },
|
||||
ALPHA: { c: '#A78BFA', d: 'High K/9, low WHIP, deep games', g: 'star', legacy: 'ACE' },
|
||||
WHIFF: { c: '#FFB347', d: 'Bat-missing arm, high K with traffic', g: 'bolt', legacy: 'STRIKEOUT ARTIST' },
|
||||
GHOST: { c: '#2DD4BF', d: 'Stolen bases, speed score', g: 'chevrons', legacy: 'SPEED THREAT' },
|
||||
HYBRID: { c: '#F472B6', d: 'Bats and pitches at elite level', g: 'half', legacy: 'TWO-WAY PLAYER' },
|
||||
FLEX: { c: '#22D3EE', d: 'Multi-position lineup flex', g: 'plus', legacy: 'UTILITY PLAYER' },
|
||||
WORKHORSE: { c: '#818CF8', d: 'Durable, deep-start arm', g: 'clock', legacy: 'INNINGS EATER' },
|
||||
CATALYST: { c: '#38BDF8', d: 'On-base leadoff catalyst', g: 'diamondLine', legacy: 'TABLE SETTER' },
|
||||
MIRROR: { c: '#34D399', d: 'Gap-to-gap line-drive bat', g: 'uparrow', legacy: 'GAP HITTER' },
|
||||
HAMMER: { c: '#FB7185', d: 'Ninth-inning save specialist', g: 'lock', legacy: 'CLOSER' },
|
||||
SINKER: { c: '#FBBF24', d: 'Groundball spot-starter / long relief', g: 'swap', legacy: 'SWINGMAN' },
|
||||
BRIDGE: { c: '#6366F1', d: 'Setup / high-leverage middle relief', g: 'shieldCheck', legacy: 'BULLPEN ARM' },
|
||||
SWITCH: { c: '#FF5C5C', d: 'Platoon-leveraged, glove-first bat', g: 'batball', legacy: 'DEFENSIVE WIZARD' },
|
||||
// Soccer
|
||||
POACHER: { c: '#FF5C5C', d: 'Penalty-box finisher', g: 'crosshair' },
|
||||
CREATOR: { c: '#4A9EFF', d: 'Chance-creating playmaker', g: 'node' },
|
||||
'TARGET MAN': { c: '#FF6B4A', d: 'Hold-up aerial striker', g: 'triangle' },
|
||||
'BOX-TO-BOX': { c: '#00D4A0', d: 'All-action central midfielder', g: 'chevrons' },
|
||||
'WING WIZARD': { c: '#2DD4BF', d: 'Dribbling wide threat', g: 'slash' },
|
||||
'SWEEPER KEEPER': { c: '#A78BFA', d: 'Distributing goalkeeper', g: 'shield' },
|
||||
FINISHER: { c: '#FF5C5C', d: 'Penalty-box finisher', g: 'crosshair', legacy: 'POACHER' },
|
||||
MAESTRO: { c: '#4A9EFF', d: 'Chance-creating playmaker', g: 'node', legacy: 'CREATOR' },
|
||||
TOWER: { c: '#FF6B4A', d: 'Hold-up aerial striker', g: 'triangle', legacy: 'TARGET MAN' },
|
||||
MOTOR: { c: '#00D4A0', d: 'All-action central midfielder', g: 'chevrons', legacy: 'BOX-TO-BOX' },
|
||||
BLADE: { c: '#2DD4BF', d: 'Dribbling wide threat', g: 'slash', legacy: 'WING WIZARD' },
|
||||
WALL: { c: '#A78BFA', d: 'Distributing goalkeeper', g: 'shield', legacy: 'SWEEPER KEEPER' },
|
||||
};
|
||||
|
||||
const FALLBACK = { c: '#9499A8', d: '', g: '' };
|
||||
|
||||
// Reverse index so an old legacy name (e.g. "POWER SLUGGER") still resolves to
|
||||
// its VYNDR Original (BOMBER) — defends any stale cached data post-rename.
|
||||
const LEGACY_INDEX = {};
|
||||
for (const [k, v] of Object.entries(ARCHETYPE_MAP)) {
|
||||
if (v.legacy) LEGACY_INDEX[v.legacy.toUpperCase()] = k;
|
||||
}
|
||||
|
||||
function archetypeInfo(name) {
|
||||
const key = (name == null ? '' : String(name)).toUpperCase();
|
||||
return ARCHETYPE_MAP[key] || FALLBACK;
|
||||
if (ARCHETYPE_MAP[key]) return ARCHETYPE_MAP[key];
|
||||
if (LEGACY_INDEX[key]) return ARCHETYPE_MAP[LEGACY_INDEX[key]];
|
||||
return FALLBACK;
|
||||
}
|
||||
|
||||
function archetypeColor(name) {
|
||||
@@ -115,8 +126,11 @@ function badgeStyle(name, variant = 'tint', size = 'sm') {
|
||||
} else {
|
||||
textColor = info.c; bg = info.c + '1F'; borderColor = info.c + '52'; glyphColor = info.c;
|
||||
}
|
||||
// Display the canonical VYNDR name even if a legacy name was passed.
|
||||
const upper = (name == null ? '' : String(name)).toUpperCase();
|
||||
const canonical = ARCHETYPE_MAP[upper] ? upper : LEGACY_INDEX[upper] || upper;
|
||||
return {
|
||||
name: (name == null ? '' : String(name)).toUpperCase(),
|
||||
name: canonical,
|
||||
desc: info.d,
|
||||
glyph: info.g,
|
||||
color: info.c,
|
||||
|
||||
@@ -175,6 +175,23 @@ function mapPitchers(game) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Should this game still show on the slate (Session 44)? Upcoming + live games
|
||||
* always show; a COMPLETED game is dropped once it's more than 24h old, so a
|
||||
* 5-day-old FINAL never lingers on the dashboard. Unknown/missing date → keep
|
||||
* (degrade open). `now` is injectable for tests.
|
||||
*/
|
||||
function isRelevantGame(game, now = Date.now()) {
|
||||
if (!game) return false;
|
||||
const state = String(game.state || game.status || '').toLowerCase();
|
||||
const isFinal = state === 'final' || state === 'post' || state === 'closed' || state === 'complete';
|
||||
if (!isFinal) return true;
|
||||
const raw = game.date || game.gameTime || game.commence_time || game.startTime;
|
||||
const t = raw ? new Date(raw).getTime() : NaN;
|
||||
if (Number.isNaN(t)) return true; // no parseable date → don't hide
|
||||
return (now - t) / 3_600_000 < 24;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseAmericanOdds,
|
||||
detectBestLines,
|
||||
@@ -183,4 +200,5 @@ module.exports = {
|
||||
formatGameTime,
|
||||
groupPropsByPlayer,
|
||||
mapPitchers,
|
||||
isRelevantGame,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user