Session 55: Self-learning loop + real-time layer (2274 tests)

Product overhaul core — the two transformative, differentiated systems:

Self-learning loop (Phase 2): outcomeService settles locked snapshot grades
against real MLB Stats API results → hit/miss/push, rolling accuracy by grade
tier (30d window). Idempotent, injectable, unit-tested. New GET /api/accuracy +
/api/ledger/accuracy + internal settle triggers + cron hook. AccuracyBadge
(dashboard/scan/landing) is honest — "LEARNING" below MIN_SAMPLE, never a fake
number. Settled HIT/MISS chips overlay the live slate.

Real-time layer (Phase 1): Slate silent 60s auto-refresh (no flash, no wipe on
transient blips) + "SIGNAL LIVE · UPDATED Xs ago" freshness strip; Ticker LIVE
badge that flashes on fresh events.

Landing (Phase 3): TopSignals shows tonight's real top-3 A-rated grades + live
accuracy — the product shown, not described.

Founder pricing: FOUNDER_CODE_EXPIRY default 2026-06-30 → 2026-12-31 (had
lapsed, disabling every founder code + the ClaimMeter pitch). That expiry — not
a tier change — was the real cause of the 4 stripe test failures.

Backend 2255 (4 failing) → 2274 (all green; +19 new, +4 fixed). Web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-10 15:39:13 -04:00
parent 8629021774
commit d09a06c054
27 changed files with 1285 additions and 17 deletions
+114
View File
@@ -0,0 +1,114 @@
'use client';
import { useEffect, useState } from 'react';
import { GradeBadge, ArchetypeBadge, AccuracyBadge } from '@/components/vyndr';
/**
* TopSignals (Session 55) — the landing hero's live intelligence preview.
*
* Pulls the top A-rated grades from tonight's REAL snapshot (not a mockup) and
* shows them as mini grade cards, with the self-learning loop's live accuracy
* line beneath. The product selling itself by working. Self-hides off-hours
* (no A-rated grades) so the landing never shows an empty shell.
*/
interface SnapGrade {
player?: string;
player_name?: string;
stat_type?: string;
stat?: string;
line?: number;
direction?: string;
grade?: string;
confidence?: number;
archetype?: string | null;
}
const SPORTS = ['mlb', 'nba', 'wnba'] as const;
const STAT_SHORT: Record<string, string> = {
total_bases: 'TB', home_runs: 'HR', hits: 'Hits', rbi: 'RBI', runs: 'Runs',
strikeouts: 'Ks', hits_allowed: 'HA', earned_runs: 'ER', innings_pitched: 'IP',
stolen_bases: 'SB', points: 'Pts', rebounds: 'Reb', assists: 'Ast', threes: '3PT',
};
function shortStat(s?: string) {
if (!s) return '';
return STAT_SHORT[s] || s.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
const isTop = (g?: string) => g === 'A+' || g === 'A';
export default function TopSignals() {
const [signals, setSignals] = useState<SnapGrade[] | null>(null);
useEffect(() => {
let active = true;
const load = async () => {
try {
const results = await Promise.all(
SPORTS.map((sp) =>
fetch(`/api/snapshot/${sp}`, { cache: 'no-store' })
.then((r) => (r.ok ? r.json() : null))
.catch(() => null),
),
);
if (!active) return;
const all: SnapGrade[] = [];
for (const res of results) {
const grades = res && Array.isArray(res.grades) ? res.grades : [];
for (const g of grades) if (isTop(g.grade)) all.push(g);
}
all.sort((a, b) => (Number(b.confidence) || 0) - (Number(a.confidence) || 0));
setSignals(all.slice(0, 3));
} catch {
if (active) setSignals([]);
}
};
load();
const id = setInterval(load, 60_000);
return () => { active = false; clearInterval(id); };
}, []);
// Self-hide off-hours (nothing graded A yet) — never an empty shell.
if (!signals || signals.length === 0) return null;
return (
<section style={{ maxWidth: 960, margin: '0 auto', padding: '8px 16px 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
<div className="mono" style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 11, letterSpacing: '0.1em', color: 'var(--text-secondary, #8A8A9A)' }}>
<span className="live-dot" aria-hidden style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--grade-a, #00D4A0)', display: 'inline-block' }} />
<span style={{ color: 'var(--grade-a, #00D4A0)', fontWeight: 700 }}>TONIGHT&apos;S TOP SIGNALS</span>
<span style={{ color: 'var(--text-tertiary, #707080)' }}>· LIVE FROM THE SLATE</span>
</div>
<AccuracyBadge variant="inline" />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 12 }}>
{signals.map((g, i) => {
const player = g.player || g.player_name || '';
const side = String(g.direction || 'over').toLowerCase() === 'under' ? 'U' : 'O';
return (
<a
key={`${player}-${i}`}
href="/signup"
className="mono"
style={{
display: 'block', textDecoration: 'none', color: 'inherit',
padding: 14, borderRadius: 12,
background: 'var(--bg-surface, #12121A)',
border: '1px solid var(--border, #1A1A24)',
borderLeft: '3px solid var(--grade-a, #00D4A0)',
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginBottom: 8 }}>
{g.archetype ? <ArchetypeBadge archetype={g.archetype} size="sm" variant="full" /> : <span style={{ fontSize: 10, color: 'var(--text-tertiary)' }} />}
{g.grade && <GradeBadge grade={g.grade} size="sm" />}
</div>
<div style={{ fontWeight: 700, fontSize: 14, color: '#fff', fontFamily: 'var(--sans, sans-serif)', marginBottom: 4 }}>{player}</div>
<div style={{ fontSize: 12, color: 'var(--text-secondary, #B8BCC8)' }}>
{shortStat(g.stat_type || g.stat)} {side}{g.line}
</div>
</a>
);
})}
</div>
</section>
);
}