toga-ai 1.0.1003 → 1.0.1004

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  | [Cart Order For user — Add New User duplicate guard + approving-manager rule](features/cart-order-for-user-and-approving-manager.md) | Cart "Order For" user section: the Add New User duplicate check (username OR email), who the Order For list can find, and the config-driven rule that blocks che |
10
10
  | [Cart Order-Total & Shipping Computation](features/cart-order-total-computation.md) | How the cart total (subtotal/shipping/tax/total) is computed and where shipping cost flows from; open when a cart total or shipping charge looks wrong. |
11
11
  | [Cart Page — config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | Current state of the Cart form (field-config-driven, section-hardcoded, two dual-mode forms) plus the DECIDED-but-unbuilt config-driven refactor to the toga2.5 |
12
- | [Catalog cache freshness — the persisted query cache and the per-query staleTime overrides](features/catalog-cache-freshness.md) | The persisted React Query cache (`localStorage["commerce"]`) plus the per-query `staleTime` overrides that still serve prices up to 24h old, how to opt a query |
12
+ | [Catalog cache freshness — the persisted query cache, per-query staleTime overrides, and focus refetch](features/catalog-cache-freshness.md) | The persisted React Query cache (`localStorage["commerce"]`), the per-query `staleTime` overrides that served prices up to 24h old, focus refetch, how to opt a |
13
13
  | [Category Tile Order (AssortmentItems.sortOrder) — merchandising a storefront category](features/category-tile-sort-order.md) | Storefront category tile order is one DB column (`AssortmentItems.sortOrder` ASC) with a NULL-sorts-first trap; open when asked to reorder items on a category p |
14
14
  | [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Per-tenant/language/role JSON config that drives almost all commerce UI text, fields, and login fetches (Layer A global + Layer B FIELDS registry); open before |
15
15
  | [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | Expedited shipping options are config-gated to show only when the cart holds a computer kit; open when touching cart shipping-method options or the gate config. |
@@ -6,7 +6,7 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-09-21
9
+ updated: 2026-10-13
10
10
  owners: ["apeterson", "tcox"]
11
11
  files:
12
12
  - src/main.tsx
@@ -39,7 +39,7 @@ Whole-repo map of the toga2-commerce React storefront (bootstrap, routing, MVVM
39
39
 
40
40
  **Multi-tenant:** one codebase serves several clients (**COMPASS**, **COMPASSCANADA**, **QUAD**, plus a **DEFAULT** fallback). The tenant is resolved at runtime from the **hostname** and drives three independent systems — API base URL, theme, per-tenant field/content config. See [multi-tenant-theming](features/multi-tenant-theming.md) and [client-fields](features/client-fields.md).
41
41
 
42
- **Critical rules:** Tenant is derived from hostname only (`hostname.toUpperCase().split(".")[0]`) — wrong host = wrong tenant, and `localhost` falls back (fields→COMPASS, theme→DEFAULT). The single `QueryClient` uses a **24h default `staleTime`** and persists the whole cache to **`localStorage["commerce"]`**, so catalog AND config/copy (FIELDS) can be up to 24h stale for a returning user — verify any data/copy change after `localStorage.removeItem('commerce')` or logout, never on a warm browser. `AuthLayout` never remounts, so `refetchOnMount` fires once per session — route-change refreshes must be wired explicitly. The registry French language key is **`fr-CA`**, not `fr` — branch on `startsWith("fr")`. api2 returns the standard `{success,data,errors}` envelope; do not break the shape.
42
+ **Critical rules:** Tenant is derived from hostname only (`hostname.toUpperCase().split(".")[0]`) — wrong host = wrong tenant, and `localhost` falls back (fields→COMPASS, theme→DEFAULT). The single `QueryClient` (`src/lib/queryClient.ts`) has default `staleTime: 0` but persists the whole cache to **`localStorage["commerce"]`**, so a returning user paints the old value first, and a per-query `staleTime` override keeps it stale (up to 24h on `_production`; removed on `TRUE-82427`, pending merge) — see [catalog cache freshness](features/catalog-cache-freshness.md); verify any data/copy change after `localStorage.removeItem('commerce')` or logout, never on a warm browser. `AuthLayout` never remounts, so `refetchOnMount` fires once per session — route-change refreshes must be wired explicitly. The registry French language key is **`fr-CA`**, not `fr` — branch on `startsWith("fr")`. api2 returns the standard `{success,data,errors}` envelope; do not break the shape.
43
43
 
44
44
  ## Tech stack (verified versions, package.json)
45
45
 
@@ -63,7 +63,7 @@ E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
63
63
  ## Bootstrap & provider stack
64
64
 
65
65
  - `src/main.tsx` — initializes **Sentry** (browser tracing + replay), mounts `<App/>` in React `StrictMode`, imports global CSS.
66
- - `src/App.tsx` — creates a single `QueryClient` (default `staleTime: 24h`) and a `createSyncStoragePersister` (localStorage key **`"commerce"`**). Provider nesting, **outermost → innermost**:
66
+ - `src/App.tsx` — wraps the single `QueryClient` from `src/lib/queryClient.ts` (kept there so non-React code such as the 401 handler can reach it) with a `createSyncStoragePersister` (localStorage key **`"commerce"`**, build-id `buster`, `throttleTime: 0`). Provider nesting, **outermost → innermost**:
67
67
 
68
68
  ```
69
69
  PersistQueryClientProvider (client=queryClient, persister → localStorage "commerce")
@@ -75,7 +75,7 @@ E2E tests use **Cypress** (`cypress/`, `cypress.config.ts`).
75
75
  ```
76
76
 
77
77
  - `App` runs a mount effect managing **edit-order mode**: reads a `localStorage.synced` flag and calls `exitEditOrderModeGlobalSyncReset()` when an edit-order session was abandoned outside `/cart`; subscribes to `useReturnOriginalUserFromViewAsStore` (the "view-as" feature).
78
- - The cache is **persisted**: query results survive refreshes. Combined with the 24h default `staleTime`, catalog data is aggressively cached — invalidate deliberately with `queryClient.invalidateQueries(...)` when freshness matters.
78
+ - The cache is **persisted**: query results survive refreshes and paint before the refetch. Invalidate deliberately with `queryClient.invalidateQueries(...)` when freshness matters; freshness rules live in [catalog cache freshness](features/catalog-cache-freshness.md).
79
79
 
80
80
  ## Routing (`src/routes.tsx`, createBrowserRouter)
81
81
 
@@ -135,7 +135,7 @@ Most use the `persist` middleware (localStorage). Verified files:
135
135
  | `useSettingsErrorStore` | Settings validation errors (suppresses data queries when set) |
136
136
 
137
137
  ### React Query (`src/queries/`, `src/hooks/useApiQuery.ts`, `useApiMutation.ts`)
138
- - Global default `staleTime` 24h; per-query overrides (user/persona data fetched fresh on login).
138
+ - Global default `staleTime: 0`. `refetchOnWindowFocus` is `false` on `_production`, React Query's default `true` on `TRUE-82427` (pending merge). A per-query `staleTime` / `refetchOnWindowFocus` beats the default — see [catalog cache freshness](features/catalog-cache-freshness.md).
139
139
  - Cache persisted to localStorage (`"commerce"`); rehydrated on load.
140
140
  - Page queries are typically `enabled` on `isAuthenticated && user?.uuid && !settingsModalEnabled`.
141
141
 
@@ -192,7 +192,7 @@ The backend (`api2`) returns the standard 2.0 envelope (`success`/`data`/`errors
192
192
  ## Gotchas
193
193
 
194
194
  - **`AuthLayout` never remounts** → `refetchOnMount` fires once per session; wire explicit invalidation for data that must refresh on navigation.
195
- - **24h `staleTime` + persisted cache** → users can see stale catalog/pricing; invalidate on the events that should bust it. **This also masks config/copy deploys:** `FIELDS` is served *through* the `["clientFields", …]` query, so a returning user rehydrates the old labels from `localStorage["commerce"]` and sees stale copy for up to 24h after a build that contains the new JSON. Verify any FIELDS change after `localStorage.removeItem('commerce')` or a logout, never on a warm browser. A build-version `buster` in `persistOptions` would fix this at deploy time but invalidates every persisted query app-wide — **open team decision, not implemented.**
195
+ - **Persisted cache paints old data first** → catalog, pricing and `FIELDS` copy (served through the `["clientFields", …]` query) come from `localStorage["commerce"]` until the refetch lands; per-query `staleTime` overrides stretch that (see [catalog cache freshness](features/catalog-cache-freshness.md)). The build-id `buster` is live on `_production` (`e546db3d`) and drops the persisted cache on a new build. Verify any FIELDS or catalog change after `localStorage.removeItem('commerce')` or a logout, never on a warm browser.
196
196
  - **Tenant resolution depends on the hostname.** On `localhost` with no `*.togacommerce` host the first DNS label won't match a tenant, so config falls back (`getClientLoginFields` → COMPASS, theme → DEFAULT). Use the per-tenant dev scripts.
197
197
  - **Theme vs. fields language keys differ** — theme/field *folder* names are uppercase (`COMPASSCANADA`, `ENGLISH`/`FRENCH`) but the runtime `FIELDS` registry keys language as `en` / **`fr-CA`** (not `fr`). Any language branch must use `startsWith("fr")` — a `=== "fr"` comparison silently never matches. See the client-fields doc.
198
198
  - **Tailwind: not every `<n>px` name is a token.** `max-h-300px` does not exist (300px is only on the height/width scales) — write `max-h-[300px]`. `w-345px`, `h-300px`, `border-1` and `shadow-subMenu` do exist. A non-existent class silently renders unstyled.
@@ -1,12 +1,12 @@
1
1
  ---
2
- title: Catalog cache freshness — the persisted query cache and the per-query staleTime overrides
2
+ title: Catalog cache freshness — the persisted query cache, per-query staleTime overrides, and focus refetch
3
3
  framework: "2.0"
4
4
  repo: toga2-commerce
5
5
  project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-08
9
+ updated: 2026-10-13
10
10
  owners: ["bala", "apeterson", "tcox"]
11
11
  files:
12
12
  - toga2-commerce/src/lib/queryClient.ts
@@ -18,6 +18,7 @@ files:
18
18
  - toga2-commerce/src/components/NavIcons/NavIconList.tsx
19
19
  - toga2-commerce/src/pages/BundleView/hooks/useBundleQueryData.ts
20
20
  - toga2-commerce/src/pages/Home/viewModel/useHomeViewModel.ts
21
+ - toga2-commerce/src/pages/Home/HomePage.tsx
21
22
  - toga2-commerce/src/pages/ItemsView/viewModel/useItemDetailsViewModel.ts
22
23
  - toga2-commerce/src/pages/Filter/viewModel/useFilterViewModel.ts
23
24
  - toga2-commerce/src/pages/Cart/viewModel/useCartViewModel.ts
@@ -25,188 +26,173 @@ files:
25
26
  - toga2-commerce/src/queries/queries.ts
26
27
  - toga2-commerce/src/hooks/useDeploymentGuard.tsx
27
28
  - toga2-commerce/src/stores/useBundleBuilderZu.ts
29
+ - toga2-commerce/src/pages/ItemsView/ItemViewPage.tsx
30
+ - toga2-commerce/src/pages/OrderDetails/hooks/useOrderFormatting.ts
31
+ - toga2-commerce/src/pages/BundleView/hooks/applyBundleFormatting.ts
32
+ - toga2-commerce/src/pages/GetSupport/GetSupportPage.tsx
28
33
  related:
29
34
  - ./inactive-item-purchase-gating.md
30
35
  - ./force-logout-on-deployment.md
36
+ - ./sso-redirect-and-session-gating.md
31
37
  - ../../../standards/frontend.md
32
38
  - ../architecture.md
33
39
  - ../../toga25-supply/features/persisted-query-cache.md
34
40
  - ../../toga25-supply/features/force-logout-on-deployment.md
35
41
  ---
36
42
 
37
- The persisted React Query cache (`localStorage["commerce"]`) plus the per-query `staleTime` overrides that still serve prices up to 24h old, how to opt a query out, and the stacked causes behind "stale data / cache won't clear / stuck login"; open for any freshness or forced-logout bug.
43
+ The persisted React Query cache (`localStorage["commerce"]`), the per-query `staleTime` overrides that served prices up to 24h old, focus refetch, how to opt a query out, and the stacked causes behind "stale data / cache won't clear / stuck login"; open for any freshness or forced-logout bug.
38
44
 
39
45
  ## Summary
40
46
 
41
- > **Status 2026-10-01:** the **global** default is already `staleTime: 0` (causes 1, 2 and 3 fixed; cause 4 open). The remaining staleness is **per-query**: several price-bearing queries hardcode `staleTime: 24h`. See [Where the staleness lives today](#where-the-staleness-lives-today-2026-10-01) before changing any knob — setting the global `staleTime` to 0 is a **no-op**, it is already 0.
47
+ > **Status 2026-10-13 — branch-dependent.** `TRUE-82427` (`93f12265`, **pending merge, not on `_production`**) removes the per-query `staleTime` overrides on item detail, filter/search, cart shipping methods, Home and BundleView, and turns `refetchOnWindowFocus` back on app-wide. On `_production` the 24h overrides in [the inventory](#where-the-staleness-lives) still apply. Confirm the branch before reasoning about freshness.
42
48
 
43
- The whole React Query cache is persisted to **`localStorage["commerce"]`** via `PersistQueryClientProvider`. Paired with a 24h `staleTime` that serves **catalogue and price state up to 24 hours out of date, across full page reloads** — in **both directions**: after an item is switched off, a user who already viewed it keeps seeing the live product page with a working Add to Cart; after it is switched back on, it stays hidden. The 24h value was the **client default** until 2026-09-22; it now survives only as **per-query overrides**.
49
+ The whole React Query cache is persisted to **`localStorage["commerce"]`** via `PersistQueryClientProvider`. With a long `staleTime` it serves catalogue and price state hours out of date **across full page reloads**, in both directions (a switched-off item stays buyable; a switched-on item stays hidden).
44
50
 
45
- Any query whose answer **gates an action** (can this be bought? is this still available?) must be opted out of *both* mechanisms. Opting out of one is not enough.
51
+ Any query whose answer **gates an action** (can this be bought? is it available?) must be opted out of *both* `staleTime` and persistence. One is not enough.
46
52
 
47
- **Three long-running production complaints all trace back to this file set**, pinned to `file:line` on 2026-09-03 (read-only, branch `TRUE-81610`): "users don't see updated data", "clearing their cache doesn't work", "users get stuck on a broken login page after a forced logout". They are **four separate causes** (below); causes 1-3 are fixed, cause 4 is open.
53
+ Three production complaints ("users don't see updated data", "clearing cache doesn't work", "stuck on a broken login page after forced logout") trace to **four causes** (below); 1-3 fixed, 4 open.
48
54
 
49
55
  ## How it works
50
56
 
51
- ### The default: fresh in memory, still persisted to disk
57
+ ### The default (`src/lib/queryClient.ts`)
52
58
 
53
- The `QueryClient` **no longer lives in `App.tsx`** — it moved to **`src/lib/queryClient.ts`**, which sets **`staleTime: 0`** and **`refetchOnWindowFocus: false`**. The cache is still persisted to `localStorage["commerce"]` via `createSyncStoragePersister`, now with a build-id **`buster`** on the persister.
59
+ | | `_production` | `TRUE-82427` (pending merge) |
60
+ |---|---|---|
61
+ | `staleTime` | `0` | `0` |
62
+ | `refetchOnWindowFocus` | `false` | React Query default `true` (key removed) |
63
+ | per-query overrides | yes — see inventory | only the out-of-scope ones |
54
64
 
55
- > **⚠ The old "24h default `staleTime`" is gone — do not "fix" freshness by setting the global default to 0.** It already is 0. Any remaining 24h behaviour comes from **per-query overrides** (next section). Verified 2026-10-01; older docs and the [architecture doc](../architecture.md) still describe the 24h default.
65
+ Cache persisted via `createSyncStoragePersister` with a build-id **`buster`** and `throttleTime: 0`.
56
66
 
57
- A per-query `staleTime` always beats the client default, so a hook that hardcodes 24h is unaffected by the global value — including the `["clientFields", …]` query that carries the per-tenant `FIELDS` content, which is why copy/config deploys can still look stale for returning users.
67
+ > **⚠ Do not "fix" freshness by setting the global `staleTime` to 0 — it already is.** A per-query `staleTime` always beats the default (including `["clientFields", …]`, which carries per-tenant `FIELDS` copy). A per-query `refetchOnWindowFocus: false` likewise blocks the app-wide `true` — `TRUE-82427` removed those too (Home ×3, BundleView ×2).
58
68
 
59
- **TOGa 2.5 Supply** reached the same default — `staleTime: 0` *because* the cache is persisted — and has the same per-hook override problem. See [supply's persisted query cache](../../toga25-supply/features/persisted-query-cache.md).
69
+ `useDeploymentGuard.tsx` no longer sets `refetchOnWindowFocus: true` on `TRUE-82427`; it inherits the default. See [force logout on deployment](./force-logout-on-deployment.md). TOGa 2.5 Supply has the same default — see [supply's persisted query cache](../../toga25-supply/features/persisted-query-cache.md).
60
70
 
61
- ### The platform's cache-bust: the Core parameters blob
71
+ ### Where the staleness lives
62
72
 
63
- `src/contexts/AuthContext.tsx:192-252` runs a `["fetchCoreParameters"]` query with `staleTime: 0` and `refetchOnWindowFocus: true`, plus a refetch on every route change. On the first fetch after login it stores the whole response as a baseline; on any later fetch it **string-compares the entire core-parameters JSON** against that baseline and, on **any** difference, calls `logout()` and `window.location.href = "/"`.
73
+ `_production` column inventoried 2026-10-01; `TRUE-82427` column verified 2026-10-13.
64
74
 
65
- > **⚠ It is not keyed on `META_LAST_REFRESH_DATETIME` alone — it compares the whole blob.** Any Core parameter change on that environment logs out **every** user. It is a deployment-grade lever, not a way to refresh one catalogue change. The narrower, notice-first sibling in TOGa 2.5 Supply is documented in [force logout on deployment](../../toga25-supply/features/force-logout-on-deployment.md).
75
+ | Query | `_production` | `TRUE-82427` |
76
+ |---|---|---|
77
+ | `useItemDetailsViewModel.ts` item detail | 24h | removed → 0 |
78
+ | `useFilterViewModel.ts` (both) filter/search | 24h | removed → 0 |
79
+ | `useCartViewModel.ts` shipping methods | 24h | removed → 0 |
80
+ | `useHomeViewModel.ts` (3) | 30s + `gcTime` 1h + focus `false` | `gcTime` 1h only |
81
+ | `useBundleQueryData.ts` (2) | 30s + `gcTime` 1h + focus `false` | `gcTime` 1h only |
82
+ | `useEditAddressFromCartForm.ts:75` countries | 24h | 24h — out of scope (dev decision) |
83
+ | `queries/queries.ts:39,46,68,78` login/prefetch | 5 min | 5 min — out of scope (dev decision) |
66
84
 
67
- ### Opting a single query out — `staleTime: 0` + `gcTime: 0` is NOT sufficient on its own
85
+ **On `_production`:** a price/availability edit can take **up to 24h** to reach the item page, and a refresh does not fix it (the copy is in `localStorage["commerce"]`).
68
86
 
69
- The item-detail queries (`fetchItemDetails`, `fetchItemDetailsViaBundle`) are set to `staleTime: 0` **and** `gcTime: 0`, **and** excluded from persistence in `App.tsx`:
87
+ ### What "fresh" means after `TRUE-82427`
70
88
 
71
- ```ts
72
- const NON_PERSISTED_QUERY_KEYS: string[] = ["fetchItemDetails", "fetchItemDetailsViaBundle"];
73
-
74
- const shouldDehydrateQuery = (query) => {
75
- const queryName = query.queryKey?.[0];
76
- if (typeof queryName === "string" && NON_PERSISTED_QUERY_KEYS.includes(queryName)) return false;
77
- return query.state.status === "success";
78
- };
79
- // … persistOptions: { dehydrateOptions: { shouldDehydrateQuery } }
80
- ```
81
-
82
- > **⚠ Branch state (verified 2026-10-08) — two different persistence filters exist; neither is on `_production`.**
83
- > - `_production`: `buster` + `throttleTime: 0`, **no `dehydrateOptions`** — nothing is excluded from persistence.
84
- > - `_beta` / `#sprint84`: the `NON_PERSISTED_QUERY_KEYS` filter above, in `App.tsx`.
85
- > - `TRUE-81382` / `_sandbox-client`: `shouldPersistQuery` in `src/queries/queries.ts`, excluding `BUNDLE_QUERY_KEYS` (`fetchBundlesByPersona`, `bundleData`, `bundleImagesData`, `fetchItemDetailsViaBundle`).
86
- >
87
- > When these meet, merge into **one** filter holding both key lists. Confirm the branch before relying on this section.
88
-
89
- **Bundle queries are uncached (TRUE-81382).** `bundleData`, `bundleImagesData`, `fetchBundlesByPersona` spread `BUNDLE_NO_CACHE_QUERY_OPTIONS` (`{ staleTime: 0, gcTime: 0 }`, `src/queries/queries.ts`) and are excluded by `shouldPersistQuery`. `useBundleBuilderZu` no longer persists the raw parent-bundle API payload. Cause: an inactive item was orderable from a stale persisted bundle. Merging with `e546db3d` (TRUE-81991): in `App.tsx` keep **both** `buster` and `dehydrateOptions`; in the bundle queries keep `BUNDLE_NO_CACHE_QUERY_OPTIONS` + `refetchOnWindowFocus: false` and drop the 30s/1h values.
90
-
91
- **Why the exclusion is required:** `gcTime: 0` only evicts a query once it becomes **inactive**. An **active** query — exactly the state the item page is in while the user is looking at it — is still dehydrated and written to `localStorage`, and is rehydrated on the next load. So `gcTime: 0` alone leaves the stale answer on disk.
92
-
93
- The default branch (`query.state.status === "success"`) preserves TanStack's normal behaviour for every other query, so this is an allowlist-shaped opt-out with no app-wide side effects.
94
-
95
- **Listings were deliberately left on the 24h cache.** Only the item-detail path gates a purchase decision; making every listing uncached would trade a real performance characteristic for no correctness gain.
96
-
97
- ### The four stacked causes behind the production complaints
89
+ A change reaches the shopper on: navigating to the page (mount), refresh / fresh URL, the tab becoming visible again, network reconnect, or a mutation that invalidates the query. Nothing polls — it does **not** update while the shopper sits still on one page. `AuthLayout`-owned queries (header, notifications, `FIELDS`) never remount, so focus refetch is their only in-session refresh.
98
90
 
99
- Verified 2026-09-03 on branch `TRUE-81610`. Read-only; none of these are fixed.
91
+ Limits to state to the business:
100
92
 
101
- **1. App-wide 24h `staleTime` + persisted cache with nothing excluded → daily stale data.** ✅ **Fixed 2026-09-22.** App default is now `staleTime: 0` with `refetchOnWindowFocus: false`; the heavy Home/BundleView hooks took `staleTime: 30_000` + `gcTime: 1h` (the agreed shape below). Yesterday's catalog, pricing **and FIELDS copy** were previously served with no network call — daily, not only after a deploy. The 24h value was **also hardcoded per-hook**, so changing only the default was not enough:
102
- - `src/pages/BundleView/hooks/useBundleQueryData.ts:26,40`
103
- - `src/pages/Home/viewModel/useHomeViewModel.ts:150,173,197`
93
+ 1. **Prices already in the cart do not update** — the cart is Zustand + `localStorage`, not React Query.
94
+ 2. **Alt-tabbing to another application is not a focus event** — see [frontend standard §4](../../../standards/frontend.md).
95
+ 3. **On a cold load the old value paints first** from `localStorage["commerce"]`, then swaps.
104
96
 
105
- **2. Cache clears are not gated on restoration → they are silently undone.** `PersistQueryClientProvider` renders children **while still restoring** (`react-query-persist-client/build/modern/PersistQueryClientProvider.js`: `isRestoring` starts `true`, `persistQueryClientRestore` resolves later). A `queryClient.clear()` + `localStorage.removeItem("commerce")` that runs before restore finishes is **undone** — the pending restore hydrates the old cache on top, and the persist subscription writes it back. All three clearing sites are unguarded:
97
+ ### Home page must accept refetched data (`TRUE-82427`)
106
98
 
107
- | Site | What it clears |
108
- |---|---|
109
- | `src/contexts/AuthContext.tsx:180-183` | logout |
110
- | `src/components/NavIcons/NavIconList.tsx:162-170` | view-as / persona switch (language-specific data) |
111
- | `src/api/axiosInstance.ts:46` | 401 forced logout |
99
+ `useHomeViewModel` copied query data into `homeDataState` only while `status === "fields-ready"`; after first paint (`"ready"`) a background refetch never reached the screen until Home remounted. Fix:
112
100
 
113
- This is the "it's not successfully clearing their caches" report. ✅ **Fixed 2026-09-22:** every clear now goes through **`clearPersistedQueryCache()` in `src/lib/queryCacheRestoration.ts`**, which
101
+ - the effect runs in `"fields-ready"` **or** `"ready"`;
102
+ - it uses functional `setHomeDataState(prev => …)` spreading `prev.data`, so `bundlesData` survives when the bundles, categories and items blocks all run in one pass;
103
+ - it depends on `homeDataState.status`, not the whole object (avoids a render loop);
104
+ - the view model no longer sets the `*ToDisplay` lists — `HomePage.tsx` rebuilds them in a **`useLayoutEffect`** on `data` via `handleSearchSubmit(inputValue)`, keeping the shopper's search text. A plain `useEffect` caused a one-frame "no results" flash.
114
105
 
115
- 1. waits for restoration (capped at **1.5s**, so a logout redirect can never hang),
116
- 2. **cancels in-flight queries** — otherwise they settle after the clear and are persisted again,
117
- 3. clears; an **epoch counter** makes a superseded clear a no-op.
106
+ ### Item detail Add to Cart must use the refetched price (`TRUE-82427`)
118
107
 
119
- The persister is also set to **`throttleTime: 0`**: the 1000ms default meant a queued write landed a second *after* the clear. Supply's equivalent is an `awaitRestoration()` promise from `useIsRestoring()` (`toga25-supply/src/contexts/AuthContext.tsx:56-77`).
108
+ `ItemViewPage.tsx` copies `itemToAddToCart` (price, lease, Item) into `itemForCart` in an effect. It depended on `[selectedItem?.partNumber, currencySymbol]`, so a refetch with a new price for the same part updated the screen but **not** what Add to Cart sent. Fix: depend on `[selectedItem, currencySymbol]`; quantity is kept.
120
109
 
121
- **3. Two auth gates read two different localStorage keys → blank page / stuck login.** ✅ **Fixed on `TRUE-82172` (2026-09-21) — not yet merged.** Moved to its own doc: [SSO redirect & session gating](./sso-redirect-and-session-gating.md). It is an auth-gating defect, not a cache defect — clearing site data only masked it.
110
+ - No loop: `selectedItem` is view-model state, and React Query structural sharing keeps its reference when data is unchanged.
111
+ - Never add `itemToAddToCart` as a dependency — it is rebuilt every render.
112
+ - **Open:** after Add to Cart, `handleAddItemToCart` resets `itemForCart` to price `0` / uuid `""` until `selectedItem` or currency changes; a second click in that window can add the item at price 0. Fix direction: rebuild price/lease from `itemToAddToCart` inside the reset.
122
113
 
123
- **4. The forced logout is too aggressive, and login never clears the cache.** `src/contexts/AuthContext.tsx:192-252`:
124
- - On **any** core-parameter change it calls `logout()` + `window.location.href = "/"` with **no notice** to the user. On an SSO client `/` re-triggers the IdP.
125
- - It **monkey-patches `window.history.pushState` and `replaceState`** (`:216-226`) to refetch on every navigation. react-router uses those same methods.
114
+ ### Opting a single query out — `staleTime: 0` + `gcTime: 0` is not enough
126
115
 
127
- Separately, commerce's `login()` **never cleared the cache**, so signing in as a different user inherited the prior user's data. ✅ **That half is fixed 2026-09-22** — `resetClientState()` runs on **both** login and logout (see [SSO & session gating](./sso-redirect-and-session-gating.md)). The force-logout aggression and the history monkey-patch are **still open**.
116
+ Item-detail queries (`fetchItemDetails`, `fetchItemDetailsViaBundle`) also need excluding from persistence:
128
117
 
129
- ### Where the staleness lives today (2026-10-01)
130
-
131
- Inventory verified read-only on 2026-10-01. Every value below is a **per-query override** that wins over the global `staleTime: 0`.
132
-
133
- | staleTime | Where |
134
- |---|---|
135
- | **24h** | `pages/ItemsView/viewModel/useItemDetailsViewModel.ts:76` (item detail) · `pages/Filter/viewModel/useFilterViewModel.ts:106,134` (filter/search results) · `pages/Cart/viewModel/useCartViewModel.ts:310` (shipping methods) · `pages/Cart/viewModel/useEditAddressFromCartForm.ts:75` |
136
- | **5 min** | `queries/queries.ts:39,46,68,78` (login / prefetch) |
137
- | **30s** (+ `gcTime: 1h`) | `pages/Home/viewModel/useHomeViewModel.ts:150,175,201` · `pages/BundleView/hooks/useBundleQueryData.ts:26,42` — the **bundle** queries among these become `0`/`0` with TRUE-81382 (above) |
138
-
139
- **Consequence — the answer to "why doesn't a price change show up":** a price or availability edit can take **up to 24 hours** to reach a shopper on the item page, and **a browser refresh does not fix it**, because the cache is persisted to `localStorage["commerce"]` and survives reload and browser restart.
118
+ ```ts
119
+ const NON_PERSISTED_QUERY_KEYS: string[] = ["fetchItemDetails", "fetchItemDetailsViaBundle"];
140
120
 
141
- ### Planned change — ticket scope (agreed 2026-10-01, NOT implemented)
121
+ const shouldDehydrateQuery = (query) => {
122
+ const queryName = query.queryKey?.[0];
123
+ if (typeof queryName === "string" && NON_PERSISTED_QUERY_KEYS.includes(queryName)) return false;
124
+ return query.state.status === "success";
125
+ };
126
+ // … persistOptions: { dehydrateOptions: { shouldDehydrateQuery } }
127
+ ```
142
128
 
143
- 1. Remove the per-query `staleTime` overrides on item detail, filter/search results, cart shipping methods, Home and BundleView, so the global `0` applies.
144
- 2. Remove `refetchOnWindowFocus: false` from `src/lib/queryClient.ts`, so React Query's default `true` applies app-wide.
145
- 3. Then delete the now-redundant `refetchOnWindowFocus: true` (and its comment) at `src/hooks/useDeploymentGuard.tsx:93`.
129
+ **Why:** `gcTime: 0` only evicts an **inactive** query. The page the user is on is **active**, so it is still dehydrated to `localStorage` and rehydrated next load.
146
130
 
147
- Open in the ticket: whether `useEditAddressFromCartForm.ts:75` and the 5-minute `queries/queries.ts` queries are in scope. **`gcTime: 1h` on Home and BundleView is deliberately left alone** — `gcTime` controls how long unused data is kept, not when a query refetches.
131
+ > **⚠ Branch state (2026-10-08) — two persistence filters exist; neither is on `_production`.**
132
+ > - `_production`: `buster` + `throttleTime: 0`, **no `dehydrateOptions`**.
133
+ > - `_beta` / `#sprint84`: `NON_PERSISTED_QUERY_KEYS` above, in `App.tsx`.
134
+ > - `TRUE-81382` / `_sandbox-client`: `shouldPersistQuery` in `src/queries/queries.ts`, excluding `BUNDLE_QUERY_KEYS` (`fetchBundlesByPersona`, `bundleData`, `bundleImagesData`, `fetchItemDetailsViaBundle`).
135
+ >
136
+ > When they meet, merge into **one** filter holding both key lists.
148
137
 
149
- ### What "fresh" will mean after that change
138
+ **Bundle queries uncached — `TRUE-81382` branch only.** There, `bundleData`, `bundleImagesData`, `fetchBundlesByPersona` spread `BUNDLE_NO_CACHE_QUERY_OPTIONS` (`{ staleTime: 0, gcTime: 0 }`) and are excluded by `shouldPersistQuery`; `useBundleBuilderZu` no longer persists the raw parent-bundle payload (cause: an inactive item was orderable from a stale persisted bundle). The constant does **not** exist on `_production` or `TRUE-82427`. **Merge decision open:** `TRUE-82427` removed `refetchOnWindowFocus: false` from these queries; `TRUE-81382` keeps it — keeping it opts bundles out of focus refetch. In `App.tsx` keep **both** `buster` and `dehydrateOptions`.
150
139
 
151
- A data change reaches the shopper on: **navigating to the page** (the query mounts), **refresh / fresh URL**, **the tab becoming visible again**, **network reconnect** (`refetchOnReconnect` defaults true), or a **mutation that invalidates the query**. Nothing polls, so it does **not** update while the shopper sits still on one page with the tab in front.
140
+ ### Decision — `gcTime` is the speed knob, `staleTime` is the correctness knob
152
141
 
153
- Side benefit: `AuthLayout`-owned queries (header, notifications, `FIELDS` copy) never remount, so today they only refresh on a full page load. Turning focus refetching on covers them — a partial answer to the "FIELDS copy is stale after a deploy" gotcha.
142
+ Decided 2026-09-03; revised 2026-10-13.
154
143
 
155
- Three limits to state to the business up front:
144
+ - React Query paints cached data instantly regardless of `staleTime`; `staleTime` only decides whether it also refetches.
145
+ - Commerce gates loading on `isLoading`, never `isFetching`; `isLoading` is `false` whenever cached data exists. So **a lower `staleTime` shows no extra spinner**.
146
+ - `gcTime` (default 5 min) decides how long unused data stays in memory — that is the real speed win. **Keep `gcTime: 1h` on Home and BundleView.**
156
147
 
157
- 1. **Prices already in the cart do NOT update.** The cart lives in Zustand + `localStorage`, not React Query.
158
- 2. **Alt-tabbing to another application and back is not a focus event** — see [frontend standard §4](../../../standards/frontend.md) and [force logout on deployment](./force-logout-on-deployment.md).
159
- 3. **On a cold load the old value paints first** from `localStorage["commerce"]` and swaps when the response lands.
148
+ **Revised 2026-10-13 (`TRUE-82427`):** the heavy hooks no longer carry `staleTime: 30_000` + `refetchOnWindowFocus: false`; global `0` + focus refetch applies, `gcTime: 1h` stays. Supersedes the 2026-09-22 "30s for heavy queries" shape.
160
149
 
161
- ### Decision — `gcTime` is the speed knob, `staleTime` is the correctness knob
150
+ **Still open:** a query that drives a **price or stock count** should gate its spinner on `isFetching`, not `isLoading`. Which commerce queries those are is not answered.
162
151
 
163
- Decided 2026-09-03. The 24h `staleTime` was reached for to make heavy item/bundle pages feel fast. **It is the wrong knob, and it buys zero perceived speed here.**
152
+ **Rejected:** a 24h `staleTime` "for speed" (buys none).
164
153
 
165
- - React Query **paints cached data instantly regardless of `staleTime`**. `staleTime` only controls whether it *also* refetches in the background. `gcTime` controls how long the cached copy is kept at all.
166
- - Commerce gates **every** loading state on `isLoading`, never `isFetching` (`useBundleQueryData.ts:22,35`; `useHomeViewModel.ts:136,157,177`). `isLoading` is `false` whenever cached data exists. So **lowering `staleTime` shows no extra spinner** — the 24h value only suppresses the refetch.
167
- - **`gcTime` is set nowhere in commerce**, so it is the 5-minute default. Navigating away from Home for 6+ minutes and back drops the query from memory and **does** show a real spinner today, despite the 24h `staleTime`. The persisted cache only helps a full reload, not in-session navigation. **Fixing `gcTime` is the actual speed win.**
154
+ ### The four stacked causes behind the production complaints
168
155
 
169
- **Agreed shape for heavy queries:**
156
+ Pinned 2026-09-03 on `TRUE-81610`.
170
157
 
171
- ```ts
172
- staleTime: 30_000, // stops a refetch storm on Home → bundle → Home clicks;
173
- // never survives a session or a deploy
174
- gcTime: 1000 * 60 * 60, // the real perceived-speed knob
175
- refetchOnWindowFocus: false,
176
- ```
158
+ **1. Long `staleTime` + persisted cache → daily stale data.** Global default fixed 2026-09-22 (TRUE-81991, `e546db3d`). Per-query overrides removed on `TRUE-82427` (pending merge).
177
159
 
178
- Keep the **app default at `0`**, like supply.
160
+ **2. Cache clears not gated on restoration → silently undone.** `PersistQueryClientProvider` renders children while still restoring; a clear that runs first is overwritten by the pending restore. ✅ Fixed 2026-09-22: every clear goes through **`clearPersistedQueryCache()`** (`src/lib/queryCacheRestoration.ts`), which waits for restore (capped **1.5s**), **cancels in-flight queries**, then clears; an epoch counter makes a superseded clear a no-op. Sites: logout (`AuthContext.tsx`), view-as switch (`NavIconList.tsx`), 401 (`axiosInstance.ts`). Supply's equivalent: `awaitRestoration()` from `useIsRestoring()`.
179
161
 
180
- **Caveat still open:** if a payload drives a **price or a stock count**, that query must gate its spinner on **`isFetching`**, not `isLoading` — a wrong price shown for one second is a real problem, unlike a stale item list. Which commerce queries those are is **not yet answered**.
162
+ **3. Two auth gates read two different localStorage keys → stuck login.** ✅ Fixed on `TRUE-82172` — see [SSO redirect & session gating](./sso-redirect-and-session-gating.md).
181
163
 
182
- **Rejected:** keeping the 24h `staleTime` "for speed" (verified it buys none), and `staleTime: 0` on the heavy hooks (too many repeats of a large payload).
164
+ **4. Forced logout too aggressive.** `AuthContext.tsx` `["fetchCoreParameters"]` string-compares the **whole** Core parameters JSON against a login baseline and on **any** difference calls `logout()` + `window.location.href = "/"` with no notice (on SSO, `/` re-triggers the IdP). It also **monkey-patches `history.pushState`/`replaceState`** to refetch on navigation. **Open.** The login-never-clears-cache half was fixed 2026-09-22 (`resetClientState()` on login and logout).
183
165
 
184
166
  ## Gotchas
185
167
 
186
- - **⚠ Setting the global `staleTime` to 0 fixes nothing — it is already 0.** The 24h behaviour is per-query (`src/lib/queryClient.ts` holds the default; the overrides are listed above).
187
- - **⚠ A price change can be 24h late on the item page and a refresh will not clear it** — the stale copy is in `localStorage["commerce"]`.
188
- - **⚠ A cart line's price never refetches.** The cart is Zustand + `localStorage`, outside React Query; no `staleTime` change touches it.
189
- - **⚠ Never verify a catalogue or FIELDS change on a warm browser.** Clear `localStorage.removeItem('commerce')` or log out first. A "it didn't deploy" report is usually a rehydrated cache.
190
- - **⚠ `gcTime: 0` does not stop persistence.** Active queries are still dehydrated. Exclude the key via `dehydrateOptions.shouldDehydrateQuery` as well.
191
- - **⚠ Nothing is excluded from persistence on `_production`** (2026-10-08) — the two filters live on `_beta` and on `TRUE-81382`/`_sandbox-client`. Check the branch.
192
- - **⚠ The core-parameters watcher is not a targeted invalidation** — any Core parameter change force-logs-out every user on the environment. Use per-query opt-out or `queryClient.invalidateQueries` for anything narrower.
193
- - **⚠ A `queryClient.clear()` that is not awaited behind cache restoration is undone.** Always clear via `clearPersistedQueryCache()`; never call `queryClient.clear()` directly. Cancelling in-flight queries is part of the clear, not an extra.
194
- - **⚠ A persister `throttleTime` above 0 can write back after a clear.** Keep it at `0`.
195
- - **⚠ Gate auth on `user`, never `accessToken`** — `fetchPublicToken` writes a public token into `accessToken`. See [SSO redirect & session gating](./sso-redirect-and-session-gating.md).
196
- - **`persistOptions.buster = __BUILD_ID__` is live on `_production`** (`e546db3d`): a new build drops the persisted cache on restore instead of hydrating it. It runs inside the app, so it cannot fix a browser-cached JS bundle — see [supply's persisted query cache](../../toga25-supply/features/persisted-query-cache.md#the-buster-gap).
197
- - **`AuthLayout` never remounts**, so `refetchOnMount` fires once per session for queries it owns — a route-change refresh has to be wired explicitly. That interacts with everything above: a query can be both stale *and* never refetched during a session.
168
+ - **⚠ A page that copies query data into local state behind a first-load guard (status, ref, or narrow effect deps) silently ignores refetches** — now visible because focus refetch is on. Fixed on `TRUE-82427`: Home, item detail Add to Cart. **Still open (found 2026-10-13):**
169
+ - OrderDetails `useOrderFormatting.ts` — effect deps lack `["trackingNumbers", uuid]` and `["currentApprovalStage", uuid]`, so their refetches are ignored.
170
+ - BundleView `applyBundleFormatting.ts` — the image effect is blocked by the `hasRunImageFormatting` ref, so an images-only refetch is ignored.
171
+ - Filter — search results come from a `useMutation` `onSuccess` on search-text change, so focus refetch never refreshes them (category browsing is fine).
172
+ - `GetSupportPage.tsx` — calls `reset()` on every `userData` change, so a refetch with changed data can wipe a message being typed.
173
+ - **⚠ A per-query `refetchOnWindowFocus: false` or `staleTime` beats the global default.** Grep for them before blaming `queryClient.ts`.
174
+ - **⚠ Never verify a catalogue or FIELDS change on a warm browser.** Run `localStorage.removeItem('commerce')` or log out first.
175
+ - **⚠ A cart line's price never refetches** (Zustand, outside React Query).
176
+ - **⚠ `gcTime: 0` does not stop persistence.** Also exclude the key via `dehydrateOptions.shouldDehydrateQuery`.
177
+ - **⚠ Nothing is excluded from persistence on `_production`** (2026-10-08).
178
+ - **⚠ The core-parameters watcher is not a targeted invalidation** — any Core parameter change logs out every user. Use `queryClient.invalidateQueries` for anything narrower.
179
+ - **⚠ Never call `queryClient.clear()` directly** — use `clearPersistedQueryCache()`. Keep persister `throttleTime` at `0`.
180
+ - **⚠ Gate auth on `user`, never `accessToken`** — see [SSO redirect & session gating](./sso-redirect-and-session-gating.md).
181
+ - **`buster = __BUILD_ID__` is live on `_production`** (`e546db3d`); it cannot fix a browser-cached JS bundle — see [supply's buster gap](../../toga25-supply/features/persisted-query-cache.md#the-buster-gap).
182
+ - **`AuthLayout` never remounts**, so `refetchOnMount` fires once per session for queries it owns.
198
183
 
199
184
  ## Change history
200
- - 2026-10-08 — Bundle queries uncached + excluded from persistence (TRUE-81382); branch map of the two persistence filters; buster marked live on `_production`. (tcox)
201
- - 2026-10-01 — Corrected: the global default is already `staleTime: 0` (`QueryClient` moved to `src/lib/queryClient.ts`, `refetchOnWindowFocus: false`, persister `buster` added). Per-query 24h overrides inventoried at `file:line`; ticket scope and the post-change freshness promise (and its cart / alt-tab / first-paint limits) recorded. No code changed. (apeterson)
202
- - 2026-09-22 — Causes 1 and 2 fixed: app default `staleTime: 0` + `refetchOnWindowFocus: false`, heavy hooks on 30s/1h, and all clears routed through `clearPersistedQueryCache()` with `throttleTime: 0`. Login now resets client state. TRUE-81991 (`e546db3d`). (apeterson)
203
- - 2026-09-21 — Cause 3 (public-token auth gate) fixed on `TRUE-82172` and moved to its own doc; causes 1, 2, 4 still open. (tcox)
204
- - 2026-09-03 — Four stacked causes pinned to `file:line`; `gcTime`-vs-`staleTime` decision recorded. (bala, apeterson)
185
+ - 2026-10-13 — `TRUE-82427` (`93f12265`, pending merge): per-query `staleTime` / `refetchOnWindowFocus: false` overrides removed, focus refetch on app-wide, guard's explicit opt-in removed, Home refetch fix, item-detail Add to Cart uses refetched price; four other refetch-ignoring pages logged as open. Countries + 5-min login queries left out of scope. 30s heavy-query shape superseded. (tcox)
186
+ - 2026-10-08 — Bundle queries uncached + excluded from persistence (TRUE-81382); branch map of the two persistence filters; buster live on `_production`. (tcox)
187
+ - 2026-10-01 — Global default already `staleTime: 0`; per-query 24h overrides inventoried; ticket scope agreed. (apeterson)
188
+ - 2026-09-22 — Causes 1 (default) and 2 fixed; login resets client state. TRUE-81991 (`e546db3d`). (apeterson)
189
+ - 2026-09-21 — Cause 3 fixed on `TRUE-82172`, moved to its own doc. (tcox)
190
+ - 2026-09-03 — Four causes pinned; `gcTime`-vs-`staleTime` decision. (bala, apeterson)
205
191
 
206
192
  ## Related
207
193
 
208
194
  - [inactive-item-purchase-gating](./inactive-item-purchase-gating.md)
209
195
  - [sso-redirect-and-session-gating](./sso-redirect-and-session-gating.md)
196
+ - [force-logout-on-deployment](./force-logout-on-deployment.md)
210
197
  - [architecture](../architecture.md)
211
198
  - [supply persisted-query-cache](../../toga25-supply/features/persisted-query-cache.md)
212
- - [supply force-logout-on-deployment](../../toga25-supply/features/force-logout-on-deployment.md)
@@ -6,9 +6,8 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-01
10
- owners: [apeterson]
11
- owners: [apeterson, bala]
9
+ updated: 2026-10-13
10
+ owners: [apeterson, bala, tcox]
12
11
  files:
13
12
  - toga2-commerce/src/hooks/useDeploymentGuard.tsx
14
13
  - toga2-commerce/src/lib/deploymentVersion.ts
@@ -38,7 +37,7 @@ Reached commerce `_production` only in merge `26e2fae7` (2026-09-30, TRUE-81991)
38
37
 
39
38
  ## How it works
40
39
 
41
- 1. One query on `META_LAST_REFRESH_DATETIME`, `enabled` only while authenticated, `staleTime: 0`, `refetchOnWindowFocus: true`, `refetchInterval` **5 minutes**.
40
+ 1. One query on `META_LAST_REFRESH_DATETIME`, `enabled` only while authenticated, `staleTime: 0`, focus refetch on (explicit `true` on `_production`; inherited from the app default on `TRUE-82427`), `refetchInterval` **5 minutes**.
42
41
  2. First observation writes the baseline (`action: "establish-baseline"`) and never signs out.
43
42
  3. A later differing value → toaster → 2.5s timer → `logout()` + reload.
44
43
  4. `logout()` clears `AUTH_STORAGE_KEYS`, including the baseline, so the next login just re-baselines.
@@ -62,9 +61,10 @@ Exactly three things, verified 2026-10-01:
62
61
  | | app-wide `refetchOnWindowFocus` | guard query |
63
62
  |---|---|---|
64
63
  | toga25-supply (`App.tsx`) | default `true` — every on-screen query refetches on focus | inherits |
65
- | toga2-commerce (`lib/queryClient.ts`) | `false` | opts back in individually |
64
+ | toga2-commerce `_production` (`lib/queryClient.ts`) | `false` | opts back in individually |
65
+ | toga2-commerce `TRUE-82427` (pending merge, 2026-10-13) | default `true` — matches supply | inherits (explicit opt-in removed) |
66
66
 
67
- Because the guard opts in, sign-out timing is **identical** in both apps. Deleting commerce's `refetchOnWindowFocus: false` buys no change in logout behavior and costs a burst of requests on every tab switch. Commerce `origin/_production` and `origin/_sandbox-client` are byte-identical for `useDeploymentGuard.tsx`, `App.tsx`, and `queryClient.ts` — sandbox-vs-prod differences were timing, not build.
67
+ Sign-out timing is **identical** in every row. `TRUE-82427` turned focus refetch on for price freshness, not logout — see [catalog cache freshness](./catalog-cache-freshness.md). Commerce `origin/_production` and `origin/_sandbox-client` are byte-identical for `useDeploymentGuard.tsx`, `App.tsx`, and `queryClient.ts` — sandbox-vs-prod differences were timing, not build.
68
68
 
69
69
  ## Gotchas
70
70
 
@@ -79,6 +79,7 @@ Because the guard opts in, sign-out timing is **identical** in both apps. Deleti
79
79
  To also fire on an in-app navigation: a `useLocation()` effect calling `refetch()` on `pathname` change, with `refetch` held in a **ref** (its identity changes every render; depending on it directly loops). Commerce can do this as-is — `<DeploymentGuard />` is inside `AuthLayout`, inside the router. **Supply cannot**: its guard sits in `App.tsx` **above** `<AppRouter />`, so `useLocation()` would throw; the guard must move inside the router first. Not implemented — deferred 2026-10-01.
80
80
 
81
81
  ## Change history
82
+ - 2026-10-13 — Commerce app-wide focus refetch on and guard opt-in removed on `TRUE-82427` (pending merge); query-config table updated. (tcox)
82
83
  - 2026-10-01 — Documented the exact refetch triggers (mount / tab-visibility / 5-min poll; **not** route change), the background-pause behavior, the first-bump no-op, the ignored `key=` param, and the commerce-vs-supply query config difference. Route-change refetch deferred. (apeterson)
83
84
  - 2026-10-01 — Trigger is now the tools Force Logout page (TRUE-82414). (bala)
84
85
  - 2026-09-25 — Built for commerce: moved out of `AuthContext` into `<DeploymentGuard />` so a toaster can be shown; added 5-min poll + focus refetch; baseline key added to `AUTH_STORAGE_KEYS`. TRUE-81991. (apeterson)
@@ -6,8 +6,8 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-01
10
- owners: [apeterson, bala]
9
+ updated: 2026-10-13
10
+ owners: [apeterson, bala, tcox]
11
11
  files:
12
12
  - toga25-supply/src/hooks/useDeploymentGuard.tsx
13
13
  - toga25-supply/src/App.tsx
@@ -32,9 +32,10 @@ On large deploys the backend bumps Core parameter `META_LAST_REFRESH_DATETIME`.
32
32
  4. **Reset UX.** Non-dismissing info toast ("A new version is available — signing you out briefly…"), wait **2.5s**, then `AuthContext` `logout()`.
33
33
  5. **Centralized logout does the wipe.** `logout()` runs `performLogout()` + `clearUser()` + `localStorage.clear()` (tokens, `zu-user`, `zu-hostname`, the persisted query cache, and the `deployment-version` baseline) + `queryClient.clear()`, then hard-redirects to `/`. Clearing the baseline is what prevents a logout loop.
34
34
 
35
- **When it actually fires, the first-bump no-op, and the ignored `key=` param are identical here** — documented once in [commerce's force-logout doc](../../toga2-commerce/features/force-logout-on-deployment.md) (sections *What actually triggers a refetch* and *Gotchas*). Supply-only difference: `App.tsx` leaves `refetchOnWindowFocus` at React Query's default `true` app-wide, so every on-screen query refetches on tab visibility — not just this one. Sign-out timing is the same in both apps.
35
+ **When it actually fires, the first-bump no-op, and the ignored `key=` param are identical here** — documented once in [commerce's force-logout doc](../../toga2-commerce/features/force-logout-on-deployment.md) (sections *What actually triggers a refetch* and *Gotchas*). `App.tsx` leaves `refetchOnWindowFocus` at React Query's default `true` app-wide, so every on-screen query refetches on tab visibility. Commerce `_production` still sets `false` app-wide; commerce `TRUE-82427` (pending merge, 2026-10-13) matches supply. Sign-out timing is the same in both apps.
36
36
 
37
37
  ## Change history
38
+ - 2026-10-13 — Noted commerce `TRUE-82427` now matches supply on app-wide focus refetch. (tcox)
38
39
  - 2026-10-01 — Trigger is now the tools Force Logout page (TRUE-82414). (bala)
39
40
  - 2026-10-01 — Recorded that the guard sits above the router (blocks `useLocation()`); moved the shared trigger/gotcha detail to the commerce doc rather than duplicating it. (apeterson)
40
41
  - 2026-09-16 — Initial implementation. (apeterson)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1003",
3
+ "version": "1.0.1004",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",