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:
+80
-2
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user