toga-ai 1.0.778 β†’ 1.0.779

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.
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-03
10
10
  owners: [jcardinal, apeterson, bala]
11
11
  files:
12
12
  - _underscore/Model/Client/Language.php
@@ -155,6 +155,32 @@ boolean rows; the item-record modal (the first boolean consumer) exposed the gap
155
155
  `_underscore`, so it takes effect only once the framework branch is deployed (framework is pulled at
156
156
  deploy).
157
157
 
158
+ ### 🚨 A message key inside `config` JSON is NEVER resolved β€” copy must ride a `*MessageId` column
159
+
160
+ `_buildBundle`/`_serializeElement` build `bundle.messages` by scanning **exactly six columns** on
161
+ `SurfaceElements` β€” `labelMessageId`, `tooltipMessageId`, `placeholderMessageId`,
162
+ `disabledTooltipMessageId`, `trueMessageId`, `falseMessageId` (`Surface.php` ~L600). **`config` is
163
+ passed through untouched and is never scanned for message keys.** A key written into `config`
164
+ therefore ships to the frontend with **no value behind it**, and the screen renders the raw key.
165
+
166
+ This is a load-bearing constraint on how *any* surface-driven copy is modelled: **one string = one
167
+ `SurfaceElement` message column.** There is no way to attach copy to a nested `config` blob.
168
+
169
+ **Real case (2026-09-03).** The Columns modal rendered `common.columns.modal.apply` as its Apply
170
+ button label. `Core/2026-08-28b` had put the seven `common.columns.modal.*` keys inside the
171
+ `columnsButton` element's `config.modal` β€” data that looks right and can never resolve. The chosen
172
+ fix was **not** to teach the resolver to scan `config` (that widens the message-resolution contract
173
+ for every surface); it was to give the modal **its own MODAL Surface** so every string rides a real
174
+ `*MessageId` on its own element and the existing collector resolves it with **zero `_underscore`
175
+ change**. See
176
+ [column-visibility](../../toga25-supply/features/column-visibility.md).
177
+
178
+ **Diagnostic order.** When the FE shows a raw message key, first check whether the key rides a
179
+ `*MessageId` column β€” *before* suspecting an unrun seed. The resolved bundle in the API response is
180
+ the proof: if the element is present but `bundle.messages` has no entry for the key, the seed ran and
181
+ the modelling is wrong. (This session's first diagnosis β€” "the seed hasn't run" β€” was wrong, and the
182
+ pasted API response is what disproved it.)
183
+
158
184
  ## FIELD elements bound to a RecordField β€” see the dedicated doc
159
185
 
160
186
  A `renderType = FIELD` element with a non-null `recordFieldId` gets `Core.RecordFields` metadata and
@@ -755,6 +781,13 @@ full stop. See
755
781
  match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
756
782
 
757
783
  ## Change history
784
+ - 2026-09-03 β€” Recorded a **hard constraint on message resolution**: the resolver collects
785
+ `bundle.messages` from **only** the six `*MessageId` columns on `SurfaceElements`; a message key
786
+ placed inside `config` JSON is **never** resolved and renders raw on screen. Found when the Columns
787
+ modal displayed `common.columns.modal.apply`; `Core/2026-08-28b` had seeded those keys into
788
+ `config.modal`. Fixed in data, not in the framework β€” the modal became its own MODAL Surface
789
+ (Core 60) so each string rides a real `*MessageId`. Also recorded the diagnostic order: a raw key
790
+ means "check the modelling" before "check whether the seed ran". (apeterson)
758
791
  - 2026-09-01 β€” Split the new **FIELD ↔ RecordField binding** capability out into its own doc
759
792
  ([surface-field-recordfield-binding](surface-field-recordfield-binding.md)) rather than growing this
760
793
  one: for a `renderType=FIELD` element with a `recordFieldId`, `_buildBundle` now folds on
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-03
9
+ updated: 2026-09-04
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - api2/Config/production.ini
@@ -65,6 +65,20 @@ setpw() {
65
65
  `MYSQL_PWD` keeps the password out of `ps` and history the same way a defaults file does β€” use it
66
66
  when the client rejects `--defaults-extra-file` (some builds do).
67
67
 
68
+ **Check the password's line SHAPE without ever seeing the value.** Mask every character that is not
69
+ a space, tab, `;`, or `"` β€” you learn whether the value is quote-wrapped and whether an inline `;`
70
+ comment is in play, and the secret never reaches your screen or scrollback:
71
+
72
+ ```bash
73
+ awk -v S=database '$0=="["S"]"{f=1;next} /^\[/{f=0} f && /^password/{
74
+ k=$0; sub(/=.*/,"=",k); v=$0; sub(/^[^=]*=/,"",v); gsub(/[^ \t;"]/,"x",v);
75
+ print "[" k v "]"; exit}' Config/production.ini
76
+ ```
77
+
78
+ On `production.ini` `[database]` this printed `[password = "xxxxxxxx"]` (checked 2026-09-04): the
79
+ double-quote wrap is real, and there is **no inline `;` comment** on password lines β€” so `setpw`
80
+ above, which strips quotes only, is correct as written and needs no comment handling.
81
+
68
82
  **Keep the password out of your shell history and out of `ps`** by building a throwaway
69
83
  credentials file from the ini instead of passing `-p` on the command line:
70
84
 
@@ -93,10 +107,25 @@ Delete the file when you are done.
93
107
 
94
108
  ## Resetting a shared environment from production (dump β†’ drop β†’ reload)
95
109
 
96
- Used 2026-09-03 to bring sandbox-client's stale `Client_Nychh` TableViews back in line with prod.
97
- The developer runs every one of these; do not execute them for them.
98
-
99
- 1. **Dump each source database**, one file each β€” prod `Core` from
110
+ Used 2026-09-03 to bring sandbox-client's stale `Client_Nychh` TableViews back in line with prod,
111
+ and again 2026-09-04 (`Core` + `Client_Nychh`). The developer runs every one of these; do not
112
+ execute them for them.
113
+
114
+ 1. 🚨 **Inventory what the reset will DESTROY β€” before you drop anything.** A shared non-prod
115
+ environment is normally *ahead* of prod, because that is where changes get tried first, so a
116
+ prod→sandbox reload silently rolls that lead back. List every `dbchanges2` file dated after the
117
+ last prod release, plus anything parked in `git stash`, and decide which ones must be re-run.
118
+ Candidates from the 2026-09-04 sandbox-client reset, all *potentially* sandbox-only:
119
+ - `Core/` β€” 2026-09-01a, -01c, -01d, -02a, -02b, -03a, -03b, -03c, -03d
120
+ - `Client/` β€” 2026-09-01b, -01c, -02a
121
+ - `Client_Nychh/` β€” 2026-09-02a, -03a, -03b
122
+ - `dbchanges2` `stash@{0}` ("TO qty fulfilled core seed"), which is applied nowhere
123
+
124
+ **Verify every candidate against the environment β€” never assume the list.** After this reset
125
+ sandbox read `Core.Surfaces` = 54, which means some of those Core surface files were **already in
126
+ prod** and must not be re-run. The repo records intent, not deployed state β€” see
127
+ [Verifying whether a migration actually ran](./verifying-a-migration-ran.md).
128
+ 2. **Dump each source database**, one file each β€” prod `Core` from
100
129
  `reader1.core.database.togahub.com`, prod tenant from `reader1.client.database.togahub.com`:
101
130
  ```
102
131
  mysqldump --single-transaction --quick --routines --triggers --events \
@@ -104,14 +133,17 @@ The developer runs every one of these; do not execute them for them.
104
133
  ```
105
134
  `--set-gtid-purged=OFF` because RDS has GTID on; `--no-tablespaces` because the `admin` user
106
135
  lacks `PROCESS` (otherwise **error 1227**).
107
- 2. 🚨 **`tail -1` every dump file and confirm it ends with `-- Dump completed` BEFORE you drop
136
+ 3. 🚨 **`tail -1` every dump file and confirm it ends with `-- Dump completed` BEFORE you drop
108
137
  anything.** A truncated dump loads without complaining. DNS dropped mid-session on a host that
109
138
  had resolved seconds earlier (`ERROR 2005 Unknown MySQL server host`) β€” the dump just stops.
110
- 3. **Prove which environment each connection actually reached.** Better than `@@hostname`: check the
139
+ 4. **Prove which environment each connection actually reached.** Better than `@@hostname`: check the
111
140
  resolved IP β€” production resolves into **`10.201.x`**, sandbox into **`10.200.x`**.
112
- 4. **Drop + create the target schema, then pipe the dump in.**
113
- 5. **Verify with a row count known to differ** (step 1 of the procedure above), against the schema
114
- you intended to copy.
141
+ 5. **Drop + create the target schema, then pipe the dump in.**
142
+ 6. **Verify with a row count known to differ** (step 1 of *before you trust ANY tenant dump*,
143
+ above), against the schema you intended to copy.
144
+ 7. **Re-run the step-1 migrations that prod did not already have, in `Core/` β†’ `Client/` fan-out β†’
145
+ `Client_<Tenant>/` order.** Client files reference Core record ids, and the client work needs the
146
+ columns the `Client/` fan-out adds β€” any other order fails or half-applies.
115
147
 
116
148
  ### ⚠ Which `mysql` / `mysqldump` binary you are running is load-bearing
117
149
 
@@ -120,10 +152,23 @@ The developer runs every one of these; do not execute them for them.
120
152
  `xamppfiles/bin/mysqldump` is **MariaDB 10.4** (rejects the flag);
121
153
  `xamppfiles/mysql/bin/mysqldump` is **MySQL 8.0.44** (correct). Run `mysqldump --version` before
122
154
  you trust it.
123
- - **A shell alias can hijack a remote connection.** XAMPP's `mysql` alias hardcodes
124
- `-uroot -pmysql --socket=…`, so a command you wrote for a remote host silently talks to localhost.
125
- Call the client by **full path** for anything remote β€” and remember an exact match with your local
126
- DB is evidence of a bad restore, not a good one (see the gotcha below).
155
+ - 🚨 **A shell alias hijacks a remote connection β€” and the error names the wrong problem.** On an
156
+ XAMPP machine `~/.zshrc:31` defines
157
+ `alias mysql='/Applications/XAMPP/xamppfiles/mysql/bin/mysql -u root -p<pw> --socket=…'`. A later
158
+ `-u admin` on the command line wins for the **user**, but the alias's `-p<pw>` **overrides
159
+ `MYSQL_PWD`** β€” so every `mysql -h <remote-host> -u admin …` dies with
160
+ `ERROR 1045 (28000): Access denied for user 'admin'@'<your ip>' (using password: YES)`, *against
161
+ the correct remote host*. That reads as a rotated credential, a missing grant, a VPN problem, or
162
+ the double-quote trap above, and it is none of them. (The same alias can also silently talk to
163
+ localhost when its `--socket` wins β€” two symptoms, one cause.)
164
+ - **The tell: `mysqldump` to the SAME host with the SAME `MYSQL_PWD` succeeds while `mysql`
165
+ 1045s.** Only `mysql` is aliased, not `mysqldump`. "Dump works, `mysql` denied" identifies the
166
+ alias immediately β€” check that before you go near the password extractor or the ini quoting.
167
+ - **Fix:** call the binary by full path
168
+ (`MYSQL=/Applications/XAMPP/xamppfiles/mysql/bin/mysql`). `\mysql` or `command mysql` also skip
169
+ alias expansion.
170
+ - An exact match with your local DB is evidence of a bad restore, not of a good one (see the
171
+ gotcha below).
127
172
 
128
173
  ## Gotchas
129
174
 
@@ -137,6 +182,12 @@ The developer runs every one of these; do not execute them for them.
137
182
  makes the dump a point-in-time snapshot, so a live table keeps moving after it: `Units` read 8,627
138
183
  on prod and 8,618 on the restored sandbox. Do not chase that; only a *category* difference
139
184
  (missing tables, order-of-magnitude counts) means the restore was wrong.
185
+ - **The reference counts written down here go stale too.** After the 2026-09-04 reset,
186
+ sandbox-client read `Core.Surfaces` = **54** and `Client_Nychh.TransferOrders` = **1,932**,
187
+ against the prod figures previously recorded here β€” 53 (2026-09-03) and 1,927 (2026-08-31).
188
+ Prod itself moved on. Restored counts landing slightly **above** an older recorded prod number
189
+ is the expected pattern, not evidence of a bad restore. Re-read the count from the source at
190
+ reset time instead of comparing to a number in this doc.
140
191
  - 🚨 **The `dbchanges2` folder name IS the target database β€” pipe each folder to its own DB.**
141
192
  `Core/` β†’ `Core`, `Client_<Name>/` β†’ that one tenant, `Client/` β†’ **every** tenant. Files reference
142
193
  tables **unqualified** by design (the cluster-isolation rule), so running a `Core/` file against a
@@ -157,6 +208,17 @@ The developer runs every one of these; do not execute them for them.
157
208
  [Verifying whether a migration actually ran](./verifying-a-migration-ran.md).
158
209
 
159
210
  ## Change history
211
+ - 2026-09-04 β€” Second sandbox-client reset (`Core` + `Client_Nychh`) from prod; the procedure held,
212
+ three gaps closed. Added a mandatory **step 1 β€” inventory what the reset will destroy** (a shared
213
+ non-prod environment is normally ahead of prod, so the reload rolls that lead back), with the
214
+ 2026-09-04 candidate file list and the unapplied stash, plus a **step 7** to re-run them in
215
+ `Core/` β†’ `Client/` β†’ `Client_<Tenant>/` order. Sharpened the shell-alias bullet: the real symptom
216
+ is **`ERROR 1045` against the CORRECT remote host** (the alias's `-p<pw>` beats `MYSQL_PWD`), not a
217
+ silent localhost connection β€” and the tell is that `mysqldump` works while `mysql` is denied,
218
+ because only `mysql` is aliased. Added a value-safe way to inspect an ini password's line shape
219
+ (confirms the double-quote wrap and that there is no inline `;` comment, so `setpw` needs no
220
+ change). Refreshed the expected-drift numbers and noted that recorded reference counts age.
221
+ (apeterson)
160
222
  - 2026-09-03 β€” Added the **reset-a-shared-environment-from-production** procedure (the `mysqldump`
161
223
  flag set, `--set-gtid-purged=OFF` for RDS GTID, `--no-tablespaces` for the missing `PROCESS`
162
224
  grant, the mandatory `tail -1` "`-- Dump completed`" check because a truncated dump loads
@@ -8,7 +8,7 @@
8
8
  | [Cart Notification Emails β€” duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/stores/useEmailOptionsStore.ts, src/stores/useCartSalesQuoteZu.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts |
9
9
  | [Cart Order-Total & Shipping Computation](features/cart-order-total-computation.md) | The Cart summary section (subtotal / shipping / tax / total) is **data-driven** from `cartData`. | toga2-commerce/src/pages/Cart/viewModel/useCartViewModel.ts, toga2-commerce/src/pages/Cart/CartPage.tsx, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/api/CartApi.ts |
10
10
  | [Cart Page β€” config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | The Cart page (`src/pages/Cart/`) is the most config-heavy page in `toga2-commerce`. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/pages/Cart/view/cartForm/CartFormSection.tsx, src/pages/Cart/view/cartForm/CartFormRenderer.tsx, src/pages/Cart/view/EditCart.tsx, src/pages/Cart/view/EditOrder.tsx, src/pages/Cart/viewModel/useEditOrderOrEditCartViewModel.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts, src/hooks/useAssignClientFields.ts |
11
- | [Catalog cache freshness β€” the 24h persisted query cache, and how to opt a query out of it](features/catalog-cache-freshness.md) | TOGa Commerce runs a **single `QueryClient` with a 24-hour default `staleTime`**, and persists it to **`localStorage["commerce"]`** through `PersistQueryClientP | toga2-commerce/src/App.tsx, toga2-commerce/src/contexts/AuthContext.tsx, toga2-commerce/src/pages/ItemsView/viewModel/useItemDetailsViewModel.ts |
11
+ | [Catalog cache freshness β€” the 24h persisted query cache, and how to opt a query out of it](features/catalog-cache-freshness.md) | TOGa Commerce runs a **single `QueryClient` with a 24-hour default `staleTime`**, and persists it to **`localStorage["commerce"]`** through `PersistQueryClientP | toga2-commerce/src/App.tsx, toga2-commerce/src/contexts/AuthContext.tsx, toga2-commerce/src/api/axiosInstance.ts, toga2-commerce/src/hooks/useAuthenticationFlow.ts, toga2-commerce/src/components/NavIcons/NavIconList.tsx, toga2-commerce/src/pages/BundleView/hooks/useBundleQueryData.ts, toga2-commerce/src/pages/Home/viewModel/useHomeViewModel.ts, toga2-commerce/src/pages/ItemsView/viewModel/useItemDetailsViewModel.ts |
12
12
  | [Category Tile Order (AssortmentItems.sortOrder) β€” merchandising a storefront category](features/category-tile-sort-order.md) | **"Move item X to the front of category Y" is a DATA change, not a code change.** The order of item tiles on a storefront category page is driven by exactly one | src/pages/Filter/api/FilterApi.ts, src/pages/Filter/viewModel/useFilterViewModel.ts, api2/Component/Api/V2/V2.php, toga2-supply/src/pages/Items/api/itemsApi.ts |
13
13
  | [Client Fields β€” per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/pages/Account/view/MySettingsView.tsx, src/contexts/helpers/getLoginSettings.ts, src/pages/Account/viewModel/useAccountViewModel.ts, src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/pages/Cart/api/CartApi.ts, src/hooks/useAuthenticationFlow.ts, src/contexts/AuthContext.tsx, src/pages/Login/viewModel/useLoginPageViewModel.ts, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx, src/components/NavIcons/NavIconItem.tsx, src/components/Submenus/AlertSubmenu.tsx, src/components/Submenus/types.ts, src/pages/Account/AccountPage.tsx, src/pages/Account/view/MyOrdersView.tsx, src/pages/GetSupport/GetSupportPage.tsx, src/pages/GetSupport/viewModel/useGetSupportViewModel.ts, src/queries/queries.ts, src/App.tsx, src/pages/Filter/FilterPage.tsx, src/pages/Filter/viewModel/FIELDS/COMPASS/ENGLISH/USER/FILTERPAGEFIELDS.json, src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/USER/HEADERFIELDS.json, src/components/AuthLayout/AuthLayout.tsx |
14
14
  | [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | On the toga2-commerce **Cart** page, expedited shipping options (**"2nd Day EOB"** and **"Next Day Air"**) are only offered in the *Shipping Method* dropdown wh | toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/viewModel/FIELDS/shared/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/CartPage.tsx |
@@ -6,15 +6,21 @@ project: TOGa Commerce
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-14
10
- owners: ["bala"]
9
+ updated: 2026-09-03
10
+ owners: ["bala", "apeterson"]
11
11
  files:
12
12
  - toga2-commerce/src/App.tsx
13
13
  - toga2-commerce/src/contexts/AuthContext.tsx
14
+ - toga2-commerce/src/api/axiosInstance.ts
15
+ - toga2-commerce/src/hooks/useAuthenticationFlow.ts
16
+ - toga2-commerce/src/components/NavIcons/NavIconList.tsx
17
+ - toga2-commerce/src/pages/BundleView/hooks/useBundleQueryData.ts
18
+ - toga2-commerce/src/pages/Home/viewModel/useHomeViewModel.ts
14
19
  - toga2-commerce/src/pages/ItemsView/viewModel/useItemDetailsViewModel.ts
15
20
  related:
16
21
  - ./inactive-item-purchase-gating.md
17
22
  - ../architecture.md
23
+ - ../../toga25-supply/features/persisted-query-cache.md
18
24
  - ../../toga25-supply/features/force-logout-on-deployment.md
19
25
  ---
20
26
 
@@ -30,26 +36,37 @@ switched back on, it stays hidden.
30
36
  Any query whose answer **gates an action** (can this be bought? is this still available?) must be
31
37
  opted out of *both* mechanisms. Opting out of one is not enough.
32
38
 
39
+ **Three long-running production complaints all trace back to this file set**, and were pinned to
40
+ `file:line` on 2026-09-03 (read-only, branch `TRUE-81610`): "users don't see updated data",
41
+ "clearing their cache doesn't work", and "users get stuck on a broken login page after a forced
42
+ logout". They are **four separate causes**, listed below. Nothing has been fixed yet.
43
+
33
44
  ## How it works
34
45
 
35
46
  ### The default: cached and persisted
36
47
 
37
- `src/App.tsx` creates the client with `staleTime: 1000 * 60 * 60 * 24` and a
38
- `createSyncStoragePersister` on `localStorage` key `"commerce"`. Everything inherits that unless it
39
- overrides it β€” including the `["clientFields", …]` query that carries the per-tenant `FIELDS`
40
- content, which is why copy/config deploys also look stale for returning users (see the
41
- [architecture gotchas](../architecture.md#gotchas)).
48
+ `src/App.tsx:26-31` creates the client with `staleTime: 1000 * 60 * 60 * 24` and a
49
+ `createSyncStoragePersister` on `localStorage` key `"commerce"` (`src/App.tsx:34-37`). Everything
50
+ inherits that unless it overrides it β€” including the `["clientFields", …]` query that carries the
51
+ per-tenant `FIELDS` content, which is why copy/config deploys also look stale for returning users
52
+ (see the [architecture gotchas](../architecture.md#gotchas)).
53
+
54
+ For comparison, **TOGa 2.5 Supply's default is `staleTime: 0`** *because* its cache is persisted.
55
+ Same library, same persister, opposite default. See
56
+ [supply's persisted query cache](../../toga25-supply/features/persisted-query-cache.md).
42
57
 
43
- ### The platform's cache-bust: `META_LAST_REFRESH_DATETIME`
58
+ ### The platform's cache-bust: the Core parameters blob
44
59
 
45
- The **only** mechanism that clears the persisted cache for everyone is the Core parameter
46
- **`META_LAST_REFRESH_DATETIME`** (`Core.Parameters` id 32). `src/contexts/AuthContext.tsx` polls it
47
- with `staleTime: 0` **on every route change and on window focus**; when the value differs from the
48
- one captured at login it calls `logout()` and forces a full reload, which clears the saved cache.
60
+ `src/contexts/AuthContext.tsx:192-252` runs a `["fetchCoreParameters"]` query with `staleTime: 0`
61
+ and `refetchOnWindowFocus: true`, plus a refetch on every route change. On the first fetch after
62
+ login it stores the whole response as a baseline; on any later fetch it **string-compares the
63
+ entire core-parameters JSON** against that baseline and, on **any** difference, calls `logout()` and
64
+ `window.location.href = "/"`.
49
65
 
50
- > **⚠ Bumping that parameter logs out EVERY user on that environment.** It is a deployment-grade
51
- > lever, not a way to refresh one catalogue change. The sibling implementation in TOGa 2.5 Supply
52
- > is documented in
66
+ > **⚠ It is not keyed on `META_LAST_REFRESH_DATETIME` alone β€” it compares the whole blob.** Any
67
+ > Core parameter change on that environment logs out **every** user. It is a deployment-grade
68
+ > lever, not a way to refresh one catalogue change. The narrower, notice-first sibling in TOGa 2.5
69
+ > Supply is documented in
53
70
  > [force logout on deployment](../../toga25-supply/features/force-logout-on-deployment.md).
54
71
 
55
72
  ### Opting a single query out β€” `staleTime: 0` + `gcTime: 0` is NOT sufficient on its own
@@ -68,6 +85,13 @@ const shouldDehydrateQuery = (query) => {
68
85
  // … persistOptions: { dehydrateOptions: { shouldDehydrateQuery } }
69
86
  ```
70
87
 
88
+ > **⚠ CORRECTION (verified 2026-09-03): this exclusion is NOT in production.** `shouldDehydrateQuery`
89
+ > exists only on `origin/_beta` and `origin/#sprint84`. `origin/_production`, `origin/_sandbox`,
90
+ > `origin/_dev` and `TRUE-81610` all pass `persistOptions: { persister: localStoragePersister }`
91
+ > with **no `dehydrateOptions` at all** β€” so **nothing is excluded from persistence in production**,
92
+ > and the inactive-item purchase gate it was built for is not actually protected there. Confirm the
93
+ > branch before relying on this section.
94
+
71
95
  **Why the exclusion is required:** `gcTime: 0` only evicts a query once it becomes **inactive**. An
72
96
  **active** query β€” exactly the state the item page is in while the user is looking at it β€” is still
73
97
  dehydrated and written to `localStorage`, and is rehydrated on the next load. So `gcTime: 0` alone
@@ -80,6 +104,104 @@ every other query, so this is an allowlist-shaped opt-out with no app-wide side
80
104
  decision; making every listing uncached would trade a real performance characteristic for no
81
105
  correctness gain.
82
106
 
107
+ ## The four stacked causes behind the production complaints
108
+
109
+ Verified 2026-09-03 on branch `TRUE-81610`. Read-only; none of these are fixed.
110
+
111
+ ### 1. App-wide 24h `staleTime` + persisted cache with nothing excluded β†’ daily stale data
112
+
113
+ `src/App.tsx:26-31` (24h default) plus a persisted cache and no `dehydrateOptions` means
114
+ **yesterday's queries are served today with no network call**. This is the "users not seeing
115
+ updated data" report, and it happens **daily** β€” not only after a deploy.
116
+
117
+ The same 24h value is **also hardcoded per-hook**, so changing only the default is not enough:
118
+
119
+ - `src/pages/BundleView/hooks/useBundleQueryData.ts:26,40`
120
+ - `src/pages/Home/viewModel/useHomeViewModel.ts:150,173,197`
121
+
122
+ ### 2. Cache clears are not gated on restoration β†’ they are silently undone
123
+
124
+ `PersistQueryClientProvider` renders children **while still restoring**
125
+ (`react-query-persist-client/build/modern/PersistQueryClientProvider.js`: `isRestoring` starts
126
+ `true`, `persistQueryClientRestore` resolves later). A `queryClient.clear()` +
127
+ `localStorage.removeItem("commerce")` that runs before restore finishes is **undone** β€” the pending
128
+ restore hydrates the old cache on top, and the persist subscription writes it back.
129
+
130
+ All three clearing sites are unguarded:
131
+
132
+ | Site | What it clears |
133
+ |---|---|
134
+ | `src/contexts/AuthContext.tsx:180-183` | logout |
135
+ | `src/components/NavIcons/NavIconList.tsx:162-170` | view-as / persona switch (language-specific data) |
136
+ | `src/api/axiosInstance.ts:46` | 401 forced logout |
137
+
138
+ **This is the "it's not successfully clearing their caches" report.** Supply already solved it with
139
+ an `awaitRestoration()` promise built from `useIsRestoring()`
140
+ (`toga25-supply/src/contexts/AuthContext.tsx:56-77`) and gates every clear behind it.
141
+
142
+ ### 3. Two auth gates read two different localStorage keys β†’ stuck on a broken login page
143
+
144
+ - The axios request interceptor gates on **`user`** (`src/api/axiosInstance.ts:112-118`). With no
145
+ `user` it calls `fetchPublicToken()`, which POSTs `/auth/public` and **writes the public token
146
+ back as `accessToken`** (`src/api/axiosInstance.ts:53-64`).
147
+ - But `src/hooks/useAuthenticationFlow.ts:213-217` gates on **`accessToken`** and returns early if
148
+ it exists.
149
+
150
+ So after a forced logout: the first API call writes a *public* `accessToken`, the auth flow sees a
151
+ token, skips **both** the SSO redirect and `/login`, and the user is logged out while the app
152
+ believes they are not.
153
+
154
+ Supply gates on `user` β€” with an explicit comment at
155
+ `toga25-supply/src/contexts/AuthContext.tsx:47-51`. This is the app-side twin of the blox
156
+ `fetchPublicToken` gotcha already recorded in `2.0/standards/frontend.md` Β§22.
157
+
158
+ ### 4. The forced logout is too aggressive, and login never clears the cache
159
+
160
+ `src/contexts/AuthContext.tsx:192-252`:
161
+
162
+ - On **any** core-parameter change it calls `logout()` + `window.location.href = "/"` with **no
163
+ notice** to the user. On an SSO client `/` re-triggers the IdP.
164
+ - It **monkey-patches `window.history.pushState` and `replaceState`** (`:216-226`) to refetch on
165
+ every navigation. react-router uses those same methods.
166
+
167
+ Separately, commerce's `login()` (`:100-148`) **never clears the cache**, so signing in as a
168
+ different user inherits the prior user's data for up to 24h. Supply clears on login.
169
+
170
+ ## Decision β€” `gcTime` is the speed knob, `staleTime` is the correctness knob
171
+
172
+ Decided 2026-09-03. The 24h `staleTime` was reached for to make heavy item/bundle pages feel fast.
173
+ **It is the wrong knob, and it buys zero perceived speed here.**
174
+
175
+ - React Query **paints cached data instantly regardless of `staleTime`**. `staleTime` only controls
176
+ whether it *also* refetches in the background. `gcTime` controls how long the cached copy is kept
177
+ at all.
178
+ - Commerce gates **every** loading state on `isLoading`, never `isFetching`
179
+ (`useBundleQueryData.ts:22,35`; `useHomeViewModel.ts:136,157,177`). `isLoading` is `false`
180
+ whenever cached data exists. So **lowering `staleTime` shows no extra spinner** β€” the 24h value
181
+ only suppresses the refetch.
182
+ - **`gcTime` is set nowhere in commerce**, so it is the 5-minute default. Navigating away from Home
183
+ for 6+ minutes and back drops the query from memory and **does** show a real spinner today,
184
+ despite the 24h `staleTime`. The persisted cache only helps a full reload, not in-session
185
+ navigation. **Fixing `gcTime` is the actual speed win.**
186
+
187
+ **Agreed shape for heavy queries:**
188
+
189
+ ```ts
190
+ staleTime: 30_000, // stops a refetch storm on Home β†’ bundle β†’ Home clicks;
191
+ // never survives a session or a deploy
192
+ gcTime: 1000 * 60 * 60, // the real perceived-speed knob
193
+ refetchOnWindowFocus: false,
194
+ ```
195
+
196
+ Keep the **app default at `0`**, like supply.
197
+
198
+ **Caveat still open:** if a payload drives a **price or a stock count**, that query must gate its
199
+ spinner on **`isFetching`**, not `isLoading` β€” a wrong price shown for one second is a real
200
+ problem, unlike a stale item list. Which commerce queries those are is **not yet answered**.
201
+
202
+ **Rejected:** keeping the 24h `staleTime` "for speed" (verified it buys none), and `staleTime: 0`
203
+ on the heavy hooks (too many repeats of a large payload).
204
+
83
205
  ## Gotchas / known issues
84
206
 
85
207
  - **⚠ Never verify a catalogue or FIELDS change on a warm browser.** Clear
@@ -87,18 +209,42 @@ correctness gain.
87
209
  rehydrated cache.
88
210
  - **⚠ `gcTime: 0` does not stop persistence.** Active queries are still dehydrated. Exclude the key
89
211
  via `dehydrateOptions.shouldDehydrateQuery` as well.
90
- - **⚠ `META_LAST_REFRESH_DATETIME` is not a targeted invalidation** β€” it force-logs-out every user
91
- on the environment. Use per-query opt-out or `queryClient.invalidateQueries` for anything
92
- narrower.
93
- - **A `persistOptions.buster` keyed on the build version** would fix stale FIELDS at deploy time but
94
- invalidates every persisted query app-wide β€” still an **open team decision, not implemented**
95
- (carried over from the architecture doc).
212
+ - **⚠ Nothing is excluded from persistence on `_production`** β€” the `shouldDehydrateQuery`
213
+ allowlist lives only on `_beta`/`#sprint84`. Check the branch.
214
+ - **⚠ The core-parameters watcher is not a targeted invalidation** β€” any Core parameter change
215
+ force-logs-out every user on the environment. Use per-query opt-out or
216
+ `queryClient.invalidateQueries` for anything narrower.
217
+ - **⚠ A `queryClient.clear()` that is not awaited behind cache restoration is undone.** All three
218
+ of commerce's clear sites have this bug.
219
+ - **⚠ Gate auth on `user`, never `accessToken`** β€” `fetchPublicToken` writes a public token into
220
+ `accessToken`.
221
+ - **A `persistOptions.buster` keyed on the build version** would fix stale data at deploy time.
222
+ Commerce has a committed `amplify.yml`, so injecting `AWS_COMMIT_ID` as a build id is
223
+ straightforward here (`package.json` `version` is `0.0.0` and `vite.config.ts` has no `define`
224
+ block, so the id must be created first). Mechanics and the scope limit β€” a buster runs inside the
225
+ app and can never fix a browser-cached JS bundle β€” are written up in
226
+ [supply's persisted query cache](../../toga25-supply/features/persisted-query-cache.md#the-buster-gap).
227
+ Still **not implemented**.
96
228
  - **`AuthLayout` never remounts**, so `refetchOnMount` fires once per session for queries it owns β€”
97
229
  a route-change refresh has to be wired explicitly. That interacts with everything above: a query
98
230
  can be both stale *and* never refetched during a session.
99
231
 
100
232
  ## Change history
101
233
 
234
+ - 2026-09-03 β€” Read-only diagnosis (branch `TRUE-81610`). **Corrected** this doc: the
235
+ `shouldDehydrateQuery` opt-out it described exists only on `origin/_beta` / `origin/#sprint84` β€”
236
+ `_production`, `_sandbox`, `_dev` have **no `dehydrateOptions`**, so nothing is excluded from
237
+ persistence in production. Also corrected the cache-bust: `AuthContext` string-compares the
238
+ **whole core-parameters blob**, not just `META_LAST_REFRESH_DATETIME`, so any Core parameter
239
+ change logs out every user. Added the four stacked causes behind the standing production
240
+ complaints (app-wide 24h `staleTime` also hardcoded in `useBundleQueryData`/`useHomeViewModel`;
241
+ all three cache clears unguarded against the `PersistQueryClientProvider` restore race; the
242
+ `user`-vs-`accessToken` gate split that leaves a user logged out while the app thinks otherwise;
243
+ the notice-less forced logout that monkey-patches `history.pushState`, plus `login()` never
244
+ clearing the cache). Recorded the decision that **`gcTime` is the speed knob and `staleTime` is
245
+ the correctness knob** β€” commerce gates every spinner on `isLoading`, so the 24h `staleTime` buys
246
+ no perceived speed, while `gcTime` is at its 5-minute default and *is* the real cause of slow
247
+ in-session navigation. No code changed. (apeterson)
102
248
  - 2026-08-14 β€” Created while building the
103
249
  [inactive-item purchase gate](./inactive-item-purchase-gating.md). Recorded that the 24h
104
250
  persisted cache masks catalogue state **in both directions** (a switched-off item kept rendering
@@ -9,12 +9,13 @@
9
9
  | [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql |
10
10
  | [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
11
11
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
12
+ | [Persisted React Query cache (localStorage `supply-chain-query-cache`)](features/persisted-query-cache.md) | `localStorage["supply-chain-query-cache"]` is **not a hand-written cache**. | toga25-supply/src/App.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/hooks/useCurrentUser.ts, toga25-supply/src/hooks/useStatusTypeValues.ts, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/vite.config.ts |
12
13
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css |
13
14
  | [Side navigation & default route β€” an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | The 2.5 side nav is **100% backend-driven** by the `navigation` surface bundle, and the same list also decides **which routes exist** and **where `/` lands**. | toga25-supply/src/routes.tsx, toga25-supply/src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts, api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
14
15
  | [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
15
16
  | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer β€” DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/stackedCurrencyJoiner.test.ts, toga25-supply/src/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx, toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
16
17
  | [Talos Integration (AppLayout host, live Aegra streaming, LangGraphβ†’blox mapper)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/api/talos.ts, toga25-supply/src/hooks/useTalosSession.ts, toga25-supply/src/hooks/useTalosThreads.ts, toga25-supply/src/hooks/useTalosThreads.test.ts, toga25-supply/src/hooks/useTalosSurface.ts, toga25-supply/src/hooks/useTalosSurface.test.ts, toga25-supply/src/App.tsx, toga25-supply/db-migrations/PLAYBOOK.md, dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, toga25-supply/src/providers/TalosStreamProvider.tsx, toga25-supply/src/utils/talosMessages.ts, toga25-supply/src/utils/talosMessages.test.ts, toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
17
- | [Transfer Orders page (TableView β†’ Core surfaces β†’ React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen β€” list + read-only record modal β€” built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/PanelSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/ContactSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/SelectTargetLocationModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferItemPickerTable.tsx, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderNote.php, _underscore/Model/Client/TransferOrderNoteType.php, dbchanges2/Client/2026-09-01b - TransferOrderNotesTables.sql, dbchanges2/Client/2026-09-01c - TransferOrderContactCreatedByAndDescription.sql, dbchanges2/Core/2026-09-01c - TransferOrderNotesRecords.sql, dbchanges2/Core/2026-09-01d - TransferOrderContactCreatedByAndDescriptionRecordFields.sql, toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/pages/TransferOrders/TransferOrder.module.css, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/usePurchaseOrderItemRows.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferSourceLocation.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useLocationContacts.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateLocationContact.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/AddContactModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferNotesField.tsx, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql |
18
+ | [Transfer Orders page (TableView β†’ Core surfaces β†’ React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen β€” list + read-only record modal β€” built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/PanelSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/ContactSelect.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/SelectTargetLocationModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferItemPickerTable.tsx, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderNote.php, _underscore/Model/Client/TransferOrderNoteType.php, dbchanges2/Client/2026-09-01b - TransferOrderNotesTables.sql, dbchanges2/Client/2026-09-01c - TransferOrderContactCreatedByAndDescription.sql, dbchanges2/Core/2026-09-01c - TransferOrderNotesRecords.sql, dbchanges2/Core/2026-09-01d - TransferOrderContactCreatedByAndDescriptionRecordFields.sql, toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/pages/TransferOrders/helpers/buildInventoryPurchaseOrderUrl.ts, toga25-supply/src/pages/TransferOrders/TransferOrder.module.css, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/usePurchaseOrderItemRows.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferSourceLocation.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useLocationContacts.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateLocationContact.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/AddContactModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferNotesField.tsx, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Core/2026-09-03a - TransferOrderStatusBadgeRecordFields.sql, dbchanges2/Client_Nychh/2026-09-03a - TransferOrdersStatusBadge.sql, dbchanges2/Core/2026-09-03b - InventoryCreateTransferOrderAction.sql, dbchanges2/Client_Nychh/2026-09-03b - InventoryCreateTransferOrderShow.sql, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx, _underscore/Model/Client/TransferOrderItem.php |
18
19
  | [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json, toga25-supply/.env.development |
19
20
  | [Cypress Testing Harness (multi-tenant, fully stubbed)](workflows/cypress-testing.md) | The Cypress harness for `toga25-supply`: an **e2e** project and a **component** project, plus a stub layer that answers **every** `/v2` call so no test touches | toga25-supply/cypress.config.ts, toga25-supply/cypress/README.md, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/tenants.ts, toga25-supply/cypress/support/api/stubApi.ts, toga25-supply/cypress/support/api/envelope.ts, toga25-supply/cypress/support/api/users.ts, toga25-supply/cypress/support/api/surfaces.ts, toga25-supply/cypress/support/api/session.ts, toga25-supply/cypress/e2e/login.cy.ts, toga25-supply/cypress/e2e/emptyNavigation.cy.ts, toga25-supply/cypress/component/harness.cy.tsx, toga25-supply/cypress/fixtures/tenants/COMPASS/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/COMPASSCANADA/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/NYCHH/surfaces/navigation.json, toga25-supply/cypress/fixtures/tenants/QUAD/surfaces/navigation.json, toga25-supply/scripts/cypress.mjs, toga25-supply/tsconfig.node.json, toga25-supply/package.json |
20
21
  | [Porting a page (or query) from toga2-supply to toga25-supply](workflows/porting-a-page-from-toga2-supply.md) | `toga25-supply` re-implements pages that already exist in `toga2-supply`. | toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/PRUDENTIAL/exportApiFields.json, toga25-supply/src/pages/SalesOrders/api/prudentialExportApi.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/components/ui/Tables/hooks/useExportableData.tsx |
@@ -0,0 +1,211 @@
1
+ ---
2
+ title: Persisted React Query cache (localStorage `supply-chain-query-cache`)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-09-03
10
+ owners: ["apeterson"]
11
+ files:
12
+ - toga25-supply/src/App.tsx
13
+ - toga25-supply/src/contexts/AuthContext.tsx
14
+ - toga25-supply/src/hooks/useDeploymentGuard.tsx
15
+ - toga25-supply/src/fieldsConfig/useClientFields.ts
16
+ - toga25-supply/src/surface/useFetchSurfaceMeta.ts
17
+ - toga25-supply/src/hooks/useCurrentUser.ts
18
+ - toga25-supply/src/hooks/useStatusTypeValues.ts
19
+ - toga25-supply/src/surface/useStatusColors.ts
20
+ - toga25-supply/vite.config.ts
21
+ related:
22
+ - ./force-logout-on-deployment.md
23
+ - ./surface-frontend.md
24
+ - ./transfer-orders-page.md
25
+ - ../architecture.md
26
+ - ../../toga2-commerce/features/catalog-cache-freshness.md
27
+ ---
28
+
29
+ ## Summary
30
+
31
+ `localStorage["supply-chain-query-cache"]` is **not a hand-written cache**. It is the **whole
32
+ React Query cache**, dehydrated to localStorage by `PersistQueryClientProvider`. Every successful
33
+ query in the app lands there and survives reloads *and* sessions.
34
+
35
+ Two consequences drive most of the "it didn't deploy" / "I'm seeing yesterday's data" reports:
36
+
37
+ 1. The app default is **`staleTime: 0`** precisely because the cache is persisted. Any hook that
38
+ sets its own non-zero `staleTime` can serve **a previous session's answer and never fetch**.
39
+ 2. Nothing in the app invalidates the blob on deploy. There is **no `buster`**. Deploys are
40
+ covered separately, and only indirectly, by `DeploymentGuard` forcing a logout.
41
+
42
+ Scattered warnings about this exist in several docs. This is the owning doc: mechanics, what is
43
+ persisted, every reset path, and the `buster` gap.
44
+
45
+ ## How it works
46
+
47
+ ### Wiring
48
+
49
+ ```ts
50
+ // src/App.tsx:27-30
51
+ const localStoragePersister = createSyncStoragePersister({
52
+ storage: window.localStorage,
53
+ key: "supply-chain-query-cache",
54
+ });
55
+ ```
56
+
57
+ `PersistQueryClientProvider` (`src/App.tsx:33`) wraps the app. The `QueryClient` default is
58
+ `staleTime: 0` (`src/App.tsx:18`).
59
+
60
+ ### What is persisted β€” everything except three namespaces
61
+
62
+ `shouldDehydrateQuery` (`src/App.tsx:63-70`) keeps TanStack's default (persist successful queries)
63
+ **minus** three query-key namespaces:
64
+
65
+ | Excluded namespace | Why |
66
+ |---|---|
67
+ | `talos` | chat titles / assistant ids are per-session user data; must not sit on disk |
68
+ | `surface-meta` | the surface bundle carries **permission and feature-gate** state |
69
+ | `surface-meta-group` | same bundle, grouped endpoint |
70
+
71
+ The surface exclusion is the important one. `useFetchSurfaceMeta` runs at `staleTime: Infinity`, so
72
+ a *persisted* copy would never refetch β€” a permission **grant**, or worse a **revoke**, would not
73
+ reach a browser that already had one until the persister's 24h `maxAge` expired. Excluding it costs
74
+ one request per page load; the in-memory cache still covers every route change.
75
+ `staleTime: Infinity` is safe there **only because** it is excluded from persisting.
76
+
77
+ ### Query key β†’ API map
78
+
79
+ | Query key | Request |
80
+ |---|---|
81
+ | `["table-data", slug, …]` | `getDataTableData` (blox) |
82
+ | `["table-meta", slug]` | `GET /table-views/meta` (blox `components/Table/hooks/useFetchTablePageMeta.js:8`) |
83
+ | `["status-colors", route]` | `GET /{route}?fields=slug,colorHex&sort=sortOrder` |
84
+ | `["currentUser"]` | `GET /users` |
85
+ | `["clientFields", slug, role]` | client `FIELDS` config |
86
+ | `["status-type-values", routes]` | status enum values |
87
+ | `["deploymentVersion"]` | `GET /core-parameters` |
88
+
89
+ ### The 24h `maxAge` is ONE timestamp for the whole blob
90
+
91
+ The persister's default `maxAge` is 24h β€” confirmed at
92
+ `node_modules/@tanstack/query-persist-client-core/build/modern/persist.js:10`. It is compared
93
+ against a single `persistedClient.timestamp`, **not per query**. So the cache is either fully
94
+ hydrated or **fully dropped** at restore. There is no partial expiry.
95
+
96
+ ### Reset paths β€” the only things that clear it
97
+
98
+ 1. **Login** β€” `src/contexts/AuthContext.tsx:113-114` (`await awaitRestoration()` then
99
+ `queryClient.clear()`), so signing in as a different user cannot inherit the prior user's data.
100
+ 2. **Logout** β€” `src/contexts/AuthContext.tsx:141-146` (same gate, plus `localStorage.clear()`).
101
+ 3. **A list page's Refresh button** β€” `queryClient.clear()` + `resetQueries(["table-data"])` on
102
+ SalesOrders, TransferOrders, ServiceRequests, Items, VendorItems, Bundles, Inventory.
103
+ 4. **The 24h `maxAge`** expiring at restore.
104
+
105
+ That is all. A deploy is **not** on the list.
106
+
107
+ ### The restoration race, and `awaitRestoration()`
108
+
109
+ `PersistQueryClientProvider` renders children **while still restoring** (`isRestoring` starts
110
+ `true`; `persistQueryClientRestore` resolves later). A `queryClient.clear()` that runs before
111
+ restore finishes is **silently undone** β€” the pending restore hydrates the old cache on top, and
112
+ the persist subscription writes it straight back.
113
+
114
+ Supply solves this at `src/contexts/AuthContext.tsx:56-77`: it turns `useIsRestoring()` into an
115
+ awaitable promise and gates **every** clearing path behind it.
116
+
117
+ ```ts
118
+ const isRestoring = useIsRestoring();
119
+ // …resolve a promise once isRestoring flips false…
120
+ const awaitRestoration = (): Promise<void> =>
121
+ restorationCompleteRef.current ?? Promise.resolve();
122
+
123
+ // then, in login() and logout():
124
+ await awaitRestoration();
125
+ queryClient.clear();
126
+ ```
127
+
128
+ **Any new cache-clearing code path must await this first.** TOGa Commerce does not, and its cache
129
+ clears are undone β€” see
130
+ [catalog cache freshness](../../toga2-commerce/features/catalog-cache-freshness.md).
131
+
132
+ ### Deploys are covered by DeploymentGuard, not by the cache
133
+
134
+ `src/hooks/useDeploymentGuard.tsx` polls the Core parameter `META_LAST_REFRESH_DATETIME`
135
+ (`GET /core-parameters`, `staleTime: 0`) every 5 minutes and on window focus. On a change it shows
136
+ a toast for 2.5s (`LOGOUT_NOTICE_DELAY_MS`) and then signs the user out β€” which clears the cache
137
+ via path 2 above. Details in
138
+ [force logout on deployment](./force-logout-on-deployment.md).
139
+
140
+ ## The `buster` gap
141
+
142
+ A `persistOptions.buster` is the standard way to drop a persisted cache on deploy. Mechanics
143
+ (verified at `query-persist-client-core/build/modern/persist.js:20-24`):
144
+
145
+ - `buster` is **one string, compared once at restore, before hydrate**.
146
+ - If it differs from the stored one, the whole persisted cache is **deleted** (`removeClient()`)
147
+ instead of hydrated.
148
+ - It must be **stable within a build and change per build**. A `Date.now()` buster would wipe the
149
+ cache on every page load.
150
+
151
+ **Blocker: this repo has no build id.** `package.json` `version` is `0.0.0` (never bumped),
152
+ `vite.config.ts` has **no `define` block**, and there is **no `amplify.yml`** β€” supply's Amplify
153
+ build is console-configured. Amplify does set `AWS_COMMIT_ID` in the build environment, so the
154
+ build id has to be created first:
155
+
156
+ ```ts
157
+ // vite.config.ts
158
+ define: { __BUILD_ID__: JSON.stringify(process.env.AWS_COMMIT_ID ?? "dev") }
159
+ // src/vite-env.d.ts
160
+ declare const __BUILD_ID__: string;
161
+ // src/App.tsx
162
+ persistOptions: { persister: localStoragePersister, buster: __BUILD_ID__, /* … */ }
163
+ ```
164
+
165
+ Type-check **both** `-p tsconfig.app.json` and `-p tsconfig.node.json` β€” `vite.config.ts` is only
166
+ covered by the node config.
167
+
168
+ **Scope limit β€” a buster cannot fix a stale JS bundle.** It runs *inside* the app, so if the
169
+ browser never downloaded the new app, the buster never runs. Diagnostic to tell the two caches
170
+ apart:
171
+
172
+ | Symptom | Cause | Does a buster help? |
173
+ |---|---|---|
174
+ | Hard refresh (Cmd+Shift+R) fixes it | bundle / CDN cache headers | No β€” irrelevant |
175
+ | Only logging out fixes it | persisted query data | Yes |
176
+
177
+ **Rejected alternative:** versioning the storage key (`…-v2`). It works, but leaves the old blob in
178
+ localStorage forever; `buster` calls `removeClient()` and cleans up.
179
+
180
+ ## Gotchas / known issues
181
+
182
+ - **⚠ A non-zero `staleTime` on a persisted query serves yesterday's data and never fetches.** Data
183
+ hooks in this app use `staleTime: 0`. Hooks that currently break this rule **and are persisted**:
184
+ `useClientFields`, `useCurrentUser`, `useStatusTypeValues`, `useStatusColors`,
185
+ `useSalesOrderUuidForServiceRequest` (all `Infinity`), and the three VendorItem form hooks
186
+ (`5 * 60_000`). Each can serve a previous session's answer.
187
+ - `useTalosSession` and `useTalosThreads` also set a non-zero `staleTime`, but their keys start with
188
+ `talos`, so they are **not** persisted β€” in-memory only, safe.
189
+ - **⚠ Never verify a config, surface, or ACL change on a warm browser.** Log out, or delete the
190
+ `supply-chain-query-cache` key in devtools. Most "it didn't deploy" reports are a rehydrated
191
+ cache.
192
+ - **⚠ `gcTime: 0` does not stop persistence.** It only evicts a query once it becomes *inactive*;
193
+ an active query is still dehydrated. To keep something off disk, exclude its namespace in
194
+ `shouldDehydrateQuery`.
195
+ - **⚠ Any new `queryClient.clear()` must `await awaitRestoration()` first**, or the restore race
196
+ undoes it.
197
+ - The `maxAge` is one timestamp for the entire blob β€” you cannot expire a single query through it.
198
+
199
+ ## Change history
200
+
201
+ - 2026-09-03 β€” Created. Consolidated the persisted-cache mechanics that were scattered across the
202
+ architecture doc and the surface/transfer-orders/talos features into one owning doc: the
203
+ three excluded namespaces and *why* surface-meta is excluded (permission/feature-gate state +
204
+ `staleTime: Infinity`), the query-key→API map, the four reset paths, that the 24h `maxAge` is a
205
+ **single timestamp for the whole blob** (all-or-nothing at restore), the `awaitRestoration()`
206
+ gate for the `PersistQueryClientProvider` restore race, and the full list of hooks that still set
207
+ a non-zero `staleTime` while being persisted. Also recorded the `buster` mechanics
208
+ (one string, compared before hydrate, deletes the whole cache on mismatch) and the blocker β€”
209
+ no build id exists in the repo (`version 0.0.0`, no `define`, no `amplify.yml`) β€” plus the fact
210
+ that a buster can never fix a browser-cached JS bundle. Read-only investigation; no code changed.
211
+ (apeterson)
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-03
9
+ updated: 2026-09-04
10
10
  owners: [apeterson, bala]
11
11
  files:
12
12
  - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts
@@ -50,6 +50,15 @@ files:
50
50
  - dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
51
51
  - dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql
52
52
  - dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql
53
+ - dbchanges2/Core/2026-09-03a - TransferOrderStatusBadgeRecordFields.sql
54
+ - dbchanges2/Client_Nychh/2026-09-03a - TransferOrdersStatusBadge.sql
55
+ - dbchanges2/Core/2026-09-03b - InventoryCreateTransferOrderAction.sql
56
+ - dbchanges2/Client_Nychh/2026-09-03b - InventoryCreateTransferOrderShow.sql
57
+ - toga25-supply/src/surface/useStatusColors.ts
58
+ - toga25-supply/src/pages/Inventory/Inventory.tsx
59
+ - toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
60
+ - toga25-supply/src/layout/ItemFulfillmentModal/useItemFulfillmentModalViewModel.tsx
61
+ - _underscore/Model/Client/TransferOrderItem.php
53
62
  related:
54
63
  - ./surface-frontend.md
55
64
  - ./meta-driven-table-data.md
@@ -109,6 +118,10 @@ tenant.
109
118
  | `Core/2026-09-01c - TransferOrderNotesRecords.sql` | Core | Records **357**/**358**, RecordFields **2550–2560**, `InherentRecordChildren` 312β†’357 β€” **also unused, and 357/358 have NO NYCHH ACL grant** |
110
119
  | `Client/2026-09-01c - TransferOrderContactCreatedByAndDescription.sql` | all tenants (template) | `TransferOrders.contactId` (FK Contacts), `createdByUserId` (FK Users), `description varchar(255)` |
111
120
  | `Core/2026-09-01d - TransferOrderContactCreatedByAndDescriptionRecordFields.sql` | Core | RecordFields **2561** `contactId` / **2562** `createdByUserId` / **2563** `description` on record 312, + `AclFieldPermissions` for roles **1** and **3** |
121
+ | `Core/2026-09-03a - TransferOrderStatusBadgeRecordFields.sql` | Core | flips the stage RecordField to `type = 'STATUS'` + points `Records.statusRecordId` at the stage record β€” the badge chain |
122
+ | `Client_Nychh/2026-09-03a - TransferOrdersStatusBadge.sql` | one tenant | the ACL grants the badge's vocabulary fetch needs (incl. the **`uuid`** RecordField β€” see the gotcha) |
123
+ | `Core/2026-09-03b - InventoryCreateTransferOrderAction.sql` | Core | the Inventory header **Create Transfer Order** action button, seeded `isVisible = 0` |
124
+ | `Client_Nychh/2026-09-03b - InventoryCreateTransferOrderShow.sql` | one tenant | the `SurfaceOverrides` row that reveals it |
112
125
 
113
126
  All of them were audited against the cluster-isolation HARD RULE: **no `Core.*` reference in any
114
127
  `Client_*` file** (the only such strings are inside comments), no `UUID()`, all table references
@@ -31,7 +31,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
31
31
  - **voice-to-voice** (TOGa Voice) β€” 4 doc(s) β†’ [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
32
32
  - **ai-bdr** (AI-BDR) β€” 13 doc(s) β†’ [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
33
33
  - **toga2-commerce** (TOGa Commerce) β€” 21 doc(s) β†’ [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
34
- - **toga25-supply** (TOGa 2.5 Supply) β€” 18 doc(s) β†’ [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
34
+ - **toga25-supply** (TOGa 2.5 Supply) β€” 19 doc(s) β†’ [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
35
35
  - **toga-blox** (TOGa Blox) β€” 14 doc(s) β†’ [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
36
36
  - **bdr** (BDR) β€” 0 doc(s) β†’ [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
37
37
 
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: nychh
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-09-03
21
+ updated: 2026-09-04
22
22
  owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
23
23
  files:
24
24
  - dbchanges2/Client_Nychh/2026-09-02a - TransferOrderNetsuitePushInterceptor.sql
@@ -58,6 +58,13 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
58
58
  run the NetSuiteβ†’TOGa Supply sync and the asset-tag verification/backfill diagnostics.
59
59
 
60
60
  ## Key features (this client)
61
+ - 🚨 **sandbox-client (`Core` + `Client_Nychh`) was reset from PRODUCTION again on 2026-09-04** β€”
62
+ the second reset in two days (the first, 2026-09-03, was over the stale `purchase-orders`
63
+ TableView). Every "verified on <date>" sandbox-client claim in this profile dated **before
64
+ 2026-09-04 now describes whatever prod had**, not what is there. The 2026-09-01 β†’ 2026-09-03
65
+ `dbchanges2` migrations may also need re-running on sandbox-client. Re-read the environment before
66
+ trusting any sandbox state below; procedure and the re-run order are in
67
+ [Targeting the right database](../../2.0/apps/dbchanges2/workflows/targeting-the-right-database.md).
61
68
  - **Transfer orders now push OUT to NetSuite as $0 sales orders (2026-09-02, built not yet run).**
62
69
  NYCHH is the first and only client on that path; it is switched on purely by its
63
70
  `transfer-orders` (record 312) `POST`/`POST` interceptor row. Destination location = NetSuite
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.778",
3
+ "version": "1.0.779",
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",