Files
civitai__civitai/tests/preview-bootstrap.spec.ts
T

151 lines
7.2 KiB
TypeScript
Raw Normal View History

perf(api): cut ambient-bootstrap trpc volume on api-primary (~10%) (#2464) * perf(api): cut ambient-bootstrap trpc volume on api-primary The app fires a cluster of per-bootstrap trpc calls that dominate api-primary request volume. Two are pure overhead (data is SSR-available or globally cached) yet hit the network on every page load: - user.getSettings (~106/s): four ambient consumers (chat icon + three migration/nav alerts) forced `staleTime: 0` on every mount, solely to observe when the batched getFeatureFlags fetch landed (flash-avoidance). Decouple that: FeatureFlagsProvider now exposes useFeatureFlagsReady() (the per-user flag overlay already runs once per bootstrap), and the consumers gate on it while reading dismissedAlerts from the SSR-seeded cache — kept current by the dismiss mutation's existing optimistic setData. Removes the per-mount getSettings round-trip (and a forced getBuzzAccount refetch in YellowBuzzMigrationNotice). - system.getBrowsingSettingAddons (~35/s): global, redis-cached, identical for all users. SSR-inject it in _app getInitialProps and pass as initialData to the outermost provider; the nested mount reads the primed cache. The client query no longer fires. Net: ~140/s of ~1,433/s api-primary trpc volume (~10%) removed, all pure-overhead procedures — each cut also drops that procedure's per-request middleware tax (auth, flag eval) off the single JS thread. useFeatureFlagsReady is additive; existing useFeatureFlags() consumers are unchanged. Trade-off: a cross-device alert dismissal won't reflect on an already-open tab until reload (acceptable for temporary migration notices). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(alerts): reconcile getSettings cache on dismiss (invalidate after optimistic update) The ambient migration/nav alerts read dismissedAlerts from the SSR-seeded getSettings cache without a per-mount refetch (the volume cut in this PR). The dismiss mutation's optimistic setData spreads `...old`, so if the cached base was incomplete (e.g. a failed SSR /api/user/settings snapshot returning {}), it could persist a truncated settings object and a stale dismissed state. Add onSettled -> utils.user.getSettings.invalidate() to each dismiss mutation: one refetch per dismiss (a rare user action, NOT per mount) restores the full authoritative settings object and confirms the server-side dismissal. Does not reintroduce the bootstrap-volume this PR removes. Does not address cross-tab live propagation (separate per-tab caches) or the failed-initial-SSR-snapshot first render — both inherent and low-impact for one-time migration notices. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(preview): SSR-inject browsingSettingsAddons regression guard for #2464 Adds a preview-only Playwright spec (preview-bootstrap.spec.ts) following the playwright.preview.config.ts (PREVIEW_URL), excluded from local `pnpm test` by the default config's `**/preview-*.ts` testIgnore. Asserts the half of #2464 that is deterministic and login-free: - `browsingSettingsAddons` IS present in __NEXT_DATA__.props.pageProps (SSR inject from _app.getInitialProps), AND - NO `system.getBrowsingSettingAddons` tRPC request fires on bootstrap (the non-batched httpLink puts the procedure name in the request path). Covered anonymously (it fired for anon in the old code) and for a gate-passing mod. The authed `user.getSettings` no-per-mount-refetch / chat-no-flash / alert dismiss-persist / getFeatureFlags fail-open cases are timing/visual/state dependent and flaky as live-preview assertions, so they're documented as a manual checklist in the spec rather than written as theatre. Wires the new spec into playwright.preview.config.ts (testMatch + preview-smoke project glob). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(preview): fix bootstrap spec goto — domcontentloaded, not networkidle `page.goto(path, { waitUntil: 'networkidle' })` never settled on this app (continuous background requests — signals/polling/beacons), so both bootstrap tests hung to the 45s navigation timeout and failed on the preview run (#2464 run pr-preview-2464-pjk4z). The request listener already captures every tRPC call regardless of load state, so switch to domcontentloaded + a 2.5s settle window — same captured request set, no doomed idle wait. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf+fix(api): parallelize SSR fetches; guard alerts against failed-settings snapshot From audit #3: - _app getInitialProps awaited getBrowsingSettingAddons() sequentially after getFeatureFlagsAsync despite no dependency — on every full render's critical path. Resolve both (and their dynamic imports) via Promise.all. - The three migration/nav alerts gated visibility on useFeatureFlagsReady() and read dismissedAlerts from the SSR-seeded getSettings cache. On the rare path where the SSR /api/user/settings snapshot fails (endpoint returns {} so the destructured settings is undefined, hence initialData undefined), the query self-heals via a mount fetch, but until it lands dismissedAlerts is undefined and a previously dismissed alert could briefly re-show. Add a "settings is defined" guard so the alert waits for that fetch on the failed path only; on the normal SSR-seeded path settings is defined immediately (no delay, no per-mount refetch reintroduced). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:16:52 -05:00
import { expect, test } from '@playwright/test';
import { storageStatePath } from './preview-fixtures';
/**
* Regression guard for PR #2464 (perf/cut-ambient-bootstrap-trpc-volume): cutting
* per-bootstrap tRPC volume on api-primary. Runs only under
* playwright.preview.config.ts (needs PREVIEW_URL).
*
* What this file CAN assert deterministically (and does):
* 1. ANON SSR-inject of `browsingSettingsAddons`. _app.getInitialProps now
* fetches the redis-cached global addon list server-side and seeds it into
* `__NEXT_DATA__.props.pageProps.browsingSettingsAddons`, passing it to
* BrowsingSettingsAddonsProvider as `initialData`. So the client must NOT
* fire `system.getBrowsingSettingAddons` on bootstrap. This fired for anon
* in the old code, so it's verifiable WITHOUT login. The tRPC client is
* non-batched (src/utils/trpc.ts httpLink), so the procedure name is in the
* request path — a substring match on the URL is a reliable network probe.
* The anon home page 307s to /login, but `_app` getInitialProps runs for
* every page, so the SSR payload + provider mount happen on /login too.
* 2. The same SSR-inject for a logged-in (gate-passing) user.
*
* What is LEFT AS MANUAL (documented, not faked here — see checklist at bottom):
* - "user.getSettings is no longer force-refetched per mount" by the four
* ambient consumers (chat icon + 3 migration/nav alerts). getSettings fires
* for many unrelated reasons during a session, so a count-based "did it
* refetch?" assertion against a live preview is flaky and would be theatre.
* - Chat-icon no-flash, alert render/dismiss/persist, and the
* getFeatureFlags-error fail-open path. These are timing/visual and
* user-state dependent; assert by hand.
*/
const ADDONS_PROCEDURE = 'system.getBrowsingSettingAddons';
fix(auth): preview-only legacy-session fallback so preview smoke can authenticate (#2786) * fix(auth): preview-only legacy-session fallback so preview smoke can authenticate The first-party OAuth cutover (#2712 + follow-ups) moved user resolution off the app DB onto getSessionUserById, which reads the shared session cache then the centralized hub (auth.civitai.com) — both backed by the PRODUCTION identity store. PR previews run against the dev DB clone, where the ci-smoke-* smoke users are seeded (datapacket-talos seed-smoke-test-users CronJob) but the hub has no row for them. So getLegacySession decoded the minted legacy cookie fine, extracted the userId, then getSessionUserById returned null (hub 404) → null session → the _app route guard 307'd every authenticated request to /login → auth.civitai.com → back → ERR_TOO_MANY_REDIRECTS. Result: ALL ~53 authenticated preview smoke tests failed (observed on pr-2781..2784 previews; #2773's smoke was green before the cutover). Production is unaffected — real users resolve via the hub normally. Fix: in getLegacySession, when getSessionUserById misses AND IS_PREVIEW, fall back to the rich `user` embedded in the minted legacy cookie — exactly what the pre-cutover gate did (read token.user straight from the cookie, no DB hit). Gated on IS_PREVIEW so production NEVER trusts the embedded user (it must resolve via the hub); zero production blast radius. Updated the preview-auth.setup.ts header to document the new mechanism. This restores pre-cutover preview behaviour as a stopgap; a longer-term option is to teach the hub/session-client a preview identity source, but that's the migration owner's call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(preview): repoint anonymous SSR-inject landing /login -> /preview-restricted The anonymous SSR-injection smoke tests (preview-bootstrap, preview-ssr-inject) used ANON_LANDING='/login' — pre-cutover the in-app /login PAGE rendered through _app, so __NEXT_DATA__ carried the SSR-injected browsingSettingsAddons / announcements / getLiveNow. The first-party-OAuth cutover (remove in-app login UI) made /login a server-side redirect to the hub: it no longer renders, so the anon landing seeded nothing → the tests failed (and, with AUTH_JWT_ISSUER unset on the preview, looped). /preview-restricted is the preview gate's OTHER allow-listed exception (resolveAuthGuard: `path !== '/preview-restricted'`) and is a plain page (no custom getServerSideProps) that renders via _app for anyone — anonymous included. Repoint ANON_LANDING there so the same _app SSR-inject path is exercised, keeping the coverage instead of dropping it. (Companion to PR #2786's preview-only legacy-session fallback for the AUTHENTICATED loop, and the datapacket-talos preview ConfigMap getting AUTH_JWT_ISSUER so /login forwards to the hub instead of degrading to / on regular previews.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 19:36:57 -05:00
// A page that renders for anonymous traffic without clearing the preview gate, so
// _app.getInitialProps (and thus the SSR inject + provider mount) runs — exactly
// the bootstrap path we want to probe. Post first-party-OAuth cutover, /login is a
// server-side REDIRECT to the hub (no in-app render), so it can't be the landing
// anymore; /preview-restricted is the gate's other allow-listed exception
// (resolveAuthGuard: `path !== '/preview-restricted'`) and renders via _app for
// anyone, anonymous included.
const ANON_LANDING = '/preview-restricted';
perf(api): cut ambient-bootstrap trpc volume on api-primary (~10%) (#2464) * perf(api): cut ambient-bootstrap trpc volume on api-primary The app fires a cluster of per-bootstrap trpc calls that dominate api-primary request volume. Two are pure overhead (data is SSR-available or globally cached) yet hit the network on every page load: - user.getSettings (~106/s): four ambient consumers (chat icon + three migration/nav alerts) forced `staleTime: 0` on every mount, solely to observe when the batched getFeatureFlags fetch landed (flash-avoidance). Decouple that: FeatureFlagsProvider now exposes useFeatureFlagsReady() (the per-user flag overlay already runs once per bootstrap), and the consumers gate on it while reading dismissedAlerts from the SSR-seeded cache — kept current by the dismiss mutation's existing optimistic setData. Removes the per-mount getSettings round-trip (and a forced getBuzzAccount refetch in YellowBuzzMigrationNotice). - system.getBrowsingSettingAddons (~35/s): global, redis-cached, identical for all users. SSR-inject it in _app getInitialProps and pass as initialData to the outermost provider; the nested mount reads the primed cache. The client query no longer fires. Net: ~140/s of ~1,433/s api-primary trpc volume (~10%) removed, all pure-overhead procedures — each cut also drops that procedure's per-request middleware tax (auth, flag eval) off the single JS thread. useFeatureFlagsReady is additive; existing useFeatureFlags() consumers are unchanged. Trade-off: a cross-device alert dismissal won't reflect on an already-open tab until reload (acceptable for temporary migration notices). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(alerts): reconcile getSettings cache on dismiss (invalidate after optimistic update) The ambient migration/nav alerts read dismissedAlerts from the SSR-seeded getSettings cache without a per-mount refetch (the volume cut in this PR). The dismiss mutation's optimistic setData spreads `...old`, so if the cached base was incomplete (e.g. a failed SSR /api/user/settings snapshot returning {}), it could persist a truncated settings object and a stale dismissed state. Add onSettled -> utils.user.getSettings.invalidate() to each dismiss mutation: one refetch per dismiss (a rare user action, NOT per mount) restores the full authoritative settings object and confirms the server-side dismissal. Does not reintroduce the bootstrap-volume this PR removes. Does not address cross-tab live propagation (separate per-tab caches) or the failed-initial-SSR-snapshot first render — both inherent and low-impact for one-time migration notices. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(preview): SSR-inject browsingSettingsAddons regression guard for #2464 Adds a preview-only Playwright spec (preview-bootstrap.spec.ts) following the playwright.preview.config.ts (PREVIEW_URL), excluded from local `pnpm test` by the default config's `**/preview-*.ts` testIgnore. Asserts the half of #2464 that is deterministic and login-free: - `browsingSettingsAddons` IS present in __NEXT_DATA__.props.pageProps (SSR inject from _app.getInitialProps), AND - NO `system.getBrowsingSettingAddons` tRPC request fires on bootstrap (the non-batched httpLink puts the procedure name in the request path). Covered anonymously (it fired for anon in the old code) and for a gate-passing mod. The authed `user.getSettings` no-per-mount-refetch / chat-no-flash / alert dismiss-persist / getFeatureFlags fail-open cases are timing/visual/state dependent and flaky as live-preview assertions, so they're documented as a manual checklist in the spec rather than written as theatre. Wires the new spec into playwright.preview.config.ts (testMatch + preview-smoke project glob). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(preview): fix bootstrap spec goto — domcontentloaded, not networkidle `page.goto(path, { waitUntil: 'networkidle' })` never settled on this app (continuous background requests — signals/polling/beacons), so both bootstrap tests hung to the 45s navigation timeout and failed on the preview run (#2464 run pr-preview-2464-pjk4z). The request listener already captures every tRPC call regardless of load state, so switch to domcontentloaded + a 2.5s settle window — same captured request set, no doomed idle wait. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf+fix(api): parallelize SSR fetches; guard alerts against failed-settings snapshot From audit #3: - _app getInitialProps awaited getBrowsingSettingAddons() sequentially after getFeatureFlagsAsync despite no dependency — on every full render's critical path. Resolve both (and their dynamic imports) via Promise.all. - The three migration/nav alerts gated visibility on useFeatureFlagsReady() and read dismissedAlerts from the SSR-seeded getSettings cache. On the rare path where the SSR /api/user/settings snapshot fails (endpoint returns {} so the destructured settings is undefined, hence initialData undefined), the query self-heals via a mount fetch, but until it lands dismissedAlerts is undefined and a previously dismissed alert could briefly re-show. Add a "settings is defined" guard so the alert waits for that fetch on the failed path only; on the normal SSR-seeded path settings is defined immediately (no delay, no per-mount refetch reintroduced). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:16:52 -05:00
// A core page a gate-passing user lands on directly (no /login bounce).
const AUTHED_LANDING = '/models';
/**
* Navigate to `path`, recording every tRPC request URL seen during the load and
* for a short settle window after, then return them. We watch `request` (fired
* the moment the client issues it) so we catch the call even if it errors.
*/
async function collectTrpcRequests(
page: import('@playwright/test').Page,
path: string
): Promise<string[]> {
const trpcUrls: string[] = [];
const onRequest = (req: import('@playwright/test').Request) => {
const url = req.url();
if (url.includes('/api/trpc/')) trpcUrls.push(url);
};
page.on('request', onRequest);
try {
// NOT 'networkidle': the app keeps background requests alive (signals /
// polling / beacons), so the network never goes idle and goto would hang to
// the navigation timeout. We don't need it — the `request` listener above
// captures every tRPC call regardless of load state; a fixed settle window
// after DOMContentLoaded covers the bootstrap burst + any deferred mount.
await page.goto(path, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2500);
} finally {
page.off('request', onRequest);
}
return trpcUrls;
}
/** Read `__NEXT_DATA__.props.pageProps.browsingSettingsAddons` from the page. */
async function readBrowsingSettingsAddons(page: import('@playwright/test').Page) {
return page.evaluate(() => {
const el = document.getElementById('__NEXT_DATA__');
if (!el?.textContent) return { present: false, value: undefined as unknown };
const data = JSON.parse(el.textContent);
const value = data?.props?.pageProps?.browsingSettingsAddons;
return { present: typeof value !== 'undefined', value };
});
}
test.describe('SSR-injected browsingSettingsAddons (anonymous)', () => {
// Explicitly anonymous: no minted session cookie.
test.use({ storageState: { cookies: [], origins: [] } });
test('addons are seeded into __NEXT_DATA__ and not fetched on bootstrap', async ({ page }) => {
const trpcUrls = await collectTrpcRequests(page, ANON_LANDING);
// (a) SSR payload carries the addon list.
const addons = await readBrowsingSettingsAddons(page);
expect(addons.present, 'browsingSettingsAddons present in __NEXT_DATA__ pageProps').toBe(true);
expect(Array.isArray(addons.value), 'browsingSettingsAddons is a list').toBe(true);
// (b) The client never issues the now-SSR-injected procedure on bootstrap.
const addonRequests = trpcUrls.filter((u) => u.includes(ADDONS_PROCEDURE));
expect(
addonRequests,
`no ${ADDONS_PROCEDURE} tRPC request should fire on bootstrap (it is SSR-injected); saw:\n${addonRequests.join(
'\n'
)}`
).toHaveLength(0);
});
});
test.describe('SSR-injected browsingSettingsAddons (logged-in)', () => {
test.use({ storageState: storageStatePath('mod') });
test('addons are seeded and not fetched on bootstrap for a gate-passing user', async ({
page,
}) => {
const trpcUrls = await collectTrpcRequests(page, AUTHED_LANDING);
const addons = await readBrowsingSettingsAddons(page);
expect(addons.present, 'browsingSettingsAddons present in __NEXT_DATA__ pageProps').toBe(true);
expect(Array.isArray(addons.value), 'browsingSettingsAddons is a list').toBe(true);
const addonRequests = trpcUrls.filter((u) => u.includes(ADDONS_PROCEDURE));
expect(
addonRequests,
`no ${ADDONS_PROCEDURE} tRPC request should fire on bootstrap (it is SSR-injected); saw:\n${addonRequests.join(
'\n'
)}`
).toHaveLength(0);
});
});
/**
* MANUAL CHECKLIST — behaviours of #2464 that are not auto-asserted above.
* (Auth IS supported by this harness, but these are timing/visual/state cases
* that would be flaky or no-op as live-preview assertions; verify by hand.)
*
* [ ] Chat icon no-flash: logged-in user WITH chat disabled — the chat icon
* must NOT appear-then-disappear on load. It should gate on
* useFeatureFlagsReady() and only render once the user flag overlay settles.
* [ ] No per-mount getSettings refetch: with the ambient consumers mounted
* (chat icon + NavTidyNotice + YellowBuzzMigrationNotice), client-side nav
* between pages must NOT trigger
perf(api): cut ambient-bootstrap trpc volume on api-primary (~10%) (#2464) * perf(api): cut ambient-bootstrap trpc volume on api-primary The app fires a cluster of per-bootstrap trpc calls that dominate api-primary request volume. Two are pure overhead (data is SSR-available or globally cached) yet hit the network on every page load: - user.getSettings (~106/s): four ambient consumers (chat icon + three migration/nav alerts) forced `staleTime: 0` on every mount, solely to observe when the batched getFeatureFlags fetch landed (flash-avoidance). Decouple that: FeatureFlagsProvider now exposes useFeatureFlagsReady() (the per-user flag overlay already runs once per bootstrap), and the consumers gate on it while reading dismissedAlerts from the SSR-seeded cache — kept current by the dismiss mutation's existing optimistic setData. Removes the per-mount getSettings round-trip (and a forced getBuzzAccount refetch in YellowBuzzMigrationNotice). - system.getBrowsingSettingAddons (~35/s): global, redis-cached, identical for all users. SSR-inject it in _app getInitialProps and pass as initialData to the outermost provider; the nested mount reads the primed cache. The client query no longer fires. Net: ~140/s of ~1,433/s api-primary trpc volume (~10%) removed, all pure-overhead procedures — each cut also drops that procedure's per-request middleware tax (auth, flag eval) off the single JS thread. useFeatureFlagsReady is additive; existing useFeatureFlags() consumers are unchanged. Trade-off: a cross-device alert dismissal won't reflect on an already-open tab until reload (acceptable for temporary migration notices). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(alerts): reconcile getSettings cache on dismiss (invalidate after optimistic update) The ambient migration/nav alerts read dismissedAlerts from the SSR-seeded getSettings cache without a per-mount refetch (the volume cut in this PR). The dismiss mutation's optimistic setData spreads `...old`, so if the cached base was incomplete (e.g. a failed SSR /api/user/settings snapshot returning {}), it could persist a truncated settings object and a stale dismissed state. Add onSettled -> utils.user.getSettings.invalidate() to each dismiss mutation: one refetch per dismiss (a rare user action, NOT per mount) restores the full authoritative settings object and confirms the server-side dismissal. Does not reintroduce the bootstrap-volume this PR removes. Does not address cross-tab live propagation (separate per-tab caches) or the failed-initial-SSR-snapshot first render — both inherent and low-impact for one-time migration notices. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(preview): SSR-inject browsingSettingsAddons regression guard for #2464 Adds a preview-only Playwright spec (preview-bootstrap.spec.ts) following the playwright.preview.config.ts (PREVIEW_URL), excluded from local `pnpm test` by the default config's `**/preview-*.ts` testIgnore. Asserts the half of #2464 that is deterministic and login-free: - `browsingSettingsAddons` IS present in __NEXT_DATA__.props.pageProps (SSR inject from _app.getInitialProps), AND - NO `system.getBrowsingSettingAddons` tRPC request fires on bootstrap (the non-batched httpLink puts the procedure name in the request path). Covered anonymously (it fired for anon in the old code) and for a gate-passing mod. The authed `user.getSettings` no-per-mount-refetch / chat-no-flash / alert dismiss-persist / getFeatureFlags fail-open cases are timing/visual/state dependent and flaky as live-preview assertions, so they're documented as a manual checklist in the spec rather than written as theatre. Wires the new spec into playwright.preview.config.ts (testMatch + preview-smoke project glob). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(preview): fix bootstrap spec goto — domcontentloaded, not networkidle `page.goto(path, { waitUntil: 'networkidle' })` never settled on this app (continuous background requests — signals/polling/beacons), so both bootstrap tests hung to the 45s navigation timeout and failed on the preview run (#2464 run pr-preview-2464-pjk4z). The request listener already captures every tRPC call regardless of load state, so switch to domcontentloaded + a 2.5s settle window — same captured request set, no doomed idle wait. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * perf+fix(api): parallelize SSR fetches; guard alerts against failed-settings snapshot From audit #3: - _app getInitialProps awaited getBrowsingSettingAddons() sequentially after getFeatureFlagsAsync despite no dependency — on every full render's critical path. Resolve both (and their dynamic imports) via Promise.all. - The three migration/nav alerts gated visibility on useFeatureFlagsReady() and read dismissedAlerts from the SSR-seeded getSettings cache. On the rare path where the SSR /api/user/settings snapshot fails (endpoint returns {} so the destructured settings is undefined, hence initialData undefined), the query self-heals via a mount fetch, but until it lands dismissedAlerts is undefined and a previously dismissed alert could briefly re-show. Add a "settings is defined" guard so the alert waits for that fetch on the failed path only; on the normal SSR-seeded path settings is defined immediately (no delay, no per-mount refetch reintroduced). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 18:16:52 -05:00
* a `user.getSettings` refetch per mount (they now read the SSR-seeded
* cache + gate on useFeatureFlagsReady, not staleTime:0 + isFetched).
* [ ] Alert render/dismiss/persist: a migration/nav alert renders from
* dismissedAlerts, dismissing it fires exactly one getSettings refetch via
* the new onSettled invalidate, and it stays dismissed across reloads.
* [ ] getFeatureFlags error fail-open: if user.getFeatureFlags errors,
* useFeatureFlagsReady() still flips true (gated on isFetched, which covers
* success OR error), so chat/alerts are not stuck hidden.
*/