toga-ai 1.0.885 → 1.0.887

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: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-22
9
+ updated: 2026-09-24
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - tools/_/app/developers/multiclientsql.php
@@ -18,6 +18,7 @@ related:
18
18
  - ./developer-tools.md
19
19
  - ./persona-gated-navigation.md
20
20
  - ../../test/features/2-0-deployment-client-sql.md
21
+ - ../../../../2.0/apps/_underscore/features/per-client-database-connections.md
21
22
  ---
22
23
 
23
24
  Tools `/developers/multi-client-sql` — fans one SQL change-set across a live list of 2.0 client/log/archive/module DBs into a copy/download script (never executes); open to use/extend it or for the `Databases` reserved-word gotcha.
@@ -31,10 +32,11 @@ Replaces `test/team/2.0 deployment/generate_client_sql.php`. Development Team pe
31
32
  1. Paste SQL, pick a list, untick/filter DBs. Generate builds the script **in the browser**: header + per-DB banner + `` USE `db`; `` + SQL. Copy / Download `.sql`. Warns if the SQL has its own `USE`.
32
33
  2. `post.php` action `lists` only returns JSON lists.
33
34
  3. Lists are dynamic — no tools commit when clients change:
34
- - **All Clients / Client Logs / Client Archives** — live read of `Core.Clients` joined to `Core.Databases` on `clientDatabaseId` / `logDatabaseId` / `archiveDatabaseId`, via config `[database_toga2core]`. Every row (no active flag).
35
- - **Module lists** — one per folder under `dbchanges2/_modules` (branch `_main`). A client is in the list when `Client_<Name>/_modules.txt` names the module (case-insensitive); DB name = client folder name. GitHub GraphQL, 2 calls (folder list, then all `_modules.txt` as aliased blobs). APCu cache 5 min + 7-day "last good" fallback shown with a warning. Warns when a module client is not in `Core.Clients`. One retry on a dropped connection; log names the failed step (`folder list` / `_modules.txt read`).
36
- 4. GitHub token: **only** the `GITHUB_TOKEN` env var (read via `getenv` + `$_SERVER` + `$_ENV`, because php-fpm `clear_env`). No config fallback (developer decision). Own curl, not `App_Design_Github`. Needs read on `agilantsolutions/dbchanges2`; set as an EB environment property in prod.
37
- 5. Every DB/folder name is checked against `^[A-Za-z0-9_]{1,64}$` before it goes into GraphQL or a `USE` line. Page writes via `textContent`/`.value` only.
35
+ - **All Clients / Client Logs / Client Archives** — live read of `Core.Clients` joined to `Core.Databases` on `clientDatabaseId` / `logDatabaseId` / `archiveDatabaseId`, via config `[database_toga2core]`. Keeps a DB only if `EXISTS` a `Core.DatabaseHosts` row (INNER JOIN `Environments` on `slug`) for the `ENVIRONMENT` env var (tools is production-only: `production`). Missing `ENVIRONMENT` → user-safe error, never an unfiltered list. Why: `Core.Clients` has never-built clients (see [per-client DB gotcha](../../../../2.0/apps/_underscore/features/per-client-database-connections.md)). `isActive` flag rejected: inactive ≠ no DB, flags drift, and it is a Core schema change.
36
+ - **Module lists** — one per folder under `dbchanges2/_modules` (branch `_main`). A client is in the list when `Client_<Name>/_modules.txt` names the module (case-insensitive); DB name = client folder name. GitHub GraphQL, 2 calls (folder list, then all `_modules.txt` as aliased blobs). APCu cache 5 min + 7-day "last good" fallback shown with a warning. Warns when a module client is not in `Core.Clients`; drops clients whose client DB is not built. One retry on a dropped connection; log names the failed step (`folder list` / `_modules.txt read`).
37
+ 4. Env vars `GITHUB_TOKEN` and `ENVIRONMENT` read by shared `envValue()` (`getenv` + `$_SERVER` + `$_ENV`, because php-fpm `clear_env`). GitHub token: **only** `GITHUB_TOKEN`. No config fallback (developer decision). Own curl, not `App_Design_Github`. Needs read on `agilantsolutions/dbchanges2`; set as an EB environment property in prod.
38
+ 5. Every list returns a `skipped` array, shown on the page ("Skipped N (database not built — no DatabaseHosts row): …").
39
+ 6. Every DB/folder name is checked against `^[A-Za-z0-9_]{1,64}$` before it goes into GraphQL or a `USE` line. Page writes via `textContent`/`.value` only.
38
40
 
39
41
  ## Gotchas
40
42
 
@@ -51,3 +53,4 @@ Replaces `test/team/2.0 deployment/generate_client_sql.php`. Development Team pe
51
53
  ## Change history
52
54
 
53
55
  - 2026-09-22 — Created; shipped and verified in production. (jcardinal)
56
+ - 2026-09-24 — Lists filtered by `Core.DatabaseHosts` for `ENVIRONMENT`; `skipped` list shown; verified prod 35 of 46. (jcardinal)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-09-24
10
10
  owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi", "bala"]
11
11
  files:
12
12
  - _underscore/Database.php
@@ -59,6 +59,7 @@ When `_underscore` serves a request for a client it opens **three distinct per-c
59
59
 
60
60
  ## Gotchas
61
61
 
62
+ - **`Core.Clients` has rows for clients whose DBs were never built** (prod 2026-09-24: 46 rows, 35 built). "DB exists" = a `Core.DatabaseHosts` row (`databaseId` + `environmentId` [+ `regionId`]) for the current `Environments.slug` — the same rows `registerClientDatabases()` resolves. Any fan-out over all clients must filter `EXISTS` on `DatabaseHosts` for the env, not `Core.Clients` alone; `isActive`-style flags are wrong (inactive clients still have DBs). Host-row count varies (e.g. `Archive_Nychh` has 1, others 4) — still built. Example: [Tools Multi-Client SQL](../../../../1.0/apps/tools/features/multi-client-sql.md).
62
63
  - **⚠ NEVER read `LAST_INSERT_ID()` (or any other per-connection session value) through `_Query`.** `_Query` defaults `_isReadHostEnabled = true` (`Query.php:8`) and routes SELECTs to `CONNECTION_READ` (`Query.php:273-276`). `LAST_INSERT_ID()` is scoped to the connection that did the INSERT, so a read-host SELECT returns **0** — silently wrong, and now an FK violation wherever the id is used as a foreign key. Use `mysqli_insert_id` on the write/transaction connection and reject `<= 0`. It is not biting today only because most client registrations leave `writeHost`/`readHost` null, and `Database.php:488` then points READ and WRITE at the **same** `mysqli` handle (so raw `mysqli_query` and `new _Query(...)` share one connection and one transaction). Configuring a real read host for any client would break every such call at once. Worked example: [Compass PEOPLE-file user lifecycle](../../../clients/compass-usa/features/people-file-user-lifecycle.md).
63
64
  - **Raw `mysqli_query` bypasses `_Query`'s automatic query-cache reset.** After a hand-rolled write, call `_Database::resetQueryCache($dbName)` yourself or later model reads serve stale rows.
64
65
 
@@ -92,3 +93,4 @@ Methodology lesson — why the wrong conclusion was reached: the model-class com
92
93
 
93
94
  ## Change history
94
95
  - 2026-09-23 — Added the read-host routing trap: `_Query` sends SELECTs to `CONNECTION_READ`, so `LAST_INSERT_ID()` read through `_Query` can return 0; noted that null `writeHost`/`readHost` currently collapses both to one handle, and that raw `mysqli_query` needs a manual `resetQueryCache()`. (mhammontree)
96
+ - 2026-09-24 — Added gotcha: filter client fan-outs by `DatabaseHosts` for the environment; `Core.Clients` includes never-built clients. (jcardinal)
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-09-24
10
10
  owners: ["bala", "mhammontree", "tcox", "apeterson", "rgirish", "jcardinal"]
11
11
  files:
12
12
  - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts
@@ -17,6 +17,7 @@ files:
17
17
  - _underscore/Model/Elite/TransferOrderStage.php
18
18
  related:
19
19
  - ../architecture.md
20
+ - ../../toga25-supply/features/bundle-item-pricing.md
20
21
  - ../../../1.0/apps/library/features/netsuite-item-assettype-sync.md
21
22
  - ../../toga2-supply/features/currency-amount-lines-editor.md
22
23
  - cxml-shipnotice-gateway.md
@@ -157,6 +158,22 @@ WHERE recordId = 351 AND field = 'c_netsuiteInternalTransferOrderStatus';
157
158
 
158
159
  `recordId 351` is `transfer-order-stages`; the equivalent row exists per tenant with a **different `id`** but often the **same `uuid`** (both Elite and NYCHH carry `b273f4d1-dac0-4093-ad1d-7c3a526ab894`), the tell that the row came from one shared seed and only one tenant's copy has the wrong flag.
159
160
 
161
+ ### ⚠ An inherent child nested in a **PUT** is ALWAYS written as a CREATE
162
+
163
+ The engine recurses into a nested one-to-many child (`Core.InherentRecordChildren`) with
164
+ `httpMethod: 'POST'` **hardcoded** (`V2.php:5415`). There is no uuid check and no PUT branch, so a
165
+ child nested in an update is never updated:
166
+
167
+ | Nested child payload on a `PUT` | Result |
168
+ |---|---|
169
+ | no `uuid` | a **duplicate row** is inserted; the parent looks unchanged |
170
+ | `uuid` of the existing row | fails — `EV-10` / MySQL `Error #: 1062 Duplicate entry … for key '<Table>_UNIQUE'` |
171
+
172
+ **Correct pattern: nest children only on a CREATE.** To change an existing child, send its own
173
+ `PUT /v2/<child-route>/{uuid}` after the parent write (two calls). This is general engine
174
+ behaviour — it bites any write to a nested child, not one feature. Worked example:
175
+ [bundle item pricing](../../toga25-supply/features/bundle-item-pricing.md).
176
+
160
177
  ### Nested inherent-child create — parent FK is auto-set, any depth
161
178
 
162
179
  - V2 auto-sets the child's FK to the parent: first field whose `FIELDOPT_FOREIGNKEY_MODEL` equals the parent class (exact normalized `_Model_Client_` string), then break (`V2.php` ~5211-5221). Payload supplies the OTHER FK only (e.g. bridge row `{ purchaseOrder: { uuid } }` under a transfer order).
@@ -166,6 +183,7 @@ WHERE recordId = 351 AND field = 'c_netsuiteInternalTransferOrderStatus';
166
183
 
167
184
  ## Gotchas
168
185
 
186
+ - **⚠ A nested inherent child on a PUT is created, never updated** (`V2.php:5415` hardcodes `POST`) — you get a duplicate row, or a 1062 duplicate-key error if you send the child's `uuid`. Update children through their own route.
169
187
  - **Nested write with only a non-identifier field silently creates duplicates.** A duplicate-generating footgun: each write inserts a new empty child and the parent links to a fresh shell (or is left with a NULL FK). It compounds on any cron/integration that writes the same entity repeatedly. Root-caused in production on the Prudential device-sync cron, which sent `contact: {c_employeeXid}` and generated ~893k empty Contact rows (~5,600/day). See [Prudential device import + contact linking](../../../clients/prudential/features/device-information-import-and-contact-linking.md).
170
188
  - **A matched nested object is UPDATED, not just linked** — partner data can mutate shared catalog rows. Matching is not read-only.
171
189
  - **A `NULL` `overrideIsIdentifier` unsets the field as an identifier** — a row added only to set a child policy silently strips it, causing EV-12. Correct only where `Core.RecordFields.isIdentifier = 1`; ship it as a `dbchanges2/Client_<Tenant>/` migration (a manual non-prod patch evaporates on reseed).
@@ -177,6 +195,11 @@ WHERE recordId = 351 AND field = 'c_netsuiteInternalTransferOrderStatus';
177
195
  - **Declaring a `c_` field on the model does not make it searchable** — the `CustomRecordFields.isIdentifier` flag is the gate; set the flag, not the child policy.
178
196
  - **Singular-FK back-reference defects:** UPDATE path drops the parent's forward FK (echoes `null`); CREATE path re-parents a value-matched child to the newest writer (stolen child row). One defect, two symptoms.
179
197
 
198
+ ## Change history
199
+ - 2026-09-24 — Documented that the engine recurses into a nested inherent child with `POST`
200
+ hardcoded (`V2.php:5415`), so a child nested in a PUT is duplicated or fails 1062; update
201
+ children through their own route (TRUE-82138). (apeterson)
202
+
180
203
  ## Related
181
204
 
182
205
  - [api2 architecture](../architecture.md) — the V2 CRUD engine (`processRoutePairs`).
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
9
+ updated: 2026-09-24
10
10
  owners: [tcox, bala, apeterson, jcardinal, rgirish]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -64,6 +64,24 @@ controller.
64
64
  the join is genuinely 1:1, or the duplicates agree. Worked example:
65
65
  [category tile sort order](../../toga2-commerce/features/category-tile-sort-order.md).
66
66
 
67
+ ### ⚠ `depth` and `calcDepth` are IGNORED inside the base64 `_` options blob
68
+
69
+ `fields`, `where`, `sort`, `join` work inside the `_` blob. **`depth` and `calcDepth` do not** —
70
+ they are only read as plain query params. Silent: no error, the request just runs at the defaults
71
+ (`depth 3`, `calcDepth 1`), so nested calculated fields come back absent and the engine looks
72
+ incapable of returning them. Mixing is fine — put the blob in `_` and add `&calcDepth=2`.
73
+ (toga-blox `assembleOptions` already appends any unrecognised option key to the query string,
74
+ which is why mixed requests work from the FE.) Verified 2026-09-24 (TRUE-82138).
75
+
76
+ ### ⚠ A field THREE levels under an inherent child is silently DROPPED, at any depth
77
+
78
+ Distinct from the max-depth omission above — raising `depth` does not help. A field two hops below
79
+ an inherent child (`bundleItems.bundleItemPrices.priceType` on `GET /v2/bundles/{uuid}`) is absent
80
+ from the payload with no error and no message; the same field is returned by the shallower
81
+ `GET /v2/bundle-items/{uuid}`. **Consequence:** a caller cannot tell one child row from another
82
+ (e.g. a unit-price row from a lease-price row) on the parent payload, and must not guess — fetch
83
+ the child route instead.
84
+
67
85
  ### How `sort` becomes `ORDER BY` — and why "descending sort is broken" is usually just NULLs
68
86
 
69
87
  `ORDER BY` is a **plain string concatenation** of the resolved column and `ASC`/`DESC` — no cast, no
@@ -565,6 +583,7 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
565
583
  - **⚠ A 500 / `EO-1` on a `where` over a calculated field ⇒ you table-PREFIXED the field name.**
566
584
  Drop the prefix: `_qtyAvailable`, not `PurchaseOrderItems._qtyAvailable`. The prefixed form is
567
585
  routed to a `HAVING` against an alias that does not exist.
586
+ - **⚠ `depth`/`calcDepth` inside the `_` blob do nothing** — send them as plain query params.
568
587
  - **⚠ Do not raise `calcDepth` for a calculated field you named in `fields=`** — it is classified
569
588
  before the gate and already resolves at the default `1`.
570
589
  - **Do not trust a 200 with missing fields.** Without an explicit `fields=` list, ACL removal is
@@ -585,6 +604,9 @@ If you need the behaviour, loop uuids client-side. Point 3 is the durable, non-o
585
604
  [V2 request logging](request-logging.md).
586
605
 
587
606
  ## Change history
607
+ - 2026-09-24 — Recorded that `depth`/`calcDepth` are ignored inside the base64 `_` options blob
608
+ (plain query params only), and that a field three levels under an inherent child is silently
609
+ dropped at any depth (TRUE-82138). (apeterson)
588
610
  - 2026-09-23 — Documented how `sort` becomes `ORDER BY` (plain concatenation, no cast or NULL
589
611
  handling), that the same expression doubles as the keyset-pagination cursor so any change must be
590
612
  mirrored in the seek `WHERE`/`getSeekFieldType()`, and that "descending sort is broken" reports are
@@ -4,6 +4,7 @@
4
4
  |-----|---------|
5
5
  | [TOGa 2.5 Supply — Architecture](architecture.md) | Architecture of toga25-supply — the React/TS Vite SPA frontend for 2.0 Supply, multi-client + meta-driven, built on `@agilant/toga-blox`; open before any struct |
6
6
  | [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | Declarative config-driven Flag/Rule grammar that resolves `isEnabled`/`isVisible`/`isComplete` on SalesOrder action-button options and approval stage inputs (ev |
7
+ | [Bundle item pricing (unit / lease rows, edit save path, duplicate rows)](features/bundle-item-pricing.md) | How a bundle item's unit/lease price is read, displayed and SAVED in supply — the two-call save (never nest the price in a PUT), `_unitPrice`/`_leasePrice` + `c |
7
8
  | [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | Resolves a client's API-fetch projection (`fields`/`ojoin`/`join`/`where`) from one colocated per-page `apiFields.json` keyed by client slug (useClientApiFields |
8
9
  | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | Config that varies by client (or client × role) — field overrides, filter buttons, group-by, column pickers, layout toggles — resolved through `src/fieldsConfig |
9
10
  | [Column State (URL-driven show/hide, order & pinning)](features/column-visibility.md) | URL-driven "Columns" button + modal (`cols` param) for show/hide + order + pinning of table columns, client-configurable then Surface-driven; open for column-vi |
@@ -0,0 +1,89 @@
1
+ ---
2
+ title: Bundle item pricing (unit / lease rows, edit save path, duplicate rows)
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-24
10
+ owners: ["apeterson"]
11
+ files:
12
+ - toga25-supply/src/layout/BundleItemRecordModalLayout/hooks/useBundleItemEditForm.tsx
13
+ - toga25-supply/src/layout/BundleRecordModalLayout/types.ts
14
+ - toga25-supply/src/layout/BundleRecordModalLayout/viewModel/FIELDS/DEFAULT/bundleRecordViewFields.json
15
+ - toga25-supply/src/layout/BundleRecordModalLayout/components/BundleItemsTable.tsx
16
+ - dbchanges2/Client_Compass/2026-09-24a - DeduplicateBundleItemPrices.sql
17
+ related:
18
+ - ../../api2/features/nested-relationship-writes.md
19
+ - ../../api2/features/v2-rest-query-contract.md
20
+ - ../../_underscore/features/calculated-sql-fields.md
21
+ - ./record-modals-and-nested-tables.md
22
+ - ../../../clients/compass-usa/workflows/changing-a-kit-line-item.md
23
+ ---
24
+
25
+ How a bundle item's unit/lease price is read, displayed and SAVED in supply — the two-call save (never nest the price in a PUT), `_unitPrice`/`_leasePrice` + `calcDepth: 2`, and the duplicate `BundleItemPrices` rows.
26
+
27
+ ## Summary
28
+
29
+ A bundle item has **many** `BundleItemPrices` rows, one per `priceTypeId` (unit, lease-48, …).
30
+ Supply shows Default Price Type / Unit Price / Lease Price in the bundle items table and edits a
31
+ price in the bundle-item modal. Two engine behaviours shape all of it: nested children are never
32
+ updated on a PUT, and the calculated price fields need `calcDepth: 2` when read through a bundle.
33
+
34
+ ## How it works
35
+
36
+ ### Reading — `_unitPrice` / `_leasePrice`
37
+
38
+ Both are `FIELD_SQL` calculated fields on `_Model_Client_BundleItem`: they resolve the row
39
+ matching `defaultPriceTypeId` with `ORDER BY id DESC LIMIT 1` (**newest** row wins).
40
+
41
+ - The table columns are sourced from these fields, not from the `bundleItemPrices` array.
42
+ - For **nested** bundle items (inside `GET /v2/bundles/{uuid}`) they need **`calcDepth: 2`**, sent
43
+ as a **plain query param** — inside the base64 `_` blob it is ignored. At the default `calcDepth: 1`
44
+ both fields are silently absent. See
45
+ [V2 REST query contract](../../api2/features/v2-rest-query-contract.md).
46
+ - `priceType` on a nested price row is **never returned** by `GET /v2/bundles/{uuid}` (three levels
47
+ under an inherent child — dropped silently). Re-fetch `GET /v2/bundle-items/{uuid}` when you need
48
+ typed rows.
49
+
50
+ ### Saving — two calls, never a nested price in a PUT
51
+
52
+ The V2 engine writes a nested inherent child with `POST` hardcoded, so a price nested in a **PUT**
53
+ inserts a duplicate row (or fails 1062 if you send its uuid) —
54
+ [nested-relationship writes](../../api2/features/nested-relationship-writes.md).
55
+
56
+ 1. `PUT /v2/bundle-items/{uuid}` — **without** the nested price.
57
+ 2. `PUT /v2/bundle-item-prices/{uuid}` — the price row.
58
+
59
+ **Create still nests** (correct there — it is a POST).
60
+
61
+ `resolvePriceRowUuid` picks the row to update **strictly by `priceType`**. If the payload rows carry
62
+ no `priceType`, it re-fetches typed rows from `/bundle-items/{uuid}`; if the type still cannot be
63
+ established it **creates a new row** rather than overwriting an unknown one. Never fall back to
64
+ "the newest row" — during a unit-price edit that can overwrite the LEASE price.
65
+
66
+ ## Gotchas
67
+
68
+ - **⚠ An edited price that looks unchanged on screen is the duplicate-row bug.** `_unitPrice` reads
69
+ the newest row (`ORDER BY id DESC`) while the FE rendered `bundleItemPrices[0]` (the oldest) — the
70
+ insert was invisible in the UI for a long time.
71
+ - **⚠ Duplicate `BundleItemPrices` rows already exist in production data**, predating this front end
72
+ (one bundle item has 15 rows, ids 13→4355). The table has no `dtCreated`/`dtUpdated`, so age cannot
73
+ be proven. Local-copy counts: `Client_Compass` 5117 rows / 2605 duplicates / 971 groups (36 groups
74
+ with differing prices); `Client_CompassCanada` 333 / 75 / 63; all other tenants zero. **Prod counts
75
+ not yet taken.**
76
+ - **Dedupe migrations exist but have only been run locally** —
77
+ `dbchanges2/Client_Compass/2026-09-24a - DeduplicateBundleItemPrices.sql` and the
78
+ `Client_CompassCanada` twin keep `MAX(id)` per `(bundleItemId, priceTypeId)` — the row the
79
+ calculated fields already read, so nothing changes on screen. Safe to run: no foreign key anywhere
80
+ references `BundleItemPrices`, and `SalesOrderItems` snapshots its own price column, so no order or
81
+ quote is affected. `currencyId`/`planId` are NULL on every row.
82
+ - **⚠ FIELDS JSON edits appear to do nothing** — the persisted React Query cache freezes them; see
83
+ [persisted query cache](./persisted-query-cache.md).
84
+
85
+ ## Change history
86
+ - 2026-09-24 — Documented the two-call save (price edits were inserting duplicates via a nested PUT),
87
+ the strict-`priceType` row match with create-not-guess fallback, the `_unitPrice`/`_leasePrice`
88
+ columns needing `calcDepth: 2` as a plain query param, and the Compass/Compass Canada
89
+ `BundleItemPrices` dedupe migrations (TRUE-82138). (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-16
9
+ updated: 2026-09-24
10
10
  owners: ["apeterson"]
11
11
  files:
12
12
  - toga25-supply/src/App.tsx
@@ -145,7 +145,11 @@ Type-check **both** `-p tsconfig.app.json` and `-p tsconfig.node.json` — `vite
145
145
 
146
146
  - **⚠ A non-zero `staleTime` on a persisted query serves yesterday's data and never fetches.** Data hooks in this app use `staleTime: 0`. Hooks that currently break this rule **and are persisted**: `useClientFields`, `useCurrentUser`, `useStatusTypeValues`, `useStatusColors`, `useSalesOrderUuidForServiceRequest` (all `Infinity`), and the three VendorItem form hooks (`5 * 60_000`). Each can serve a previous session's answer.
147
147
  - `useTalosSession` and `useTalosThreads` also set a non-zero `staleTime`, but their keys start with `talos`, so they are **not** persisted — in-memory only, safe.
148
+ - **⚠ An edit to a client `FIELDS` JSON looks like it does nothing — and it is NOT Vite HMR.** `useClientFields` is `staleTime: Infinity` **and** persisted, so the old config is frozen in `supply-chain-query-cache`. Clear that key to see the edit. Proper fix: exclude `clientFields` in `shouldDehydrateQuery` (`src/App.tsx:42`).
148
149
  - **⚠ Never verify a config, surface, or ACL change on a warm browser.** Log out, or delete the `supply-chain-query-cache` key in devtools. Most "it didn't deploy" reports are a rehydrated cache.
149
150
  - **⚠ `gcTime: 0` does not stop persistence.** It only evicts a query once it becomes *inactive*; an active query is still dehydrated. To keep something off disk, exclude its namespace in `shouldDehydrateQuery`.
150
151
  - **⚠ Any new `queryClient.clear()` must `await awaitRestoration()` first**, or the restore race undoes it.
151
152
  - The `maxAge` is one timestamp for the entire blob — you cannot expire a single query through it.
153
+
154
+ ## Change history
155
+ - 2026-09-24 — Noted the frozen-`FIELDS`-JSON symptom (persisted `useClientFields` at `staleTime: Infinity`, mistaken for broken Vite HMR) and the `shouldDehydrateQuery` fix. (apeterson)
@@ -33,7 +33,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
33
33
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
34
34
  - **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
35
35
  - **toga2-commerce** (TOGa Commerce) — 26 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
36
- - **toga25-supply** (TOGa 2.5 Supply) — 21 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
36
+ - **toga25-supply** (TOGa 2.5 Supply) — 22 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
37
37
  - **toga-blox** (TOGa Blox) — 15 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
38
38
  - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
39
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.885",
3
+ "version": "1.0.887",
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",