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:
+18
-6
@@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import Hero from '@/components/Hero';
|
||||
import { ClaimMeter, ModelRecord } from '@/components/vyndr';
|
||||
import { ClaimMeter, ModelRecord, Skeleton, SkeletonList } from '@/components/vyndr';
|
||||
// Session 55 — live top A-rated grades pulled from tonight's real snapshot,
|
||||
// with the self-learning loop's accuracy line. The product shown, not described.
|
||||
import TopSignals from '@/components/TopSignals';
|
||||
@@ -47,7 +47,18 @@ function hasStoredSession(): boolean {
|
||||
export default function Home() {
|
||||
const { user, loading } = useAuth();
|
||||
const router = useRouter();
|
||||
const [maybeSignedIn] = useState<boolean>(() => hasStoredSession());
|
||||
// DS1 (DESIGN-SPEC §4, audit #5) — React #418 hydration fix. Reading
|
||||
// localStorage inside a useState INITIALIZER runs during render: the server
|
||||
// (no `window`) computes `false` and emits the marketing tree, but the first
|
||||
// CLIENT render of a signed-in visitor reads the stored session, computes
|
||||
// `true`, and emits the "Loading" placeholder instead — a server/client HTML
|
||||
// mismatch that made React discard and re-render the whole page (#418). The
|
||||
// client-only value is now deferred behind a mounted flag: the first client
|
||||
// render matches the server (both treat it as `false`), then the stored-
|
||||
// session check flips it post-mount. SSR HTML is never discarded.
|
||||
const [mounted, setMounted] = useState(false);
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
const maybeSignedIn = mounted && hasStoredSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && user) router.replace('/dashboard');
|
||||
@@ -56,11 +67,12 @@ export default function Home() {
|
||||
// Suppress the marketing render ONLY for visitors with a stored session
|
||||
// (they're about to redirect) — anonymous first paint is instant.
|
||||
if (user || (loading && maybeSignedIn)) {
|
||||
// Signed-in visitor mid-redirect to /dashboard — a layout-matched skeleton,
|
||||
// never a text wall (§4).
|
||||
return (
|
||||
<section style={{ minHeight: '80vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<p className="mono" style={{ color: 'var(--text-tertiary)', fontSize: 13, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
|
||||
Loading the slate
|
||||
</p>
|
||||
<section style={{ maxWidth: 1100, margin: '0 auto', padding: '32px 16px' }} aria-busy="true">
|
||||
<Skeleton height={40} width={280} radius={10} style={{ marginBottom: 24 }} />
|
||||
<SkeletonList count={4} height={96} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user