Repository navigation
Load only the active locale and open the app offline (T11: PF-01, PF-02, PF-04, PF-05, PF-06, PF-09) - #215
Merged
Conversation
C3 (#199) added icon-1024.png and the 192/512/maskable manifest icons, which still fell back to revalidating on every request. The test now reads public/ so a new icon size can't miss the rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Spanish and Hungarian strings are their own chunks, fetched when chosen; English stays in the entry as the default and fallback. The first render waits for the chosen language, and a later choice wins over one still loading. - AppLayout (header, navigation, debate dialogs) loads only under /app. mountApp starts the page chunk and the layout while the entry starts, instead of when React first reaches them. - The landing reveal is a CSS transition, so / no longer loads framer-motion's 118 KB chunk. - The session check and layout load show the route's skeleton instead of a full-screen spinner. - check-build-boundaries fails if the es/hu catalogs or AppLayout re-enter the entry or the landing, login, home, generate or attack graphs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The build writes a precache list into sw.js from the Vite manifest: the shell's scripts and styles, the signed-in layout, Home, Library, History, Timer and Settings, the es/hu strings and Inter's Latin fonts. A missing manifest key fails the build. Offline, app routes get the precached app-shell.html (the paths vercel.json rewrites) and a public page gets only its own HTML, stored under its own path. Before, every navigation was stored as /index.html, so any route could come back as the last page visited. /auth/callback, Supabase paths and /_vercel/ stay network-only. Caches carry the build's id and the next build's worker deletes them. vite preview applies the vercel.json rewrites, so the offline test loads what production serves. The test's async waitForFunction calls passed at once without checking (Playwright treats the returned Promise as truthy); they now poll. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Offline, supabase-js retried every failed read three more times, so opening Settings sent about 70 doomed requests in 30 seconds (user_roles four times, user_settings 18). REST, Functions and Storage reads now wait for the 'online' event and go out once; an abort still ends them. Writes fail at once as before, and Auth is left alone because auth-js holds its session lock during a user lookup. TanStack Query also starts with the browser's real online state, so a page opened offline pauses its queries instead of running them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choosing a language whose strings were never cached, offline, failed its import and triggered the stale-deploy reload, which without a service worker replaces the page with the browser's offline error. Offline the chunk is unreachable, not stale, so the error stands and the app keeps the current language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… startup shell /expocit/sources is prerendered and Vercel serves that file before the rewrite, so offline it now gets its own stored HTML instead of the shell (whose page chunk is not precached). vite preview serves prerendered pages before the rewrites too. The non-prerendered first render now also runs if waiting for the language rejects. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checkout needs the network. Offline the button stays disabled with a short notice under it (one new string, pricingModal.offlineUpgrade, in all three catalogs) and re-enables on the 'online' event. Done here now that T4 (#207) has merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
T9 (#206) recognises flowchart starter text saved in any language by comparing it with each catalog. With Spanish and Hungarian loaded only on demand, a chart saved in Hungarian and opened in English kept its Hungarian starter text. The flowchart route's loader now fetches every catalog before the page renders. Only that page (and a hover prefetch of it) pays for them, and they are precached for offline. The flowchart tests now load through the route loader; without this change four of them fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Offline Supabase reads are no longer held until 'online'. A held read kept the token of whoever started it and could replay after sign-out, and a browser that wrongly reports offline would never send it. Every request is now sent; one that fails while the browser is offline ends as an abort, which postgrest-js does not retry. TanStack Query is no longer seeded from navigator.onLine; failed queries refetch on 'online' instead. - A language chosen offline before its strings were cached is applied when the connection returns. - The first render waits at most 3 s for the language's strings. - The service worker's build id also covers the unhashed public files it serves cache-first (theme-init.js, icons), and an offline public page prefers the copy saved on the last visit over the one stored at install. - The landing page's code is precached with its HTML, so / opened offline is not replaced by the error page. - test:sw now visits /auth/callback before checking it is never cached, and checks the precached files sit in the install-time shell cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- After a client-side navigation into /app (sign-in, the callback), the page chunk now starts with the layout chunk instead of after it. - The offline no-retry rule also matches reads through the dev:preview staging proxy path. - hero-visual.jpg and placeholder.svg also feed the worker's build id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Warning Review limit reachedEnable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Next included review available in 24 minutes. View limit details
Note 🎁 Summarized by CodeRabbit Free
Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Batch T11 from the 22 Oct QA report: performance and offline. One PR, scoped to PF-01, PF-02, PF-04, PF-05, PF-06, PF-09 and the icon cache header. No migrations, no Edge Function changes, nothing to deploy beyond the frontend.
Measurements
Both builds (
origin/mainat 508f243, and this branch) were served fromdist/by a local HTTP/2 + gzip server that applies thevercel.jsonapp-shell rewrites. Every Supabase, Jam, Sentry and Vercel request is blocked, and signed-in routes use a stub session. Absolute numbers are local and better than production's; compare the columns, not the absolute values.Time until the page's own content is on screen, mobile conditions (150 ms RTT, 1.6 Mbps down, 4x CPU slowdown, HTTP cache disabled), median of 3 cold loads. For
/this is when React takes over the prerendered page; for the others, when the route's<h1>appears./(React takes over)/login/app/appin Spanish/app/generate/app/settings/app/sparringAssets transferred before the heading appeared:
/app467 → 409 KB,/app/generate626 → 569 KB,/app/sparring681 → 625 KB.JavaScript per route (gzip -9; the entry and its static imports, plus the route's own chunks, with the signed-in layout counted for
/approutes):main-*.js//login/app/app/generate/app/settings/app/sparring/app/library/app/history/app/timerLighthouse 13.5.0 (performance only, median of 3, DevTools throttling, fresh profile per run). Scores and TBT improved on most routes, but treat this table with care. Lighthouse picks the startup shell's text, or late "Loading…" text, as LCP.
/app/settingsreads about 40 s in both builds because a blocked Supabase call keeps that text changing. On the baseline, Chrome reported Generate's and Sparring's route chunks as 0 bytes, finishing faster than the throttled link allows, which flatters the "before" column there. The timing table above is the comparison to rely on.//login/app/app/generate/app/settings/app/sparringDesktop Lighthouse (simulated throttling) is 99–100 on every route in both builds. The QA baseline (34fb585, before C1: mobile 51–73, LCP 5.4–11.9 s on signed-in routes) was measured on production, so it is not comparable with these local runs.
What changed, by issue
PF-02: the entry shipped all three locales
src/i18n.ts: English stays in the entry; Spanish and Hungarian (app and/toolsstrings) are dynamic chunks loaded by a small loader. The first render waits for the chosen language (appLanguageReady()inmountApp), so an ES or HU visitor goes straight from the startup shell to translated text. If the language is changed twice quickly, the later choice wins over strings still loading; i18next's own backend would let the late load win and would treat a failed read as done for good. If the strings can't load (offline and never cached), the app stays on the current language./app/flowchartinsrc/lib/route-modules.ts) now fetches all catalogs before the page renders. Without that, a chart saved in Hungarian and opened in English kept its Hungarian starter nodes, and four of T9's tests caught it. They now load through the route loader.scripts/check-build-boundaries.mjsnow fails the build if the es/hu catalogs come back into the entry or the landing, login, home, generate or attack static graphs.use-reduced-motionchunk no longer loads on/. The landingRevealis now a CSS transition with an IntersectionObserver, and reduced motion keeps the fade without the slide. Charts (recharts), PDF and DOCX export were already off the initial routes (the boundary check enforces this).PF-01: 5 to 12 s LCP on phones
AppLayout(header, navigation, Current Debate dialogs with the select/popover/form code) is lazy, so/and/loginno longer load app-only code. The boundary check now enforces that as well.mountAppstarts loading the page chunk for the current path, and the layout under/app, while the entry is still starting up. Before, that request only started once React rendered down to thelazy()boundary, after AuthProvider's session check.RequireAuthwaits for AuthProvider'sgetUserround trip before rendering any/apppage. That gate exists so a revoked session never renders. The page chunk now downloads during that wait instead of after it.PF-04 and PF-09: offline app pages failed, and every navigation was cached as
/index.htmldist/sw.js(serviceWorkerPrecacheinvite.config.ts), generated from the Vite manifest, not a hand-kept list. It covers the shell's scripts and styles, the signed-in layout, Home, Library, History, Timer and Settings, the es/hu strings, and Inter's Latin fonts. A missing manifest key fails the build. This is 88 files, 1.7 MB raw, about 520 KB compressed. Hashed assets may come from the HTTP cache, so the page that registered the worker doesn't download them twice.public/sw.js, navigations to the pathsvercel.jsonrewrites toapp-shell.html(/app*,/login,/share/*;/expocit/sourcesis rewritten too but its prerendered file wins, so it counts as a public page) go to the network first and, offline, get the precached shell. Public pages are stored under their own path, and offline a page is only answered with itself, or with the offline page if it was never visited. Before, every navigation response was stored as/index.html, so/appoffline could come back as Terms./auth/callback, the Supabase paths (including the staging-preview proxy) and/_vercel/(Analytics and Speed Insights) stay network-only. Before, the/_vercel/scripts were cached forever.debate-lab-v6-<build id>-{shell,runtime,pages}. The next build's worker deletes them on activation, because a page cached by an old build references assets the new build no longer has. Install fails, and the browser retries later, if the two HTML documents or any precached asset can't be stored. Updates still wait for the normal lifecycle (noskipWaiting).role="status") shows on/app. The timer works offline: its sounds use Web Audio, so there are no files to cache.vite previewnow applies thevercel.jsonrewrites, so the offline test and local measurements loadapp-shell.htmlfor app routes, as production does. E2E runs on the dev server, so it is unaffected.scripts/test-service-worker-offline.mjs(npm run test:sw) found a bug in the test itself: itspage.waitForFunction(async ...)calls, including the ones already on main, passed at once without checking anything, because Playwright treats the returned Promise as truthy. They now poll from Node. The test now proves:/app, without being visited first;<html lang="es">;/auth/callbackand the API paths never enter Cache Storage;src/lib/reload-stale-chunk.tsno longer reloads for a failed chunk whilenavigator.onLineis false. Offline, choosing a language that was never cached would have triggered the stale-deploy reload, and without a worker that replaces the page with the browser's offline error.PF-05: offline,
user_roles(and every read) was retried/app/settingsoffline sent about 70 Supabase requests in 30 s. Each GET went out 4 times (supabase-js's own retry: 1 s, 2 s, 4 s), even though theuser_rolesquery hasretry: false, anduser_settingswent out 18 times.src/lib/supabase/client.ts: every request is still sent. A REST, Functions or Storage read that fails whilenavigator.onLineis false is ended as an abort, the one failure postgrest-js doesn't retry, so it goes out once instead of four times. Online, the retries on transient failures are unchanged, and a browser that wrongly reports offline still sends every request. Writes and Auth are untouched.online. Its own reconnect refetch needs anofflineevent first, and a page opened offline never gets one. This is how Settings'user_rolescheck resumes.src/test/supabase/offline-reads.test.ts, and the first one fails without the change (4 attempts).hydrateStoresin AuthProvider) still don't rerun on reconnect, as on main. Offline the app shows the locally persisted data. Rehydrating ononlinewould touch AuthProvider's account-switch logic, so I left it out.PricingModal.tsxthe button is disabled while offline, with a short notice under it (aria-describedby), and it re-enables ononline. That needed one new string,pricingModal.offlineUpgrade, in all three catalogs (the key-parity test requires all three). T13 may want to reword it.PF-06: plain "Opening " loading states
RouteSkeleton) since perf: smooth route loading and navigation #51 (9127589). "Opening " is its screen-reader label (sr-only), not visible text.RequireAuth's full-screen spinner during the session check was still plain. It now shows the route's skeleton inside a<main>landmark, and so does the new lazy-layout fallback, so/approutes show the skeleton while AuthProvider checks the session and the layout loads.Icon cache header (fold-in from C3)
vercel.jsonnow coversapple-touch-icon.png,icon-192.png,icon-512.png,icon-1024.pngandicon-maskable-512.png. The test readspublic/, so a new icon file without a matching rule fails it (checked by removingicon-1024from the rule).hero-visualis a.jpgin the repo; it was already covered.Left open, owned by other batches
src/lib/flowchart.ts(from T9, Fix Flowchart QA issues (T9: FC-01–06, PF-03) #206) importsMarkerTypefromreactflow, which pulls all of@reactflow/coreinto the Generate route. Using the string'arrowclosed'(typed asMarkerType) there would drop it.check-build-boundariesdoesn't catch this, because its pattern looks fornode_modules/reactflow/and the code lives in@reactflow/core. Generate is T8's area, so I left it alone; widening the pattern should go with that fix.SparringScorecard.tsx, statically imported). Lazy-loading the scorecard is a Sparring component change (T8).Verification
Local, on the final head (CI minutes are used up, so these are local passes, not CI passes).
npm run verify(full ladder) at02e6509c, exit 0:docs:check,lint(eslint, knip and repo rules) andtypecheckpassednpm test: 81 files, 586 tests passedtest:collaboration: 5 files, 30 tests passedtest:edge: 457 passed, 0 failedbuild: vite build, prerender, sitemap,check-site(48 HTML files, no problems),check-build-boundaries(now also rejects the es/hu catalogs and AppLayout in initial graphs)test:sw:{"upgradeActivated":true,"precachedRoutesOffline":["/","/app","/app/library","/app/history","/app/settings","/app/timer"],"authCallbackNeverCached":true,"timerRunsOffline":true,"languageSwitchOffline":true,"publicPagesKeepTheirOwnHtml":true,"offlineIndicator":true,"unvisitedRouteShowsRecovery":true,"intentPrefetchedRouteOffline":true,"offlineLogo":true,"apiCacheEntries":0}@smoke): 43 passedChecks against the built app in Chromium (stub session, every non-local request blocked or answered locally):
/app: Home, Library, History, Settings and Timer open, the timer runs, the offline indicator shows, and switching to Spanish works (all insidetest:sw)./app,/app/settingsand/: app routes go from the startup shell to the skeleton and page in the chosen language, with no English text and no raw keys in between. Prerendered public pages still show their English HTML first and then switch, as on main; per-language public pages are T12's job (IN-04).aria-describedbyset), and enabled again when back online.user_roles1 instead of 4). Failed queries run again when the connection returns./app: the layout and Home chunks start within 5 ms of each other.Tests added or changed:
src/test/i18n.test.ts(language race),src/test/supabase/offline-reads.test.ts,src/test/lib/reload-stale-chunk.test.ts(offline),src/test/lib/flowchart.test.tsandsrc/test/pages/FlowchartPage.test.tsx(load through the route loader),src/test/vercel-deployment-config.test.ts(icons read frompublic/, service-worker assertions),scripts/test-service-worker-offline.mjs. Each new assertion was checked to fail without its fix.verify-debatelab recipes against the full local stack (Vite dev server, local Supabase Auth and Postgres, Edge Functions, fake model provider), with the shared stack lock taken by
verify.sh upand released bydown, at02e6509c:drive all --project desktop-chromium: 56 passed, 2 skipped (7.6 min)drive all --project mobile-chromium: 56 passed, 2 skipped (9.0 min). The skips on both are the two Workspaces autosave recipes, which needup --collab; Workspaces is hidden in production.This covers real sign-in (demo code, email link, callback), the lazy signed-in layout, the RequireAuth skeleton and every tool page. The recipes run on the dev server, where the service worker is not registered;
test:swcovers the worker.Review rounds
claude-opus-5-5, read the full diff): no High findings; 2 Medium, 5 Low.onlinekept the previous account's token and could replay after sign-out: fixed by not holding reads at all.onlineManagerseeding would pause queries forever: fixed the same way, and the seeding is removed.theme-init.jsand the icons, and an offline public page could get the install-time copy over a newer visit: both fixed./opened offline could turn into the error page: its chunk is now precached, andtest:swasserts the precache contents./auth/callbackcache check could not fail:test:swnow visits the callback first.online. The related flowchart memo is covered under L-A below.claude-opus-5-5): no High or Medium findings, and all seven round-1 findings confirmed resolved. Three Lows:dev:previewstaging proxy: fixed.hero-visual.jpgandplaceholder.svgwere left out of the worker's build id: fixed.starterTextsinsrc/lib/flowchart.ts(T9's file) is memoised once per session. If a catalog fails to load before the first flowchart render (offline before the worker installed), a chart saved in that language keeps its original starter text until a reload. The fix is in T9's file: build the memo only once every catalog is present.Needs a manual check in a real browser
/appsigned in, and confirm LCP improved on the deployed build (the local harness can't reproduce Supabase latency)./apponce, go offline (airplane mode), and open Library, History, Timer and Settings from the home screen icon.Migrations: none. Edge Functions to deploy: none.
🤖 Generated with Claude Code