Session 19: Sports design overhaul — player cards with headshots, game card redesign, scan page tonight's players, odds diagnostic logging, tier gate utility (1444 tests)
This commit is contained in:
+142
-2
@@ -1,10 +1,150 @@
|
||||
# VYNDR — Build State
|
||||
|
||||
## Last Updated
|
||||
2026-06-11
|
||||
2026-06-12
|
||||
|
||||
## Current Phase
|
||||
SHIP BUILD v18.0 — Internal admin dashboard + Tank01 prefetch endpoint (Session 18)
|
||||
SHIP BUILD v19.0 — Sports experience overhaul: player cards, game-card redesign, scan page revamp (Session 19)
|
||||
|
||||
## Session 19 (2026-06-12) — SHIPPED
|
||||
|
||||
The platform had every backend piece in place but read like a
|
||||
spreadsheet. Same player name listed four times in a row, blank
|
||||
scan page, generic game headers. This session restructured the
|
||||
visual hierarchy so the player is the hero of every card.
|
||||
|
||||
### PHASE 1 — NBA proxy diagnosis
|
||||
|
||||
User reported "/api/odds/nba returns 503 while Express has live
|
||||
data." Trace: NBA and WNBA proxies are byte-identical in shape.
|
||||
Probe of production confirmed **all three** sports (NBA, WNBA,
|
||||
MLB) return 503 with the same payload — root cause is upstream
|
||||
of the proxy. The Express `oddsService.getOdds` 503 path fires
|
||||
when odds-api fails AND no Redis cache exists.
|
||||
|
||||
Likely production cause: ODDS_API_KEY rotation, quota exhaustion,
|
||||
or Redis disconnect (cache always empty so every request goes
|
||||
live, then fails). Not fixable from code without env access.
|
||||
|
||||
Code change: added a `console.error` line at the 503 fallthrough
|
||||
that surfaces upstream status + axios error code + truncated
|
||||
upstream body. Next time someone gets paged with a 503, the log
|
||||
gives them the answer instead of "Odds service unavailable."
|
||||
|
||||
Test: pinned the log shape (`upstream_status=`, sport name, body
|
||||
substring) so a future log-cleanup PR can't silently delete it.
|
||||
|
||||
### PHASE 2 — PlayerCard + headshot utility
|
||||
|
||||
`web/src/lib/playerHeadshot.ts` exposes `getHeadshotUrl({sport,
|
||||
playerId, espnId, cachedPhotoUrl})` with fallback chain:
|
||||
- cached photo URL → league CDN → ESPN CDN → silhouette
|
||||
- League CDNs: `cdn.nba.com/headshots/nba/latest/260x190/{id}.png`,
|
||||
`cdn.wnba.com/headshots/wnba/...`,
|
||||
`img.mlbstatic.com/mlb-photos/...`
|
||||
- ESPN CDN used ONLY when no league ID and `espnId` present
|
||||
- Soccer doesn't get a synthetic URL — API-Football's `photo`
|
||||
field is cached separately and passed as `cachedPhotoUrl`
|
||||
|
||||
`web/public/images/player-silhouette.svg` — 64x64 generic
|
||||
silhouette, dark-theme colors.
|
||||
|
||||
`web/src/components/PlayerCard.tsx` — new component. Header
|
||||
(headshot + name + team) over N PropRow children. `<img onError>`
|
||||
falls back to the silhouette so a CDN 404 doesn't leave a broken
|
||||
image. Exports `groupPropsByPlayer(props)` helper.
|
||||
|
||||
`web/src/components/GameCard.tsx` updated:
|
||||
- Imports PlayerCard + groupPropsByPlayer
|
||||
- Visibility budget (`defaultVisible=4`) now applies to PLAYERS,
|
||||
not raw props — previously a single player with 4+ props
|
||||
consumed the whole budget and other players were hidden
|
||||
- "+ N more prop(s)" → "+ N more player(s)"
|
||||
|
||||
### PHASE 3 — Game card header redesign
|
||||
|
||||
`teamAbbr(fullName, sport)` exported from GameCard:
|
||||
- Override table for 30+ well-known multi-word names (Los
|
||||
Angeles Lakers → LAL, St. Louis Cardinals → STL, etc.)
|
||||
- Two-word names fall back to the first word's 3 letters
|
||||
- Soccer composes initials when 2+ words, else truncates
|
||||
|
||||
Header now shows: `🏀 BOS vs DEN [NBA]` in bold mono, with the
|
||||
sport label on a colored badge to the right. Below: full names
|
||||
in muted text + time/venue meta line. Sport colors:
|
||||
- NBA #E94B3C · WNBA #FFB347 · MLB #1E90FF · Soccer #00D4A0
|
||||
|
||||
### PHASE 4 — Scan page tonight's players
|
||||
|
||||
New "TONIGHT'S PLAYERS" chip grid above the search input, pulled
|
||||
from `/api/odds/{sport}` (the canonical list of players who have
|
||||
props posted today — same source The Slate uses). Each chip:
|
||||
24×24 headshot + name. Click prefills the player and, when only
|
||||
ONE stat type has props for that player, prefills the stat too.
|
||||
|
||||
Section auto-hides when the array is empty (off-season, odds-api
|
||||
down, etc.) — no sad "couldn't load tonight's players" stripe.
|
||||
|
||||
Search dropdown enhanced: every suggestion now has a 28×28
|
||||
headshot. Falls back to silhouette via onError for players the
|
||||
CDN doesn't have yet.
|
||||
|
||||
### PHASE 5 — CSP img-src expanded
|
||||
|
||||
`web/next.config.ts` — img-src now includes `cdn.wnba.com` and
|
||||
`img.mlbstatic.com`. Was `cdn.nba.com` + `a.espncdn.com`.
|
||||
|
||||
### PHASE 6 — Tier-gate utility (wired in Session 20)
|
||||
|
||||
`web/src/lib/tierGate.ts` — exports `canSeeFullLists(tier)`,
|
||||
`canSeeGradeDetails(tier)`, `getVisibleCount(tier, totalCount)`,
|
||||
`getHiddenCount(tier, totalCount)`. Free users see top 3; africa,
|
||||
analyst, desk see everything. Free + africa see grade letters but
|
||||
NOT detailed grade breakdowns (analyst+ only).
|
||||
|
||||
Not consumed yet — exported for the streaks/hot-lists work
|
||||
planned in Session 20.
|
||||
|
||||
### Honest scope flags
|
||||
|
||||
- I did not run the actual UI in a browser. The web build is
|
||||
clean, types resolve, and the Slate's data flow is intact, but
|
||||
I can't verify the visual end state without a live render.
|
||||
- Headshot CDNs will 404 for some players (rookies the league
|
||||
hasn't shot yet, traded players whose league ID we haven't
|
||||
re-mapped). The onError fallback prevents broken images, but
|
||||
expect ~5–15% silhouette rate on coverage.
|
||||
- The NBA proxy 503 is NOT fixed in code. The diagnostic log
|
||||
helps the next operator pinpoint the root cause; the fix
|
||||
itself needs env config access.
|
||||
|
||||
### Battery
|
||||
|
||||
- Express suite: **112 passed / 1444 tests** (+1 — odds service
|
||||
diagnostic log test; baseline 1443)
|
||||
- Web build: **clean** — all new routes register, no TS errors,
|
||||
no ESLint failures
|
||||
- All new TypeScript modules tree-shake into existing pages
|
||||
|
||||
### Files changed (Session 19)
|
||||
|
||||
**Created:**
|
||||
- `web/src/lib/playerHeadshot.ts`
|
||||
- `web/src/lib/tierGate.ts`
|
||||
- `web/src/components/PlayerCard.tsx`
|
||||
- `web/public/images/player-silhouette.svg`
|
||||
|
||||
**Modified:**
|
||||
- `src/services/oddsService.js` — diagnostic log at 503 path
|
||||
- `tests/unit/oddsService.test.js` — pinned log shape
|
||||
- `web/src/components/GameCard.tsx` — PlayerCard integration +
|
||||
teamAbbr + sport-colored header
|
||||
- `web/src/app/scan/page.tsx` — tonight's players chip grid +
|
||||
headshot-enriched search suggestions
|
||||
- `web/next.config.ts` — CSP img-src for cdn.wnba.com +
|
||||
img.mlbstatic.com
|
||||
|
||||
---
|
||||
|
||||
## Session 18 (2026-06-11) — SHIPPED
|
||||
|
||||
|
||||
Reference in New Issue
Block a user