DS1 (design): speed + trust bugs

Fixes the three DESIGN-SPEC Part 4 + #17 audit findings.

1. React #418 hydration mismatch (landing → dashboard entry). The
   `maybeSignedIn` value was computed in a useState INITIALIZER that reads
   localStorage during render: server (no window) → false → emits the
   marketing tree; a signed-in visitor's first CLIENT render → true → emits
   the loading placeholder. Whole-subtree server/client mismatch → React
   discarded and re-rendered the page. Deferred behind a mounted flag so the
   first client render matches the server; the stored-session check flips
   post-mount. SSR HTML is no longer discarded.

2. Loading walls → skeletons. New tokenized Skeleton primitive
   (.vyndr-skeleton, reduced-motion-safe via the global rule). Swapped into
   every text-wall loader: dashboard slate load ("Loading the slate…"), /desk
   ("Assembling the pack…"), /ledger ("Loading…"), scan ("Loading the model…"),
   and the landing redirect placeholder. No bare text loader remains.

3. scan→ledger persistence. Root cause: the scan page read its bearer token
   from localStorage['sb-token'] — a key written ONLY by the OAuth callback —
   so email/password users posted /api/scan anonymously and the ledger write
   (gated on an authed user) was silently skipped. Now uses the authoritative
   session.access_token (matching the ledger read path). Extracted the row
   builder to web/src/lib/ledgerRow.js (shared, testable).

Tests: +17 (scanLedgerPersistence write→mine round-trip + scope + idempotency;
ds1SpeedTrust hydration/skeleton/persistence source invariants). Full suite
233 suites / 2793 green; web build exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-12 19:25:32 -04:00
parent 24af247b29
commit 1c681df5d3
12 changed files with 498 additions and 45 deletions
+96
View File
@@ -0,0 +1,96 @@
/**
* DS1 (DESIGN-SPEC §4 + §17) — source invariants for the Speed + Trust bugs.
* - React #418 hydration: no client-only value rendered in the SSR path
* without a mounted guard.
* - Loading walls: every named loader is a skeleton, not a text wall.
* - Persistence: the scan write uses the authoritative session token.
*/
const fs = require('fs');
const path = require('path');
const WEB = path.join(__dirname, '..', '..', 'web', 'src');
const read = (rel) => fs.readFileSync(path.join(WEB, rel), 'utf8');
describe('DS1 — React #418 hydration safety (§4, audit #5)', () => {
const landing = read('app/page.tsx');
test('landing no longer reads localStorage inside a useState initializer', () => {
// The exact antipattern that caused #418: the client-only stored-session
// check ran during render, so server (false) and client (true) diverged.
expect(landing).not.toMatch(/useState<boolean>\(\s*\(\)\s*=>\s*hasStoredSession\(\)\s*\)/);
expect(landing).not.toMatch(/useState\(\s*\(\)\s*=>\s*hasStoredSession/);
});
test('the client-only value is deferred behind a mounted flag', () => {
// Server + first client render both compute maybeSignedIn from mounted=false.
expect(landing).toMatch(/const\s+\[mounted,\s*setMounted\]\s*=\s*useState\(false\)/);
expect(landing).toMatch(/setMounted\(true\)/);
expect(landing).toMatch(/maybeSignedIn\s*=\s*mounted\s*&&\s*hasStoredSession\(\)/);
});
test('hasStoredSession still guards against server-side window access', () => {
expect(landing).toMatch(/typeof window === 'undefined'/);
});
});
describe('DS1 — loading walls are skeletons, never text (§4, audit #5)', () => {
const files = {
'dashboard slate load': 'app/dashboard/page.tsx',
'/desk assembling': 'app/desk/page.tsx',
'/ledger loading': 'app/ledger/page.tsx',
'scan loading': 'app/scan/page.tsx',
'landing redirect': 'app/page.tsx',
};
const BANNED = [
'Loading the slate',
'Assembling the pack',
'Loading the model',
/<p[^>]*>\s*Loading…\s*<\/p>/, // the bare ledger text loader
];
for (const [name, rel] of Object.entries(files)) {
test(`${name} contains no text-wall loader`, () => {
const src = read(rel);
for (const b of BANNED) {
if (typeof b === 'string') expect(src).not.toContain(b);
else expect(src).not.toMatch(b);
}
});
}
test('each loader path renders the shared Skeleton', () => {
for (const rel of ['app/dashboard/page.tsx', 'app/desk/page.tsx', 'app/ledger/page.tsx', 'app/scan/page.tsx', 'app/page.tsx']) {
const src = read(rel);
expect(src).toMatch(/Skeleton/);
expect(src).toMatch(/from '@\/components\/vyndr'/);
}
});
test('the Skeleton primitive is tokenized (no raw hex) and reduced-motion-safe', () => {
const skel = read('components/vyndr/Skeleton.tsx');
// Uses the CSS class, no inline colors.
expect(skel).toMatch(/vyndr-skeleton/);
expect(skel).not.toMatch(/#[0-9a-fA-F]{3,8}/);
const css = fs.readFileSync(path.join(WEB, 'app', 'globals.css'), 'utf8');
expect(css).toMatch(/\.vyndr-skeleton\s*\{/);
expect(css).toMatch(/var\(--bg-surface\)/);
// The global prefers-reduced-motion rule freezes every animation.
expect(css).toMatch(/@media \(prefers-reduced-motion: reduce\)/);
});
});
describe('DS1 — scan→ledger persistence uses the authoritative token (§17)', () => {
const scan = read('app/scan/page.tsx');
test('scan pulls the bearer token from the live Supabase session', () => {
expect(scan).toMatch(/session,/); // destructured from useAuth
expect(scan).toMatch(/session\?\.access_token/);
});
test('scan no longer relies solely on the OAuth-only legacy localStorage key', () => {
// The stale key may remain only as a FALLBACK after the session token.
const m = scan.match(/session\?\.access_token\s*\|\|[\s\S]{0,120}sb-token/);
expect(m).toBeTruthy();
});
});
+153
View File
@@ -0,0 +1,153 @@
/**
* DS1 (DESIGN-SPEC §17) — scan→ledger persistence.
*
* Proves a completed scan's ledger row (built by the SAME builder the
* /api/scan route uses) is readable by the /api/ledger/mine query: scoped by
* user_id, keyed by player_key = nameKey, deduped idempotently. This is the
* "every grade, no hiding" guarantee the audit found broken.
*/
const { buildManualLedgerRow, LEDGER_CONFLICT_COLS, gameDateET } = require('../../web/src/lib/ledgerRow');
const { nameKey, normalizeName } = require('../../web/src/lib/playerName');
// A minimal in-memory stand-in for the service-role Supabase client, modelling
// exactly the two operations that matter: the route's upsert(row, {onConflict,
// ignoreDuplicates}) and the /api/ledger/mine read
// (.select().eq('user_id',id).order('graded_at',desc).limit(n)).
function makeFakeSupabase() {
const rows = [];
const conflictCols = LEDGER_CONFLICT_COLS.split(',');
const keyOf = (r) => conflictCols.map((c) => String(r[c])).join('|');
return {
_rows: rows,
from() {
return {
async upsert(row, opts = {}) {
const k = keyOf(row);
const existing = rows.find((r) => keyOf(r) === k);
if (existing) {
if (!opts.ignoreDuplicates) Object.assign(existing, row);
return { data: null, error: null };
}
rows.push({ ...row, id: `row-${rows.length + 1}` });
return { data: null, error: null };
},
// Chainable read that mirrors the mine route.
select() {
const filters = {};
let orderCol = null; let asc = true; let lim = Infinity;
const q = {
eq(col, val) { filters[col] = val; return q; },
order(col, o = {}) { orderCol = col; asc = o.ascending !== false; return q; },
limit(n) { lim = n; return q; },
then(resolve) {
let out = rows.filter((r) => Object.entries(filters).every(([c, v]) => r[c] === v));
if (orderCol) {
out = [...out].sort((a, b) => (a[orderCol] < b[orderCol] ? -1 : a[orderCol] > b[orderCol] ? 1 : 0));
if (!asc) out.reverse();
}
out = out.slice(0, lim);
resolve({ data: out, error: null });
},
};
return q;
},
};
},
};
}
// The mine query, byte-for-byte in spirit with routes/ledger.js GET /mine.
async function mineQuery(sb, userId, limit = 100) {
const { data } = await sb.from('ledger_entries')
.select('*')
.eq('user_id', userId)
.order('graded_at', { ascending: false })
.limit(limit);
return data;
}
describe('scan→ledger persistence (DS1 §17)', () => {
const USER_A = 'user-aaaa';
const USER_B = 'user-bbbb';
test('a completed scan write is immediately readable by the mine query', async () => {
const sb = makeFakeSupabase();
const row = buildManualLedgerRow({
userId: USER_A,
sport: 'MLB',
player: 'Aaron Judge',
stat: 'home_runs',
line: 1.5,
side: 'over',
book: 'draftkings',
grade: 'A',
confidence: 71,
projection: 1.9,
edge: 12.4,
});
await sb.from('ledger_entries').upsert(row, { onConflict: LEDGER_CONFLICT_COLS, ignoreDuplicates: true });
const mine = await mineQuery(sb, USER_A);
expect(mine).toHaveLength(1);
const got = mine[0];
expect(got.user_id).toBe(USER_A);
// The join key the ledger/model reads on is player_key = nameKey.
expect(got.player_key).toBe(nameKey('Aaron Judge'));
expect(got.player_name).toBe(normalizeName('Aaron Judge').display);
expect(got.sport).toBe('mlb');
expect(got.stat).toBe('home_runs');
expect(got.side).toBe('over');
expect(got.line).toBe(1.5);
expect(got.grade).toBe('A');
// game_id embeds the ET game_date so the settle pass can find it.
expect(got.game_id).toBe(`manual:mlb:${gameDateET()}:${nameKey('Aaron Judge')}`);
expect(got.game_date).toBe(gameDateET());
});
test('the mine query is scoped: another user cannot read the row', async () => {
const sb = makeFakeSupabase();
const row = buildManualLedgerRow({
userId: USER_A, sport: 'MLB', player: 'Shohei Ohtani', stat: 'total_bases',
line: 1.5, side: 'over', book: 'fanduel', grade: 'B',
});
await sb.from('ledger_entries').upsert(row, { onConflict: LEDGER_CONFLICT_COLS, ignoreDuplicates: true });
expect(await mineQuery(sb, USER_A)).toHaveLength(1);
expect(await mineQuery(sb, USER_B)).toHaveLength(0);
});
test('re-scanning the same prop is idempotent (never duplicates the row)', async () => {
const sb = makeFakeSupabase();
const mk = () => buildManualLedgerRow({
userId: USER_A, sport: 'MLB', player: 'Mookie Betts', stat: 'hits',
line: 0.5, side: 'over', book: 'draftkings', grade: 'A',
});
await sb.from('ledger_entries').upsert(mk(), { onConflict: LEDGER_CONFLICT_COLS, ignoreDuplicates: true });
await sb.from('ledger_entries').upsert(mk(), { onConflict: LEDGER_CONFLICT_COLS, ignoreDuplicates: true });
expect(await mineQuery(sb, USER_A)).toHaveLength(1);
});
test('DATA SEMANTICS: absent beats wrong — no snapshot ⇒ locked_odds/team null, model fields carried', () => {
const row = buildManualLedgerRow({
userId: USER_A, sport: 'WNBA', player: "A'ja Wilson", stat: 'points',
line: 22.5, side: 'over', grade: 'B', confidence: 60, projection: 24,
// no team / lockedOdds supplied (no snapshot match)
});
expect(row.locked_odds).toBeNull();
expect(row.team).toBeNull();
expect(row.opponent).toBeNull();
expect(row.model_value).toBe(24);
expect(row.confidence).toBe(60);
// Non-numeric model fields degrade to null, never 0 (Number(null)===0 bug).
expect(buildManualLedgerRow({ userId: USER_A, sport: 'MLB', player: 'X', stat: 'hits', line: 1, side: 'over' }).confidence).toBeNull();
expect(buildManualLedgerRow({ userId: USER_A, sport: 'MLB', player: 'X', stat: 'hits', line: 1, side: 'over' }).model_value).toBeNull();
});
test('the dedupe key matches the migration-019 constraint the route upserts on', () => {
expect(LEDGER_CONFLICT_COLS).toBe('user_id,player_key,stat,line,side,game_id');
});
});