toga-ai 1.0.692 → 1.0.693

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.
@@ -235,10 +235,41 @@ Applied for Quad: the sales-orders listing now defaults to `dateOrder DESC` (mos
235
235
  first) instead of `number DESC` (`dbchanges2/Client_Quad/2026-08-18c -
236
236
  SalesOrderListingSortByDateOrderDesc.sql`).
237
237
 
238
+ ### 🚨 An ACL gap on the DEFAULT-SORT column surfaces as a 500 SQL error, not an authorization message
239
+
240
+ Found 2026-08-28 on the NYCHH `transfer-orders` view. The two mechanisms are individually sane and
241
+ compose into a MySQL 1054:
242
+
243
+ 1. **`AclFieldPermissions` filters the SELECT column list, and an ungranted field is dropped
244
+ SILENTLY** — no message, no `EZ-2`. (That is the same silent-drop behaviour as a missing
245
+ `AclRecordPermissions` grant deleting a *join*.)
246
+ 2. **`_Model_Client_TableView::meta()` still emits `table.sort` naming that field's slug.** The sort
247
+ block (`TableView.php:343`) sits **OUTSIDE** the permitted-field branch, so it does not know the
248
+ column was just filtered away.
249
+ 3. blox's `getDataTableData` then misses on `lookupTableViewFieldsBySlug`, **`console.warn`s — and
250
+ pushes `"-" + undefined` anyway**, producing `ORDER BY TransferOrders.undefined DESC` →
251
+ **MySQL 1054**, surfaced as a 500.
252
+
253
+ So the visible failure is a broken SQL query while the actual defect is one missing field grant, on
254
+ one column, in one tenant. **Any tenant missing a field grant on its view's default-sort column hits
255
+ this** — it is not transfer-order-specific.
256
+
257
+ Two candidate fixes, neither applied yet (worth a ticket):
258
+
259
+ - **Backend:** emit `meta.table.sort` only for fields that survived the ACL filter.
260
+ - **Frontend (blox):** skip a sort whose slug does not resolve — it *already* warns; it just pushes
261
+ the bad value regardless.
262
+
263
+ Until one lands, treat "table 500s with `ORDER BY <table>.undefined`" as a **field-ACL diagnosis**,
264
+ not a SQL-authoring one.
265
+
238
266
  ## Gotchas / known issues
239
267
 
240
268
  - **Default-sort updates must JOIN `TableViewFields`, not subquery `TableViews`** — a subquery
241
269
  on the same view triggers MySQL error 1093. See *Default sort* above.
270
+ - **`ORDER BY <table>.undefined` (MySQL 1054) means a missing FIELD GRANT, not a bad migration.**
271
+ The default-sort column was dropped from the projection by `AclFieldPermissions` while
272
+ `meta.table.sort` kept naming it. See *An ACL gap on the default-sort column* above.
242
273
  - **`recordFieldId` is cross-DB** — it indexes `Core.RecordFields`, not anything in the client DB.
243
274
  Resolve the id in `Core`, but insert the `TableViewFields` row in the client DB.
244
275
  - **Don't assume `Core.RecordFields`/`Core.Records` ids are stable across environments** — look them
@@ -255,6 +286,14 @@ SalesOrderListingSortByDateOrderDesc.sql`).
255
286
  *and* that query — and a grep for the property alone under-reports what a missing column breaks.
256
287
 
257
288
  ## Change history
289
+ - 2026-08-28 — Recorded a **platform bug worth a ticket**: a missing `AclFieldPermissions` grant on a
290
+ view's **default-sort** column produces a 500 (MySQL 1054), not an authorization message. The ACL
291
+ filter drops the column from the SELECT silently, `TableView::meta()`'s sort block
292
+ (`TableView.php:343`) sits outside the permitted-field branch and still emits `table.sort` for it,
293
+ and blox's `getDataTableData` warns but pushes `"-" + undefined` anyway →
294
+ `ORDER BY <table>.undefined`. Two candidate fixes noted (emit the sort only for surviving fields;
295
+ or have blox skip an unresolvable sort slug). Found on the NYCHH `transfer-orders` view.
296
+ (apeterson)
258
297
  - 2026-08-28 — Pinned **where `isGroupable` actually exists**: present on local and sandbox-client
259
298
  (both hand-applied), **absent on production and beta**. So a `TableViewFields` INSERT naming the
260
299
  column passes locally and fails on prod — omit it from new TableView migrations until the fan-out
@@ -3,8 +3,8 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
- | [Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql, dbchanges2/Client_Compass/2026-08-28 - GrantViewOrdersRoleSalesOrdersNavigation.sql, dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql |
7
- | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
6
+ | [Re-runnable additive migrations (uuid4 in SQL, INSERT guards, the DISTINCT trap, conditional ALTERs)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql, dbchanges2/Client_Compass/2026-08-28 - GrantViewOrdersRoleSalesOrdersNavigation.sql, dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql |
7
+ | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
8
8
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
9
9
  | [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
10
10
  | [Framework branch running AHEAD of schema — environment-wide 1054/EO-1 after a deploy](workflows/framework-branch-ahead-of-schema.md) | A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database went backwards — **the code went forwards**. | _underscore/Model/Core/RecordField.php, _underscore/Model/Client/TableViewField.php, _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/Urgency.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql, dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql |
@@ -1,5 +1,5 @@
1
1
  ---
2
- title: Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)
2
+ title: Re-runnable additive migrations (uuid4 in SQL, INSERT guards, the DISTINCT trap, conditional ALTERs)
3
3
  framework: "2.0"
4
4
  repo: dbchanges2
5
5
  project: Database Changes
@@ -163,6 +163,43 @@ open-orders cron that reads the dimension it fills runs at 3:00 AM.
163
163
  **Test a registration file twice against a throwaway local copy of the table**: the first run must
164
164
  insert exactly one row, the second must insert zero and raise no error. That is the whole contract.
165
165
 
166
+ ### Re-runnable ALTERs: MySQL 8 has no `ADD COLUMN IF NOT EXISTS`
167
+
168
+ A `Client/` file runs against **every tenant**, and tenants disagree about which columns they
169
+ already have — so a bare `ALTER TABLE … ADD COLUMN` breaks on the tenants that already have it and
170
+ aborts the rest of the file. MySQL 8 offers no `IF NOT EXISTS` for `ADD COLUMN` (MariaDB does; do
171
+ not copy MariaDB SQL here). Gate each ALTER on `information_schema` and run it through
172
+ `PREPARE`/`EXECUTE`, so it compiles to a harmless `DO 0` where the column already exists:
173
+
174
+ ```sql
175
+ SET @sql = (
176
+ SELECT IF(
177
+ COUNT(*) = 0,
178
+ 'ALTER TABLE TransferOrderItems ADD COLUMN dtCreated DATETIME NULL',
179
+ 'DO 0'
180
+ )
181
+ FROM information_schema.COLUMNS
182
+ WHERE
183
+ TABLE_SCHEMA = DATABASE() AND
184
+ TABLE_NAME = 'TransferOrderItems' AND
185
+ COLUMN_NAME = 'dtCreated'
186
+ );
187
+ PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
188
+ ```
189
+
190
+ - **`TABLE_SCHEMA = DATABASE()`** keeps it cluster-safe — the file never names a database, so the
191
+ same statement is correct in every `Client_*` schema.
192
+ - Guard **indexes** the same way against `information_schema.STATISTICS` (and remember a composite
193
+ unique key is only recorded against its first column — see
194
+ [tableview-field-metadata](../../api2/features/tableview-field-metadata.md)).
195
+ - The `PREPARE` shape is what makes the guard work at all: **DDL cannot sit inside an `IF`** in a
196
+ plain script, so the condition has to select the *statement text*, not branch around it.
197
+
198
+ Worked example: `Client/2026-08-28b - TransferOrderItemsTimestamps.sql` (adds
199
+ `TransferOrderItems.dtCreated`/`dtUpdated` + indexes, and a guarded `qtyCommitted`). **The existing
200
+ `Client/2026-08-18` `qtyCommitted` file uses a bare `ALTER` and is exactly the shape this avoids** —
201
+ it is not the template to copy.
202
+
166
203
  ### `LAST_INSERT_ID()` into a session variable ties the whole file to one session
167
204
 
168
205
  The common shape — `INSERT` a parent, `SET @parentId = LAST_INSERT_ID();`, then insert children
@@ -201,6 +238,14 @@ concluding a migration is production-safe.
201
238
  [dbchanges2 architecture](../architecture.md).
202
239
 
203
240
  ## Change history
241
+ - 2026-08-28 — Added **conditional (re-runnable) ALTERs**: MySQL 8 has no
242
+ `ADD COLUMN IF NOT EXISTS`, so a `Client/` all-tenant file must gate each `ALTER` on
243
+ `information_schema` (with `TABLE_SCHEMA = DATABASE()` for cluster safety) and run it through
244
+ `PREPARE`/`EXECUTE`, compiling to `DO 0` where the column already exists — DDL cannot sit inside an
245
+ `IF`, which is why the condition selects the statement *text*. From
246
+ `Client/2026-08-28b - TransferOrderItemsTimestamps.sql`; the existing `Client/2026-08-18`
247
+ `qtyCommitted` file's bare `ALTER` is the anti-pattern it replaces. Retitled the doc, since it now
248
+ covers re-runnable migrations generally rather than INSERTs only. (apeterson)
204
249
  - 2026-08-28 — Added the **local dry-run pattern for destructive/rebuild migrations**: replay the
205
250
  file against a local copy inside `START TRANSACTION; … ROLLBACK;`, then prove idempotence by
206
251
  concatenating the file to itself and running both copies in one transaction. Used while writing
@@ -14,6 +14,7 @@ files:
14
14
  - dbchanges2/Client_Compass/
15
15
  - dbchanges2/Client_CompassCanada/
16
16
  - dbchanges2/Client_Quad/
17
+ - dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql
17
18
  - dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql
18
19
  - dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql
19
20
  - dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql
@@ -181,6 +182,14 @@ it `isVisible: true`. Both `Core/2026-06-29a` (Item) and `Core/2026-06-29d` (Ven
181
182
  off." A client opts a role in via a `SurfaceOverride` (`IS_VISIBLE=1`), scoped by role/persona. This is
182
183
  the seed-authoring corollary of the Core neutral-default re-baseline recorded in the resolver doc.
183
184
 
185
+ **Neutral does NOT automatically mean OFF — test (b), not the habit (2026-08-28).** The rule is
186
+ "universal **and** safe-if-silently-inherited", so a control that passes both seeds **ON**.
187
+ `Core/2026-08-28b` seeds the three `transfer-orders-list-actions` buttons (Refresh / Columns /
188
+ Transfer Order) **`isVisible = 1`**: a list screen with no refresh and no create is not a useful
189
+ neutral default, none of the three is an unsafe affordance to inherit, and only NYCHH can reach the
190
+ page at all. Contrast the record-modal **Edit** button, which fails (b) outright. State which of the
191
+ two tests you applied in the file header.
192
+
184
193
  ## Sales-order button overrides — element identities & the id-agnostic authoring house style
185
194
 
186
195
  When a client opts its roles into the sales-order action/filter buttons (the resolved per-client
@@ -589,7 +598,11 @@ See [sales-order-status-filter-surface](../../_underscore/features/sales-order-s
589
598
 
590
599
  Blocks assigned by `Core/2026-08-28a - TransferOrderSurfaceSeed.sql`: **`Core.Messages` 227–250**,
591
600
  **`Core.Surfaces` 47–51**, **`Core.SurfaceElements` 170–186** (the `transfer-orders` nav element +
592
- the LIST surface + 4 record-modal SECTION surfaces + the `transfer-order-stage` vocabulary). See
601
+ the LIST surface + 4 record-modal SECTION surfaces + the `transfer-order-stage` vocabulary), then
602
+ **extended the same day** by `Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql` to
603
+ **`Messages` 227–260**, **`Surfaces` 47–52**, **`SurfaceElements` 170–189** (the
604
+ `transfer-orders-list-actions` BUTTON_BAR + the shared columns-modal messages). Every id was
605
+ verified free on **local AND production** before authoring. See
593
606
  [Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
594
607
 
595
608
  **⚠ The reserved blocks are not an inventory of every surface id — pre-2026-08-21 surfaces sit
@@ -905,6 +918,41 @@ next reserved block, and a second confirmation of the "verify the block is free
905
918
  environment, not just yours" rule from the 2026-08-24 incident — sandbox-client's lower ceiling would
906
919
  have made 45–46 look free.
907
920
 
921
+ ### SHARED (`common.*`) message keys — guard on `messageKey`, not on `id`
922
+
923
+ Copy that belongs to a **generic component** rather than to one record type is seeded under a
924
+ **shared `common.*` key**, alongside the existing `common.yes` / `common.no` / `common.active` /
925
+ `common.inactive`. `Core/2026-08-28b` moved the seven column-visibility modal strings to
926
+ **`common.columns.modal.*`** for exactly this reason: a tenant then translates them **once for every
927
+ list screen** instead of once per record type. (The older per-record
928
+ `items.columns.modal.*` / `salesOrder.columns.modal.*` keys are left alone — folding those three
929
+ shipped screens onto the common keys is a clean follow-up.)
930
+
931
+ **The guard changes with the key's ownership.** A record-scoped seed can be guarded on its literal
932
+ reserved `id`; a **shared** key cannot, because another seed may legitimately create it first, at a
933
+ different id. So guard a `common.*` insert with `NOT EXISTS (… WHERE messageKey = '…')` — the
934
+ *semantic* key — and accept that the row may end up outside your reserved block.
935
+
936
+ ### A partial-migration cleanup: fix the ENVIRONMENT, not the migration file
937
+
938
+ When a developer has already run an **earlier revision** of a seed, re-running the corrected file
939
+ hits `Duplicate entry '251' for key 'messages.PRIMARY'`. The resolution that keeps the repo honest:
940
+
941
+ - **Leave the migration file UNCHANGED.** It is correct for a fresh environment, and prod/sandbox
942
+ never ran it. Editing it to dodge one developer's local state corrupts the record of intent (see
943
+ the 2.0 standard: *migrations record intent, not deployed state*).
944
+ - **Hand over a scratchpad-only undo** — never a committed file, because the executor runs every
945
+ folder against production.
946
+ - **Delete BY UUID, never by id range.** Per the 2026-08-24 incident, an id-range cleanup nearly
947
+ took the legitimate deny-action toast rows with it.
948
+ - **Delete in FK order: `SurfaceElements` → `Surfaces` → `Messages`.** Both
949
+ `SurfaceElements.labelMessageId` and `SurfaceElements.surfaceId` are `RESTRICT`.
950
+
951
+ > ⚠ **`SurfaceOverrides.surfaceElementId` is a SOFT FK — no constraint.** So an override written
952
+ > against a deleted element **survives the delete** and silently re-attaches to whatever row takes
953
+ > that id next. This is the same failure shape as the NYCHH inventory-topology landmine above:
954
+ > sweep the overrides yourself when you delete elements; the database will not.
955
+
908
956
  ## A new-vertical Core seed: two authoring rules confirmed on the transfer-order build (2026-08-28)
909
957
 
910
958
  - **A TABLE surface names its view by SLUG, never by `tableViewId`.** `Core/2026-08-28a` seeds
@@ -922,8 +970,28 @@ have made 45–46 look free.
922
970
  the same distinction). ⚠ Any `Client/` file writing to `ThemeTokens` inherits a dependency on
923
971
  `Client/2026-06-25a - SurfaceClientTables.sql` having run in that schema — audit the tenant list
924
972
  before a prod run.
973
+ - **A BUTTON_BAR element needs an `Actions` row only when it needs a REGISTRY HANDLER.** The three
974
+ `transfer-orders-list-actions` elements seed with **no `Actions` rows at all**: with nothing
975
+ registered in the FE `actionRegistry`, `SurfaceActionBar` falls through to
976
+ `onDispatch(config.valueKey)`, which is precisely the host-owned behaviour Refresh / Columns /
977
+ Create want. Seed an `Action` when the *behaviour* lives in the registry; seed a bare
978
+ `config.valueKey` when it lives in the page.
925
979
 
926
980
  ## Change history
981
+ - 2026-08-28 — **Extended the transfer-order reserved blocks** to `Messages` **227–260**, `Surfaces`
982
+ **47–52**, `SurfaceElements` **170–189** (`Core/2026-08-28b`, the `transfer-orders-list-actions`
983
+ BUTTON_BAR; all ids verified free on local **and** production first). Three authoring rules added:
984
+ a **neutral default can be ON** — apply the safe-if-inherited test rather than the seed-OFF habit
985
+ (the three list-action buttons seed `isVisible=1`; a record-modal Edit button still seeds off);
986
+ a BUTTON_BAR element needs an **`Actions` row only when it needs a registry handler**, otherwise a
987
+ bare `config.valueKey` falls through to `onDispatch`; and **shared `common.*` Messages must be
988
+ guarded on `messageKey`, not id**, since another seed may create the key first at a different id
989
+ (the columns-modal copy moved to `common.columns.modal.*` so a tenant translates it once for every
990
+ list screen). Also recorded the **partial-migration cleanup** procedure — leave the migration file
991
+ unchanged, hand over a scratchpad-only undo, delete **by uuid** in FK order
992
+ (`SurfaceElements` → `Surfaces` → `Messages`, both element FKs are RESTRICT) — and that
993
+ **`SurfaceOverrides.surfaceElementId` is a soft FK with no constraint**, so an override outlives a
994
+ deleted element and silently re-attaches to whatever takes that id next. (apeterson)
927
995
  - 2026-08-28 — Registered the **transfer-order reserved blocks** (`Messages` 227–250, `Surfaces`
928
996
  47–51, `SurfaceElements` 170–186, `Core/2026-08-28a`) and recorded the read-only ceiling snapshot
929
997
  taken before reserving them: local **and** production identical at 46/169/226 while sandbox-client
@@ -84,6 +84,15 @@ Order matters — each step assumes the prior one ran.
84
84
  - **Drift before consolidation** — loose `Client/` files must already be applied to all existing
85
85
  clients before they're consolidated/archived (otherwise the new blank diverges from live
86
86
  clients). See the dbchanges2 architecture doc.
87
+ - **🚨 The blank can be WRONG, not just behind — an open example (2026-08-28).**
88
+ `Client/2026-06-03- BLANK_CLIENT_DATABASE.sql` creates `TransferOrderItems` (`CREATE` at ~line
89
+ 9462) **without `dtCreated`/`dtUpdated`**, but `_Model_Client_TransferOrderItem` declares them as
90
+ `FIELD_DATETIME_CREATED`/`FIELD_DATETIME_UPDATED`, so `_Model` puts them in **every SELECT it
91
+ builds**. **Every tenant provisioned from this blank therefore has a table the model cannot
92
+ read** — nothing in a request has to ask for the columns. `Client/2026-08-28b -
93
+ TransferOrderItemsTimestamps.sql` repairs existing tenants; **the blank itself is still unfixed**,
94
+ so the next onboarding reproduces it. When a model-vs-blank mismatch like this is found, fix
95
+ **both**: a catch-up `Client/` file for live tenants *and* the blank.
87
96
 
88
97
  ## Gotchas
89
98
 
@@ -100,6 +109,12 @@ Order matters — each step assumes the prior one ran.
100
109
 
101
110
  ## Change history
102
111
 
112
+ - 2026-08-28 — Recorded an **open blank-template defect**: `TransferOrderItems` is created without
113
+ the `dtCreated`/`dtUpdated` columns the model declares as `FIELD_DATETIME_CREATED/UPDATED` and
114
+ therefore selects on every read, so every tenant built from the blank ships a table the model
115
+ cannot read. Live tenants are repaired by `Client/2026-08-28b`; the blank is still unfixed. Added
116
+ the general rule: a model-vs-blank mismatch needs **both** a catch-up `Client/` file and a blank
117
+ fix. (apeterson)
103
118
  - 2026-07-20 — Noted that a local browser wizard now automates steps 2–9 and the dbchanges2 blank
104
119
  consolidation (TRUE-79864). (mhammontree)
105
120
  - 2026-06-23 — Initial capture from the Fordham onboarding (TRUE-79702). (mhammontree)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-11
9
+ updated: 2026-08-28
10
10
  owners: ["bala", "mhammontree", "apeterson"]
11
11
  files:
12
12
  - dbchanges2/Client_Growrk/2026-05-28.sql
@@ -101,6 +101,33 @@ part, and it is what makes a missing column fatal. Standard, non-`c_` fields use
101
101
  `Core.RecordFields` + `AclFieldPermissions` instead; see
102
102
  [ACL permission chain](../../_underscore/features/acl-permission-chain.md).)
103
103
 
104
+ ## The drift can run the OTHER way: prod has state the repo never created
105
+
106
+ Everything above assumes the client DB is **behind** the repo. The mirror case is just as common and
107
+ harder to see: **production carries hand-applied state that no migration file creates**, so the
108
+ *fresh* environment is the broken one.
109
+
110
+ Worked example (NYCHH transfer orders, 2026-08-28) — four layers, all applied by hand when the
111
+ NetSuite TransferOrder POST was enabled, none of them in `dbchanges2`: `AclRecordPermissions` for
112
+ records 312/313, their `AclLogicGroups`/`AclLogicGroupExpressions`/`AclRecordExpressions` chain,
113
+ `AclFieldPermissions` for all 17 of their `RecordFields`, and the
114
+ `TransferOrderItems.dtCreated`/`dtUpdated` columns. Local had none of it and **failed one layer at a
115
+ time, each with a completely different-looking error**.
116
+
117
+ Three things this changes about the audit:
118
+
119
+ - **ACL rows are drift too, not just columns.** An `information_schema` diff will never find a
120
+ missing `AclFieldPermissions` row. Diff the ACL tables for the records in scope as well.
121
+ - **Diff the WHOLE feature area up front rather than chasing one 500 at a time.** One systematic
122
+ local-vs-prod diff of every table (and ACL row) the feature touches is cheaper than four
123
+ sequential debugging sessions, and it is the only way to find layers that have not failed *yet*.
124
+ - **The blast radius is never just the screen you are on.** The same missing rows meant the NetSuite
125
+ transfer-order **importer** could not be stood up in a fresh environment either.
126
+
127
+ The repair is a **faithful reproduction** file — same roles, same permission bits as prod, no
128
+ widening — anti-joined on natural keys so it is a no-op where the state already exists. Detail:
129
+ [NYCHH transfer-order inventory & quantities](../../../../clients/nychh/features/transfer-order-inventory-quantities.md).
130
+
104
131
  ## Steps
105
132
 
106
133
  1. **Get the real exception first.** Resolve `error.id` from the response envelope against
@@ -162,6 +189,15 @@ part, and it is what makes a missing column fatal. Standard, non-`c_` fields use
162
189
 
163
190
  ## Change history
164
191
 
192
+ - 2026-08-28 — Added the **inverse drift direction**: production carrying hand-applied state the repo
193
+ never created, so the *fresh* environment is the broken one. From the NYCHH transfer-order case —
194
+ four untracked layers (record ACL for 312/313, their logic-group/expression chain, field ACL for
195
+ all 17 RecordFields, and two `TransferOrderItems` timestamp columns) that failed locally one at a
196
+ time with different-looking errors. Durable additions: **ACL rows are drift too** and an
197
+ `information_schema` diff will never see them; **diff the whole feature area up front** instead of
198
+ chasing sequential 500s; the blast radius extends past the UI (the NetSuite importer was equally
199
+ unbuildable); and the repair is a **faithful reproduction** — same roles, same bits, no widening,
200
+ anti-joined so it no-ops where the state exists. (apeterson)
165
201
  - 2026-08-27 — Added a pointer to the **third drift direction**: when the 1054 starts right after a
166
202
  deploy, is not client-specific, and the column exists in no environment, the **framework branch is
167
203
  ahead of schema** and the repair is a branch merge, not a catch-up file — see
@@ -7,7 +7,7 @@
7
7
  | [AdvancedSelect (virtualized single/multi select)](features/advanced-select.md) | `AdvancedSelect<T>` is a **fully controlled, virtualized** single/multi select built from scratch (not react-select) on **`@tanstack/react-virtual`**, for large | toga-blox/src/components/AdvancedSelect/AdvancedSelect.tsx, toga-blox/src/components/AdvancedSelect/AdvancedSelect.types.ts, toga-blox/src/components/AdvancedSelect/AdvancedSelect.module.css |
8
8
  | [API client (axios wrapper, auth, table-data fetchers)](features/api-client.md) | `src/api/` is a thin **axios** wrapper that standardizes the **2.0 API envelope**, manages auth (Bearer + refresh), serializes complex query options, and provid | toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/api/index.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/api/apiFunctions.ts, toga-blox/src/api/auth.ts, toga-blox/src/api/genericApi.ts, toga-blox/src/api/types.ts, toga-blox/src/api/tableData |
9
9
  | [BaseInput (react-hook-form field factory)](features/base-input.md) | `BaseInput` is a **form-field factory** driven by react-hook-form. | toga-blox/src/components/BaseInput/BaseInput.tsx, toga-blox/src/components/BaseInput/BaseInput.types.ts, toga-blox/src/components/BaseInput/BaseInput.module.css, toga-blox/src/components/BaseInput/components |
10
- | [blox authoring defect backlog (fix IN blox, not around it)](features/blox-authoring-defects.md) | The standing list of **defects to fix inside `@agilant/toga-blox`**. | toga-blox/src/api/auth.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/components/BaseInput, toga-blox/tsconfig.json, toga-blox/package.json |
10
+ | [blox authoring defect backlog (fix IN blox, not around it)](features/blox-authoring-defects.md) | The standing list of **defects to fix inside `@agilant/toga-blox`**. | toga-blox/src/components/BaseButton, toga-blox/src/api/auth.ts, toga-blox/src/api/axiosInstance.ts, toga-blox/src/reactQuery/queryHelpers.ts, toga-blox/src/components/BaseInput, toga-blox/tsconfig.json, toga-blox/package.json |
11
11
  | [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/components/Table/themeConfig/toga.module.css, toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
12
12
  | [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | `TableRecordModal` is a **generic, presentational modal shell** for showing a single table record (row) in detail — typically opened from a table row click. | toga-blox/src/components/TableRecordModal/TableRecordModal.tsx, toga-blox/src/components/TableRecordModal/index.ts, toga-blox/src/components/TableRecordModal/tableRecordModal.module.css |
13
13
  | [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | The `Table` component (`src/components/Table/`) is the **TanStack Table v8** building block behind the [Primary Table templates](primary-table-templates.md). | toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, toga-blox/src/components/Table/index.ts, toga-blox/src/components/Table/types.ts, toga-blox/src/components/Table/utils/buildTanstackColumns.tsx, toga-blox/src/components/Table/utils/resolveCellType.tsx, toga-blox/src/components/Table/components/cellTypes, toga-blox/src/components/Table/components/actionCells, toga-blox/src/components/Table/components/columnFiltersAndSorts, toga-blox/src/components/Table/hooks, toga-blox/src/components/Table/themeConfig |
@@ -15,4 +15,4 @@
15
15
  | [Toaster — host-token styling contract](features/toaster.md) | The blox `Toaster` is a CSS-Module component whose every visual property reads a `--toaster-*` custom property supplied by the **host app**. | toga-blox/src/components/Toaster/Toaster.module.css, toga-blox/src/components/Toaster/Toaster.tsx, toga-blox/src/components/Toaster/ToasterContext.tsx, toga2-commerce/src/styles/index.css, toga2-commerce/src/components/Toasters/ToasterList.tsx |
16
16
  | [Dynamic npm Publish Pipeline (branch → channel)](workflows/dynamic-publish-pipeline.md) | How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`) publishes a per-environment npm **channel** (dist-tag) from a `_<mode> | toga-blox/.github/workflows/publish.yml, toga-blox/package.json, toga-blox/src/utils/getFontAwesomeIcon.tsx |
17
17
  | [Landing a Large Long-Lived Feature Branch (merge, never rebase)](workflows/landing-a-large-feature-branch.md) | The procedure for landing a long-lived, very large feature branch (months old, hundreds of commits) into `_production` in `toga-blox-npm`. | toga-blox/package.json, toga-blox/.github/workflows/publish.yml |
18
- | [Local-linking an unpublished blox into a consumer app (incl. a React 19 / Next host)](workflows/local-link-into-a-consumer-app.md) | How to develop a **blox component that is not published yet** against a real consuming app, on Windows, including the hard case: a **React 19 / Next.js (Turbopa | toga-blox/package.json, toga-blox/postcss.config.cjs |
18
+ | [Local-linking an unpublished blox into a consumer app (incl. a React 19 / Next host)](workflows/local-link-into-a-consumer-app.md) | How to develop a **blox component that is not published yet** against a real consuming app, on Windows, including the hard case: a **React 19 / Next.js (Turbopa | toga-blox/package.json, toga-blox/postcss.config.cjs, toga25-supply/vite.config.ts |
@@ -6,9 +6,10 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-27
9
+ updated: 2026-08-28
10
10
  owners: [apeterson]
11
11
  files:
12
+ - toga-blox/src/components/BaseButton
12
13
  - toga-blox/src/api/auth.ts
13
14
  - toga-blox/src/api/axiosInstance.ts
14
15
  - toga-blox/src/reactQuery/queryHelpers.ts
@@ -89,6 +90,24 @@ mechanism), and a **fix direction** (a suggestion, not a decision). Every one of
89
90
  today's `"/"` for compatibility), or have `performLogout` simply not navigate and let the caller's
90
91
  `onLogout` own it.
91
92
 
93
+ ## Styling defects
94
+
95
+ ### 4. `BaseButton` — `primary` and `primaryAction` are NOT interchangeable variants
96
+
97
+ **Mechanism.** The two class names look like a naming pair but carry different property sets:
98
+ `.primaryBtnAction` sets a **font-size** (default 14px) **and** `height: 26px`; `.primaryBtn` sets
99
+ **neither** — it inherits font-size from context and has no height at all.
100
+
101
+ **Consumer symptom.** Switching a button between the two variants — the obvious move when a surface
102
+ element is seeded `PRIMARY` — silently changes **type size and button height**, with nothing in the
103
+ prop name suggesting geometry is involved. It reads as a CSS bug in the consuming app.
104
+
105
+ **Fix direction.** Make the two variants differ in **colour only**, moving the shared size/height
106
+ declarations to the base button class. Until then a consumer must compensate at the instance —
107
+ `toga25-supply`'s `SurfaceActionBar` variant map pins `fontSize: 14px` for `primary` (see
108
+ [surface-frontend](../../toga25-supply/features/surface-frontend.md)). **Do not patch the shared blox
109
+ stylesheet from a consumer**: every one of the ~40 call sites inherits it.
110
+
92
111
  ## Already-known authoring items (carried over from §22)
93
112
 
94
113
  These are long-standing and were previously recorded only as consumer-side advice:
@@ -122,6 +141,11 @@ These are long-standing and were previously recorded only as consumer-side advic
122
141
  time**, with no PR. Sequence auth fixes deliberately.
123
142
 
124
143
  ## Change history
144
+ - 2026-08-28 — Added styling defect **4: `BaseButton` `primary` vs `primaryAction` are not
145
+ interchangeable** — `.primaryBtnAction` sets font-size **and** `height: 26px`, `.primaryBtn` sets
146
+ neither, so swapping variants silently changes type size and button height. Found wiring the
147
+ `transfer-orders-list-actions` PRIMARY button in toga25-supply; compensated at the instance (the
148
+ `SurfaceActionBar` variant map pins 14px) rather than by editing the shared stylesheet. (apeterson)
125
149
  - 2026-08-27 — Created the doc §22 had been pointing at since 2026-08-18. Seeded with the three auth
126
150
  defects found while debugging toga25-supply SSO (`handleClientAuthentication`'s misleading name +
127
151
  untyped return, `fetchPublicToken` persisting the public token to `localStorage`,
@@ -7,10 +7,11 @@ client: shared
7
7
  type: workflow
8
8
  status: active
9
9
  updated: 2026-08-28
10
- owners: [tcox]
10
+ owners: [tcox, apeterson]
11
11
  files:
12
12
  - toga-blox/package.json
13
13
  - toga-blox/postcss.config.cjs
14
+ - toga25-supply/vite.config.ts
14
15
  related:
15
16
  - ./dynamic-publish-pipeline.md
16
17
  - ../features/talos-assistant.md
@@ -59,6 +60,35 @@ Result: exactly one React in the graph. **framer-motion 11 and Pro FontAwesome b
59
60
  React 19** — the `^18` peer range is a declaration, not a runtime wall (a published consumer
60
61
  still needs the peer range widened).
61
62
 
63
+ ### 2b. Vite consumers: dedupe EVERY peer that carries a React context — not just React
64
+
65
+ **The rule: any peer dep that provides a React context or hooks must be in `resolve.dedupe` when
66
+ blox is consumed through a symlink.** It is not enough that blox declares the peer correctly — Vite
67
+ resolves a symlinked package's imports **relative to the link target**, so blox's hooks pick up
68
+ whatever sits in `toga-blox-npm/node_modules`, and you end up with two copies of the library.
69
+
70
+ Worked example (toga25-supply, 2026-08-28) — **"No QueryClient set, use QueryClientProvider" with
71
+ `QueryClientProvider` plainly visible in the component stack.** Two copies of
72
+ `@tanstack/react-query`: the app's provider wrote one React context while blox's hooks
73
+ (`useFetchTablePageMeta`, `useTableData`) read another. `vite.config.ts` deduped
74
+ `react` / `react-dom` / `@tanstack/react-table` but **not react-query**.
75
+
76
+ ```ts
77
+ dedupe: [
78
+ "react", "react-dom", "@tanstack/react-table",
79
+ "@tanstack/react-query", // ← the fix
80
+ "@tanstack/react-query-persist-client",
81
+ "@tanstack/react-query-devtools",
82
+ ]
83
+ ```
84
+
85
+ - **Editing `dedupe` is not enough on its own — `rm -rf node_modules/.vite` and hard-reload.** The
86
+ duplicate is baked into Vite's **pre-bundle**, so the old graph is served until the cache is
87
+ cleared.
88
+ - **This only reproduces on machines using the local checkout**, so across a team it reads as an
89
+ *intermittent* bug that "works for everyone else." A `QueryClient`/context error with the provider
90
+ visibly mounted is almost always this.
91
+
62
92
  ### 3. Disable blox's PostCSS config while linked
63
93
 
64
94
  Rename `toga-blox-npm/postcss.config.cjs` to `postcss.config.cjs.off` for the duration.
@@ -89,6 +119,13 @@ The consumer reads blox's `dist`, so rebuild blox after every change.
89
119
  drags every component's CSS into the consumer bundle.
90
120
 
91
121
  ## Change history
122
+ - 2026-08-28 — Added the Vite half of the one-copy rule: **every peer carrying a React context must
123
+ be in `resolve.dedupe`**, not just React. `toga25-supply` threw "No QueryClient set" **with
124
+ `QueryClientProvider` visibly in the stack** because `@tanstack/react-query` was missing from
125
+ `dedupe` while `react-table` was there — the app's provider and blox's hooks held different
126
+ contexts. Fix = add the react-query family to `dedupe` **and** `rm -rf node_modules/.vite` (the
127
+ duplicate is baked into the pre-bundle). Reproduces only on machines using the local checkout, so
128
+ it presents as intermittent across a team. (apeterson)
92
129
  - 2026-08-27 — Initial doc. Recorded the procedure proven while wiring blox `Talos` into `ai-bdr`
93
130
  from an unpublished branch: junction the checkout in, junction the host's React 19 over blox's
94
131
  bundled React 18 (keeping `.r18` originals) so there is one React, and rename blox's Tailwind
@@ -6,7 +6,7 @@
6
6
  | [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | A declarative, fully config-driven rule engine that resolves the boolean-ish flags (`isEnabled`, `isVisible`, `isComplete`) on SalesOrder action-button options. | toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.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/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
7
7
  | [Client API-Fetch Fields (useClientApiFields / apiFields.json)](features/client-api-fetch-fields.md) | The mechanism that resolves a client's **API-fetch projection** — which `fields` / `ojoin` / `join` / `where` to request from the TOGa API for a given page — fr | toga25-supply/src/fieldsConfig/resolveApiConfig.ts, toga25-supply/src/fieldsConfig/useClientApiFields.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx |
8
8
  | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
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 |
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
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/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/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 |
@@ -14,7 +14,7 @@
14
14
  | [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
15
  | [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/App.tsx, 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/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
16
  | [Talos Integration (AppLayout host + adapter wiring)](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/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/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/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
18
- | [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 |
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/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/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferableItemRows.ts, 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
+ | [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
19
  | [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 |
20
20
  | [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 |
@@ -6,17 +6,23 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-23
9
+ updated: 2026-08-28
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/src/components/ColumnVisibilityModal/
13
+ - toga25-supply/src/surface/useColumnVisibilityModalConfig.ts
14
+ - toga25-supply/src/surface/index.ts
13
15
  - toga25-supply/src/pages/SalesOrders/SalesOrders.tsx
14
16
  - toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
15
17
  - toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx
18
+ - dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql
16
19
  related:
17
20
  - ../architecture.md
18
21
  - client-configurable-fields.md
19
22
  - meta-driven-table-data.md
23
+ - surface-frontend.md
24
+ - transfer-orders-page.md
25
+ - ../../dbchanges2/features/surface-layer-schema.md
20
26
  ---
21
27
 
22
28
  ## What it is
@@ -75,11 +81,40 @@ const isFieldVisible = (f) => visibleSlugSet ? visibleSlugSet.has(f.slug) : f.is
75
81
  6. **Verify:** `npx tsc --noEmit`, validate each JSON parses, click live (toggle, Apply, copy URL,
76
82
  reload → state persists).
77
83
 
84
+ ## Where the copy comes from — SHARED Core Messages, and NO FE fallbacks (2026-08-28)
85
+
86
+ Step 5's per-page `FIELDS/<CLIENT>/…` block is the **legacy** source. New screens take the modal's
87
+ copy from the **Surface layer**, under **shared `common.columns.modal.*` `Core.Messages` keys**
88
+ (`Core/2026-08-28b`).
89
+
90
+ - **Why `common.*` and not per-record.** Every page previously hardcoded the same seven strings,
91
+ which put the English **in the bundle, where `MessageTranslations` can never reach it**. Shared
92
+ keys — the pattern already used by `common.yes` / `common.no` / `common.active` /
93
+ `common.inactive` — mean a tenant translates the modal **once for every list screen** instead of
94
+ once per record type.
95
+ - **The English fallbacks were removed from the FE deliberately.** A hardcoded default silently
96
+ masks a missing seed and reintroduces the second source of truth this whole migration exists to
97
+ delete. **Trade-off accepted and recorded: an unseeded environment renders the raw message key.**
98
+ (This is a deliberate divergence from the generic surface guidance that every consumer keeps a
99
+ design-literal fallback — for *this* component the missing seed must be visible.)
100
+ - **`useColumnVisibilityModalConfig`** (`src/surface/`, exported from the `@/surface` barrel) is the
101
+ reusable reader: given any `BUTTON_BAR` bundle carrying a `config.role = 'columnsButton'` element,
102
+ it reads that element's `config.modal` and resolves the copy + icon. It lives with the other
103
+ surface infra and carries the same `TODO(blox)` promotion marker.
104
+ - **Seed guard:** because these are shared keys, the `INSERT` is `NOT EXISTS`-guarded on
105
+ **`messageKey`**, not on a literal id — another seed may legitimately create them first at a
106
+ different id. See
107
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
108
+ - **Follow-up (clean, not done):** the three shipped screens still on
109
+ `items.columns.modal.*` / `salesOrder.columns.modal.*` can fold onto the common keys + this hook.
110
+ Those older keys were deliberately left alone.
111
+
78
112
  ## Gotchas
79
113
 
80
114
  - **No hardcoded copy in the component** — every user-facing string (incl. Close `aria-label`) is a
81
- required `labels` prop from client FIELDS; fallbacks live in the view model's resolution, not the
82
- component, not default prop values. Only structural glyphs (checkbox tick, close `xmark`) stay.
115
+ required `labels` prop. On the **legacy** path the fallbacks live in the view model's resolution
116
+ (never in the component or default props); on the **surface** path there are **no fallbacks at
117
+ all** (see above). Only structural glyphs (checkbox tick, close `xmark`) stay.
83
118
  - **Remount key is REQUIRED.** toga-blox's `PrimaryTableRow` memoizes
84
119
  `useMemo(() => row.getVisibleCells(), [row])`; TanStack caches `row` by data, so a columns-only
85
120
  change doesn't change `row` → stale cells. Symptom: **toggling off removes the header but leaves the
@@ -95,4 +130,14 @@ const isFieldVisible = (f) => visibleSlugSet ? visibleSlugSet.has(f.slug) : f.is
95
130
  meta later won't appear for an old link until Reset (intended).
96
131
 
97
132
  ## Change history
133
+ - 2026-08-28 — Modal copy moved from per-page hardcoded strings / client `FIELDS` to **shared
134
+ `common.columns.modal.*` `Core.Messages`** (`Core/2026-08-28b`), so a tenant translates it once for
135
+ every list screen instead of per record type — the old strings sat in the bundle where
136
+ `MessageTranslations` could never reach them. **Removed the English FE fallbacks deliberately**: a
137
+ hardcoded default masks a missing seed and recreates the second source of truth; the accepted
138
+ trade-off is that an unseeded environment renders the raw key. Added
139
+ `useColumnVisibilityModalConfig` (`src/surface/`, barrel-exported) as the reusable reader for any
140
+ `BUTTON_BAR` slug carrying a `columnsButton` element. Seed is guarded on `messageKey` rather than a
141
+ literal id because the keys are shared. Folding the three shipped screens off
142
+ `items.columns.modal.*` / `salesOrder.columns.modal.*` is an open follow-up. (apeterson)
98
143
  - 2026-06-23 — Documented from the `add-column-visibility` skill during initial knowledge seed. (apeterson)
@@ -574,6 +574,24 @@ developer asked for the **whole component**, not just badges. The old
574
574
  [surface-resolver](../../_underscore/features/surface-resolver.md) — terms are Core-global, no
575
575
  per-term gating).
576
576
 
577
+ ## `SurfaceActionBar` labeled mode honours `element.variant` (2026-08-28)
578
+
579
+ `renderMode="labeled"` previously hardcoded `variant="secondaryAction"` for **every** button, so a
580
+ surface element seeded `PRIMARY` could never render as primary — the seed was silently ignored.
581
+ There is now an **`element.variant` → `BaseButton` variant map**, falling back to `secondaryAction`,
582
+ so Sales Orders / Items / Vendor Items render exactly as before while the new
583
+ `transfer-orders-list-actions` PRIMARY create button renders correctly.
584
+
585
+ The map also **pins `fontSize: 14px` for the `primary` variant**, because blox's two "primary"
586
+ classes are not interchangeable — see
587
+ [blox authoring defects](../../toga-blox/features/blox-authoring-defects.md): `.primaryBtnAction`
588
+ sets font-size **and** `height: 26px`; `.primaryBtn` sets **neither**. Swapping the variant silently
589
+ changes type size and button height. The compensation belongs **at the instance / in this map**, not
590
+ in an edit to the shared blox stylesheet.
591
+
592
+ ⚠ This is **shared infra** — `SurfaceActionBar` renders every migrated screen's chrome, so a change
593
+ here is a cross-screen change. Re-check Sales Orders, Items and Vendor Items after touching it.
594
+
577
595
  ## Gotchas
578
596
 
579
597
  - **The VIP `/contacts` API call is gated by an approval-details element's `labelSuffix.valueKey`,
@@ -778,6 +796,13 @@ button except **View Log** renders and does nothing on click. Whether the SR mod
778
796
  actions surface is **still an open decision**, not an oversight.
779
797
 
780
798
  ## Change history
799
+ - 2026-08-28 — **`SurfaceActionBar` labeled mode now honours `element.variant`** (shared infra):
800
+ `renderMode="labeled"` had hardcoded `variant="secondaryAction"` for every button, so a `PRIMARY`
801
+ seed could never render as primary. Added an `element.variant` → `BaseButton` variant map with a
802
+ `secondaryAction` fallback (Sales Orders / Items / Vendor Items unchanged), pinning
803
+ `fontSize: 14px` for `primary` because blox's `.primaryBtn` sets neither font-size nor height while
804
+ `.primaryBtnAction` sets both — compensated at the instance rather than by editing the shared blox
805
+ stylesheet. Driven by the new `transfer-orders-list-actions` bar. (apeterson)
781
806
  - 2026-08-28 — Recorded two durable FE surface rules from the denial-reason investigation (research
782
807
  only; no code written). (1) **A hidden marker's RULE can select a MODE**, not just a section on/off
783
808
  — el **108** `sectionLayoutRule` (`renderType TEXT`, `isVisible=0`) is the live precedent; reach for
@@ -17,20 +17,30 @@ files:
17
17
  - toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts
18
18
  - toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts
19
19
  - toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx
20
+ - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx
21
+ - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts
22
+ - toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferableItemRows.ts
23
+ - toga25-supply/src/surface/useColumnVisibilityModalConfig.ts
20
24
  - toga25-supply/src/routes.tsx
21
25
  - dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql
26
+ - dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql
22
27
  - dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql
28
+ - dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql
23
29
  - dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
24
30
  - dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql
31
+ - dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql
25
32
  related:
26
33
  - ./surface-frontend.md
27
34
  - ./meta-driven-table-data.md
28
35
  - ./record-modals-and-nested-tables.md
36
+ - ./column-visibility.md
29
37
  - ../../dbchanges2/features/surface-layer-schema.md
30
38
  - ../../_underscore/features/surface-resolver.md
31
39
  - ../../_underscore/features/tableview-joins.md
32
40
  - ../../api2/features/v2-rest-query-contract.md
41
+ - ../../api2/features/tableview-field-metadata.md
33
42
  - ../../../../clients/nychh/profile.md
43
+ - ../../../../clients/nychh/features/transfer-order-inventory-quantities.md
34
44
  ---
35
45
 
36
46
  ## Summary
@@ -43,12 +53,16 @@ reserved-id seed → per-tenant TableView → per-tenant nav opt-in → surface-
43
53
  **It was fully greenfield.** An audit of production, beta, sandbox-client **and all 36 client
44
54
  databases** on 2026-08-28 found **no transfer-order TableView anywhere**. Nothing was extended.
45
55
 
56
+ A **Create Transfer Order** modal was started the same day: the UI and its interaction rules are
57
+ built, the data layer is **disabled behind a flag** pending three unanswered data-model questions
58
+ (see below).
59
+
46
60
  The vertical is **platform-shared with NYCHH as the first (and today only) tenant**: the Core
47
61
  surfaces and the stage theme tokens are neutral/shared, and only the TableView and the navigation
48
62
  opt-in are `Client_Nychh`. That is the same shape as the Sales Order surfaces with Compass as first
49
63
  tenant.
50
64
 
51
- ## The four migrations
65
+ ## The migrations
52
66
 
53
67
  | File | DB scope | What it does |
54
68
  |---|---|---|
@@ -56,8 +70,11 @@ tenant.
56
70
  | `Core/2026-08-28a - TransferOrderSurfaceSeed.sql` | Core | nav element, LIST surface, 4 record surfaces, stage vocabulary, 24 Messages |
57
71
  | `Client/2026-08-28a - TransferOrderStageThemeTokens.sql` | all tenants (template) | 5 `color.transferStage.*` tokens |
58
72
  | `Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql` | one tenant | the single `SurfaceOverrides` row that reveals the nav item |
73
+ | `Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql` | one tenant | reproduces the **untracked** record/field ACL prod already has for records 312/313 |
74
+ | `Client/2026-08-28b - TransferOrderItemsTimestamps.sql` | all tenants (template) | `TransferOrderItems.dtCreated`/`dtUpdated` (+ indexes), guarded `qtyCommitted` |
75
+ | `Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql` | Core | the `transfer-orders-list-actions` BUTTON_BAR + `common.columns.modal.*` Messages |
59
76
 
60
- All four were audited against the cluster-isolation HARD RULE: **no `Core.*` reference in any
77
+ All of them were audited against the cluster-isolation HARD RULE: **no `Core.*` reference in any
61
78
  `Client_*` file** (the only such strings are inside comments), no `UUID()`, all table references
62
79
  unqualified.
63
80
 
@@ -191,6 +208,94 @@ fields. One FK hop out from the order they need `calcDepth 2`; at the default of
191
208
  the loaded state swaps element types at the same tree position and replays the slide-up animation —
192
209
  see the gotcha in [record-modals-and-nested-tables](./record-modals-and-nested-tables.md).
193
210
 
211
+ ## The prod-only state this vertical sits on top of — four UNTRACKED layers
212
+
213
+ Standing the page up locally failed **one layer at a time**, each with a different-looking error,
214
+ because production and sandbox-client carry transfer-order state that **no `dbchanges2` file
215
+ creates**. It was all hand-applied when the NetSuite TransferOrder POST was enabled:
216
+
217
+ 1. `AclRecordPermissions` for records **312** (`transfer-orders`) and **313**
218
+ (`transfer-order-items`), roles 1 and 3;
219
+ 2. their `AclLogicGroups` → `AclLogicGroupExpressions` → `AclRecordExpressions` chain;
220
+ 3. `AclFieldPermissions` for **all 17 `RecordFields`** of 312/313 (roles 1 and 3, `isWritable = 1`);
221
+ 4. the `TransferOrderItems.dtCreated` / `dtUpdated` **columns**.
222
+
223
+ So **a fresh environment cannot reproduce production** — and the blast radius is bigger than this
224
+ UI: the NetSuite transfer-order importer cannot be stood up in a fresh environment either.
225
+ `Client_Nychh/2026-08-28c` reproduces layers 1–3 faithfully (**same roles, same permission bits as
226
+ prod — not a widening**; role 2 *Developer* is deliberately **not** granted because prod/sandbox
227
+ don't have it and role 1 satisfies the UI), anti-joined on natural keys: 2 expressions + 4 record
228
+ permissions + 34 field permissions insert on local, **0 on sandbox** — dry-run verified in both
229
+ directions. Layer 4 is `Client/2026-08-28b`. Client-side detail:
230
+ [NYCHH transfer-order inventory & quantities](../../../../clients/nychh/features/transfer-order-inventory-quantities.md).
231
+
232
+ **Follow-up recorded, not done:** run a systematic `information_schema` diff of local vs prod for
233
+ the whole transfer-order area rather than discovering the gaps one 500 at a time — see
234
+ [client schema drift audit](../../dbchanges2/workflows/client-schema-drift-audit.md).
235
+
236
+ ## The list-actions BUTTON_BAR (`Core/2026-08-28b`)
237
+
238
+ `transfer-orders-list-actions`, **Surface 52**, elements **187–189**: Refresh (`SECONDARY`,
239
+ `config.valueKey 'refresh'`), Columns (`SECONDARY`, `config.role 'columnsButton'` +
240
+ `config.modal` message keys), Transfer Order (`PRIMARY`, `config.valueKey 'createTransferOrder'`).
241
+ This closes the "no BUTTON_BAR surface" gap the first build left open.
242
+
243
+ - **No `Actions` rows are needed.** With no handler registered in the FE `actionRegistry`,
244
+ `SurfaceActionBar` already falls through to `onDispatch(valueKey)` — which is exactly the
245
+ host-owned behaviour these three want. Only a control that needs a *registry handler* needs an
246
+ `Action` row.
247
+ - **Seeded `isVisible = 1`, deliberately against the usual Core-OFF default.** A list screen with
248
+ no refresh and no create is not a useful neutral default, and none of the three is unsafe if
249
+ inherited — only NYCHH can reach the page at all. See
250
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md) for when Core-neutral
251
+ means OFF and when it does not.
252
+ - The Columns button's copy comes from **shared `common.columns.modal.*` Messages**, consumed
253
+ through the new `useColumnVisibilityModalConfig` hook — see
254
+ [column-visibility](./column-visibility.md).
255
+
256
+ ## Create Transfer Order modal — UI built, data layer deliberately NOT faked
257
+
258
+ `view/CreateTransferOrderModal/` (+ `hooks/useTransferableItemRows.ts`), opened from the header
259
+ button seeded above. The interaction rules are the durable part:
260
+
261
+ - **Single-source lock.** The first checked row fixes the source location for the whole order.
262
+ Source Location is **derived and read-only — never picked**; the Location filter is pinned and
263
+ disabled while locked, and **restores its pre-lock value** when the lock releases.
264
+ - **A row is an ITEM AT A LOCATION**, kept only where availability >= 1.
265
+ - **Dirty means "different from the state the form OPENED IN"**, not "different from empty" — so a
266
+ pre-scoped open is clean, which is what makes the discard guard trustworthy.
267
+ - **Qty error state machine:** touched on first keystroke; the error surfaces on **blur** only if
268
+ touched; then it live-revalidates.
269
+ - **The Review gate names the FIRST unmet condition** rather than a generic "incomplete".
270
+
271
+ **Three open questions block the data layer** (the picker + fetch are written but **disabled behind
272
+ a flag** — nothing is stubbed with fake data):
273
+
274
+ 1. **Availability per (item, location) has no API.** `Item._qtyAvailable` is item-level with no
275
+ location dimension; the real source is `Units` (carries `itemId` + `locationId` + a
276
+ disposition). Unresolved: which `UnitDispositions` slug counts as available, and whether to
277
+ aggregate client-side or add a grouped table view. NYCHH has **3,226 active items**, so
278
+ browser-side grouping will not hold.
279
+ 2. **The design's target-location picker is a TREE** (breadcrumbs, drill-down, ancestor path), but
280
+ **no parent FK was found on `_Model_Client_Location`**. If Locations are flat, the dialog has to
281
+ become a searchable select and the design needs revisiting. Picker left disabled.
282
+ 3. **Commit flips unit dispositions to In Transit — a real inventory mutation.** Unresolved whether
283
+ the API owns that or a worker does.
284
+
285
+ ### Line-item fetch is off behind a FLAG, not commented out
286
+
287
+ `TRANSFER_ORDER_LINES_ENABLED = false` until `Client/2026-08-28b` is applied everywhere. A flag,
288
+ not a commented-out call: the repo rule bans committed commented-out code, and a flag keeps the
289
+ hook typechecked.
290
+
291
+ > ⚠ **A DISABLED React Query v5 query reports `status: 'pending'` FOREVER.** Any `isLoading` derived
292
+ > from it must be read **through the flag**, or the UI hangs on its skeleton. This is the **third**
293
+ > place in this codebase to hit it (`useFetchSurfaceMetaGroup` already documents it).
294
+
295
+ With lines off, the three summary tiles render an **em-dash, not `0`** — all three are derived from
296
+ the lines, so `0` would be a *wrong* number rather than a missing one — and the empty row reads
297
+ "temporarily unavailable", not "no items".
298
+
194
299
  ## v1 scope — seeded but hidden
195
300
 
196
301
  Money (Unit Value / Total / Total Value) and Target Contact are **dropped for v1 but seeded as real
@@ -208,15 +313,21 @@ visible in Core (neutral default; other tenants populate `TransferOrders.purchas
208
313
 
209
314
  ## Rollout status & open items
210
315
 
211
- - **Applied and verified on LOCAL only.** Not run on sandbox-client or production. Nothing committed
212
- to git as of 2026-08-28. `npx tsc --noEmit` exits 0; the UI has **not** been exercised in a browser.
316
+ - **Applied and verified on LOCAL only.** Not run on sandbox-client or production.
317
+ - **Git (2026-08-28):** commit **`9293904`** on branch `TRUE-80852` in toga25-supply (3 files — the
318
+ surface-driven header + the extracted hook), **not pushed**. The dbchanges2 migrations are
319
+ uncommitted in a separate repo, and **the FE commit is inert without `Core/2026-08-28b`**, so the
320
+ two must land together (the commit body names the dependency). `.env.development` was
321
+ **deliberately excluded** from the commit — its diff repoints every developer's dev server, which
322
+ must not ride inside a feature ticket (see
323
+ [amplify-deployment](../workflows/amplify-deployment.md)).
213
324
  - **Beta is blocked.** It lacks the `TransferOrderStages` table entirely (the `Client/2026-08-18*`
214
325
  files never landed there), so `Client_Nychh/2026-08-28a` would fail on a missing FK target.
215
326
  - **Open dependency:** `Client/2026-08-28a` writes to `ThemeTokens`, a table created by
216
327
  `Client/2026-06-25a - SurfaceClientTables.sql`. Any prod tenant that never received `2026-06-25a`
217
328
  will error, and the full 36-tenant list was **not** audited.
218
- - **No `transfer-orders-list-actions` BUTTON_BAR surface**, so Refresh/Columns are plain buttons
219
- rather than `SurfaceActionBar`-driven like Sales Orders and Vendor Items.
329
+ - ~~No `transfer-orders-list-actions` BUTTON_BAR surface~~ — **closed** by `Core/2026-08-28b`
330
+ (Surface 52, elements 187–189); the header is now `SurfaceActionBar`-driven like Sales Orders.
220
331
  - **Test data lives in the session scratchpad, never in dbchanges2** (24 seed orders + an unseed
221
332
  script). The dbchanges2 executor runs **every** folder against production, so throwaway fixtures
222
333
  must not be committed there. Origin/destination were picked by co-prime strides over ranked active
@@ -226,6 +337,13 @@ visible in Core (neutral default; other tenants populate `TransferOrders.purchas
226
337
 
227
338
  - **A missing `AclRecordPermissions` grant on a joined record deletes a column, not a request.** The
228
339
  Status column would have silently vanished; record 351 had no grants at all.
340
+ - **A missing `AclFieldPermissions` grant on the DEFAULT-SORT column is a 500, not a 403.** The
341
+ field is dropped from the SELECT list silently, but `TableView::meta()` still emits
342
+ `table.sort` naming its slug → the FE builds `ORDER BY TransferOrders.undefined` → MySQL 1054.
343
+ Mechanism and the two candidate platform fixes:
344
+ [tableview-field-metadata](../../api2/features/tableview-field-metadata.md).
345
+ - **A disabled React Query v5 query is `pending` forever** — derive `isLoading` through the flag,
346
+ never from the query alone (see the line-item flag above).
229
347
  - **Never bind a Core TABLE surface to a `tableViewId`** — TableView ids are per-tenant and would not
230
348
  survive the Core/Client cluster split. Use `config.tableViewSlug`.
231
349
  - **Renaming a `TransferOrderStages` row silently greys out the badge** — the vocabulary matches the
@@ -241,6 +359,23 @@ visible in Core (neutral default; other tenants populate `TransferOrders.purchas
241
359
  earlier migration without names, so a name-based lookup on them finds nothing.
242
360
 
243
361
  ## Change history
362
+ - 2026-08-28 — Second pass, same day. Discovered the vertical sits on **four untracked layers** of
363
+ prod/sandbox state (record + field ACL for records 312/313 and their logic-group chain, plus the
364
+ `TransferOrderItems.dtCreated/dtUpdated` columns) — a fresh environment cannot reproduce
365
+ production, and the NetSuite importer is blocked by the same gap; wrote `Client_Nychh/2026-08-28c`
366
+ (faithful reproduction, role 2 deliberately excluded) and `Client/2026-08-28b`. Closed the
367
+ BUTTON_BAR gap with `Core/2026-08-28b` (`transfer-orders-list-actions`, Surface 52, elements
368
+ 187–189, seeded **visible** and needing no `Actions` rows because unregistered keys fall through to
369
+ `onDispatch(valueKey)`). Started the **Create Transfer Order modal** — interaction rules
370
+ implemented (single-source lock with a derived read-only Source Location, dirty-vs-opened-state,
371
+ the qty touched/blur error machine, a Review gate that names the first unmet condition) with the
372
+ data layer **disabled behind a flag rather than faked**, blocked on three open questions: no
373
+ per-(item, location) availability API (`Units` + a disposition is the real source; 3,226 active
374
+ items rules out client-side grouping), no parent FK found on `_Model_Client_Location` so the
375
+ design's location *tree* may not be buildable, and unowned responsibility for the In-Transit
376
+ disposition flip. Recorded the React-Query-v5 disabled-query `pending` trap (third occurrence) and
377
+ the ACL-gap-on-default-sort 500. Committed `9293904` (TRUE-80852), not pushed;
378
+ `.env.development` deliberately left out. (apeterson)
244
379
  - 2026-08-28 — First capture: built the Transfer Orders vertical from scratch after confirming no
245
380
  transfer-order TableView existed in prod, beta, sandbox-client or any of the 36 client DBs. Seeded
246
381
  the `Client_Nychh` `transfer-orders` TableView (3 joins, 4 columns) plus the full 4-table ACL chain
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-27
9
+ updated: 2026-08-28
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga25-supply/amplify.yml
@@ -14,8 +14,11 @@ files:
14
14
  - toga25-supply/src/hooks/useAuthenticationFlow.ts
15
15
  - toga25-supply/vite.config.ts
16
16
  - toga25-supply/package.json
17
+ - toga25-supply/.env.development
17
18
  related:
18
19
  - ../architecture.md
20
+ - ../features/transfer-orders-page.md
21
+ - ../../toga-blox/workflows/local-link-into-a-consumer-app.md
19
22
  ---
20
23
 
21
24
  ## What it is
@@ -90,6 +93,22 @@ i.e. an optional per-subdomain override, falling back to `VITE_API`. The `.env.<
90
93
  are committed and git-tracked; they contain **only public `VITE_API` base URLs, no secrets**,
91
94
  and are the single source of truth for environment API endpoints.
92
95
 
96
+ ## Local development — `npm run dev` does NOT point at your local API
97
+
98
+ `npm run dev` runs Vite in mode **`development`**, so **`.env.development` wins**, and that file
99
+ ships `VITE_API = https://api.dev.sandbox.togahub.com/v2`. A developer who assumes "dev = my
100
+ machine" is reading and writing **dev-sandbox** data.
101
+
102
+ Worse, **`.env.development` is git-TRACKED**, so developers keep flipping it to their local API —
103
+ which generates noise diffs and accidental commits, and a stray commit **repoints every other
104
+ developer's dev server**. (This is why the transfer-orders FE commit deliberately excluded it; see
105
+ [Transfer Orders page](../features/transfer-orders-page.md).)
106
+
107
+ **Suggested fix, not yet done:** keep a neutral committed default in `.env.development` and let each
108
+ developer override in a **gitignored `.env.development.local`** (Vite loads `.local` last and it is
109
+ the intended per-developer layer). Until that lands, check `git status` before committing on any
110
+ branch where you pointed the dev server at localhost.
111
+
93
112
  ## Adding a new environment
94
113
 
95
114
  1. Create `.env.<mode>` with the public `VITE_API` base URL.
@@ -131,6 +150,12 @@ and are the single source of truth for environment API endpoints.
131
150
  has ever been developed against a linked local checkout.
132
151
 
133
152
  ## Change history
153
+ - 2026-08-28 — Recorded that **`npm run dev` does not point at a local API**: mode `development`
154
+ loads the tracked `.env.development`, which ships
155
+ `VITE_API = https://api.dev.sandbox.togahub.com/v2` — so "dev" reads/writes dev-sandbox data. The
156
+ file being tracked is itself a hazard (flip-and-commit noise repoints everyone's dev server);
157
+ suggested a neutral committed default plus a gitignored `.env.development.local` — not done.
158
+ (apeterson)
134
159
  - 2026-08-27 — Fixed `_production` never declaring `@agilant/toga-blox` (no dependency entry, no
135
160
  lock entry, ~200 imports) — it resolved only via a local **symlink**, and `npm ci` in `preBuild`
136
161
  would have failed. Added blox plus the four peers the symlink had masked
@@ -4,5 +4,5 @@
4
4
  |-----|-----------|---------|-------|
5
5
  | [NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)](features/netsuite-transfer-order-import.md) | 1.0 | NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk pattern), so the shared NetSuite → TOGa Supply importer (`App_ | library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/netsuite.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/common_sync_togasupply.php |
6
6
  | [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
7
- | [NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement](features/transfer-order-inventory-quantities.md) | 2.0 | The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity model** on the NYCHH `Item` override, plus the **client-override | _underscore/Model/Nychh/Item.php, _underscore/Model/Nychh/TransferOrder.php, _underscore/Model/Nychh/TransferOrderStage.php, _underscore/Model/Client/TransferOrder.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql |
8
- | [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
7
+ | [NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement](features/transfer-order-inventory-quantities.md) | 2.0 | The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity model** on the NYCHH `Item` override, plus the **client-override | _underscore/Model/Nychh/Item.php, _underscore/Model/Nychh/TransferOrder.php, _underscore/Model/Nychh/TransferOrderStage.php, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
8
+ | [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.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/Client/2026-08-28b - TransferOrderItemsTimestamps.sql |
@@ -6,18 +6,23 @@ project: _Underscore
6
6
  client: nychh
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-27
10
- owners: [jcardinal]
9
+ updated: 2026-08-28
10
+ owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - _underscore/Model/Nychh/Item.php
13
13
  - _underscore/Model/Nychh/TransferOrder.php
14
14
  - _underscore/Model/Nychh/TransferOrderStage.php
15
15
  - _underscore/Model/Client/TransferOrder.php
16
+ - _underscore/Model/Client/TransferOrderItem.php
16
17
  - dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql
17
18
  - dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql
19
+ - dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql
20
+ - dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql
18
21
  related:
19
22
  - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
20
23
  - ../../../2.0/apps/api2/features/v2-api-error-codes.md
24
+ - ../../../2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md
25
+ - ../../../2.0/apps/toga25-supply/features/transfer-orders-page.md
21
26
  - ../../growrk/features/transfer-order-flow.md
22
27
  - ./netsuite-transfer-order-import.md
23
28
  - ../profile.md
@@ -31,6 +36,10 @@ that let a NetSuite-sourced TransferOrder actually POST through the V2 API. The
31
36
  feeds these POSTs is in
32
37
  [NYCHH NetSuite → TransferOrders import](./netsuite-transfer-order-import.md).
33
38
 
39
+ > 🚨 **Most of that enablement was hand-applied and never written into `dbchanges2`** (four layers —
40
+ > see below). It was reconstructed on 2026-08-28; until those files run, a fresh environment can
41
+ > reproduce neither the UI nor the importer.
42
+
34
43
  The general, framework-level lessons from the field-exposure work here are recorded on the shared
35
44
  [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md) and
36
45
  [V2 API error codes](../../../2.0/apps/api2/features/v2-api-error-codes.md) docs; this doc keeps the
@@ -83,6 +92,51 @@ peeled in order:
83
92
  > dead end.) Recorded framework-wide on the
84
93
  > [ACL permission chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md).
85
94
 
95
+ ## 🚨 The enablement above was HAND-APPLIED — four layers exist on prod with no migration behind them
96
+
97
+ Discovered 2026-08-28 while standing the Transfer Orders UI up locally: beyond the two 08-27 files,
98
+ **production and sandbox-client carry transfer-order state that no `dbchanges2` file creates.** It
99
+ was all applied by hand when the NetSuite TransferOrder POST was enabled:
100
+
101
+ | Layer | What is missing from the repo |
102
+ |---|---|
103
+ | a | `AclRecordPermissions` for records **312** (`transfer-orders`) / **313** (`transfer-order-items`), roles **1** and **3** |
104
+ | b | their `AclLogicGroups` → `AclLogicGroupExpressions` → `AclRecordExpressions` chain |
105
+ | c | `AclFieldPermissions` for **all 17 `RecordFields`** of 312/313 (roles 1 and 3, `isWritable = 1`) |
106
+ | d | the `TransferOrderItems.dtCreated` / `dtUpdated` **columns** |
107
+
108
+ **Consequence: a fresh environment cannot reproduce production**, and that is not just a UI problem
109
+ — **the NetSuite transfer-order importer cannot be stood up in a fresh environment either.** Local
110
+ had none of it and failed **one layer at a time, each with a different-looking error** (a dropped
111
+ column, an `EZ-1`, a 500), which is what makes this class of drift so expensive to diagnose.
112
+
113
+ **`Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql`** reproduces (a)–(c) **faithfully — same
114
+ roles, same permission bits as prod. It is not a widening.** Role **2** (Developer) is deliberately
115
+ **not** granted: prod and sandbox don't have it, and role 1 satisfies the UI. Every statement is
116
+ anti-joined on natural keys, so it inserts **2 expressions + 4 record permissions + 34 field
117
+ permissions on local and 0 on sandbox** — dry-run verified in both directions.
118
+ Layer (d) is `Client/2026-08-28b` (see the next section).
119
+
120
+ **Follow-up, recorded not done:** a systematic `information_schema` diff of local vs prod across the
121
+ whole transfer-order area, instead of discovering gaps one 500 at a time — procedure in
122
+ [client schema drift audit](../../../2.0/apps/dbchanges2/workflows/client-schema-drift-audit.md).
123
+
124
+ ## `TransferOrderItems` is missing `dtCreated`/`dtUpdated` in the BLANK TEMPLATE — every tenant
125
+
126
+ `_Model_Client_TransferOrderItem` declares `dtCreated`/`dtUpdated` as
127
+ `FIELD_DATETIME_CREATED`/`FIELD_DATETIME_UPDATED`, so `_Model` puts them in **every SELECT it
128
+ builds** — nothing in a request has to ask for them. But the blank-client template
129
+ (`Client/2026-06-03- BLANK_CLIENT_DATABASE.sql`, `CREATE` at ~line 9462) **does not create them**.
130
+ So **every tenant provisioned from the template has a table the model cannot read.** This is not
131
+ NYCHH-specific; NYCHH is just where it surfaced.
132
+
133
+ `Client/2026-08-28b - TransferOrderItemsTimestamps.sql` adds both columns (+ their indexes) and,
134
+ guarded, `qtyCommitted`. ⚠ **The template itself still needs fixing** or the next new tenant
135
+ reproduces the defect — not done (it is a generated dump plus an already-run migration). Recorded on
136
+ [new-client onboarding](../../../2.0/apps/dbchanges2/workflows/client-onboarding.md); the
137
+ conditional-`ALTER` authoring technique the file uses is in
138
+ [re-runnable additive migrations](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md).
139
+
86
140
  ## `isActive` is defaulted in the ORM, not sent in the payload (shared model)
87
141
 
88
142
  `TransferOrders.isActive` is **NOT NULL**, but `isActive` is **not a registered/writable V2 field** —
@@ -104,6 +158,16 @@ pattern (default a NOT NULL non-writable column in the model, never send it) is
104
158
 
105
159
  ## Change history
106
160
 
161
+ - 2026-08-28 — Discovered that the transfer-order enablement on **prod and sandbox-client is
162
+ UNTRACKED**: record ACL for 312/313 (roles 1 and 3), their logic-group/expression chain, field ACL
163
+ for all 17 RecordFields, and the `TransferOrderItems.dtCreated`/`dtUpdated` columns all exist with
164
+ **no `dbchanges2` file behind them** — so a fresh environment can reproduce neither the UI nor the
165
+ NetSuite importer, and local failed one layer at a time with a different-looking error each time.
166
+ Wrote `Client_Nychh/2026-08-28c` (faithful reproduction of the ACL layers — same roles, same bits,
167
+ role 2 deliberately excluded; 2+4+34 rows on local, 0 on sandbox) and `Client/2026-08-28b` for the
168
+ columns. Root cause of the column gap is the **blank-client template**, which never creates the two
169
+ timestamps the model always selects — every tenant is affected and the template is still unfixed.
170
+ (apeterson)
107
171
  - 2026-08-27 — Built the NYCHH two-branch inventory quantity model on `_Model_Nychh_Item` (on hand =
108
172
  received − fulfilled; available = on hand − committed; fulfilled = Σ shipped TO fulfillments;
109
173
  backordered = ordered − received; added `_qtyOnOrder`), per the 2026-08-17 TOGa Supply meeting.
@@ -25,6 +25,8 @@ files:
25
25
  - dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql
26
26
  - dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql
27
27
  - dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql
28
+ - dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql
29
+ - dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql
28
30
  related:
29
31
  - ../../2.0/apps/_underscore/features/tracking-number-bridges.md
30
32
  - ../../2.0/apps/_underscore/features/tableview-joins.md
@@ -72,8 +74,17 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
72
74
  tenant already grants Sales Orders and Inventory — NYCHH does not scope navigation by role). The
73
75
  Core surfaces and the stage colour tokens are **shared/neutral**, hidden for every other client
74
76
  until they opt in. Not yet run on sandbox-client or production; **beta is blocked** because it
75
- lacks the `TransferOrderStages` table. See
77
+ lacks the `TransferOrderStages` table. A **Create Transfer Order** modal is started (UI built, data
78
+ layer flagged off pending three open data-model questions). See
76
79
  [Transfer Orders page](../../2.0/apps/toga25-supply/features/transfer-orders-page.md).
80
+ - 🚨 **The transfer-order ACL + schema on prod/sandbox are UNTRACKED (2026-08-28).** Record and field
81
+ ACL for records 312/313, their logic-group/expression chain, and the
82
+ `TransferOrderItems.dtCreated`/`dtUpdated` columns were all hand-applied when the NetSuite
83
+ TransferOrder POST was enabled — **no `dbchanges2` file creates any of it**, so a fresh environment
84
+ can reproduce neither the UI nor the importer. Reconstructed in `Client_Nychh/2026-08-28c` (ACL,
85
+ faithful — role 2 deliberately excluded) and `Client/2026-08-28b` (columns; root cause is the blank
86
+ client template, which affects **every** tenant). See
87
+ [NYCHH transfer-order inventory & quantities](./features/transfer-order-inventory-quantities.md).
77
88
  - **Two-branch inventory quantities + TransferOrder V2 enablement** — NYCHH-only on-hand/available/
78
89
  fulfilled/backordered model on `_Model_Nychh_Item`, plus the client-override models + ACL rows that
79
90
  let a NetSuite TransferOrder POST succeed. See
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.692",
3
+ "version": "1.0.693",
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",