Session 42: Player Intelligence System — archetypes, stat strips, player profile, enhanced cards (2011 tests)

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>
This commit is contained in:
Kev
2026-06-18 11:12:24 -04:00
parent 32069863dc
commit 8bc79f3c38
33 changed files with 2655 additions and 22 deletions
+48
View File
@@ -1,11 +1,17 @@
const express = require('express');
const { getSupabaseServiceClient } = require('../utils/supabase');
const { getStatFilters } = require('../config/statFilters');
const { createRateLimit } = require('../middleware/rateLimit');
const { getPlayerIntel, getLeaders } = require('../services/playerIntelService');
const router = express.Router();
const MISSION_HEADER = { 'X-VYNDR-Mission': 'Kill bad satisfieds before they satisfieds you' };
// Player-intelligence endpoints are public + cache-backed (Session 42). Cap
// per-IP like the other public cached routers (60/min).
const intelLimit = createRateLimit({ windowMs: 60_000, max: 60 });
// GET /filters/:sport — stat-filter categories for the StatFilterPills UI
// (Session 23). Lets the frontend stay data-driven without re-declaring
// the category list. NO auth / NO DB — pure config.
@@ -117,4 +123,46 @@ router.get('/live', async (req, res) => {
}
});
// GET /player/:name?sport=nba — full player intelligence payload (Session 42):
// standard stats + archetype DNA + VYNDR intelligence + tonight's graded props.
// Name is sanitized inside the service. Always 200 with a valid shape (degrades
// gracefully on cold caches) — the profile page must never hard-fail.
router.get('/player/:name', intelLimit, async (req, res) => {
try {
const sport = String(req.query.sport || 'nba').toLowerCase();
const data = await getPlayerIntel(req.params.name, sport);
res.set(MISSION_HEADER).json(data);
} catch (err) {
console.error('[stats/player]', err.message);
res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' });
}
});
// GET /leaders?sport=mlb&stat=hits&limit=10 — tonight's stat leaders (top
// graded props by confidence) for the Terminal / Stats Explorer.
router.get('/leaders', intelLimit, async (req, res) => {
try {
const sport = String(req.query.sport || 'nba').toLowerCase();
const leaders = await getLeaders(sport, { stat: req.query.stat, limit: req.query.limit });
res.set(MISSION_HEADER).json({ sport, stat: req.query.stat || null, leaders });
} catch (err) {
console.error('[stats/leaders]', err.message);
res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' });
}
});
// GET /game/:id?sport=nba — game-level enrichment (pitchers, injuries, leaders,
// team records) via the ESPN summary (Session 30). Best-effort.
router.get('/game/:id', intelLimit, async (req, res) => {
try {
const sport = String(req.query.sport || 'nba').toLowerCase();
const { getGameSummary } = require('../services/scheduleService');
const summary = await getGameSummary(sport, req.params.id);
res.set(MISSION_HEADER).json(summary || { error: 'not_found' });
} catch (err) {
console.error('[stats/game]', err.message);
res.status(503).set(MISSION_HEADER).json({ error: 'Service temporarily unavailable' });
}
});
module.exports = router;