toga-ai 1.0.591 → 1.0.593

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: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-04
9
+ updated: 2026-08-17
10
10
  owners: ["jcardinal", "mhammontree"]
11
11
  files:
12
12
  - library/app/error/capture.php
@@ -177,6 +177,15 @@ exists in `api2`/`worker2`. So 1.0 reads the AL1 container config at
177
177
 
178
178
  ## Gotchas / known issues
179
179
 
180
+ - **⚠ A grouped `Logs.Issue` row can show a STALE error id — read the live api2 transaction log for
181
+ the current error.** Two facts combine to mislead: (1) `Event.errorMessage` is **`varchar(255)`**,
182
+ so a long API response is **truncated** — often before the trailing error id; and (2)
183
+ `Logs.Issue.subject` / `Issue.errorMessage` hold the **first-occurrence** text, so a long-lived
184
+ issue keeps showing the error id from whenever it was first seen even after the per-call error has
185
+ changed. Observed 2026-08-17: issue `3H` still displayed `EO-1` / `3G-6` (the original
186
+ undefined-method fatal) while the current per-call failure was actually `EV-11` (an FK 1451 on a
187
+ DELETE). **To see what is failing *now*, read the live api2 transaction log
188
+ (`Logs_<client>.Api.responsePayload`) for the specific call — not the grouped `Issue` row.**
180
189
  - **Any PHP warning inside capture code would kill the request.** 1.0's
181
190
  `App_Error::handleError()` routes **every** PHP warning into `handleException()`, which renders
182
191
  the red box and calls `exit()` — so a single warning raised while capturing (an unreachable IMDS
@@ -220,6 +229,12 @@ exists in `api2`/`worker2`. So 1.0 reads the AL1 container config at
220
229
 
221
230
  ## Change history
222
231
 
232
+ - 2026-08-17 — Recorded a diagnostic trap: a grouped `Logs.Issue` row can show a **stale error id**
233
+ because `Event.errorMessage` is `varchar(255)` (truncates a long API response, often before the
234
+ error id) and `Issue.subject`/`errorMessage` keep the **first-occurrence** text — observed issue
235
+ `3H` still showing `EO-1`/`3G-6` while the live per-call error was `EV-11` (FK 1451). Read the live
236
+ `Logs_<client>.Api.responsePayload` for the actual current error. Surfaced diagnosing the Compass
237
+ USA NetSuite SO sync freeze. (jcardinal)
223
238
  - 2026-08-05 — Recorded the legacy `DELETE_DOA_PRE-SENTRY_error.php` handler leaking the full
224
239
  config array (prod DB / SES / NetSuite / Payeezy / SSL Store credentials + ticket PII) on a
225
240
  live towfoundation.togaview.com error page, meaning that host evaluates as dev mode in
@@ -6,7 +6,7 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-10
9
+ updated: 2026-08-17
10
10
  owners: [jcardinal, mhammontree, bala]
11
11
  files:
12
12
  - library/app/api/toga2.php
@@ -328,6 +328,16 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
328
328
  **and** the consuming code. (The general 2.0-side metadata behavior — reverse relations resolved via
329
329
  the child model's `FOREIGNKEY_MODEL` and named as the camelCase plural of the child model — is a
330
330
  framework-level fact captured separately.)
331
+ - **⚠ Depth alone does NOT return every reverse-hasMany collection — `GET /sales-orders` at depth 4
332
+ omits `itemFulfillmentItems` and `invoiceItems`.** Verified 2026-08-17 against prod
333
+ `Logs_Compass.Api` responses (no fields whitelist): a depth-4 sales-order GET returns
334
+ `salesOrderItems` and the **PO-link bridge** collections
335
+ (`salesOrderItemPurchaseOrderItems`, `purchaseOrderItemSalesOrderItems`) but **not**
336
+ `itemFulfillmentItems` or `invoiceItems`. So any importer code that guards on
337
+ `$salesOrderItem->itemFulfillmentItems` / `->invoiceItems` from that GET is a **silent no-op** —
338
+ same reverse-hasMany trap as above, now with a concrete consequence: the NetSuite SO renumber fix
339
+ keys off a recorded "could-not-delete" set instead of trusting those collections (see
340
+ [NetSuite → TOGa Supply per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md)).
331
341
  - **`TOGA_AIG.Customers.emailAddress` is `varchar(255)`** (V1 legacy, latin1_swedish_ci) — wide
332
342
  enough to hold several `'; '`-joined emails, so TRUE-79401 needed **no** schema/dbchanges migration
333
343
  (the earlier fear that it was ~VARCHAR(55) was wrong) — but **it is a real ceiling: ~6–7
@@ -386,6 +396,13 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
386
396
 
387
397
  ## Change history
388
398
 
399
+ - 2026-08-17 — Recorded that **depth-4 `GET /sales-orders` omits the `itemFulfillmentItems` and
400
+ `invoiceItems` reverse-hasMany collections** (it does return `salesOrderItems` and the PO-link
401
+ bridges), verified against prod `Logs_Compass.Api` — so code guarding on those collections from
402
+ that GET is a silent no-op, reinforcing the reverse-hasMany-fields trap. Surfaced fixing the
403
+ NetSuite SO renumber freeze (Compass USA), which now keys off a recorded "could-not-delete" set;
404
+ see [NetSuite → TOGa Supply per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md).
405
+ (jcardinal)
389
406
  - 2026-08-12 — Recorded the **`send()` return contract**: it always returns a response carrying
390
407
  `isSuccess` (early-returns on true, falls through to `return $response` on false, throws when the
391
408
  property is absent), so `empty($response->isSuccess)` is a safe failure check on anything it
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-12
9
+ updated: 2026-08-17
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -87,13 +87,28 @@ its IDLE write. **There is no timeout, memory cap, or resource limit anywhere in
87
87
  never *tune the window size down*. (Corollary: a window that has grown back to the cap is positive
88
88
  evidence the section is completing.)
89
89
 
90
- ### ⚠ A per-record API failure does NOT stop the cursor the record is skipped forever
91
-
92
- The cursor write (`LAST_SYNC_DATETIME_*`) and `finishModeIteration()` both run **unconditionally
93
- after** the record loop. A record that throws mid-loop is captured to Sentry and **stepped over**,
94
- while the section still reports success and advances the cursor past its window. So
95
- **"section IDLE + cursor advancing" is not proof of a complete import** it only proves the run
96
- didn't die.
90
+ ### ⚠ Per-record isolation is SECTION-SPECIFIC and SALES_ORDERS has NONE (corrected 2026-08-17)
91
+
92
+ Whether a bad record is "stepped over" or freezes the section depends entirely on whether **that
93
+ section's** record loop wraps the `sync*FromNetsuite()` call in a per-record `try/catch`. It is not
94
+ uniform across the six sections, and the earlier blanket claim here ("a per-record failure does not
95
+ stop the cursor the record is skipped forever") is **false for the SALES_ORDERS section**.
96
+
97
+ - **Where a section DOES isolate per record:** the cursor write (`LAST_SYNC_DATETIME_*`) and
98
+ `finishModeIteration()` run after the loop, so a caught record is skipped and the section still
99
+ reports IDLE and advances. There, **"section IDLE + cursor advancing" is not proof of a complete
100
+ import** — it only proves the run didn't die (recovery = rewind the cursor; re-import is
101
+ idempotent).
102
+ - **⚠ SALES_ORDERS has NO per-record `try/catch`.** The `SALES_ORDERS` foreach in
103
+ `common_sync_togasupply.php` (~L637-673) calls `App_Api_Toga2::syncSalesOrderFromNetsuite()`
104
+ **bare**. `send()` throws on any non-success by default, so **one poison order throws, propagates,
105
+ and aborts the whole section BEFORE** the cursor PUT + `finishModeIteration()`. The mode stays
106
+ `1-RUNNING`, the watermark freezes, the window shrinks ÷3 each run, and the **same** order is
107
+ re-tried and re-throws every 5 minutes — a permanent freeze, not a skip. So a stuck
108
+ `SALES_ORDERS` at `1-RUNNING` with a days-frozen watermark means **it is throwing on a specific
109
+ order** (go read the live api2 transaction log for that client — see the poison-order gotcha
110
+ below). The "shrinking window is a bug signal" rule still holds; the "record is stepped over"
111
+ corollary does **not** for this section.
97
112
 
98
113
  Proven concretely on Elite: sales order **169441** (NetSuite internal id 4624062, trandate
99
114
  2024-04-15, customer PO `BEGGS/04152024/…`) was simply absent from `Client_Elite.SalesOrders`
@@ -361,9 +376,46 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
361
376
  a new line is actually created. Full root cause, the prod cleanup migration, and the
362
377
  deploy-fix-before-cleanup sequencing live in the
363
378
  [Compass ODP Duplicate PO-Line Cleanup](../../../clients/compass-usa/workflows/odp-duplicate-po-line-cleanup.md).
379
+ - **⚠ A single un-deletable renumbered line could freeze the entire SALES_ORDERS section (fixed
380
+ 2026-08-17).** When NetSuite renumbers a line, `syncSalesOrderFromNetsuite` renumbers by
381
+ **DELETE `/sales-order-items/{uuid}` then re-create in the per-item loop**. Its "pinned row" guard
382
+ only skipped rows carrying `itemFulfillmentItems` / `salesOrderItemPurchaseOrderItems` /
383
+ `purchaseOrderItemSalesOrderItems`; an **INVOICED** line
384
+ (`InvoiceItems.salesOrderItemId`, `ON DELETE RESTRICT`) was **not** guarded, so the DELETE failed
385
+ with MySQL **1451** → api2 **EV-11** 400, and — because the SALES_ORDERS loop has no per-record
386
+ `try/catch` (see the corrected gotcha above) — the throw aborted the section, freezing Compass USA
387
+ for ~5 days (`NETSUITE_EXECUTION_MODE_SALES_ORDERS='1-RUNNING'`, watermark stuck at
388
+ `2026-08-12 06:13:26`) while it re-tried the same poison order every run (SO **7447** /
389
+ `SalesOrderItem` 47828, invoiced by `InvoiceItem` 25515). **Fix (library-only, PHP 7.2-safe, no
390
+ signature changes)** in `syncSalesOrderFromNetsuite`: (1) the renumber-DELETE now passes
391
+ `$throwExceptionOnApiError=false`, checks `empty($resp->isSuccess)`, and records any row it cannot
392
+ delete in a new `$pinnedRenumberLineUniqueKeys` set (keyed by `c_netsuiteLineUniqueKey`); (2) the
393
+ per-item PUT loop treats a row in that set as **pinned** — it keeps its current `lineNumber` (no
394
+ `UNIQUE(salesOrderId,lineNumber)` collision) and its PUT is made non-throwing + `error_log` **for
395
+ pinned rows only** (normal rows keep throw-on-error so real failures still surface); (3) the
396
+ trailing "remove dropped items" DELETE now skips `invoiceItems` and checks `isSuccess`. Net: **no
397
+ single order can freeze the section again regardless of which `RESTRICT` child blocks a delete**;
398
+ the invoiced line simply stays at its existing `lineNumber` (a documented unresolvable
399
+ NetSuite-vs-invoiced case). Deploy `library` only — no SQL, no other repos — then the section
400
+ completes, mode flips IDLE, and the backlog backfills idempotently. Note the collection guard added
401
+ for `invoiceItems` is **best-effort only**: depth-4 `GET /sales-orders` does **not** return the
402
+ `itemFulfillmentItems` or `invoiceItems` reverse-hasMany collections (only the PO-link bridges), so
403
+ the fix keys off the recorded "could-not-delete" set rather than trusting those collections — see
404
+ [App_Api_Toga2 client & bridge](../../library/features/toga2-api-client-and-bridge.md).
364
405
 
365
406
  ## Change history
366
407
 
408
+ - 2026-08-17 — Fixed a Compass USA ~5-day SALES_ORDERS freeze: an **invoiced** line that NetSuite
409
+ renumbered could not be DELETEd (FK `InvoiceItems.salesOrderItemId` `RESTRICT` → MySQL 1451 →
410
+ api2 EV-11), and because the SALES_ORDERS loop has **no per-record `try/catch`** the throw aborted
411
+ the whole section (mode stuck `1-RUNNING`, watermark frozen, poison order SO 7447 re-tried every
412
+ run). `syncSalesOrderFromNetsuite` now makes the renumber-DELETE non-throwing, records
413
+ un-deletable rows in a `$pinnedRenumberLineUniqueKeys` set, and keeps a pinned row at its existing
414
+ `lineNumber` with a non-throwing PUT (normal rows still throw on error); the trailing dropped-item
415
+ DELETE now skips `invoiceItems` and checks `isSuccess`. **Corrected** the prior gotcha that claimed
416
+ a per-record failure never stops the cursor — that is **section-specific and false for
417
+ SALES_ORDERS**, which has no per-record isolation. Library-only deploy; no SQL/other repos.
418
+ Diagnosis touched worker/api2/_underscore (read-only). (jcardinal)
367
419
  - 2026-08-12 — TRUE-80499 (Elite onboarding, shared-engine work). **Fixed** the sparse-client
368
420
  `TypeError`: the two per-client item lookups are now initialised at
369
421
  `common_sync_togasupply.php:74-75` instead of only inside the items loop, so a client whose items
@@ -18,7 +18,7 @@
18
18
  | [TOGa IQ Sprint Dashboard API (Record Scripts)](features/sprint-dashboard-api.md) | The internal **TOGa IQ sprint dashboard** is served in production by **six api2 Record Scripts** on `_Model_Team_Sprint` (`_underscore/Model/Team/Sprint.php`), | _underscore/Model/Team/Sprint.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-07-24a - SprintDashboardRecordScripts.sql, dbchanges2/Client_True/2026-07-24b - SprintDashboardScriptAcl.sql |
19
19
  | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
20
20
  | [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
21
- | [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql |
21
+ | [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql |
22
22
  | [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
23
23
  | [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, api2/Component/Api/V2/Response/Oauth/Oauth.php, api2/Controller/Index.php, toga2-supply/src/globalTypes.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/api/toga.ts, _underscore/Model/Client/TrackingNumber.php |
24
24
  | [V2 request deadlock-retry — route-scoped in-process replay](features/v2-deadlock-retry.md) | api2's front controller can **detect a MySQL deadlock (1213) / lock-wait timeout (1205) and replay the whole request in-process**, so a transient lock collision | api2/Controller/Index.php, _underscore/Database.php, _underscore/Query.php |
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-14
10
- owners: ["mhammontree", "dfranks", "bala", "snaredla"]
9
+ updated: 2026-08-17
10
+ owners: ["mhammontree", "dfranks", "bala", "snaredla", "jcardinal"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - api2/Controller/Index.php
@@ -448,6 +448,13 @@ and the failing environment**. It is a small table, and the drift is usually exa
448
448
 
449
449
  ## Change history
450
450
 
451
+ - 2026-08-17 — Corroborated that the unguarded-dispatch fatal is **systemic, not client-specific**:
452
+ the prod "undefined method" issue bucket (`Logs.Issue` ref 3G / id 59) grouped **both**
453
+ `_Model_Compass_Usa_SalesOrderItem::postPost()` **and** `_Model_Elite_SalesOrder::postPut()` — two
454
+ different tenants, one shared fragility. (This was the original Compass USA failure on 2026-08-12,
455
+ before it was mitigated on the 2.0 side and the client's NetSuite sync progressed to an unrelated
456
+ FK failure downstream.) Reinforces the standing fix direction: guard all four `V2.php` call sites
457
+ with `method_exists()`. Investigation only, no code change. (jcardinal)
451
458
  - 2026-08-14 — TRUE-80489, from building the Compass inactive-standalone-item order guard.
452
459
  (1) **A PRE interceptor on a PARENT record fires on the nested child POST, with the CHILD as the
453
460
  payload.** `POST /sales-orders/{uuid}/sales-order-items` resolves `SalesOrders` as the
@@ -12,6 +12,8 @@ files:
12
12
  - _underscore/Model/Client/TableView.php
13
13
  - api2/Component/Api/V2/V2.php
14
14
  - dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql
15
+ - dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql
16
+ - dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql
15
17
  related:
16
18
  - tableview-apiwhereclause-row-filtering.md
17
19
  - ../../toga25-supply/features/meta-driven-table-data.md
@@ -33,7 +35,20 @@ it.
33
35
  - **`Client_*.TableViews`** — one row per view (per client DB, e.g. `Client_Nychh`).
34
36
  - **`Client_*.TableViewFields`** — one row per **column** of a view. Key columns: `recordFieldId`
35
37
  (which field this column shows), `tableViewJoinId` (which join in the view the field is reached
36
- through), `slug` (the frontend/tanstack column id), `isVisible` (0 = projected-but-hidden).
38
+ through), `slug` (the frontend/tanstack column id), `isVisible` (0 = projected-but-hidden),
39
+ and the per-view **presentation flags** `isCopyable` / `isSortable` / `isFilterable`. These
40
+ four flags **are** per-view — the same underlying field can be copyable in one view and not in
41
+ another.
42
+
43
+ ### A column's `type` / `precision` are NOT per-view — they come from `Core.RecordFields`
44
+
45
+ A field's rendered **type and precision are canonical**, read from the shared
46
+ `Core.RecordFields.type` / `Core.RecordFields.precision` — there is **no per-view type/precision
47
+ override** on `TableViewFields`. Changing `Core.RecordFields.type`/`precision` therefore changes
48
+ the column's type/format **everywhere that field appears, for every client**. That is the
49
+ intended canonical behavior — if you only want the change in one view, this mechanism cannot do
50
+ it. (Reference: `Core.RecordFields` id 369 = `ItemFulfillmentItems.quantity` set to
51
+ `type=NUMBER`, `precision=0`, which normalizes that column across all fulfillment views/clients.)
37
52
  - **`Client_*.TableViewJoins`** — the view's joins; also drive `options.join`/`ojoin` on the data
38
53
  request (see [tableview-apiwhereclause-row-filtering](tableview-apiwhereclause-row-filtering.md)).
39
54
 
@@ -87,9 +102,17 @@ per-client folder — the reference migration
87
102
  - **Make column migrations idempotent** (`NOT EXISTS` guard) — they run per client DB.
88
103
  - **`isVisible=0` still costs a fetch** — the field is selected into the projection; only the render
89
104
  is suppressed.
105
+ - **`type`/`precision` are canonical (Core-wide), not per-view.** Editing `Core.RecordFields`
106
+ changes every view/client that shows the field; there is no per-view type override. `isCopyable`,
107
+ `isSortable`, `isFilterable`, `isVisible` are the per-view knobs (on `TableViewFields`).
90
108
 
91
109
  ## Change history
92
110
 
111
+ - 2026-08-17 — Documented that a column's `type`/`precision` are canonical from `Core.RecordFields`
112
+ (no per-view override — a change hits every view/client) while `isCopyable`/`isSortable`/
113
+ `isFilterable`/`isVisible` are per-view `TableViewFields` flags. Confirmed setting
114
+ `Core.RecordFields` id 369 (`ItemFulfillmentItems.quantity`) to `type=NUMBER precision=0` and
115
+ toggling `isCopyable` per-view on the shared item-fulfillment table views. (apeterson)
93
116
  - 2026-07-20 — Documented the backend TableView column-metadata model (per-client
94
117
  `TableViews`/`TableViewFields`/`TableViewJoins`, `recordFieldId` → shared `Core.RecordFields`
95
118
  cross-DB ref, `isVisible=0` projected-but-hidden columns) and the dbchanges2 recipe for adding a
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-23
9
+ updated: 2026-08-17
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx
@@ -80,7 +80,13 @@ Sticky columns use a `--sticky-offset` CSS variable summed from neighboring colu
80
80
  - Frozen widths measure only **mounted** rows; virtualized tables size from the first paint set.
81
81
  - `headerSpan` adds a separate top row (`colSpan`); only the leaf header row maps to colgroup.
82
82
  - Resize is non-persistent (local state, resets when columns change).
83
+ - **A visible column can look "missing" under `shrinkColumns`.** `shrinkColumns` (modal tables)
84
+ freezes each column to its measured header+body content width, so a column whose content is
85
+ minimal collapses to a sliver and appears absent even though it built. If a column seems gone,
86
+ confirm `isVisible=1` first (that is the only real gate), then look at its shrink/freeze width —
87
+ not the column set.
83
88
 
84
89
  ## Change history
90
+ - 2026-08-17 — Noted that under `shrinkColumns` a genuinely visible column can render at a sliver width and look "missing" — check `isVisible` and the measured/frozen width, not the column set (apeterson).
85
91
  - 2026-06-23 — Documented the three Primary Table entry points, measure-and-freeze sizing, virtualization, and sticky columns (apeterson).
86
92
  - 2026-06-22 — Added `shrinkColumns` / `minColumnWidth` shrink-to-fit for modal tables; freeze logic uses natural content width (apeterson).
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-23
9
+ updated: 2026-08-17
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga-blox/src/components/Table/index.ts
@@ -134,8 +134,31 @@ Per-column header controls, each a popover trigger with `data-active` / `data-op
134
134
  - The filter `includes` mode treats comma-separated values as OR.
135
135
  - `EditableCell` currency formats **on blur**, phone formats **on keystroke**.
136
136
  - Cell type config (`columnTypeConfig` prop) overrides the hardcoded `COLUMN_TYPE_CONFIG`.
137
+ - **Field `type`/`precision` come from the backend `Core.RecordFields`, not from the table-view
138
+ field.** `resolveCellType` keys off `tableViewField.type` but that value is populated upstream
139
+ from the shared Core RecordField — there is no per-view type override. Changing a column's type
140
+ or precision is a `Core.RecordFields` DB change (see
141
+ [api2 tableview-field-metadata](../../api2/features/tableview-field-metadata.md)), not a blox
142
+ change. `isCopyable`/`isVisible`/`isSortable`/`isFilterable` *are* per-view flags.
143
+ - **The only column-visibility gate is `field.isVisible`.** `buildTanstackColumns` builds a column
144
+ for every visible field. The `accessorFn` walks the `tableViewField` dot-path
145
+ (`SalesOrderItems.lineNumber` → `row.SalesOrderItems.lineNumber`), so a joined/context field is
146
+ blank unless `tableViewMeta.joins` actually provides the join. `settings.contextTable`/
147
+ `contextField` are used **only** to resolve the column *label* (`useAssignTableFieldLabels`),
148
+ never for data fetching. A visible column can still *look* missing when `shrinkColumns` freezes
149
+ it to a tiny width because its measured header+body content is minimal (see
150
+ [primary-table-templates](primary-table-templates.md)).
151
+ - **Table/surface meta is cached with `Infinity` staleTime/gcTime** (`useFetchTablePageMeta`), so
152
+ after a DB metadata change (`TableViewFields`, `Core.RecordFields`) a **hard reload** is required
153
+ to see it. And the app consumes toga-blox's built **`dist/`**, not `src/` — editing blox source
154
+ requires `npm run build` before it takes effect at runtime.
137
155
 
138
156
  ## Change history
157
+ - 2026-08-17 — Added gotchas on meta→column mapping: field `type`/`precision` come from
158
+ `Core.RecordFields` (not per-view) while `isCopyable`/`isVisible`/`isSortable`/`isFilterable` are
159
+ per-view; `isVisible` is the only column gate; `accessorFn` dot-walks and needs the join;
160
+ `contextTable`/`contextField` drive labels only; meta is cached `Infinity` (hard reload after DB
161
+ meta change) and the app runs `dist/` (rebuild required). (apeterson)
139
162
  - 2026-06-23 — Documented the Table component layers, cell types, filters/sorts, hooks, and theming (apeterson).
140
163
  - 2026-06-23 — `HeaderFilterSearch`: search icon hidden on input focus; `data-active` added to the icon span; clear button no longer widens the input (fixed-width menu); mode dropdown excludes the selected mode (apeterson).
141
164
  - 2026-06-22 — Column sizing reworked to measure-and-freeze using natural content width; `MIN_COLUMN_WIDTH` floor (120px) applies only when no explicit width is set (apeterson).
@@ -9,7 +9,7 @@
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/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
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/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
- | [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/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/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 |
12
+ | [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/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/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 |
13
13
  | [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/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/helpers/surfaceBundleToTenantFields.ts, 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/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 |
14
14
  | [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 |
15
15
  | [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
@@ -6,9 +6,11 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-14
9
+ updated: 2026-08-17
10
10
  owners: [apeterson]
11
11
  files:
12
+ - toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx
13
+ - toga25-supply/src/layout/PrimaryTableServerLayout/types.ts
12
14
  - toga25-supply/src/layout/ItemRecordModalLayout/
13
15
  - toga25-supply/src/layout/SalesOrderRecordModalLayout/
14
16
  - toga25-supply/src/layout/SalesOrderItemsTableLayout/
@@ -92,6 +94,13 @@ caches the unfiltered first render (uuid null) and never refetches. Guard with `
92
94
  when uuid is null, `additionalData` returns `{}`. Typically `isPaginationEnabled: false` +
93
95
  `isInfiniteScrollingEnabled: true` (but see [meta-driven-table-data](meta-driven-table-data.md)).
94
96
 
97
+ **Read-only (non-clickable) modal tables — `disableRowClick`.** `PrimaryTableServerLayout` takes
98
+ an optional `disableRowClick?: boolean` (its `types.ts`); when set it passes `onRowClick={undefined}`
99
+ to `PrimaryTableServerTemplate` so rows render non-interactive (no cursor/hover affordance, no
100
+ navigation). `ItemFulfillmentModal` sets it because the fulfillment table is read-only. It is
101
+ scoped so all other `PrimaryTableServerLayout` consumers keep normal row-click behavior — pass it
102
+ only for genuinely read-only tables.
103
+
95
104
  ## Pattern 5 — `GenericNestedTables` (config-driven multi-level / swap tables)
96
105
 
97
106
  A config-driven renderer: declare an array of `TableLevel` objects instead of hand-wiring each
@@ -204,6 +213,9 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
204
213
  calling `apiDelete(route)`.
205
214
 
206
215
  ## Change history
216
+ - 2026-08-17 — Added `disableRowClick?: boolean` to `PrimaryTableServerLayout` (passes
217
+ `onRowClick={undefined}` to the template) for read-only modal tables; `ItemFulfillmentModal` uses
218
+ it. Scoped so other consumers keep normal row-click behavior. (apeterson)
207
219
  - 2026-08-14 — Fixed unassigning an approval-workflow stage: clearing the assignee now emits a
208
220
  DELETE `/approval-decisions/{uuid}` (the assignment lives on the `ApprovalDecisions` record)
209
221
  instead of silently dropping the field. Gated to fire only for a still-undecided stage
@@ -6,6 +6,7 @@ apps:
6
6
  - api2
7
7
  - toga2-supply
8
8
  - toga25-supply
9
+ - toga-blox
9
10
  - toga2-commerce
10
11
  - worker
11
12
  - worker2
@@ -15,7 +16,7 @@ project: _Underscore
15
16
  client: compass-canada
16
17
  type: profile
17
18
  status: active
18
- updated: 2026-08-12
19
+ updated: 2026-08-17
19
20
  owners: [jcardinal, bala, tcox, apeterson]
20
21
  files: []
21
22
  related:
@@ -6,6 +6,7 @@ apps:
6
6
  - api2
7
7
  - toga2-supply
8
8
  - toga25-supply
9
+ - toga-blox
9
10
  - toga2-commerce
10
11
  - worker
11
12
  - worker1.5
@@ -17,7 +18,7 @@ project: _Underscore
17
18
  client: compass-usa
18
19
  type: profile
19
20
  status: active
20
- updated: 2026-08-13
21
+ updated: 2026-08-17
21
22
  owners: [jcardinal, bala, tcox, apeterson, dfranks]
22
23
  files: []
23
24
  related:
@@ -6,6 +6,7 @@ apps:
6
6
  - api2
7
7
  - toga2-supply
8
8
  - toga25-supply
9
+ - toga-blox
9
10
  - dbchanges2
10
11
  - worker2
11
12
  - worker
@@ -14,7 +15,7 @@ project: _Underscore
14
15
  client: nychh
15
16
  type: profile
16
17
  status: active
17
- updated: 2026-08-05
18
+ updated: 2026-08-17
18
19
  owners: ["jcardinal", "apeterson", "bala"]
19
20
  files: []
20
21
  related:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.591",
3
+ "version": "1.0.593",
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",