8bc79f3c38
Built from the Claude Design "VYNDR Player Intelligence" bundle (10 sections). - Archetypes: src/services/archetypeService.js — 41 archetypes (15 NBA / 5 WNBA-unique / 15 MLB / 6 soccer), classify -> primary+secondary+blend. Frontend visual map web/src/lib/archetypes.js (colors verified == backend). ArchetypeBadge (full/ghost/tint + glyphs) + ArchetypeBlend (DNA bar). - StatStrip (compact/expanded): player name once, horizontal mono stats, inline GradeBadge props, onPlayerClick -> profile. - Stats API: extended src/routes/stats.js with /player/:name, /leaders, /game/:id (rate-limited). Aggregation in playerIntelService.js (sanitizes name param; grades cache; graceful on cold cache). Next proxies added. - Player Profile /player/[name]: all 9 design sections, graceful empty states. - Enhanced GameCard (MLB pitchers + player-grouped StatStrips) + GradeResultCard (archetype strip + stat context + VYNDR intelligence, optional/self-hiding via gradeAdapter.buildIntelFields). Player-name links wired everywhere. - Settings page replaces the S41 redirect (account/subscription/notifications/ display/responsible-play/danger-zone with DELETE-gated delete). LINKS to the real /settings/security MFA page — does not replace it. + BookChip. - Bonus: Stats Explorer /explore (real /api/stats/leaders leaderboard); added Explore + Settings to Nav MORE. Deferred (need data pipelines, Session 43): Team Hub, Offseason Intel, Slate redesign, Stats Explorer sub-panels. Backend 1940 -> 2011 tests (+71), 157 suites. Web build clean (exit 0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
83 lines
3.3 KiB
JavaScript
83 lines
3.3 KiB
JavaScript
// Session 41 — P0 audit fixes. Frontend assertions read page source as text
|
||
// (same pattern as the Phase D–H suites); backend stat-gate acceptance is
|
||
// covered in tests/integration/analyze.test.js.
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const ROOT = path.join(__dirname, '..', '..');
|
||
const WEB = path.join(ROOT, 'web', 'src');
|
||
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
|
||
|
||
describe('Session 41 — backend MLB stat_type whitelist', () => {
|
||
const analyze = fs.readFileSync(path.join(ROOT, 'src', 'routes', 'analyze.js'), 'utf8');
|
||
const scan = fs.readFileSync(path.join(ROOT, 'src', 'routes', 'scan.js'), 'utf8');
|
||
const mlbStats = ['hits', 'strikeouts', 'total_bases', 'rbi', 'home_runs', 'earned_runs', 'hits_allowed', 'innings_pitched'];
|
||
|
||
it.each(mlbStats)('/api/analyze gate whitelists MLB stat %s', (stat) => {
|
||
expect(analyze).toContain(`'${stat}'`);
|
||
});
|
||
|
||
it.each(mlbStats)('/api/scan (parlay) gate whitelists MLB stat %s', (stat) => {
|
||
expect(scan).toContain(`'${stat}'`);
|
||
});
|
||
});
|
||
|
||
describe('Session 41 — broken-route redirects', () => {
|
||
// Session 42 — /settings is now a real settings page (replaced the S41
|
||
// redirect). The 404 it fixed is still fixed; the route just renders content.
|
||
it('/settings is a real page (no longer a redirect stub)', () => {
|
||
const src = read('app/settings/page.tsx');
|
||
expect(src).not.toContain("redirect('/profile')");
|
||
expect(src).toContain('DANGER ZONE');
|
||
});
|
||
|
||
it('/report redirects to /blog (THE REPORT link target)', () => {
|
||
const src = read('app/report/page.tsx');
|
||
expect(src).toContain("redirect('/blog')");
|
||
});
|
||
|
||
it('/settings/security stays the real MFA page (NOT clobbered into a redirect)', () => {
|
||
// The audit spec wanted this redirected too, but it is a working MFA
|
||
// enrollment flow — overwriting it would be a security-feature regression.
|
||
const src = read('app/settings/security/page.tsx');
|
||
expect(src).toContain('mfa');
|
||
expect(src).not.toContain("redirect('/profile')");
|
||
});
|
||
});
|
||
|
||
describe('Session 41 — profile reads tier from useAuth', () => {
|
||
const src = read('app/profile/page.tsx');
|
||
|
||
it('destructures tier from useAuth (same source as the nav)', () => {
|
||
expect(src).toMatch(/tier:\s*authTier/);
|
||
});
|
||
|
||
it('derives the displayed tier from the auth session', () => {
|
||
expect(src).toContain('authTier || profile.tier');
|
||
});
|
||
});
|
||
|
||
describe('Session 41 — self-hosted fonts (no Google Fonts CDN)', () => {
|
||
const layout = read('app/layout.tsx');
|
||
const globals = read('app/globals.css');
|
||
|
||
it('layout uses next/font instead of a runtime <link>', () => {
|
||
expect(layout).toContain("from 'next/font/google'");
|
||
expect(layout).toContain('Inter(');
|
||
expect(layout).toContain('JetBrains_Mono(');
|
||
});
|
||
|
||
it('removed the runtime Google Fonts stylesheet <link>', () => {
|
||
// The historical reference survives in a code comment; what must be gone
|
||
// is the actual CDN stylesheet href that caused the 503.
|
||
expect(layout).not.toMatch(/href=["'][^"']*fonts\.googleapis\.com/);
|
||
expect(layout).not.toContain('rel="stylesheet"');
|
||
});
|
||
|
||
it('globals.css :root maps --sans/--mono onto the next/font variables', () => {
|
||
expect(globals).toContain('--sans: var(--font-sans)');
|
||
expect(globals).toContain('--mono: var(--font-mono)');
|
||
});
|
||
});
|