1c681df5d3
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>
79 lines
2.7 KiB
JavaScript
79 lines
2.7 KiB
JavaScript
/* DS1 (DESIGN-SPEC §17 — scan→ledger persistence).
|
|
*
|
|
* The single row-builder for a MANUAL scan's ledger_entries row. Shared
|
|
* (CommonJS) so the Next scan route builds the row through it AND the Jest
|
|
* suite can prove a written row is readable by the /api/ledger/mine query
|
|
* (same user_id scope, same player_key = nameKey, same dedupe key).
|
|
*
|
|
* DATA SEMANTICS: line/book are the REAL book values the user scanned;
|
|
* locked_odds/team attach only when the cache-only snapshot confirms them
|
|
* (absent beats wrong). Only grade/edge/confidence/model_value are MODEL. */
|
|
|
|
const { nameKey, normalizeName } = require('./playerName');
|
|
|
|
/** The columns the dedupe UNIQUE constraint (migration 019) is keyed on, and
|
|
* the onConflict target for the upsert. Kept here as the single source. */
|
|
const LEDGER_CONFLICT_COLS = 'user_id,player_key,stat,line,side,game_id';
|
|
|
|
/** Today's date in ET (YYYY-MM-DD) — the game_date the settle pass keys on. */
|
|
function gameDateET(now = new Date()) {
|
|
return new Intl.DateTimeFormat('en-CA', {
|
|
timeZone: 'America/New_York', year: 'numeric', month: '2-digit', day: '2-digit',
|
|
}).format(now);
|
|
}
|
|
|
|
const numOrNull = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
|
|
/**
|
|
* Build the ledger_entries row for a completed manual scan.
|
|
* @param {{
|
|
* userId: string,
|
|
* sport: string,
|
|
* player: string,
|
|
* stat: string,
|
|
* line: number,
|
|
* side: string,
|
|
* book?: string | null,
|
|
* team?: string | null,
|
|
* lockedOdds?: string | number | null,
|
|
* grade?: string | null,
|
|
* edge?: number | null,
|
|
* confidence?: number | null,
|
|
* projection?: number | null,
|
|
* now?: Date,
|
|
* }} opts
|
|
* @returns {Record<string, unknown>} a row ready for `sb.from('ledger_entries').upsert(row, ...)`.
|
|
*/
|
|
function buildManualLedgerRow({
|
|
userId, sport, player, stat, line, side, book,
|
|
team = null, lockedOdds = null,
|
|
grade = null, edge = null, confidence = null, projection = null,
|
|
now = new Date(),
|
|
}) {
|
|
const s = String(sport || '').toLowerCase();
|
|
const playerKey = nameKey(player);
|
|
const gameDate = gameDateET(now);
|
|
return {
|
|
user_id: userId,
|
|
player_key: playerKey,
|
|
player_name: normalizeName(player).display || player,
|
|
sport: s,
|
|
stat: String(stat || '').toLowerCase(),
|
|
line,
|
|
side,
|
|
locked_odds: lockedOdds != null ? String(lockedOdds) : null,
|
|
book: book || 'draftkings',
|
|
team,
|
|
opponent: null,
|
|
grade: grade ?? null,
|
|
edge: numOrNull(edge),
|
|
confidence: numOrNull(confidence),
|
|
model_value: numOrNull(projection),
|
|
graded_at: now.toISOString(),
|
|
game_id: `manual:${s}:${gameDate}:${playerKey}`,
|
|
game_date: gameDate,
|
|
};
|
|
}
|
|
|
|
module.exports = { buildManualLedgerRow, gameDateET, LEDGER_CONFLICT_COLS };
|