toga-ai 1.0.778 → 1.0.780

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.
@@ -4,7 +4,7 @@
4
4
  |-----|---------|-------|
5
5
  | [Proposed — git-sourced base+overlay JSON authoring for the Surface layer](architecture/surface-authoring-proposal.md) | A **proposal / handoff recommendation** (not implemented) that the Surface layer's *authoring* model move off hand-authored SQL against the `SurfaceOverrides` E | _underscore/Model/Core/Surface.php, _underscore/Model/Client/SurfaceOverride.php |
6
6
  | [_underscore Framework Architecture](architecture.md) | `_underscore` is the shared PHP backend framework for **all 2.0 applications**. | _underscore/_underscore.php, _underscore/Loader.php, _underscore/Framework.php, _underscore/Model.php, _underscore/Database.php, _underscore/Query.php, _underscore/Route.php, _underscore/Component.php |
7
- | [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, _underscore/Model/Core/Page.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql |
7
+ | [ACL Permission Chain (Record & Field Authorization)](features/acl-permission-chain.md) | Authorization in the 2.0 API is **metadata-driven**: whether a role may Create/Read/Update/Delete a record is decided by rows across **four linked tables**, not | api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, _underscore/Model/Core/Page.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/TrackingNumber.php, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-02c - TrackingNumberNeedsReturnLabelFieldPermission.sql, dbchanges2/Client/2026-07-22c - TrackingNumberMeasureIdsFieldPermission.sql, dbchanges2/Client_Quad/2026-08-24 - Quad Multi Currency Item Pricing.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, dbchanges2/Client_Nychh/2026-09-03a - TransferOrdersStatusBadge.sql |
8
8
  | [Address Uniqueness Normalization (unit identifier + 5-digit ZIP comparison)](features/address-uniqueness-normalization.md) | When a business rule says *"only one X per physical address"*, comparing address rows field-for-field does **not** work: the same dwelling is spelled many diffe | _underscore/Model/Rate/Entitlement.php |
9
9
  | [Address Validation (carrier waterfall + validateAddress scripted endpoint)](features/address-validation.md) | `_Model_Client_Address::validateAddress` verifies a US address against a **carrier waterfall (USPS → FedEx → UPS)** and returns a single canonical, carrier-norm | _underscore/Model/Client/Address.php, _underscore/Component/Library/Carriers/Usps/Usps.php |
10
10
  | [_ApiRequest — JSON encode/decode & api-logging behavior](features/apirequest-json-content-type.md) | `_ApiRequest` is the 2.0 outbound HTTP client. | _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, _underscore/Component/Api/Clickup/Clickup.php |
@@ -22,6 +22,7 @@ files:
22
22
  - dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql
23
23
  - dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql
24
24
  - dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql
25
+ - dbchanges2/Client_Nychh/2026-09-03a - TransferOrdersStatusBadge.sql
25
26
  ---
26
27
 
27
28
  ## Summary
@@ -97,6 +98,17 @@ Two distinct failure shapes, and the second is a page-killer:
97
98
  `undefined`. Code that checks `=== null` will not detect it.
98
99
  - **A requested JOINED field with no grant fails the ENTIRE request with `EZ-2`.** One missing grant
99
100
  on one joined column therefore blanks every record the page asked for — not just that column.
101
+ - **⚠ An ungranted `uuid` fails the ENTIRE request too — even though you never asked for it.**
102
+ `uuid` is V2's `IDENTIFIER_FIELD` (`api2 Component/Api/V2/V2.php:13`) and is **force-added to every
103
+ record read**, so it is always "requested" whether or not it appears in your `fields=` list. If the
104
+ calling role has no `AclFieldPermissions` grant on that record's `uuid` RecordField, the response is
105
+ **403 `EZ-2`** naming `"fields": ["uuid"]`. This is the opposite of the first bullet: an ordinary
106
+ ungranted field is silently dropped, `uuid` is fatal.
107
+
108
+ **Rule: grant the `uuid` RecordField alongside every field you actually read.** Seen 2026-09-03 on
109
+ `/transfer-order-statuses` — the fetch asked only for `slug,name,colorHex` and still 403'd on
110
+ `uuid`. The error text is self-diagnosing here: an `EZ-2` whose `fields` list contains a field you
111
+ never sent is this.
100
112
 
101
113
  **Two practices follow directly:**
102
114
 
@@ -652,6 +664,14 @@ hardcoded `Core.RecordFields` id literals instead of a subselect.
652
664
  and every repo is on the **same branch** so the generated model matches the DB.
653
665
 
654
666
  ## Change history
667
+ - 2026-09-04 — ⚠ Recorded that an **ungranted `uuid` 403s the whole request**, unlike an ordinary
668
+ ungranted field which is silently dropped: `uuid` is V2's `IDENTIFIER_FIELD`
669
+ (`api2 Component/Api/V2/V2.php:13`) and is force-added to **every** record read, so a role with no
670
+ `AclFieldPermissions` row on that record's `uuid` RecordField gets `EZ-2` listing
671
+ `"fields": ["uuid"]` even when `uuid` was never requested. Hit on `/transfer-order-statuses` while
672
+ wiring the transfer-order status badge (the fetch asked only for `slug,name,colorHex`). Rule: grant
673
+ the `uuid` RecordField alongside the fields you read; and an `EZ-2` naming a field you never sent
674
+ is this. (apeterson)
655
675
  - 2026-09-04 — Recorded the **custom-field silent-omission trap** after a production incident (Rate
656
676
  AIG): a `c_` field with no `AclCustomFieldPermissions` row for the calling role is **dropped from a
657
677
  GET response with HTTP 200** and a correct `totalRecordCount` when `fields=` is not sent — the
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-03
10
10
  owners: [jcardinal, apeterson, bala]
11
11
  files:
12
12
  - _underscore/Model/Client/Language.php
@@ -155,6 +155,32 @@ boolean rows; the item-record modal (the first boolean consumer) exposed the gap
155
155
  `_underscore`, so it takes effect only once the framework branch is deployed (framework is pulled at
156
156
  deploy).
157
157
 
158
+ ### 🚨 A message key inside `config` JSON is NEVER resolved — copy must ride a `*MessageId` column
159
+
160
+ `_buildBundle`/`_serializeElement` build `bundle.messages` by scanning **exactly six columns** on
161
+ `SurfaceElements` — `labelMessageId`, `tooltipMessageId`, `placeholderMessageId`,
162
+ `disabledTooltipMessageId`, `trueMessageId`, `falseMessageId` (`Surface.php` ~L600). **`config` is
163
+ passed through untouched and is never scanned for message keys.** A key written into `config`
164
+ therefore ships to the frontend with **no value behind it**, and the screen renders the raw key.
165
+
166
+ This is a load-bearing constraint on how *any* surface-driven copy is modelled: **one string = one
167
+ `SurfaceElement` message column.** There is no way to attach copy to a nested `config` blob.
168
+
169
+ **Real case (2026-09-03).** The Columns modal rendered `common.columns.modal.apply` as its Apply
170
+ button label. `Core/2026-08-28b` had put the seven `common.columns.modal.*` keys inside the
171
+ `columnsButton` element's `config.modal` — data that looks right and can never resolve. The chosen
172
+ fix was **not** to teach the resolver to scan `config` (that widens the message-resolution contract
173
+ for every surface); it was to give the modal **its own MODAL Surface** so every string rides a real
174
+ `*MessageId` on its own element and the existing collector resolves it with **zero `_underscore`
175
+ change**. See
176
+ [column-visibility](../../toga25-supply/features/column-visibility.md).
177
+
178
+ **Diagnostic order.** When the FE shows a raw message key, first check whether the key rides a
179
+ `*MessageId` column — *before* suspecting an unrun seed. The resolved bundle in the API response is
180
+ the proof: if the element is present but `bundle.messages` has no entry for the key, the seed ran and
181
+ the modelling is wrong. (This session's first diagnosis — "the seed hasn't run" — was wrong, and the
182
+ pasted API response is what disproved it.)
183
+
158
184
  ## FIELD elements bound to a RecordField — see the dedicated doc
159
185
 
160
186
  A `renderType = FIELD` element with a non-null `recordFieldId` gets `Core.RecordFields` metadata and
@@ -755,6 +781,13 @@ full stop. See
755
781
  match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
756
782
 
757
783
  ## Change history
784
+ - 2026-09-03 — Recorded a **hard constraint on message resolution**: the resolver collects
785
+ `bundle.messages` from **only** the six `*MessageId` columns on `SurfaceElements`; a message key
786
+ placed inside `config` JSON is **never** resolved and renders raw on screen. Found when the Columns
787
+ modal displayed `common.columns.modal.apply`; `Core/2026-08-28b` had seeded those keys into
788
+ `config.modal`. Fixed in data, not in the framework — the modal became its own MODAL Surface
789
+ (Core 60) so each string rides a real `*MessageId`. Also recorded the diagnostic order: a raw key
790
+ means "check the modelling" before "check whether the seed ran". (apeterson)
758
791
  - 2026-09-01 — Split the new **FIELD ↔ RecordField binding** capability out into its own doc
759
792
  ([surface-field-recordfield-binding](surface-field-recordfield-binding.md)) rather than growing this
760
793
  one: for a `renderType=FIELD` element with a `recordFieldId`, `_buildBundle` now folds on
@@ -12,7 +12,7 @@
12
12
  | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, plus item **feature** text — `Features.name`, `ItemCategoryFeatureGroups.name`, `ItemFeatures | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, _underscore/Model/Client/FeatureTranslation.php, _underscore/Model/Client/ItemCategoryFeatureGroupTranslation.php, _underscore/Model/Client/ItemFeatureTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Client/2026-07-13a - FeatureTranslations.sql, dbchanges2/Client/2026-07-13b - FeatureTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql, dbchanges2/Core/2026-07-13 - FeatureTranslationsRecord.sql |
13
13
  | [/auth/login resolves the client from the email domain, not the Bearer token (cross-client user path)](features/login-cross-client-user-resolution.md) | `POST /v2/auth/login` (email/password user login) can silently swap the target client mid-request. | api2/Component/Api/V2/V2.php, _underscore/String.php |
14
14
  | [Nested FK object embedding is gated by the CHILD record's own ACL](features/nested-fk-acl-embedding.md) | When the V2 JSON engine serializes a foreign-key field into a **nested object** (in `getFullModelData()`, ~V2.php L6016-6060), it re-checks the **child** record | api2/Component/Api/V2/V2.php, dbchanges2/Client_Compass/2026-07-23b - PurchaseOrdersRecordReadAcl.sql |
15
- | [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/ContactEmailAddress.php |
15
+ | [Nested-relationship writes & child matching (link vs. create)](features/nested-relationship-writes.md) | When a 2.0 API write payload (`POST`/`PUT`) contains a **nested related object** (e.g. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/ContactEmailAddress.php, _underscore/Model/Client/TransferOrderStage.php |
16
16
  | [Record Scripts (computed/aggregate /v2 endpoints — the authoring contract)](features/record-scripts.md) | In api2 you almost never write a controller. | api2/Component/Api/V2/V2.php, _underscore/Model/Team/Sprint.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Query.php |
17
17
  | [V2 Request Logging & Where Requests Land (client vs core log DB)](features/request-logging.md) | The V2 engine logs **every inbound request** — success *and* failure, with response code and payload — and routes each log entry to the **client** log or the ** | api2/Component/Api/V2/V2.php, api2/Controller/Index.php, _underscore/Model/Client/Logs/Api.php, _underscore/Model/Core/Logs/Api.php |
18
18
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
@@ -6,11 +6,12 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-26
10
- owners: ["bala", "mhammontree", "tcox"]
9
+ updated: 2026-09-04
10
+ owners: ["bala", "mhammontree", "tcox", "apeterson"]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
13
13
  - _underscore/Model/Client/ContactEmailAddress.php
14
+ - _underscore/Model/Client/TransferOrderStage.php
14
15
  related:
15
16
  - ../architecture.md
16
17
  - ../../toga2-supply/features/currency-amount-lines-editor.md
@@ -311,6 +312,33 @@ completely invisible to the UI — see
311
312
  Same family as the 201-with-dropped-data case above: **a success status from V2 proves only that the
312
313
  parent was written.** Assert child row counts, or write children explicitly.
313
314
 
315
+ ## ⚠ Not every record can be linked by `uuid` — read `searchableIdentifierFields`
316
+
317
+ `{ uuid }` is the house convention for a nested FK, and it works so consistently that it reads like a
318
+ platform guarantee. **It is not.** A record is linkable only by the fields flagged as identifiers,
319
+ and a handful of lookup tables have **no `uuid` identifier at all**. Sending `{ uuid }` to one of
320
+ those is a **400 `EV-12`**, not a silent miss.
321
+
322
+ ```
323
+ // WRONG — TransferOrderStages has no uuid identifier
324
+ transferOrderStage: { uuid: "…" }
325
+ // → 400 EV-12, "searchableIdentifierFields": ["id","c_netsuiteInternalTransferOrderStatus"]
326
+
327
+ // CORRECT
328
+ transferOrderStage: { id: 7 }
329
+ ```
330
+
331
+ - **The error body is self-diagnosing — read `searchableIdentifierFields` first.** It lists exactly
332
+ what that record accepts. No DB query needed.
333
+ - **When you fall back to `id`, resolve it at request time by a stable natural key (name/slug), never
334
+ hardcode it.** `id` values differ per tenant database, so a literal that works in one client DB
335
+ points at a different row (or nothing) in another. The transfer-order create modal looks its
336
+ opening stage up by name `"Pending"` on each request for exactly this reason.
337
+ - **The other EV-12 flavor is different.** `searchableIdentifierFields: ["id"]` on a nested FK
338
+ resolved *through* a custom field means a `c_` field is missing `isIdentifier = 1` — that is a
339
+ metadata bug to fix (see above), whereas "this record genuinely has no `uuid`" is a payload shape
340
+ to accept.
341
+
314
342
  ## Gotcha
315
343
 
316
344
  - **Nested write with only a non-identifier field silently creates duplicates.** This is a
@@ -322,6 +350,14 @@ parent was written.** Assert child row counts, or write children explicitly.
322
350
 
323
351
  ## Change history
324
352
 
353
+ - 2026-09-04 — ⚠ Recorded that **`{ uuid }` is a convention, not a guarantee**: some lookup records
354
+ (e.g. `TransferOrderStages`) have **no `uuid` identifier**, so a nested `{ uuid }` is a hard 400
355
+ `EV-12` naming `searchableIdentifierFields: ["id","c_netsuiteInternalTransferOrderStatus"]`. Read
356
+ that list first — it is self-diagnosing — and when falling back to `{ id }`, resolve the id at
357
+ request time from a stable natural key (name/slug), because ids differ per tenant DB. Distinguished
358
+ this from the existing `searchableIdentifierFields: ["id"]` flavor, which is a missing
359
+ `isIdentifier` on a `c_` field. Found building the transfer-order create POST. (apeterson)
360
+
325
361
  - 2026-08-26 — Added: **the UPDATE path sets status 200 unconditionally after its children loop**, so
326
362
  a nested `PUT` reports success even when a child write failed (unique-key rejection, ACL denial).
327
363
  Any parent-with-children editor that needs to know whether the children landed must write them
@@ -4,7 +4,7 @@
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
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/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.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 | dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, 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 |
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 | dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, 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, dbchanges2/Core/2026-09-03c - ColumnsModalSurfaceSeed.sql, dbchanges2/Core/2026-09-03d - ColumnsModalRetireConfigModal.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, _underscore/Model/Client/RecordFieldSetting.php, _underscore/Model/Client/ClientRecordFieldSetting.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 |
@@ -278,6 +278,17 @@ concluding a migration is production-safe.
278
278
  **`(id, uuid, dtCreated, messageKey, name, defaultValue)`** — a guess of `slug` / `message`
279
279
  (inferred from a sibling's values row) died on `Unknown column 'slug'`. When in doubt,
280
280
  `SHOW COLUMNS FROM <Table>` costs one round-trip.
281
+ - **⚠ Rewriting a `VALUES` seed as `INSERT … SELECT` silently FLATTENS per-row columns.** The rewrite
282
+ is the normal way to add a `NOT EXISTS` guard, and it is easy to carry one row's value into the
283
+ constant list for every row. Caught 2026-09-03: converting a `Core.SurfaceElements` seed hardcoded
284
+ `'NEUTRAL'` as the `variant` for all 11 elements, losing the `PRIMARY` / `SECONDARY` ones. Nothing
285
+ errors — the enum accepts it. **Diff the rewritten file column-by-column against the original
286
+ `VALUES` rows before running it**, and treat any column that differed between rows as suspect.
287
+ - **⚠ A scalar sub-select for a foreign key writes `NULL` when it misses — it does not fail.**
288
+ `(SELECT id FROM Core.Messages WHERE messageKey = '…')` returns `NULL` if the key was never seeded,
289
+ and a nullable FK column takes it, leaving a structurally broken row that looks inserted. Prefer a
290
+ `JOIN` (so a missing lookup inserts **no row** at all) or add the lookup to the guard's
291
+ `WHERE EXISTS`.
281
292
  - **`Core.RecordFields` has NO `sql` column.** A calculated field's SQL is **not** stored in
282
293
  metadata — it lives in the `_underscore` model as a `FIELD_SQL` static method. Do not go looking
283
294
  for a migration to change a calculated field's expression; there is none. See
@@ -288,7 +299,11 @@ concluding a migration is production-safe.
288
299
  `SEQ_IN_INDEX = 1`**, not on `INDEX_NAME` — an existing index under a different name goes
289
300
  undetected by a name-based guard and you add a duplicate. Found while sizing (and then dropping)
290
301
  the Compass `Users` search indexes. (ajean)
291
-
302
+ - 2026-09-03 — Two silent-failure traps found self-auditing a `Core.SurfaceElements` seed against the
303
+ team rules: (1) rewriting a `VALUES` seed into `INSERT … SELECT` to add the `NOT EXISTS` guard
304
+ **flattened** the per-row `variant` to a constant `'NEUTRAL'`, dropping `PRIMARY`/`SECONDARY` with
305
+ no error; (2) a scalar sub-select used for a message FK writes `NULL` rather than failing when the
306
+ key is missing. Both leave a row that looks inserted and is wrong. (apeterson)
292
307
  - 2026-09-01 — Added the **fan-out rollback rule**: a `Client/` grant migration that fans out across
293
308
  every tenant must use **hardcoded v4 uuid literals**, not the generator, so its rollback
294
309
  (`DELETE … WHERE uuid IN (…)`) can only touch rows that file created. A natural-key rollback on
@@ -101,10 +101,13 @@ files:
101
101
  - dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql
102
102
  - dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql
103
103
  - dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql
104
+ - dbchanges2/Core/2026-09-03c - ColumnsModalSurfaceSeed.sql
105
+ - dbchanges2/Core/2026-09-03d - ColumnsModalRetireConfigModal.sql
104
106
  related:
105
107
  - ../../_underscore/features/surface-resolver.md
106
108
  - ../../_underscore/features/sales-order-denial-reason.md
107
109
  - ../../toga25-supply/features/transfer-orders-page.md
110
+ - ../../toga25-supply/features/column-visibility.md
108
111
  ---
109
112
 
110
113
  ## What it is
@@ -1292,7 +1295,64 @@ before your own run — that handoff is a snapshot, and the migrations that cons
1292
1295
  uncommitted at the time. See
1293
1296
  [Transfer Orders page](../../toga25-supply/features/transfer-orders-page.md).
1294
1297
 
1298
+ ## A MODAL that needs surface-driven copy becomes its OWN Surface (`columns-modal`, Core 60, 2026-09-03)
1299
+
1300
+ **Rule: never park copy in `config` JSON.** The resolver collects `bundle.messages` from **only** the
1301
+ six `*MessageId` columns on `SurfaceElements`; `config` is passed through untouched and is never
1302
+ scanned. A message key seeded into `config` therefore has no value behind it and the screen renders
1303
+ the raw key. `Core/2026-08-28b` had put the seven `common.columns.modal.*` keys inside the
1304
+ `columnsButton` element's `config.modal` — data that looks right and can never resolve. See
1305
+ [surface-resolver](../../_underscore/features/surface-resolver.md).
1306
+
1307
+ The fix is a **data** fix, not a framework fix: `Core/2026-09-03c - ColumnsModalSurfaceSeed.sql`
1308
+ creates a shared `columns-modal` MODAL surface where **every string rides its own element's
1309
+ `labelMessageId`**, which the existing collector already resolves.
1310
+
1311
+ - **Surface 60** `columns-modal`; **Messages 293–298**; **SurfaceElements 222–232** (11 elements,
1312
+ `config.role` = `shownSection`, `hideAll`, `hiddenSection`, `showAll`, `reorder`, `pinToggle`,
1313
+ `visibilityToggle`, `reset`, `cancel`, `apply`, `close`).
1314
+ - **Elements are matched by `config.role`** — `SurfaceElements` has no `slug` column and the bundle
1315
+ exposes only a uuid as `key`. Same convention as `filterButton` / `columnsButton`.
1316
+ - **Element visibility gates CAPABILITY, not just chrome.** `isVisible = 0` on `pinToggle` removes
1317
+ pinning; on `reorder` removes drag-to-reorder. A tenant switches a feature off from data — the same
1318
+ marker-element idea as the Talos feature switch above, applied inside a shared component.
1319
+
1320
+ ### Split the seed from the cleanup so the rollout is order-independent
1321
+
1322
+ Retiring the dead `config.modal` is a **second** file, `Core/2026-09-03d -
1323
+ ColumnsModalRetireConfigModal.sql`, which must run **only after toga25-supply is deployed**. With the
1324
+ two split, either order is safe: seed-then-deploy leaves a harmless unused `config`, and
1325
+ deploy-then-seed is the normal path. Putting both in one file would make the migration a
1326
+ deploy-blocker.
1327
+
1328
+ > ⚠ Neither file has been run on **any** environment yet (as of 2026-09-03).
1329
+
1330
+ ### Id block consumed — and PROD did not match sandbox
1331
+
1332
+ The first id choice **collided on production**: `Messages` 289–294 had four already taken
1333
+ (`MAX(id)` = 292) and `Surfaces` **59 was taken**. Renumbered after querying the **production Core
1334
+ reader**. Sandbox Core differed substantially from prod — surface 59 was free on prod but taken on
1335
+ sandbox, and the `common.columns.modal.*` keys sit at **different ids per environment**.
1336
+
1337
+ This is the third confirmation of the rule above: verify a reserved block is free on **every**
1338
+ environment and take the **highest** `MAX(id)+1`, never the one in front of you, and never the repo.
1339
+
1340
+ **Handoff published (verify before your own run):** next `Core.Surfaces.id` = **61**, next
1341
+ `Core.SurfaceElements.id` = **233**, next `Core.Messages.id` = **299**. Both consuming files were
1342
+ still unrun at the time, so this is a reservation, not a deployed state.
1343
+
1295
1344
  ## Change history
1345
+ - 2026-09-03 — Recorded the rule that **a modal needing surface-driven copy becomes its own Surface**:
1346
+ the resolver reads messages only from `*MessageId` columns and never from `config`, so
1347
+ `Core/2026-08-28b`'s `config.modal` keys could never resolve and the Columns modal rendered raw
1348
+ keys. `Core/2026-09-03c` seeds a shared `columns-modal` MODAL surface (**60**, Messages **293–298**,
1349
+ SurfaceElements **222–232**) with one message column per string; elements are matched by
1350
+ `config.role`, and `isVisible = 0` on `pinToggle` / `reorder` gates those **capabilities** per
1351
+ tenant. Cleanup of the dead `config.modal` is deliberately a separate file (`Core/2026-09-03d`) that
1352
+ runs only after the FE deploy, so the rollout is safe in either order. The first id block collided
1353
+ on **production** (Messages `MAX(id)` = 292, Surfaces 59 taken) while sandbox looked free — third
1354
+ confirmation to verify a block on every environment. Handoff: next Surfaces = **61**,
1355
+ SurfaceElements = **233**, Messages = **299** (both files still unrun everywhere). (apeterson)
1296
1356
  - 2026-09-03 — Flagged that the Compass / Compass Canada stacked-Total `CONFIG` override must **not**
1297
1357
  gain a `"suffix":"+"`: blox renders a field `suffix` unconditionally while the stacked lease line
1298
1358
  auto-hides on null/0, so a seeded `+` dangles on non-lease orders. The joiner is conditional FE code
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-03
9
+ updated: 2026-09-04
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - api2/Config/production.ini
@@ -65,6 +65,20 @@ setpw() {
65
65
  `MYSQL_PWD` keeps the password out of `ps` and history the same way a defaults file does — use it
66
66
  when the client rejects `--defaults-extra-file` (some builds do).
67
67
 
68
+ **Check the password's line SHAPE without ever seeing the value.** Mask every character that is not
69
+ a space, tab, `;`, or `"` — you learn whether the value is quote-wrapped and whether an inline `;`
70
+ comment is in play, and the secret never reaches your screen or scrollback:
71
+
72
+ ```bash
73
+ awk -v S=database '$0=="["S"]"{f=1;next} /^\[/{f=0} f && /^password/{
74
+ k=$0; sub(/=.*/,"=",k); v=$0; sub(/^[^=]*=/,"",v); gsub(/[^ \t;"]/,"x",v);
75
+ print "[" k v "]"; exit}' Config/production.ini
76
+ ```
77
+
78
+ On `production.ini` `[database]` this printed `[password = "xxxxxxxx"]` (checked 2026-09-04): the
79
+ double-quote wrap is real, and there is **no inline `;` comment** on password lines — so `setpw`
80
+ above, which strips quotes only, is correct as written and needs no comment handling.
81
+
68
82
  **Keep the password out of your shell history and out of `ps`** by building a throwaway
69
83
  credentials file from the ini instead of passing `-p` on the command line:
70
84
 
@@ -93,10 +107,25 @@ Delete the file when you are done.
93
107
 
94
108
  ## Resetting a shared environment from production (dump → drop → reload)
95
109
 
96
- Used 2026-09-03 to bring sandbox-client's stale `Client_Nychh` TableViews back in line with prod.
97
- The developer runs every one of these; do not execute them for them.
98
-
99
- 1. **Dump each source database**, one file each — prod `Core` from
110
+ Used 2026-09-03 to bring sandbox-client's stale `Client_Nychh` TableViews back in line with prod,
111
+ and again 2026-09-04 (`Core` + `Client_Nychh`). The developer runs every one of these; do not
112
+ execute them for them.
113
+
114
+ 1. 🚨 **Inventory what the reset will DESTROY — before you drop anything.** A shared non-prod
115
+ environment is normally *ahead* of prod, because that is where changes get tried first, so a
116
+ prod→sandbox reload silently rolls that lead back. List every `dbchanges2` file dated after the
117
+ last prod release, plus anything parked in `git stash`, and decide which ones must be re-run.
118
+ Candidates from the 2026-09-04 sandbox-client reset, all *potentially* sandbox-only:
119
+ - `Core/` — 2026-09-01a, -01c, -01d, -02a, -02b, -03a, -03b, -03c, -03d
120
+ - `Client/` — 2026-09-01b, -01c, -02a
121
+ - `Client_Nychh/` — 2026-09-02a, -03a, -03b
122
+ - `dbchanges2` `stash@{0}` ("TO qty fulfilled core seed"), which is applied nowhere
123
+
124
+ **Verify every candidate against the environment — never assume the list.** After this reset
125
+ sandbox read `Core.Surfaces` = 54, which means some of those Core surface files were **already in
126
+ prod** and must not be re-run. The repo records intent, not deployed state — see
127
+ [Verifying whether a migration actually ran](./verifying-a-migration-ran.md).
128
+ 2. **Dump each source database**, one file each — prod `Core` from
100
129
  `reader1.core.database.togahub.com`, prod tenant from `reader1.client.database.togahub.com`:
101
130
  ```
102
131
  mysqldump --single-transaction --quick --routines --triggers --events \
@@ -104,14 +133,17 @@ The developer runs every one of these; do not execute them for them.
104
133
  ```
105
134
  `--set-gtid-purged=OFF` because RDS has GTID on; `--no-tablespaces` because the `admin` user
106
135
  lacks `PROCESS` (otherwise **error 1227**).
107
- 2. 🚨 **`tail -1` every dump file and confirm it ends with `-- Dump completed` BEFORE you drop
136
+ 3. 🚨 **`tail -1` every dump file and confirm it ends with `-- Dump completed` BEFORE you drop
108
137
  anything.** A truncated dump loads without complaining. DNS dropped mid-session on a host that
109
138
  had resolved seconds earlier (`ERROR 2005 Unknown MySQL server host`) — the dump just stops.
110
- 3. **Prove which environment each connection actually reached.** Better than `@@hostname`: check the
139
+ 4. **Prove which environment each connection actually reached.** Better than `@@hostname`: check the
111
140
  resolved IP — production resolves into **`10.201.x`**, sandbox into **`10.200.x`**.
112
- 4. **Drop + create the target schema, then pipe the dump in.**
113
- 5. **Verify with a row count known to differ** (step 1 of the procedure above), against the schema
114
- you intended to copy.
141
+ 5. **Drop + create the target schema, then pipe the dump in.**
142
+ 6. **Verify with a row count known to differ** (step 1 of *before you trust ANY tenant dump*,
143
+ above), against the schema you intended to copy.
144
+ 7. **Re-run the step-1 migrations that prod did not already have, in `Core/` → `Client/` fan-out →
145
+ `Client_<Tenant>/` order.** Client files reference Core record ids, and the client work needs the
146
+ columns the `Client/` fan-out adds — any other order fails or half-applies.
115
147
 
116
148
  ### ⚠ Which `mysql` / `mysqldump` binary you are running is load-bearing
117
149
 
@@ -120,10 +152,23 @@ The developer runs every one of these; do not execute them for them.
120
152
  `xamppfiles/bin/mysqldump` is **MariaDB 10.4** (rejects the flag);
121
153
  `xamppfiles/mysql/bin/mysqldump` is **MySQL 8.0.44** (correct). Run `mysqldump --version` before
122
154
  you trust it.
123
- - **A shell alias can hijack a remote connection.** XAMPP's `mysql` alias hardcodes
124
- `-uroot -pmysql --socket=…`, so a command you wrote for a remote host silently talks to localhost.
125
- Call the client by **full path** for anything remote — and remember an exact match with your local
126
- DB is evidence of a bad restore, not a good one (see the gotcha below).
155
+ - 🚨 **A shell alias hijacks a remote connection — and the error names the wrong problem.** On an
156
+ XAMPP machine `~/.zshrc:31` defines
157
+ `alias mysql='/Applications/XAMPP/xamppfiles/mysql/bin/mysql -u root -p<pw> --socket=…'`. A later
158
+ `-u admin` on the command line wins for the **user**, but the alias's `-p<pw>` **overrides
159
+ `MYSQL_PWD`** — so every `mysql -h <remote-host> -u admin …` dies with
160
+ `ERROR 1045 (28000): Access denied for user 'admin'@'<your ip>' (using password: YES)`, *against
161
+ the correct remote host*. That reads as a rotated credential, a missing grant, a VPN problem, or
162
+ the double-quote trap above, and it is none of them. (The same alias can also silently talk to
163
+ localhost when its `--socket` wins — two symptoms, one cause.)
164
+ - **The tell: `mysqldump` to the SAME host with the SAME `MYSQL_PWD` succeeds while `mysql`
165
+ 1045s.** Only `mysql` is aliased, not `mysqldump`. "Dump works, `mysql` denied" identifies the
166
+ alias immediately — check that before you go near the password extractor or the ini quoting.
167
+ - **Fix:** call the binary by full path
168
+ (`MYSQL=/Applications/XAMPP/xamppfiles/mysql/bin/mysql`). `\mysql` or `command mysql` also skip
169
+ alias expansion.
170
+ - An exact match with your local DB is evidence of a bad restore, not of a good one (see the
171
+ gotcha below).
127
172
 
128
173
  ## Gotchas
129
174
 
@@ -137,6 +182,12 @@ The developer runs every one of these; do not execute them for them.
137
182
  makes the dump a point-in-time snapshot, so a live table keeps moving after it: `Units` read 8,627
138
183
  on prod and 8,618 on the restored sandbox. Do not chase that; only a *category* difference
139
184
  (missing tables, order-of-magnitude counts) means the restore was wrong.
185
+ - **The reference counts written down here go stale too.** After the 2026-09-04 reset,
186
+ sandbox-client read `Core.Surfaces` = **54** and `Client_Nychh.TransferOrders` = **1,932**,
187
+ against the prod figures previously recorded here — 53 (2026-09-03) and 1,927 (2026-08-31).
188
+ Prod itself moved on. Restored counts landing slightly **above** an older recorded prod number
189
+ is the expected pattern, not evidence of a bad restore. Re-read the count from the source at
190
+ reset time instead of comparing to a number in this doc.
140
191
  - 🚨 **The `dbchanges2` folder name IS the target database — pipe each folder to its own DB.**
141
192
  `Core/` → `Core`, `Client_<Name>/` → that one tenant, `Client/` → **every** tenant. Files reference
142
193
  tables **unqualified** by design (the cluster-isolation rule), so running a `Core/` file against a
@@ -157,6 +208,17 @@ The developer runs every one of these; do not execute them for them.
157
208
  [Verifying whether a migration actually ran](./verifying-a-migration-ran.md).
158
209
 
159
210
  ## Change history
211
+ - 2026-09-04 — Second sandbox-client reset (`Core` + `Client_Nychh`) from prod; the procedure held,
212
+ three gaps closed. Added a mandatory **step 1 — inventory what the reset will destroy** (a shared
213
+ non-prod environment is normally ahead of prod, so the reload rolls that lead back), with the
214
+ 2026-09-04 candidate file list and the unapplied stash, plus a **step 7** to re-run them in
215
+ `Core/` → `Client/` → `Client_<Tenant>/` order. Sharpened the shell-alias bullet: the real symptom
216
+ is **`ERROR 1045` against the CORRECT remote host** (the alias's `-p<pw>` beats `MYSQL_PWD`), not a
217
+ silent localhost connection — and the tell is that `mysqldump` works while `mysql` is denied,
218
+ because only `mysql` is aliased. Added a value-safe way to inspect an ini password's line shape
219
+ (confirms the double-quote wrap and that there is no inline `;` comment, so `setpw` needs no
220
+ change). Refreshed the expected-drift numbers and noted that recorded reference counts age.
221
+ (apeterson)
160
222
  - 2026-09-03 — Added the **reset-a-shared-environment-from-production** procedure (the `mysqldump`
161
223
  flag set, `--set-gtid-purged=OFF` for RDS GTID, `--no-tablespaces` for the missing `PROCESS`
162
224
  grant, the mandatory `tail -1` "`-- Dump completed`" check because a truncated dump loads
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-31
9
+ updated: 2026-09-03
10
10
  owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga-blox/src/components/BaseButton
@@ -135,6 +135,28 @@ declares, on class names `getStickyClass` never emits. Dead rules like this look
135
135
  someone greps for the divider. Both were deleted 2026-08-31; grep the emitting code before trusting a
136
136
  CSS rule, and check the token it names actually exists in a host theme.
137
137
 
138
+ ### 6. Button HEIGHT is a literal with no token — a toolbar cannot be sized from the host theme
139
+
140
+ Defect 4 is about swapping variants; this is the shape of the same problem for a consumer who never
141
+ swaps anything. Measured 2026-09-03 in
142
+ `dist/components/BaseButton/BaseButton.module.css`:
143
+
144
+ - `.secondaryBtnAction` hard-codes **`height: 26px`** — **no `var(--…)`, so no host token can reach
145
+ it**.
146
+ - `.primaryBtn` declares **no height at all** (computes to ~25px).
147
+ - `font-size` **is** tokened (14px default).
148
+
149
+ **Consumer symptom.** One page toolbar renders its buttons at three different heights, and the
150
+ developer cannot fix it from `themeConfig.json` — the number they need to change is not exposed.
151
+ Nothing in the theme layer hints that geometry is off-limits, so the time is lost hunting the app's
152
+ own CSS.
153
+
154
+ **Fix direction.** Same as defect 5: convert both to `var(--baseButton-height, <existing literal>)`
155
+ (and give `.primaryBtn` the same base height), which is non-breaking because the fallback preserves
156
+ today's look. Until then a consumer must own the chrome: `toga25-supply` built its own
157
+ `ToolbarButton` rather than retheme tokens shared by ~all buttons in the app — see
158
+ [toolbar button chrome](../../toga25-supply/features/toolbar-button-chrome.md).
159
+
138
160
  ## Already-known authoring items (carried over from §22)
139
161
 
140
162
  These are long-standing and were previously recorded only as consumer-side advice:
@@ -168,6 +190,12 @@ These are long-standing and were previously recorded only as consumer-side advic
168
190
  time**, with no PR. Sequence auth fixes deliberately.
169
191
 
170
192
  ## Change history
193
+ - 2026-09-03 — Added styling defect **6: button height is a literal with no token**.
194
+ `.secondaryBtnAction` hard-codes `height: 26px` and `.primaryBtn` sets no height at all, so a
195
+ consumer cannot size a toolbar from the host theme and the cause is invisible from the theme layer.
196
+ Found building toga25-supply's page toolbar (three buttons, three heights); compensated by an
197
+ app-owned `ToolbarButton`, not by retheming shared tokens. Fix direction is defect 5's:
198
+ `var(--baseButton-height, <literal>)`, non-breaking via the fallback. (apeterson)
171
199
  - 2026-08-31 — Added styling defect **5: hardcoded visual values in the shared stylesheets cannot be
172
200
  removed by a consumer** — a literal has no host-token escape hatch. Worked example: the `supply`
173
201
  table card shadow, which a toga25-supply design review needed gone; converted to
@@ -8,7 +8,7 @@
8
8
  | [Cart Notification Emails — duplicate prevention](features/cart-notification-emails.md) | On the cart "Notifications" section a user can add CC email addresses to an order. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/stores/useEmailOptionsStore.ts, src/stores/useCartSalesQuoteZu.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts |
9
9
  | [Cart Order-Total & Shipping Computation](features/cart-order-total-computation.md) | The Cart summary section (subtotal / shipping / tax / total) is **data-driven** from `cartData`. | toga2-commerce/src/pages/Cart/viewModel/useCartViewModel.ts, toga2-commerce/src/pages/Cart/CartPage.tsx, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/api/CartApi.ts |
10
10
  | [Cart Page — config-driven form architecture (current state + planned refactor)](features/cart-page-config-architecture.md) | The Cart page (`src/pages/Cart/`) is the most config-heavy page in `toga2-commerce`. | src/pages/Cart/CartPage.tsx, src/pages/Cart/view/cartForm/CartForm.tsx, src/pages/Cart/view/cartForm/CartFormSection.tsx, src/pages/Cart/view/cartForm/CartFormRenderer.tsx, src/pages/Cart/view/EditCart.tsx, src/pages/Cart/view/EditOrder.tsx, src/pages/Cart/viewModel/useEditOrderOrEditCartViewModel.ts, src/pages/Cart/viewModel/FIELDS/*/*/*/CARTPAGE.ts, src/hooks/useAssignClientFields.ts |
11
- | [Catalog cache freshness — the 24h persisted query cache, and how to opt a query out of it](features/catalog-cache-freshness.md) | TOGa Commerce runs a **single `QueryClient` with a 24-hour default `staleTime`**, and persists it to **`localStorage["commerce"]`** through `PersistQueryClientP | toga2-commerce/src/App.tsx, toga2-commerce/src/contexts/AuthContext.tsx, toga2-commerce/src/pages/ItemsView/viewModel/useItemDetailsViewModel.ts |
11
+ | [Catalog cache freshness — the 24h persisted query cache, and how to opt a query out of it](features/catalog-cache-freshness.md) | TOGa Commerce runs a **single `QueryClient` with a 24-hour default `staleTime`**, and persists it to **`localStorage["commerce"]`** through `PersistQueryClientP | toga2-commerce/src/App.tsx, toga2-commerce/src/contexts/AuthContext.tsx, toga2-commerce/src/api/axiosInstance.ts, toga2-commerce/src/hooks/useAuthenticationFlow.ts, toga2-commerce/src/components/NavIcons/NavIconList.tsx, toga2-commerce/src/pages/BundleView/hooks/useBundleQueryData.ts, toga2-commerce/src/pages/Home/viewModel/useHomeViewModel.ts, toga2-commerce/src/pages/ItemsView/viewModel/useItemDetailsViewModel.ts |
12
12
  | [Category Tile Order (AssortmentItems.sortOrder) — merchandising a storefront category](features/category-tile-sort-order.md) | **"Move item X to the front of category Y" is a DATA change, not a code change.** The order of item tiles on a storefront category page is driven by exactly one | src/pages/Filter/api/FilterApi.ts, src/pages/Filter/viewModel/useFilterViewModel.ts, api2/Component/Api/V2/V2.php, toga2-supply/src/pages/Items/api/itemsApi.ts |
13
13
  | [Client Fields — per-tenant / language / role content & config](features/client-fields.md) | Almost no user-facing text, field layout, or page config is hard-coded in `toga2-commerce`. | src/pages/Account/view/MySettingsView.tsx, src/contexts/helpers/getLoginSettings.ts, src/pages/Account/viewModel/useAccountViewModel.ts, src/fieldsConfig/index.ts, src/fieldsConfig/getClientLoginFields.ts, src/fieldsConfig/clientFields/COMPASS.json, src/fieldsConfig/clientFields/COMPASSCANADA.json, src/fieldsConfig/clientFields/QUAD.json, src/pages/Cart/api/CartApi.ts, src/hooks/useAuthenticationFlow.ts, src/contexts/AuthContext.tsx, src/pages/Login/viewModel/useLoginPageViewModel.ts, src/hooks/useAssignClientFields.ts, src/hooks/useDynamicConditionalFieldOptions.ts, src/stores/useFieldsStore.ts, src/components/BaseDetailField/BaseDetailField.tsx, src/components/NavIcons/NavIconItem.tsx, src/components/Submenus/AlertSubmenu.tsx, src/components/Submenus/types.ts, src/pages/Account/AccountPage.tsx, src/pages/Account/view/MyOrdersView.tsx, src/pages/GetSupport/GetSupportPage.tsx, src/pages/GetSupport/viewModel/useGetSupportViewModel.ts, src/queries/queries.ts, src/App.tsx, src/pages/Filter/FilterPage.tsx, src/pages/Filter/viewModel/FIELDS/COMPASS/ENGLISH/USER/FILTERPAGEFIELDS.json, src/pages/Home/viewModel/FIELDS/COMPASS/ENGLISH/USER/HEADERFIELDS.json, src/components/AuthLayout/AuthLayout.tsx |
14
14
  | [Config-Driven Expedited Shipping Gating (Cart)](features/expedited-shipping-gating.md) | On the toga2-commerce **Cart** page, expedited shipping options (**"2nd Day EOB"** and **"Next Day Air"**) are only offered in the *Shipping Method* dropdown wh | toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/viewModel/FIELDS/shared/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/CartPage.tsx |