toga-ai 1.0.241 → 1.0.243

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.
@@ -13,6 +13,6 @@
13
13
  | [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 |
14
14
  | [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 |
15
15
  | [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 |
16
- | [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, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.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 |
16
+ | [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, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.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 |
17
17
  | [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_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
18
18
  | [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,10 +6,12 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-29
9
+ updated: 2026-06-30
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Core/Surface.php
13
+ - dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql
14
+ - dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql
13
15
  - dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
14
16
  - dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
15
17
  - dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
@@ -69,6 +71,29 @@ Inputs come from `$api` (the same accessor `_Model_Client_TableView::meta` uses)
69
71
  A `metaDebug()` inspector returns the resolved bundle plus the raw cascade layers (shipped with the
70
72
  resolver, not "later"). It is **not** named `debug()` — see gotchas.
71
73
 
74
+ ## Grouped resolution — `metaGroup()`
75
+
76
+ `_Model_Core_Surface::metaGroup(&$api, string $slugs): object` — a scripted-API method exposed at
77
+ `GET /v2/surfaces/meta-group?slugs=a,b,c` for screens (e.g. a record-detail modal) that need many
78
+ section surfaces at once: **one round trip instead of N**. It splits the comma-separated `slugs`
79
+ list (capped at `const META_GROUP_MAX_SLUGS = 25`, logging truncation), resolves **each slug via the
80
+ existing per-slug `resolve()` cache path**, and returns `{group, surfaces: {slug => bundle|null}}`.
81
+ It is **composition only** — no new cache layer; an unknown slug yields the structured **empty
82
+ bundle** so one missing surface never fails the whole group. (Chosen over a `parentSurfaceId` GROUP
83
+ row after CTO review — a slug list keeps the model schema-stateless and avoids id drift.)
84
+
85
+ `resolve()` now declares an explicit `: object` return type.
86
+
87
+ ### Message/label resolution fixes (latent SECTION/BUTTON_BAR bugs)
88
+ The grouped SalesOrders section migration is the first real consumer of the SECTION/BUTTON_BAR path,
89
+ which surfaced latent resolution gaps in `_buildBundle`/`_resolveElement`:
90
+ - Element message slots (`label`/`tooltip`/`placeholder`/`disabledTooltip`) are now remapped from
91
+ **message-id → messageKey** before return, because `bundle.messages` is keyed by `messageKey` —
92
+ previously labels silently fell back to the element key.
93
+ - `disabledTooltipMessageId` is collected into the batch and emitted as a `disabledTooltip` slot.
94
+ - `bundle.surface.titleMessageKey` is set from `titleMessageId`.
95
+ - The 2nd `_loadMessages` return is merged into `$messageKeyById`; dangling message ids are logged.
96
+
72
97
  ## Caching & invalidation
73
98
 
74
99
  - Disk cache keyed `surface:meta:{app}:{slug}:{client}:{sortedPersonaIds}:{sortedRoleIds}:{lang}`.
@@ -137,7 +162,20 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
137
162
  `record-fields` already expose Public READ. Only the `surfaces` record gets Public read — the
138
163
  admin-CRUD sibling records stay Super-User-only (the resolver reads their tables server-side).
139
164
 
165
+ - **The meta-group dispatch grant is per-client-DB too.** A 403 EZ-1 on `meta-group` on dev-sandbox
166
+ was the same mechanism as the original `meta` 403: the `meta-group` RecordScript needs its own
167
+ per-role `Client.AclRecordScripts` dispatch grant. Verified via toga-db — `meta` script id 25 was
168
+ granted roles 1/3/4 but `meta-group` (id 27) had **none** until the grant ran. Any new surfaces
169
+ scripted-API method repeats this: record READ never authorizes dispatch.
170
+
140
171
  ## Change history
172
+ - 2026-06-30 — Added `metaGroup()` (`GET /v2/surfaces/meta-group?slugs=a,b,c`): batched composition
173
+ over the existing per-slug `resolve()` cache, capped at `META_GROUP_MAX_SLUGS=25`, unknown slug →
174
+ empty bundle (chosen over a `parentSurfaceId` GROUP row post-CTO-review). Fixed latent
175
+ SECTION/BUTTON_BAR resolution (message-id→messageKey remap for label/tooltip/placeholder/
176
+ disabledTooltip, titleMessageKey, dangling-id logging); added `: object` to `resolve()`. The
177
+ `meta-group` script needed its own per-client `AclRecordScripts` dispatch grant (403 EZ-1 until
178
+ granted). (jcardinal)
141
179
  - 2026-06-29 — Deploy fixes (round 2): grant the surfaces `meta`/`debug` RecordScripts in
142
180
  `Client.AclRecordScripts` per role (Public/SuperUser/Base) — record-level `AclRecordPermissions`
143
181
  alone left every meta call 403 EZ-1 because V2 dispatch authorizes via `AclRecordScripts`
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-29
9
+ updated: 2026-06-30
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - api2/Component/Api/V2/V2.php
@@ -56,7 +56,14 @@ traffic), behavior is **byte-for-byte unchanged** — pure data, zero extra work
56
56
  meta 403'd until the `meta`/`debug` scripts were granted per role in `AclRecordScripts`. See
57
57
  [surface-resolver](../../_underscore/features/surface-resolver.md) gotchas.
58
58
 
59
+ - **Grouped meta endpoint `GET /v2/surfaces/meta-group?slugs=a,b,c`** (one round trip for a
60
+ multi-section screen) is a distinct RecordScript and needs its **own** per-role
61
+ `Client.AclRecordScripts` dispatch grant — it 403'd EZ-1 on dev-sandbox until granted, exactly like
62
+ `meta` did. See [surface-resolver](../../_underscore/features/surface-resolver.md).
63
+
59
64
  ## Change history
65
+ - 2026-06-30 — Noted the grouped `meta-group` endpoint (batched multi-slug resolve) and that it needs
66
+ its own per-role `AclRecordScripts` dispatch grant (403 EZ-1 until granted). (jcardinal)
60
67
  - 2026-06-29 — Documented the second ACL layer: scripted-API dispatch is authorized via
61
68
  `Client.AclRecordScripts` (per `recordScriptId`+`roleId`), not record `AclRecordPermissions` —
62
69
  record READ alone still 403'd meta until the dispatch grants were seeded. (jcardinal)
@@ -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/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/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 |
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/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 |
7
7
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | How to manually stand up a new 2.0 client (tenant). | 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-06-29
9
+ updated: 2026-06-30
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql
@@ -18,6 +18,11 @@ files:
18
18
  - dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
19
19
  - dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql
20
20
  - dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql
21
+ - dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql
22
+ - dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql
23
+ - dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql
24
+ - dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql
25
+ - dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql
21
26
  - dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
22
27
  - dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql
23
28
  - dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql
@@ -82,6 +87,19 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
82
87
  purchase-orders/items/units via per-level fetchSlugs and has no single backing Record; carries
83
88
  `groupByLabels` + `entityTags` in surface `config`) + `inventory-list-actions` (BUTTON_BAR:
84
89
  Group by / Refresh). Actions anchored to `items(21)` because `Actions.recordId` is NOT NULL.
90
+ - `Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql` — registers the `meta-group`
91
+ RecordScript; seeds the previously-missing `order-recurring` SECTION surface (cardType
92
+ `recurringFields`, `isVisible 0`); seeds 5 display-toggle SECTION surfaces (invoices / shipments /
93
+ additional-order-details / notes / admin-notes), **each carrying a single `sectionVisibility`
94
+ marker element** (see gotcha — the override cascade only overrides ELEMENTS, never a surface's own
95
+ `isVisible`, so display-section visibility cannot be per-role-overridden without a marker element).
96
+ Core→Core only, so this file is prod-safe.
97
+ - `Client/2026-06-30a - SurfaceMetaGroupAcl.sql` — the `meta-group` `AclRecordScripts` dispatch grant
98
+ (roles 1/3/4), mirroring the `meta` grant.
99
+ - `Client_Compass/2026-06-30a` / `Client_CompassCanada/2026-06-30a` — per-client **MANAGER**-scope
100
+ SalesOrder section overrides; `Client_Quad/2026-06-30a` — Quad's **client-level** section deltas.
101
+ ⚠ These three Client-DB seeds currently resolve **Core** ids via cross-DB subselect — see the
102
+ prod cross-cluster gotcha; they are paused before prod.
85
103
  - `Client/2026-06-29c - SurfaceRecordScriptAcl.sql` — grants the surfaces `meta`/`debug` RecordScripts
86
104
  in **`Client.AclRecordScripts`** per role (meta → Public(1)/SuperUser(3)/Base(4); debug →
87
105
  SuperUser(3)). V2 authorizes scripted-API dispatch via `AclRecordScripts`, NOT record
@@ -122,7 +140,31 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
122
140
  - **CLIENT-aclDatabase records** (`SurfaceOverrides`, `MessageTranslations`, `ThemeTokens`) need the
123
141
  full 4-step ACL chain + field permissions in **each** client DB; resolve `roleId` by subselect.
124
142
 
143
+ - **⚠ PROD BLOCKER (ON HOLD) — Client-DB surface seeds must NOT resolve Core ids via cross-DB
144
+ subselect.** The `Client_Compass`/`Client_CompassCanada`/`Client_Quad` SalesOrder override seeds
145
+ resolve Core ids (`Core.RecordScripts`/`Core.Surfaces`/`Core.SurfaceElements`) with a cross-DB
146
+ subselect. This works only on the dev-sandbox **all-in-one** instance; in **production Core is a
147
+ separate cluster** and cross-DB queries require a single instance, so these seeds **break in prod**.
148
+ The established prod-safe pattern is `Client/2026-06-23b - ItemTranslationsAcl.sql`: **hardcode Core
149
+ ids as literals**, resolve only Client-local ids (e.g. `roleId` by name). The fix is paused; the
150
+ earlier `2026-06-25c` / `2026-06-29c` / `2026-06-25d` surface seeds **likely share the same
151
+ anti-pattern** and need auditing. Core→Core seeds are fine (same cluster). Also still open:
152
+ re-seeding NYCHH/Prudential/SPGlobal (deferred).
153
+ - **Display-section visibility cannot be per-role-overridden without an element.** The override
154
+ cascade overrides **ELEMENTS only**, never a surface's own `isVisible`. The 5 display-toggle SECTION
155
+ surfaces therefore each carry a single `sectionVisibility` marker element so a Client override can
156
+ flip a section per role/persona. Also discovered: `elementType` in `orderViewFields.json` is **dead
157
+ config** (no renderer reads it); and MANAGER/Quad summary tooltips can't be removed via the cascade
158
+ because `TOOLTIP_MESSAGE` is numeric-only (cosmetic, deferred).
159
+
125
160
  ## Change history
161
+ - 2026-06-30 — Added the SalesOrders SECTION-surface migration seeds: Core `meta-group` RecordScript +
162
+ `order-recurring` + 5 display-toggle SECTION surfaces (each with a `sectionVisibility` marker
163
+ element, since the cascade overrides elements not a surface's own isVisible), the `meta-group`
164
+ dispatch ACL grant, and per-client (Compass/CompassCanada MANAGER, Quad client-level) section
165
+ overrides. Recorded the ⚠ prod blocker: Client-DB seeds resolving Core ids via cross-DB subselect
166
+ break in prod (Core is a separate cluster) — must hardcode Core id literals per the
167
+ `ItemTranslationsAcl.sql` pattern; fix paused, earlier surface seeds likely share it. (jcardinal)
126
168
  - 2026-06-29 — Added VendorItems + Inventory LIST seeds (`VendorItemsSurfaceSeed.sql` recordId 19 /
127
169
  TableView 10; `InventorySurfaceSeed.sql` presentation-only, recordId+tableViewId NULL, Actions
128
170
  anchored to items(21)). Added `SurfaceRecordScriptAcl.sql` (per-role `AclRecordScripts` dispatch
@@ -8,5 +8,5 @@
8
8
  | [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
9
9
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
10
10
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/, toga25-supply/src/hooks/useTableCellInteractions.ts |
11
- | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
11
+ | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts |
12
12
  | [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
@@ -6,10 +6,13 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-29
9
+ updated: 2026-06-30
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - toga25-supply/src/surface/useFetchSurfaceMeta.ts
13
+ - toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
14
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx
15
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts
13
16
  - toga25-supply/src/surface/evaluateSurfaceRule.ts
14
17
  - toga25-supply/src/surface/actionRegistry.ts
15
18
  - toga25-supply/src/surface/SurfaceActionBar.tsx
@@ -51,6 +54,12 @@ feature flag — see below). Backend: [surface-resolver](../../_underscore/featu
51
54
  `raw` / `raw.data` / `raw.data.<route-key>` — because V2 nests a scripted-API return value one
52
55
  level deeper, under a route-keyed slot of `data` (e.g. `data.meta`, `data.surfaces`). On a malformed
53
56
  payload it falls back to an empty-elements bundle so the page never crashes.
57
+ - **`useFetchSurfaceMetaGroup(slugs[])`** — the grouped sibling of `useFetchSurfaceMeta`, for a screen
58
+ needing many section surfaces at once. **Order-insensitive** (the slug list is normalized so cache
59
+ hits don't depend on argument order), `staleTime: Infinity`, hits the grouped endpoint
60
+ `GET /v2/surfaces/meta-group?slugs=...`, and extracts the `{surfaces}` map via `extractGroup()` /
61
+ `looksLikeGroup()` (the same structural-fingerprint approach as `extractBundle`). Returns `{}` for
62
+ unmigrated screens (`enabled:false`), so a non-roster client does no fetch.
54
63
  - **`evaluateSurfaceRule`** — the Tier-1 rule evaluator: a **frozen `all/any/none` + `{field,op,value}`
55
64
  grammar** that **throws on an unknown op** (generalized from the SalesOrders
56
65
  `buildPatchedTenantFields`/`evaluateEnableRule`/`resolveFlag` helpers). Evaluated client-side against
@@ -114,20 +123,35 @@ modal copy, visibility/enable) to Surface; **leave data-fetch wiring (fetch slug
114
123
  nested-table topology) in JSON/code.** Not every screen reduces to a generic `SurfaceSection` swap —
115
124
  see SalesOrders below.
116
125
 
117
- ## SalesOrders remaining migrationbespoke, NOT a generic SurfaceSection swap (deferred)
118
-
119
- The SalesOrders detail migration is the plan's explicitly-deferred "blessed escape hatch"/approval
120
- work it is bespoke-component + business-logic work, not a generic swap. `SalesOrderSummaryGrid` is
121
- fully tenant-data-driven through three **bespoke renderers**: `detailSection` (blox `DetailSection` +
122
- custom `renderField`), `locationCard` (`OrderSectionCard`, structured contact data), and `totalsCard`
123
- (blox `BaseTotals` **with client-specific recurring-lease business logic**:
124
- `_totalLease !== 0 recurringKey` fields). The generic `SurfaceSection` (label/value) cannot
125
- represent `BaseTotals`/`OrderSectionCard`, and the early-seeded order-details/ship-to/bill-to/
126
- order-summary SECTION surfaces do **not** cleanly map onto the grid's arbitrary client-keyed section
127
- model. A faithful migration needs a **registered-renderer approach** keyed off surface element config
128
- (`detailSection`/`locationCard`/`totalsCard` their components), with the lease logic staying in
129
- code/`_underscore`, plus reconciling those seeds and the Compass approval-modal state machine.
130
- **Deferred to a dedicated design-first session.**
126
+ ## SalesOrders record-modal sectionsmigrated via the registered-renderer seam (roster-gated)
127
+
128
+ The SalesOrders detail sections are now migrated onto Surface for the **seeded roster clients only**,
129
+ via the registered-renderer approach (not a generic `SurfaceSection` swap `SalesOrderSummaryGrid`'s
130
+ three bespoke renderers `detailSection`/`locationCard`/`totalsCard` are untouched).
131
+
132
+ - **`surfaceBundleToTenantFields()`** (`SalesOrders/helpers/`) adapts the grouped Surface bundle into
133
+ the existing **`TenantFields`** shape, so the existing `SalesOrderSummaryGrid` renderers and
134
+ `getDetailSections` are untouched — **only the SOURCE of section config moves JSON → Surface.**
135
+ Business logic stays in code: the recurring-lease `_totalLease` gate in the grid and approval
136
+ gating are **not** moved. (Registered-renderer seam: *config describes, code decides.*)
137
+ - **Roster gate `SALES_ORDER_SURFACE_MIGRATED_CLIENTS = [COMPASS, COMPASSCANADA, QUAD]`** (an
138
+ in-code roster in `surfaceBundleToTenantFields.ts`; the view-model
139
+ `useSalesOrderRecordModalLayoutModel.tsx` consults it). The view-model resolves sections from
140
+ Surface **only** for roster clients; every other client (incl. never-migrated
141
+ NYCHH/Prudential/SPGlobal) skips the fetch and stays fully JSON-driven. Merge is **JSON-base +
142
+ Surface-wins.** ⚠ **Why a roster, not a flag:** the Core SECTION surfaces are **shared** and seeded
143
+ from Compass's shape — without the gate every client would inherit Compass's base and regress. A
144
+ **code** roster (not an env flag) is deliberate: a client is removed from the roster only once its
145
+ own seeds are verified.
146
+ - **JSON cleanup:** the migrated section blocks (orderDetails/shipTo/billTo/orderSummary/
147
+ orderRecurring + the 5 display sections) were **deleted** from the COMPASS / COMPASSCANADA / QUAD
148
+ `orderViewFields.json` files. Data-wiring (`salesOrderDetailsConfig`/`itemFulfillmentClickRule`/
149
+ `recordActionFields`/`recordApprovals`) was **kept**. The DEFAULT `orderViewFields.json` is left
150
+ **intact** — non-roster clients are still JSON-driven.
151
+
152
+ **Still JSON / deferred:** the SalesOrders **action-bar / approval-modal** migration remains on JSON;
153
+ seeding NYCHH/Prudential/SPGlobal is deferred; the Client-DB prod cross-cluster seed fix is on hold
154
+ (see [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md)).
131
155
 
132
156
  ## Gotchas
133
157
 
@@ -147,6 +171,14 @@ code/`_underscore`, plus reconciling those seeds and the Compass approval-modal
147
171
  treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
148
172
 
149
173
  ## Change history
174
+ - 2026-06-30 — Migrated the SalesOrders record-modal sections onto Surface for the roster clients
175
+ [COMPASS, COMPASSCANADA, QUAD] via the registered-renderer seam: added `useFetchSurfaceMetaGroup`
176
+ (order-insensitive grouped `/surfaces/meta-group` fetch) + `surfaceBundleToTenantFields()` (adapts
177
+ the grouped bundle to the existing `TenantFields` shape; bespoke grid renderers + lease/approval
178
+ logic stay in code). Roster-gated (code roster, JSON-base + Surface-wins) so non-roster clients
179
+ don't inherit Compass's shared base. Deleted the migrated section blocks from COMPASS/COMPASSCANADA/
180
+ QUAD `orderViewFields.json` (data-wiring kept; DEFAULT intact). Action-bar/approval-modal still
181
+ JSON. (jcardinal)
150
182
  - 2026-06-29 — Migrated VendorItems (screen 2, recipe-exact) and Inventory (screen 3,
151
183
  **presentation-only by decision** — fetch topology stays JSON; only title/buttons/modal copy/entity
152
184
  tags moved). Fixed the "s.elements is not iterable" crash: `extractBundle()` now finds the bundle by
@@ -5,6 +5,7 @@ apps:
5
5
  - _underscore
6
6
  - api2
7
7
  - toga2-supply
8
+ - toga25-supply
8
9
  - toga2-commerce
9
10
  - worker
10
11
  - worker2
@@ -5,6 +5,7 @@ apps:
5
5
  - _underscore
6
6
  - api2
7
7
  - toga2-supply
8
+ - toga25-supply
8
9
  - toga2-commerce
9
10
  - worker
10
11
  - dbchanges2
@@ -5,6 +5,7 @@ apps:
5
5
  - _underscore
6
6
  - api2
7
7
  - toga2-supply
8
+ - toga25-supply
8
9
  - toga2-commerce
9
10
  - worker2
10
11
  - dbchanges2
@@ -12,7 +13,7 @@ project: _Underscore
12
13
  client: quad
13
14
  type: profile
14
15
  status: active
15
- updated: 2026-06-26
16
+ updated: 2026-06-30
16
17
  owners: ["jcardinal", "bala"]
17
18
  files: []
18
19
  related:
@@ -0,0 +1,74 @@
1
+ ---
2
+ type: session
3
+ slug: surface-layer
4
+ title: Surface grouped-meta + SalesOrders section migration (Compass/CompassCanada/Quad)
5
+ author: jcardinal
6
+ repos: [_underscore, dbchanges2, toga25-supply]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-06-30
11
+ updated: 2026-06-30
12
+ ---
13
+
14
+ # Session: surface-layer
15
+ **Date:** 2026-06-30
16
+ **Project/Repo:** _underscore / dbchanges2 / toga25-supply (2.0)
17
+ **Task:** Build a grouped surface-meta resolver and migrate the toga25-supply SalesOrders record-modal sections onto the Surface UI presentation layer for the seeded clients (Compass, CompassCanada, Quad), keeping non-migrated clients on JSON.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+ - **`metaGroup` grouped resolver** — `_Model_Core_Surface::metaGroup(&$api, string $slugs): object` at `GET /v2/surfaces/meta-group?slugs=a,b,c`. Splits/dedupes the slug list (cap `META_GROUP_MAX_SLUGS=25`, logs truncation), calls the existing `resolve()` per slug (shares its per-slug file cache), returns `{group, surfaces:{slug=>bundle|null}}`. `php -l` clean. Verified live on dev-sandbox (200 after the ACL grant).
23
+ - **Resolver message-resolution fixes** (`Surface.php` `_buildBundle`/`_resolveElement`): remap element message slots id→messageKey before return (labels previously fell back to the element key); collect `disabledTooltipMessageId` + emit `disabledTooltip`; set `surface.titleMessageKey`; merge the 2nd `_loadMessages` return into `$messageKeyById`; log dangling message ids; `resolve()` now `: object`. These were latent because SECTION/BUTTON_BAR rendering was never shipped.
24
+ - **FE grouped fetch + adapter**: `useFetchSurfaceMetaGroup(slugs[])` (order-insensitive key, staleTime Infinity, `extractGroup` by `{surfaces}` fingerprint) and `surfaceBundleToTenantFields()` adapter → existing `TenantFields` shape, so `SalesOrderSummaryGrid` renderers + `getDetailSections` are untouched.
25
+ - **Migrated-client roster gate**: `SALES_ORDER_SURFACE_MIGRATED_CLIENTS=[COMPASS,COMPASSCANADA,QUAD]` in the view-model; only roster clients fetch+resolve Surface sections (merge = JSON-base + Surface-wins), everyone else stays JSON.
26
+ - **DB seeds (applied on dev-sandbox, working)**: Core `2026-06-30a` (meta-group RecordScript + `order-recurring` surface + 5 display-toggle marker surfaces); Client `2026-06-30a` meta-group AclRecordScripts grant; Compass/CompassCanada/Quad `2026-06-30a` SurfaceOverrides for section deltas.
27
+ - **403 EZ-1 on meta-group fixed** by applying the Client AclRecordScripts dispatch grant per client DB. Verified via toga-db: `meta` script id 25 → roles 1,3,4; `meta-group` id 27 had no grant until applied.
28
+ - **JSON cleanup**: removed the migrated section blocks from COMPASS/COMPASSCANADA/QUAD `orderViewFields.json` (kept salesOrderDetailsConfig/itemFulfillmentClickRule/recordActionFields/recordApprovals). DEFAULT left intact.
29
+ - **/capture PUSHED** — 4 surface feature docs + 3 client profiles updated on team `_main` (commits f732160 + fc8de41).
30
+
31
+ ## What did NOT work — DO NOT RETRY THESE
32
+ - **Client-DB seeds resolving Core ids via cross-DB subselect** (`Core.RecordScripts`/`Core.Surfaces`/`Core.SurfaceElements` from a `Client_*` file). Works on the dev-sandbox all-in-one instance but **WILL FAIL in production** — Core is a separate MySQL cluster there, and cross-DB queries require one instance. This is the current id-agnostic approach in all 4 Client seeds; it is the prod blocker (see Blockers).
33
+ - **Parent `parentSurfaceId` GROUP-row design for grouped meta** — rejected by CTO review: adds Core schema state + a new TYPE, must be seeded per-client per-env for a grouping the FE already knows statically, and reintroduces id drift. The slug-list `metaGroup` is schema-stateless.
34
+ - **Surface-only cutover for all clients** (no roster gate) — would regress non-seeded clients (NYCHH/Prudential/SPGlobal/DEFAULT) because the Core section surfaces are SHARED and seeded from Compass's shape; they'd inherit it.
35
+ - **Overriding a section's visibility via the Surface row's own `isVisible`** — the resolver cascade (`_loadOverrides`) only overrides ELEMENTS, never a surface's `isVisible`. Hence each display-toggle surface carries one `sectionVisibility` marker element that the overrides target.
36
+ - **Removing MANAGER/Quad summary shipping/tax tooltips via override** — `_castOverride` treats `TOOLTIP_MESSAGE` as numeric-only, so it can't null a tooltip. Cosmetic; deferred.
37
+ - **Seeding `elementType` for Quad** — confirmed dead config (no `.ts/.tsx` reads it); needs no migration.
38
+
39
+ ## Not tried yet (candidates for next session)
40
+ - **Prod cross-cluster seed fix** (the on-hold blocker): rewrite the 4 Client seeds to hardcode Core ids as literals + resolve `roleId` by name (per `Client/2026-06-23b - ItemTranslationsAcl.sql`), zero `Core.` refs. User chose "I'll give you the prod ids" — apply the Core seed to prod Core, run the lookup query (in session memory / below) to get literal recordScriptId/surfaceId/surfaceElementId, then bake them in. Also audit the EXISTING `2026-06-25c`/`2026-06-29c`/`2026-06-25d` surface seeds for the same anti-pattern.
41
+ - **Action-bar / approval-modal migration** — still JSON; pulls in the Compass approval state machine + Tier-2 `stepTwoAssigned`. The `sales-order-actions` BUTTON_BAR surface is already seeded but not yet consumed by the FE.
42
+ - **Seed NYCHH / Prudential / SPGlobal** (and add to the roster) once confirmed live on toga25-supply; they need Quad-like client-level overrides to match the DEFAULT shape.
43
+
44
+ ## Current file state
45
+ | File | Status | Notes |
46
+ |------|--------|-------|
47
+ | _underscore/Model/Core/Surface.php | modified | metaGroup() + message-resolution fixes; php -l clean |
48
+ | dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql | created | meta-group script + order-recurring + 5 display surfaces + markers. Prod-safe (Core→Core). |
49
+ | dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql | created | meta-group dispatch grant. ⚠ cross-DB Core ref — prod fix pending. |
50
+ | dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql | created | MANAGER hide invoices/admin-notes. ⚠ cross-DB. |
51
+ | dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql | created | MANAGER deltas (shippingMethod/invoices/admin-notes), role by name. ⚠ cross-DB. |
52
+ | dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql | created | client-level deltas (costCenter/shippingMethod/recurringTotal + 3 display hides). ⚠ cross-DB. |
53
+ | toga25-supply/src/surface/useFetchSurfaceMeta.ts | modified | added useFetchSurfaceMetaGroup + extractGroup |
54
+ | toga25-supply/src/surface/index.ts | modified | re-export group hook |
55
+ | toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts | created | adapter + slug list + migrated-client roster |
56
+ | toga25-supply/.../SalesOrderRecordModalLayout/helpers/index.ts | modified | re-export adapter + roster |
57
+ | toga25-supply/.../viewModel/useSalesOrderRecordModalLayoutModel.tsx | modified | roster-gated Surface merge |
58
+ | toga25-supply/.../FIELDS/{COMPASS,COMPASSCANADA,QUAD}/orderViewFields.json | modified | section blocks deleted; data-wiring kept |
59
+
60
+ ## Decisions made
61
+ - **Grouped meta = slug-list `metaGroup`, not a parent GROUP row** (CTO AGREE-with-alternative): schema-stateless, reuses resolve()/cache, avoids id drift.
62
+ - **Migrated-client roster gate in code, not an env flag**: protects unseeded/unknown clients from the shared Compass-shaped Core base. Remove a client from the roster only after its seeds are verified; then its JSON sections are safe to delete.
63
+ - **Display sections = field-less SECTION surfaces with one `sectionVisibility` marker element**: only way to express per-role section visibility given the resolver overrides elements, not surface.isVisible.
64
+ - **Data-wiring stays JSON** (salesOrderDetailsConfig, itemFulfillmentClickRule): presentation-only migration, config/logic boundary (Inventory precedent).
65
+ - **Prod-safe Client-seed pattern = literal Core ids + Client-local roleId-by-name** (per ItemTranslationsAcl). Deferred to the prod pass.
66
+
67
+ ## Blockers
68
+ - **PROD CROSS-CLUSTER (on hold, user-paused):** the 4 Client-DB seeds use `Core.*` cross-DB subselects that break against prod's separate Core cluster. Fix recorded in memory [[surface-prod-crossdb-fix-pending]]. Lookup query to get prod literal ids is in the session thread. Dev-sandbox is unaffected (all-in-one) and fully working.
69
+
70
+ ## Exact next step
71
+ > When resuming the prod fix: apply `Core/2026-06-30a` to the prod Core cluster, then run the labeled lookup query against prod Core (returns meta-group recordScriptId + surfaceId/surfaceElementId for order-details/c_erpEntityId, ship-to/shippingMethod.name, order-recurring/isTotal, and the invoices/admin-notes/additional-order-details/notes `sectionVisibility` markers). Paste results, then rewrite the 4 Client `2026-06-30a` seeds to hardcode those Core ids as literals and resolve only `roleId` by name — removing every `Core.` reference — following `dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql`.
72
+
73
+ ---
74
+ _Saved by /session-save on 2026-06-30_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.241",
3
+ "version": "1.0.243",
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",