toga-ai 1.0.400 → 1.0.401

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.
@@ -23,7 +23,7 @@
23
23
  | [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
24
24
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
25
25
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql |
26
- | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.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, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
26
+ | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.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, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
27
27
  | [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
28
28
  | [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Growrk/2026-07-13a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
29
29
  | [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
9
+ updated: 2026-07-21
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - _underscore/Model/Core/Surface.php
@@ -36,6 +36,7 @@ files:
36
36
  - _underscore/Model/Client/MessageTranslation.php
37
37
  - _underscore/Model/Client/ThemeToken.php
38
38
  - _underscore/Model/Core/Page.php
39
+ - api2/Component/Api/V2/V2.php
39
40
  related:
40
41
  - ../../dbchanges2/features/surface-layer-schema.md
41
42
  - ../../api2/features/surface-meta-option.md
@@ -253,22 +254,34 @@ rule would hit. **Scope:** the Approve element on **surface 8** (`sales-order-re
253
254
  and **surface 3** (`sales-order-listing-row-actions`, el 29). The approval-workflow-modal Approve
254
255
  element does not exist yet and is **deferred**.
255
256
 
256
- > ⚠ **Not resolvable server-side today.** The rule carries a `{"type":"stepTwoAssigned"}` named node,
257
- > which the backend Tier-2 path cannot resolve (see the Tier-2 dispatch note above — no
258
- > `resolveSurfaceActionState`, no backend `stepTwoAssigned`) **and** the frozen Tier-1 FE evaluator
259
- > `evaluateSurfaceRule` **throws** on `{type}` nodes by design. Only the **legacy** FE engine
260
- > (`evaluateEnableRule`) can evaluate it today; enabling the surface path needs both (a) teaching
261
- > `evaluateSurfaceRule` the named check and (b) feeding it live approval-stage data. See
257
+ > ⚠ **Resolved on the FE, not server-side (Option B, 2026-07-21).** The rule carries a
258
+ > `{"type":"stepTwoAssigned"}` named node, which the backend Tier-2 path cannot resolve (no
259
+ > `resolveSurfaceActionState`, no backend `stepTwoAssigned`). Rather than build the Tier-2 capability,
260
+ > **Option B** teaches the FE `evaluateSurfaceRule` a `SURFACE_NAMED_RULES` escape hatch
261
+ > (`stepTwoAssigned`/`poNumberEntered`) and feeds it live approval-stage data on both consuming
262
+ > surfaces — an **intentional deviation** from the frozen-grammar/escalate-to-Tier-2 design note. The
263
+ > gate is now evaluated live on the FE. See
262
264
  > [surface-frontend](../../toga25-supply/features/surface-frontend.md).
263
265
  - **MANAGER**: opts into **approve + deny only** (`IS_VISIBLE=1`); rules left at the Core base
264
266
  (`pendingApproval` only, single-stage). No approvals-filter override → inherits Core `isVisible=0`
265
267
  (hidden).
266
- - **Quad** — Approve + Deny only, visible for **role ids 7/8/9** (`IS_VISIBLE=1`), with **no** rule
267
- override (status stays at the Core base `pendingApproval`). This is deliberately **per-role**, unlike
268
- Quad's usual client-wide (`roleId NULL`) pattern — per request. Approvals filter button is
268
+ - **Quad** — Approve + Deny visible for **role ids 7/8/9** (`IS_VISIBLE=1`, deliberately per-role
269
+ unlike Quad's usual client-wide pattern). **New 2026-07-21:** Approve carries a client-wide
270
+ (`roleId NULL`) `ENABLED_RULE` = `{"type":"poNumberEntered"}` on **both** surface 8 (el 17) and
271
+ surface 3 (el 29) — "disable Approve until a PO is entered" (the `isPoComplete` semantic), plus a
272
+ `MessageTranslations` row overriding the disabled-tooltip text. Approvals filter button is
269
273
  client-wide with `CONFIG` `_status=[pendingApproval]`; the "Enter PO Details" button (element 32) is
270
274
  made client-wide visible (⚠ dual-gate — see the schema doc; the FE may also read `config.isVisible`).
271
275
 
276
+ **Approvals-gate marker (Compass / Compass Canada, 2026-07-21).** The corrected Approve `ENABLED_RULE`
277
+ (`(pendingInitialApproval AND stepTwoAssigned) OR pendingApproval`) was applied to both surfaces (el 17
278
+ + el 29, ADMIN-scoped, rule JSON in `c_longValue`, `value` NULL). Because Compass's `2026-07-17a`
279
+ already inserted an `ENABLED_RULE` for approve el 17, the migration **UPDATEs** that row then
280
+ guarded-INSERTs el 29. This also required enabling the **approvals-gate marker** for Compass/CC
281
+ (`Client_<X>/2026-07-21b - SalesOrderApprovalsGateEnable.sql`) — without it `approvalsEnabled=false`
282
+ FE-side, stage data never fetches, and `stepTwoAssigned` **fails open** (silently enabling the button).
283
+ See the fail-open gotcha in [surface-frontend](../../toga25-supply/features/surface-frontend.md).
284
+
272
285
  > ✅ **Regression (mostly) closed:** the two-stage rule overrides and the record-action/filter opt-ins
273
286
  > for Compass / Compass Canada that were wiped in the earlier rebuild were **re-added 2026-07-17**
274
287
  > (`Client_Compass`/`Client_CompassCanada/2026-07-17a`+`b`). These surface DB changes remain **local/
@@ -309,6 +322,22 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
309
322
 
310
323
  ## Gotchas
311
324
 
325
+ - **Per-client surface message overrides silently never apply unless the JWT carries
326
+ `id.client.languageId`.** `_Model_Core_Surface::_loadMessages` overlays `Core.Messages.defaultValue`
327
+ with `Client_<X>.MessageTranslations` rows **only when `languageId > 0`**, and `Surface.php` resolves
328
+ `languageId` **solely** from the JWT `id.client.languageId` claim. But auth (`api2/V2.php`)
329
+ historically set only `id.language` (the string code `"en"`) and **never** `id.client.languageId`, so
330
+ `languageId` was always `0`, the overlay was skipped, and every per-client surface message override
331
+ (e.g. a client-specific disabled-tooltip) fell back to the Core default with **no error**. (There was
332
+ an explicit TODO at `V2.php` ~L5738.) **Fix (2026-07-21):** `/auth/login` now embeds
333
+ `$id->client->languageId = (int)$language->id` in **both** the configured-language and default-`en`
334
+ branches (English gets a real `languageId` too; the overlay keys on `languageId > 0` and falls back
335
+ to Core defaults where a client has no `MessageTranslations` row). ⚠ **CAVEAT — only `/auth/login`
336
+ was fixed.** The refresh (`V2.php` ~L1227), oauth, api, delegator, encrypted, and public token paths
337
+ still don't embed `id.client.languageId`, so **after an access-token refresh (~3600s) `languageId` is
338
+ lost** and per-client surface messages revert until those paths are updated too. (This is a **separate
339
+ claim** from `id.language`, which drives the data-translation sidecar — see
340
+ [language-translation-layer](../../api2/features/language-translation-layer.md); do not conflate.)
312
341
  - **A `SurfaceOverrides` row with `attribute='CONFIG'` carrying a RULE value is silently DEAD —
313
342
  encode rules as `VISIBILITY_RULE`/`ENABLED_RULE`, never as `CONFIG`.** `_castOverride` routes the
314
343
  `CONFIG` attribute into `$config`, **NOT** `$visibilityRule`/`$enabledRule`; a `CONFIG` row whose
@@ -391,6 +420,18 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
391
420
  match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
392
421
 
393
422
  ## Change history
423
+ - 2026-07-21 — Recorded **Option B**: the Approve step-two/PO gate is now evaluated **live on the FE**
424
+ (FE `evaluateSurfaceRule` gained a `SURFACE_NAMED_RULES` escape hatch) rather than via the no-op
425
+ backend Tier-2 — an intentional deviation from the frozen-grammar/escalate note (updated the
426
+ "not resolvable server-side" caveat). Added the **Quad** Approve `ENABLED_RULE`
427
+ `{"type":"poNumberEntered"}` (client-wide, both surfaces) + disabled-tooltip `MessageTranslations`
428
+ override, and the **Compass/CC** approvals-gate marker enable (`2026-07-21b`) needed so
429
+ `stepTwoAssigned` doesn't fail open. New gotcha: per-client surface **message** overrides silently
430
+ never apply unless the JWT carries `id.client.languageId` (`_loadMessages` overlays
431
+ `MessageTranslations` only when `languageId>0`; `Surface.php` reads it solely from that claim, but
432
+ auth only set `id.language`) — fixed in `api2/V2.php` `/auth/login` (both language branches), with the
433
+ caveat that refresh/oauth/api/delegator/encrypted/public paths still lose it after a token refresh.
434
+ (apeterson)
394
435
  - 2026-07-20 — Clarified the **backend Tier-2 gate reality**: `_resolveTier2State` (Surface.php ~280)
395
436
  dispatches by **action slug** to `resolveSurfaceActionState()` (`TIER2_CAPABILITY_METHOD`), **not**
396
437
  by `config.tier2` — `config.tier2` is a **dead marker nothing reads** (backend or FE). The surface
@@ -3,5 +3,5 @@
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
- | [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/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 |
6
+ | [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/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 |
7
7
  | [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/ |
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
9
+ updated: 2026-07-21
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
@@ -54,6 +54,12 @@ files:
54
54
  - dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql
55
55
  - dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql
56
56
  - dbchanges2/Core/2026-07-17 - README - RUN ORDER.md
57
+ - dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql
58
+ - dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql
59
+ - dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql
60
+ - dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql
61
+ - dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql
62
+ - dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql
57
63
  related:
58
64
  - ../../_underscore/features/surface-resolver.md
59
65
  ---
@@ -403,6 +409,17 @@ rule resumes.
403
409
  earlier `2026-06-25c` / `2026-06-29c` / `2026-06-25d` surface seeds **likely share the same
404
410
  anti-pattern** and need auditing. Core→Core seeds are fine (same cluster). Also still open:
405
411
  re-seeding NYCHH/Prudential/SPGlobal (deferred).
412
+ - **A `VISIBILITY_RULE`/`ENABLED_RULE` override row can exist in the DB with the CORRECT `c_longValue`
413
+ yet be silently ignored because its `attribute` coerced to `''`.** The `SurfaceOverrides.attribute`
414
+ ENUM must be widened per client (`Client/2026-07-15 - SurfaceOverridesAttributeRuleValues.sql`)
415
+ **before** any rule-override insert. If a client (this session: **Quad**) never ran it, MySQL
416
+ **non-strict mode silently coerces** the invalid `VISIBILITY_RULE`/`ENABLED_RULE` value to `''`
417
+ (empty string) on INSERT — the row lands with the right `c_longValue` but `attribute=''`, so the
418
+ resolver ignores it and the rule never ships. **Symptom:** the override row is visibly present in the
419
+ DB (correct `c_longValue`) but `enabledRule`/`visibilityRule` is absent or wrong in the
420
+ `/surfaces/meta` API response. **Fix:** run the ENUM-widening file for the client, DELETE the junk
421
+ `attribute=''` rows, then re-insert. (`NOT EXISTS` guards keyed on `attribute` never match the junk
422
+ rows, so duplicates also pile up.)
406
423
  - **Display-section visibility cannot be per-role-overridden without an element.** The override
407
424
  cascade overrides **ELEMENTS only**, never a surface's own `isVisible`. The 5 display-toggle SECTION
408
425
  surfaces therefore each carry a single `sectionVisibility` marker element so a Client override can
@@ -425,6 +442,16 @@ rule resumes.
425
442
  override is added). **Open follow-up.**
426
443
 
427
444
  ## Change history
445
+ - 2026-07-21 — Added the sales-order **Approve enable-gate** per-client overrides (`Client_*/2026-07-21a`):
446
+ Compass/CC ADMIN `ENABLED_RULE` = `(pendingInitialApproval AND stepTwoAssigned) OR pendingApproval`
447
+ on surface 8 el 17 + surface 3 el 29 (rule JSON in `c_longValue`; Compass UPDATEs its existing
448
+ `07-17a` el-17 row then guarded-INSERTs el 29), plus a Compass/CC **approvals-gate enable**
449
+ (`2026-07-21b`) required so the FE `stepTwoAssigned` predicate doesn't fail open. Quad = client-wide
450
+ Approve `ENABLED_RULE` `{"type":"poNumberEntered"}` on both surfaces + a disabled-tooltip
451
+ `MessageTranslations` override. Sharpened the ENUM-coercion gotcha: a rule-override row can exist with
452
+ the correct `c_longValue` yet be silently ignored because `attribute` coerced to `''` (Quad never ran
453
+ the ENUM-widening file) — symptom is the row present in DB but the rule absent from the `/surfaces/meta`
454
+ response; fix = run the ENUM file, delete the `attribute=''` junk rows, re-insert. (apeterson)
428
455
  - 2026-07-20 — Corrected the **row-actions surface** (`sales-order-listing-row-actions`, Core id 3;
429
456
  distinct from the header-actions surface 9) default visibility and added the client opt-in.
430
457
  `Core/2026-07-20e` narrows an earlier over-broad `2026-07-20d` hide — re-enables Approve (el 29) +
@@ -264,40 +264,70 @@ in [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md):
264
264
 
265
265
  ## Two FE rule engines (surface Tier-1 vs legacy named-rule) — do not conflate
266
266
 
267
- There are **two** rule evaluators in this app, and a business predicate like the corrected Approve
268
- rule can only run on the legacy one today:
269
-
270
- - **`src/surface/evaluateSurfaceRule.ts`** — the **surface Tier-1** evaluator. Grammar is
271
- **deliberately FROZEN**: `all`/`any`/`none` + leaf field ops (`eq`/`ne`/`in`/`nin`/`gt`/`gte`/`lt`/`lte`).
272
- It **THROWS on any `{type:…}` named node by design** ("new logic does NOT go here — escalate to a
273
- Tier-2 PHP capability method"). Evaluated client-side against the loaded record.
267
+ There are **two** rule evaluators in this app. As of 2026-07-21 the surface evaluator now has a
268
+ **sanctioned named-predicate escape hatch** (Option B — see below), so a business predicate like the
269
+ Approve gate can finally run on the surface path FE-side:
270
+
271
+ - **`src/surface/evaluateSurfaceRule.ts`** — the **surface Tier-1** evaluator. Base grammar is
272
+ `all`/`any`/`none` + leaf field ops (`eq`/`ne`/`in`/`nin`/`gt`/`gte`/`lt`/`lte`). It historically
273
+ **THREW on any `{type:…}` node** ("escalate to a Tier-2 PHP capability method"). It now instead
274
+ resolves `{type:<SurfaceNamedRule>}` nodes against a **`SURFACE_NAMED_RULES` map** (still throws
275
+ `"Unknown rule node"` on an unregistered type). `SurfaceNamedRule` (`src/surface/types.ts`) =
276
+ `"stepTwoAssigned" | "poNumberEntered"`. Both predicates read off the `{order}`-shaped surface
277
+ record: `stepTwoAssigned` reads `order.currentStage`/`order.stages`; `poNumberEntered` =
278
+ `!!order.purchaseOrderDetails.purchaseOrder` (mirrors the legacy `isPoComplete`). Evaluated
279
+ client-side against the loaded record.
274
280
  - **`src/pages/SalesOrders/helpers/evaluateEnableRule.ts`** (`evaluateRule`) — the **legacy** engine:
275
- `all`/`any`/`not` + `field` + **`{type}` NAMED_RULES**, including **`stepTwoAssigned`** (which reads
276
- `ctx.currentStage`/`ctx.stages`).
277
-
278
- **Consequence:** the corrected Approve enable rule
279
- (`(pendingInitialApproval AND stepTwoAssigned) OR pendingApproval` — see
280
- [surface-resolver](../../_underscore/features/surface-resolver.md)) carries a `{type:stepTwoAssigned}`
281
- node, so **only the legacy engine can evaluate it today**. Moving the Approve button fully onto the
282
- surface path requires **both** (a) teaching `evaluateSurfaceRule` the named `stepTwoAssigned` check and
283
- (b) feeding it live approval-stage data — neither is done. (The backend Tier-2 path is also a no-op:
284
- no `resolveSurfaceActionState`, no backend `stepTwoAssigned`.)
281
+ `all`/`any`/`not` + `field` + **`{type}` NAMED_RULES** (`stepTwoAssigned` reading
282
+ `ctx.currentStage`/`ctx.stages`). Still present for the JSON-driven path.
283
+
284
+ ### Option B — named predicates evaluated live on the FE (design deviation, intentional)
285
+ This **intentionally deviates** from the original "frozen grammar → escalate to a Tier-2 PHP
286
+ capability method" design note (see
287
+ [surface-resolver](../../_underscore/features/surface-resolver.md)). Per **Option B** the Approve
288
+ step-two / PO gate is evaluated **live on the FE**, because the backend Tier-2 path is a no-op (no
289
+ model implements `resolveSurfaceActionState`, no backend `stepTwoAssigned`) and the rule is not in
290
+ legacy JSON. Named predicates (`stepTwoAssigned`/`poNumberEntered`) are the sanctioned FE-side escape
291
+ hatch for logic the frozen field grammar cannot express; the rule JSON itself lives in each client's
292
+ `SurfaceOverride` `c_longValue` (Core stays neutral).
293
+
294
+ ### `resolveElementState` — the flag composition
295
+ `resolveElementState.ts` computes an element's `isEnabled` as **`staticFlag AND enabledRule AND
296
+ meta.surface`** (static flag AND the Tier-1/named-predicate `enabledRule` result AND any
297
+ server-computed `meta.surface` state). ⚠ **`SurfaceActionBar` carries its own DUPLICATE copy of
298
+ `resolveElementState`** — any change to the composition logic must be made in **both** places or the
299
+ action bar and the generic path will diverge.
285
300
 
286
301
  ### Approval-stage data plumbing for `stepTwoAssigned`
287
302
  `stepTwoAssigned` needs the order's approval-stage data on the surface record. The surface record is
288
- shaped `{ order }`, so the predicate reads **`order.currentStage` / `order.stages`**.
303
+ shaped `{ order }`, so the predicate reads **`order.currentStage` / `order.stages`**. Both surfaces
304
+ now carry it (2026-07-21):
289
305
 
290
- - **Record-modal action bar (surface 8) — DONE.** `useSalesOrderRecordModalLayoutModel.tsx` already
291
- fetches `approvalCurrentStage`/`approvalStages`; they are now **merged onto the order (`orderWithPo`)
292
- as `currentStage`/`stages`** (parallel to the existing `purchaseOrderDetails` merge). Purely additive.
293
- - **Listing row dropdown (surface 3) — PENDING.** `src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts`
294
- currently fetches `order` + `purchaseOrderDetails` but **NOT** approval stages, so `stepTwoAssigned`
295
- cannot resolve on the row path yet. It needs a `useFetchApprovalStages` call + the same
296
- `currentStage`/`stages` merge (a per-open fetch cost tradeoff on the row path), **and**
297
- `evaluateSurfaceRule` still needs the named-check support above.
306
+ - **Record-modal action bar (surface 8) — DONE.** `useSalesOrderRecordModalLayoutModel.tsx` fetches
307
+ `approvalCurrentStage`/`approvalStages`, merged onto the order (`orderWithPo`) as
308
+ `currentStage`/`stages` (parallel to the `purchaseOrderDetails` merge). Additive.
309
+ - **Listing row dropdown (surface 3) — DONE.** `useSalesOrderRowRecordState.ts` now calls
310
+ `useFetchApprovalStages` (gated on `enabled && approvalsEnabled`) and merges `currentStage`/`stages`
311
+ onto `record.order`, mirroring the record-modal view model. Both FE surfaces now carry
312
+ `order.currentStage`/`order.stages`/`order.purchaseOrderDetails`.
298
313
 
299
314
  ## Gotchas
300
315
 
316
+ - **A named predicate that FAILS OPEN silently enables a button when its data isn't fetched.**
317
+ `stepTwoAssigned` returns `true` when `order.currentStage` is `null` (a fail-OPEN default). So if the
318
+ surface's approval-stage data is not plumbed in (e.g. `approvalsEnabled=false`, so
319
+ `useFetchApprovalStages` never runs, or the row-path merge is missing), the predicate resolves `true`
320
+ and the Approve button is **silently enabled** regardless of stage. A named predicate's default
321
+ branch is load-bearing — decide fail-open vs fail-closed deliberately, and confirm the data it reads
322
+ is actually fetched on **every** surface that uses the rule (surface 8 AND surface 3). This is why
323
+ the Compass/CC approvals-gate marker (`approvalsEnabled`) must be enabled per client — without it the
324
+ stage data never fetches and `stepTwoAssigned` fails open.
325
+ - **Surface meta is cached `staleTime: Infinity` / `gcTime: Infinity` for the whole session
326
+ (`useFetchSurfaceMeta.ts`).** After ANY `SurfaceOverride` / `MessageTranslation` DB change, a **hard
327
+ reload / re-login is required** or the browser keeps the pre-change meta for the entire session. This
328
+ repeatedly presents as "I ran the SQL but nothing changed" during debugging — the SQL is fine; the
329
+ meta is cached. (Server-side raw-SQL surface edits ALSO need the on-disk resolver cache cleared — see
330
+ [surface-resolver](../../_underscore/features/surface-resolver.md); both caches must be busted.)
301
331
  - **The single-meta envelope can hide the real bundle behind a DECOY `elements: []`.** The V2 envelope
302
332
  can arrive as `{ surfaces: { meta: <real-bundle> }, elements: [] }` — a decoy empty `elements: []`
303
333
  sits at the top level with **no `surface` descriptor**. A `looksLikeBundle` check matching on
@@ -394,6 +424,20 @@ shaped `{ order }`, so the predicate reads **`order.currentStage` / `order.stage
394
424
  treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
395
425
 
396
426
  ## Change history
427
+ - 2026-07-21 — **Option B: moved the Approve enable-gate onto the surface FE path.** Added a
428
+ `SURFACE_NAMED_RULES` map + a `{type:SurfaceNamedRule}` branch to `evaluateSurfaceRule.ts`
429
+ (`SurfaceNamedRule` = `"stepTwoAssigned"|"poNumberEntered"` in `types.ts`) — the sanctioned FE-side
430
+ escape hatch for logic the frozen field grammar can't express, an intentional deviation from the
431
+ original "frozen grammar → escalate to Tier-2 PHP" note (backend Tier-2 is a no-op). `stepTwoAssigned`
432
+ reads `order.currentStage`/`order.stages`; `poNumberEntered` = `!!order.purchaseOrderDetails.purchaseOrder`.
433
+ Plumbed approval-stage data onto the **row-actions** surface (surface 3): `useSalesOrderRowRecordState.ts`
434
+ now calls `useFetchApprovalStages` (gated on `enabled && approvalsEnabled`) + merges `currentStage`/`stages`
435
+ onto `record.order` (previously PENDING; record-modal surface 8 was already done). Documented that
436
+ `resolveElementState` composes `isEnabled = staticFlag AND enabledRule AND meta.surface` and that
437
+ `SurfaceActionBar` holds a DUPLICATE copy. Gotchas added: (1) `stepTwoAssigned` FAILS OPEN when
438
+ `currentStage` is null → silently enables the button if stage data isn't fetched (hence the per-client
439
+ approvals-gate marker); (2) surface meta is `staleTime/gcTime: Infinity` so a DB override change needs a
440
+ hard reload/re-login ("ran the SQL but nothing changed"). (apeterson)
397
441
  - 2026-07-21 — Fixed the surface-driven **status badge dot rendering colorless** in the sales-order
398
442
  record-modal header: the resolved bundle ships vocabulary color TOKEN SLUGS
399
443
  (`color.status.*`/`bg.status.*`) but its `theme` map is empty (`{}`), so `resolveThemeToken` returns
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.400",
3
+ "version": "1.0.401",
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",