Session 38: Design system Phase G — living layer, i18n/odds, a11y, paywall, parlay math (1890 tests)

VYNDR 2.0 conversion, Phase G (the systems that make the design alive). All 5
wired. Frontend-only; zero backend changes.

- lib/parlayMath.js: correlation model (0.62/0.34/0.06/0) + parlayGrade penalty
  + grade->odds + combined odds (frontend; backend parlayService unchanged).
- lib/oddsFormat.js: fmtOdds across american/decimal/fractional/implied with the
  totals-pass-through rule (safer than the prototype's parseAm, which would
  mis-convert 228.5) + region presets.
- lib/prefs.js: applyPrefs sets <html data-*> (the S33 a11y CSS layer) + load/save.
- lib/liveTick.js: single tick engine (SSR/test-safe, no auto-start, fresh state).
- lib/checkout.js: checkoutUrl(plan).
- LiveLayer (useLive/LiveNumber/HeartbeatBar) under the Nav ticker; GlobalHosts in
  layout applies prefs + registers __prefs/__goPaywall/__checkout + hosts the
  Preferences and Paywall modals. Nav read-meter is now a paywall trigger.

Gotchas: useEffect can't return a Set.delete unsub directly (boolean != cleanup);
header grew to 124px so layout paddingTop + Slate sticky-top updated to match.

18 new tests. Backend 1872 -> 1890, 146 suites, zero regressions. Web build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-16 10:37:31 -04:00
parent f88961885c
commit 956a7455eb
13 changed files with 949 additions and 8 deletions
+80 -2
View File
@@ -4,8 +4,86 @@
2026-06-16
## Current Phase
SHIP BUILD v37.0 — VYNDR 2.0 design system, Phase F: mobile parity —
5-tab bar, More sheet, mobile CSS, PWA manifest/viewport (Session 37)
SHIP BUILD v38.0 — VYNDR 2.0 design system, Phase G: systems — living layer,
i18n/odds, a11y toggles, paywall/checkout, parlay correlation math (Session 38)
## Session 38 (2026-06-16) — SHIPPED
Phase G: the systems that make the design alive. All 5 wired (minimal-wire path
where a full path risked regressions, per the scope guidance). Frontend-only;
ZERO backend changes. Backend 1872 → **1890 tests** (+18), 146 suites, zero
regressions. Web build clean (exit 0).
### Testable CommonJS lib modules (the reliable surface)
- **`lib/parlayMath.js`** (§12) — `getCorrelation` (same player 0.62 / same team
0.34 / same league 0.06 / cross-sport 0), `parlayGrade` (averages leg grades +
bumps the slip down a tier when any pair correlates >0.4), `GRADE_ODDS`,
amToDec/decToAm, `combinedDecimal`/`combinedAmerican`, `correlationPairs`.
Frontend model that powers the Parlay Lab matrix; the BACKEND parlayService
(Session 28) still owns server combined odds + suggestions.
- **`lib/oddsFormat.js`** (§9) — `fmtOdds(value, format)` (american/decimal/
fractional/implied), `REGIONS`/`CURRENCIES`/`regionPreset`. **Deliberately
SAFER than the prototype's `parseAm`**, whose regex accepted decimals and
would mis-convert O/U totals (228.5 → fake implied %). Our `parseMoneyline`
only treats explicitly-signed integer STRINGS (+150/-110) and integer NUMBERS
(the grade→odds map) as odds; totals/lines/spreads ("228.5","229","-7.5") pass
through unchanged. (Honest spec-over-prototype correction, like the S32 NFL keys.)
- **`lib/prefs.js`** (§10) — `applyPrefs` sets `<html data-motion|contrast|text|
cb|font>` (the CSS layer from Session 33 keys off these), `loadPrefs`/
`savePrefs` to `localStorage('vyndr_prefs')`. Injectable element + storage →
fully unit-tested.
- **`lib/liveTick.js`** (§8) — single tick store, fan-out subscribers. Does NOT
auto-start on import (SSR/test-safe); `start()` begins the 1s interval (unref'd),
`tick()` advances + emits a FRESH state object (so React re-renders); `reset()`
for tests.
- **`lib/checkout.js`** (§12) — `checkoutUrl(plan)` → `/api/checkout?tier=…`.
### React glue + wiring
- **`components/vyndr/LiveLayer.tsx`** — `useLive()` hook (starts the shared tick,
one interval many subscribers), `LiveNumber` (count-tick pop on change),
`HeartbeatBar` (scrolling EKG + SIGNAL LIVE + live graded count + breathing
neural % + sync clock). Mounted under the Ticker in the Nav.
- **`components/vyndr/GlobalHosts.tsx`** — mounted once in layout. On mount:
applies stored prefs to `<html>`, registers `window.__prefs` / `__goPaywall` /
`__checkout`. Hosts the **Preferences modal** (region→odds+currency cascade,
odds format, text size, reduce-motion, high-contrast, colorblind, readable
font — persisted) and the **Paywall modal** (Analyst/Desk tiers → `__checkout`).
- **Nav** — mounts HeartbeatBar; added a globe **prefs trigger** (`__prefs`); the
free-tier read meter is now a button → `__goPaywall` (the §12 live paywall
trigger). Header is now nav 60 + ticker 32 + heartbeat 30 → layout `main`
paddingTop 96 → **124**; Slate sticky header `top` 64 → 122 to match.
### Animations gated behind reduced-motion
The living-layer classes (ekg-track, live-dot, count-tick, etc.) are already in
the Session-33 reduced-motion kill list (both `prefers-reduced-motion` and
`html[data-motion="reduced"]`), which the prefs toggle now sets. The tick still
fires (data updates); only the animation is killed.
### Files created
- `web/src/lib/{parlayMath,oddsFormat,prefs,liveTick,checkout}.js`
- `web/src/components/vyndr/{LiveLayer,GlobalHosts}.tsx`
- `tests/unit/vyndrSystems.test.js` (18 tests: correlation tiers + grade penalty
+ odds combine, odds formats + totals-pass-through + region presets, applyPrefs
+ storage round-trip, tick fan-out/increment, checkout URL, React-glue wiring)
### Files modified
- `web/src/app/layout.tsx` (GlobalHosts mount + paddingTop), `web/src/components/
Nav.tsx` (HeartbeatBar + prefs + paywall meter), `web/src/components/Slate.tsx`
(sticky top)
### Notes / gotchas
- BUILD GOTCHA (caught + fixed): a `useEffect` returning `liveTick.subscribe(...)`
directly failed type-check — `Set.delete` returns boolean, not a valid effect
cleanup. Wrapped in `() => { unsub(); }`.
- FLAKY (pre-existing, NOT this session): one full-suite run showed 1 failing
backend test (computeFeatures/oddsService async-timing warnings); it passed on
re-run (1890/1890 twice). Unrelated to these frontend changes — flagged for a
future stabilization pass.
- Did NOT touch the PWA/service worker (Session 27/37 own it) per the no-overlap rule.
---
## Session 37 (2026-06-16) — SHIPPED
## Session 37 (2026-06-16) — SHIPPED