toga-ai 1.0.605 → 1.0.607

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.
@@ -7,11 +7,13 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-08-13
10
- owners: ["bala", "tcox", "dfranks"]
10
+ owners: ["bala", "tcox", "dfranks", "apeterson"]
11
11
  files:
12
12
  - _underscore/Email.php
13
13
  - _underscore/String.php
14
14
  - worker2/Worker/Infrastructure/Email/Send.php
15
+ - _underscore/Model/Client/Logs/Email.php
16
+ - _underscore/Model/Client/Logs/EmailAttachment.php
15
17
  related:
16
18
  - email-template-sending.md
17
19
  - string-html-entity-helpers.md
@@ -84,6 +86,19 @@ derive one from the other).
84
86
  reading the row, not by waiting for mail.
85
87
  - **~1-minute latency, not instant.** Anything that assumes an email is sent synchronously at the
86
88
  `send()` call is wrong — transmission happens on the next Send-worker tick.
89
+ - **⚠ The `Logs_<Client>.Email` + `EmailAttachment` queue tables must be PROVISIONED, or every
90
+ send throws `Table 'logs_<client>.email' doesn't exist`.** The two tables the pipeline INSERTs
91
+ into (`Email`, `EmailAttachment`, accessed via the `DB_CLIENT_LOGS` alias) are backed only by the
92
+ models `_underscore/Model/Client/Logs/Email.php` + `EmailAttachment.php` — **no dbchanges2
93
+ migration ever created them**, so a client whose `Logs_<Client>` schema predates (or never ran)
94
+ the create is missing them, and the very first `_Email::send()` for that client 500s *after* the
95
+ calling code is otherwise correct. Symptom seen on `logs_quad`: `"Table 'logs_quad.email' doesn't
96
+ exist"` immediately after the Quad approval-email code fix. Interim fix = hand-authored
97
+ `CREATE TABLE` DDL matching the `Logs_<Client>` house style (`status` ENUM
98
+ `PENDING`/`SENT`/`FAILED`; `body`/`to`/`cc`/`bcc` as text; **no FKs**, per the logs-DB
99
+ convention). **Durable follow-up (pending): add the `Email`/`EmailAttachment` tables to the
100
+ `Logs_<Client>` blank-client template + a dated dbchanges2 migration so every environment/client
101
+ inherits them.**
87
102
  - **The Send worker forces HTML mode.** `sendEmail()` calls `$mailer->IsHTML(true)`
88
103
  **unconditionally**, ignoring the sender's `setIsHtml(false)`. A caller that queued a
89
104
  "plain-text" message is still transmitted HTML-mode. Author bodies accordingly.
@@ -160,6 +175,12 @@ derive one from the other).
160
175
  for a hotfix.
161
176
 
162
177
  ## Change history
178
+ - 2026-08-18 — Recorded that the `Logs_<Client>.Email` + `EmailAttachment` queue tables are
179
+ **not created by any dbchanges2 migration** (only the models exist), so a client whose
180
+ `Logs_<Client>` schema lacks them 500s on the first `send()` with `"Table 'logs_<client>.email'
181
+ doesn't exist"` — hit on `logs_quad` right after the Quad approval-email fix. Interim = hand
182
+ DDL (status enum, text body/to/cc/bcc, no FKs); durable follow-up = add both tables to the
183
+ `Logs_<Client>` blank template + a dated migration. (apeterson)
163
184
  - 2026-08-13 — Documented that **`send()` resolves the `Logs_<Client>` database itself** from the
164
185
  client identifier (`Core.Clients` → `Databases` → `DatabaseHosts` for the environment + region) and
165
186
  **throws unless exactly one row matches** — so `setClientIdentifier()` takes a lookup key that must
@@ -7,7 +7,7 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-08-12
10
- owners: ["dfranks", "jcardinal", "mhammontree", "ajean", "bala"]
10
+ owners: ["dfranks", "jcardinal", "mhammontree", "ajean", "bala", "apeterson"]
11
11
  files:
12
12
  - _underscore/Error.php
13
13
  - _underscore/Database.php
@@ -485,6 +485,15 @@ gracefully — it silently deletes all observability for the whole tier. Any cha
485
485
  - **⚠ A missing `Logs.Issue` column silently disables ALL error capture on that environment** —
486
486
  see the open defect above. Symptom: `identifiers.captureFailure` in the API response and nothing
487
487
  anywhere else.
488
+ - **⚠ The whole `Logs` cluster schema must be PROVISIONED on every environment, or capture
489
+ itself throws and swallows the real error.** The six singular `Logs` tables
490
+ (`Issue`/`Event`/`IssueFingerprint`/…) are created by the `dbchanges2/Logs/` migrations; an
491
+ environment whose `Logs` DB never ran them (or ran a partial subset) fails *inside*
492
+ `_Error::captureException()` with e.g. `"Table 'Logs.IssueFingerprint' doesn't exist"`. Because
493
+ this is the error-capture layer, the failure masks the **original** exception, so a real 500 on
494
+ that box may be under-logged or invisible. Seen on the client sandbox environment (`IssueFingerprint`
495
+ absent). Fix = run the `dbchanges2/Logs/` migrations on that environment's `Logs` DB. Diagnose a
496
+ suspiciously-empty capture by confirming the `Logs` tables exist before chasing the app code.
488
497
  - **Read-your-writes: reads go to the READ host and cannot see an uncommitted write.** A
489
498
  second `$issue->save()` re-reads the row via `_Model::initialize()`, and that SELECT goes to
490
499
  the read host — a separate connection that cannot see the still-uncommitted INSERT on the
@@ -652,6 +661,12 @@ clientUserId). **Neither was built.** As built instead:
652
661
 
653
662
  ## Change history
654
663
 
664
+ - 2026-08-18 — Recorded the environment-provisioning gotcha: the `Logs` cluster tables
665
+ (`Issue`/`Event`/`IssueFingerprint`/…) come from the `dbchanges2/Logs/` migrations, and an
666
+ environment missing them fails inside `_Error::captureException()` (e.g. `"Table
667
+ 'Logs.IssueFingerprint' doesn't exist"`), masking the original exception. Seen on the client
668
+ sandbox environment. (apeterson)
669
+
655
670
  - 2026-08-12 — **Corrected the 2026-08-06 open defect and marked it RESOLVED.** The sandbox-dev
656
671
  `Logs.Issue` drift was **seven** missing columns, not one: `clickupPriority` (`2026-08-03a`) **plus the
657
672
  whole `2026-08-05a` lifecycle migration** (`status`, `dtAutoResolved`, `baselineGapSeconds`,
@@ -18,7 +18,7 @@
18
18
  | [TOGa IQ Sprint Dashboard API (Record Scripts)](features/sprint-dashboard-api.md) | The internal **TOGa IQ sprint dashboard** is served in production by **six api2 Record Scripts** on `_Model_Team_Sprint` (`_underscore/Model/Team/Sprint.php`), | _underscore/Model/Team/Sprint.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-07-24a - SprintDashboardRecordScripts.sql, dbchanges2/Client_True/2026-07-24b - SprintDashboardScriptAcl.sql |
19
19
  | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
20
20
  | [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
21
- | [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql |
21
+ | [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql, dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql, dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql, dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql |
22
22
  | [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
23
23
  | [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, api2/Component/Api/V2/Response/Oauth/Oauth.php, api2/Controller/Index.php, toga2-supply/src/globalTypes.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/api/toga.ts, _underscore/Model/Client/TrackingNumber.php |
24
24
  | [V2 request deadlock-retry — route-scoped in-process replay](features/v2-deadlock-retry.md) | api2's front controller can **detect a MySQL deadlock (1213) / lock-wait timeout (1205) and replay the whole request in-process**, so a transient lock collision | api2/Controller/Index.php, _underscore/Database.php, _underscore/Query.php |
@@ -14,6 +14,7 @@ files:
14
14
  - dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql
15
15
  - dbchanges2/Core/2026-08-17a - ItemFulfillmentQuantityFieldTypeNumber.sql
16
16
  - dbchanges2/Client/2026-08-17a - ItemFulfillmentColumnsCopyable.sql
17
+ - dbchanges2/Client_Quad/2026-08-18c - SalesOrderListingSortByDateOrderDesc.sql
17
18
  related:
18
19
  - tableview-apiwhereclause-row-filtering.md
19
20
  - ../../toga25-supply/features/meta-driven-table-data.md
@@ -93,8 +94,42 @@ per-client folder — the reference migration
93
94
  `dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql` adds the hidden
94
95
  `Items.uuid` column to the `items-for-purchase-orders` view and was verified against `Client_Nychh`.
95
96
 
97
+ ## Default sort is DB-driven (`TableViews.sortPrimary*`), emitted as `meta.table.sort`
98
+
99
+ A view's **default sort** is not code — it lives on the `Client_*.TableViews` row:
100
+
101
+ - **`sortPrimaryTableViewFieldId`** → FK to `TableViewFields.id` (the column to sort by), plus
102
+ **`sortPrimaryDirection`** (`'ASC'`/`'DESC'`).
103
+ - **`sortSecondaryTableViewFieldId`** (+ its direction) for a tie-break sort.
104
+
105
+ The API emits it as **`meta.table.sort = [{slug, dir}]`** (the `slug` is the referenced
106
+ `TableViewFields.slug`), which the frontend applies as the initial ordering.
107
+
108
+ **To change a listing's default sort per client, use a multi-table JOIN UPDATE — never a
109
+ subquery on `TableViews`.** Because `sortPrimaryTableViewFieldId` points at a
110
+ `TableViewFields` row of the *same* view, a `SET ... = (SELECT id FROM TableViewFields WHERE
111
+ tableViewId = TableViews.id ...)` self-references `TableViews` and MySQL rejects it with
112
+ **error 1093** ("can't specify target table for update in FROM clause"). Instead JOIN
113
+ `TableViewFields` on its `slug` and set the id from the joined row:
114
+
115
+ ```sql
116
+ UPDATE TableViews tv
117
+ JOIN TableViewFields tvf
118
+ ON tvf.tableViewId = tv.id AND tvf.slug = 'dateOrder'
119
+ SET tv.sortPrimaryTableViewFieldId = tvf.id,
120
+ tv.sortPrimaryDirection = 'DESC'
121
+ WHERE tv.slug = '<listing-slug>';
122
+ ```
123
+
124
+ This is the same shape as the `Client_Compass` item-fulfillment view's default-sort override.
125
+ Applied for Quad: the sales-orders listing now defaults to `dateOrder DESC` (most recent
126
+ first) instead of `number DESC` (`dbchanges2/Client_Quad/2026-08-18c -
127
+ SalesOrderListingSortByDateOrderDesc.sql`).
128
+
96
129
  ## Gotchas / known issues
97
130
 
131
+ - **Default-sort updates must JOIN `TableViewFields`, not subquery `TableViews`** — a subquery
132
+ on the same view triggers MySQL error 1093. See *Default sort* above.
98
133
  - **`recordFieldId` is cross-DB** — it indexes `Core.RecordFields`, not anything in the client DB.
99
134
  Resolve the id in `Core`, but insert the `TableViewFields` row in the client DB.
100
135
  - **Don't assume `Core.RecordFields`/`Core.Records` ids are stable across environments** — look them
@@ -108,6 +143,11 @@ per-client folder — the reference migration
108
143
 
109
144
  ## Change history
110
145
 
146
+ - 2026-08-18 — Documented DB-driven **default sort**: `TableViews.sortPrimaryTableViewFieldId`
147
+ (+ `sortPrimaryDirection`, and the secondary pair) FK into `TableViewFields`, emitted as
148
+ `meta.table.sort=[{slug,dir}]`; and the JOIN-`TableViewFields`-on-slug UPDATE recipe that
149
+ avoids MySQL error 1093 (a self-subquery on `TableViews` is rejected). Applied to Quad's
150
+ sales-orders listing (default `dateOrder DESC`). (apeterson)
111
151
  - 2026-08-17 — Documented that a column's `type`/`precision` are canonical from `Core.RecordFields`
112
152
  (no per-view override — a change hits every view/client) while `isCopyable`/`isSortable`/
113
153
  `isFilterable`/`isVisible` are per-view `TableViewFields` flags. Confirmed setting
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-14
9
+ updated: 2026-08-18
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
@@ -641,6 +641,19 @@ rule resumes.
641
641
  messageKey; appId/recordId inherited from the `order-details` sibling by slug), Core→Core only
642
642
  (prod-safe). Per-client SurfaceOverride divergence not yet seeded — shared Core default + FE wiring
643
643
  only. (apeterson)
644
+ - 2026-08-18 — Seeded the **"Enter PO Details" row-action `MENU_ITEM`** on the
645
+ `sales-order-listing-row-actions` surface (id 3): `Core/2026-08-18a` adds Message
646
+ `salesOrder.action.enterPo` + Action `salesOrder.enterPo` (`OPEN_MODAL`,
647
+ `config {kind:poNumber, valueKey:poNumber}`) + a `renderType='MENU_ITEM'` element, seeded Core
648
+ **neutral off** (`isVisible=0`/`isEnabled=0`). A row action needs a full Action+Message+element
649
+ triple (not a clone of the record-actions BUTTON — different FE dispatch seam, see
650
+ [surface-frontend](../../toga25-supply/features/surface-frontend.md)). Quad opts in via
651
+ `Client_Quad/2026-08-18b` (`IS_VISIBLE=1` client-wide, `IS_ENABLED=1` roles 7/8/9,
652
+ `VISIBILITY_RULE` `order._status in [pendingApproval]`). Separately, `Client_Quad/2026-08-18a`
653
+ gates the existing record-actions "Enter PO Details" **BUTTON** (surface
654
+ `sales-order-record-actions`, `config.valueKey=poNumber`) with a `VISIBILITY_RULE` limited to
655
+ `pendingApproval` — Quad is single-stage (only `pendingApproval`), whereas Compass/CompassCanada
656
+ widen the same gate to include `pendingInitialApproval`. (apeterson)
644
657
  - 2026-07-21 — Added the sales-order **Approve enable-gate** per-client overrides (`Client_*/2026-07-21a`):
645
658
  Compass/CC ADMIN `ENABLED_RULE` = `(pendingInitialApproval AND stepTwoAssigned) OR pendingApproval`
646
659
  on surface 8 el 17 + surface 3 el 29 (rule JSON in `c_longValue`; Compass UPDATEs its existing
@@ -10,6 +10,6 @@
10
10
  | [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | On large deployments the backend bumps the Core parameter `META_LAST_REFRESH_DATETIME`. | toga25-supply/src/hooks/useDeploymentGuard.tsx, toga25-supply/src/App.tsx |
11
11
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
12
12
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts |
13
- | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
13
+ | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
14
14
  | [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json |
15
15
  | [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-14
9
+ updated: 2026-08-18
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga25-supply/src/surface/applyColSpan.ts
@@ -39,6 +39,7 @@ files:
39
39
  - toga25-supply/src/pages/Login/LoginPage.tsx
40
40
  - toga25-supply/src/pages/SalesOrders/SalesOrders.tsx
41
41
  - toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx
42
+ - toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts
42
43
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx
43
44
  - toga25-supply/src/pages/Items/ItemsPage.tsx
44
45
  - toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx
@@ -115,6 +116,24 @@ feature flag — see below). Backend: [surface-resolver](../../_underscore/featu
115
116
  change.
116
117
  - **`actionRegistry`** — string action key → existing app handler (the proven `MODAL_RENDERERS`
117
118
  pattern). The DB ships keys + payloads; the app supplies the actual handlers.
119
+
120
+ ### Row-menu `MENU_ITEM` dispatch is a DIFFERENT path than a record-actions `BUTTON` — you cannot clone one as the other
121
+ The listing **row menu** (`SurfaceRowActions.tsx`) and the record-modal **action bar**
122
+ (`SalesOrderTopBar` / `SurfaceActionBar`) dispatch through two distinct seams, so moving a
123
+ control from one surface to the other is **not** a copy of the element:
124
+ - **Row menu items** render as **`renderType='MENU_ITEM'`**, take their label from a
125
+ **`Core.Messages`** row, and dispatch **by `action.key` → `actionRegistry`**. So a new row
126
+ action needs a real **`Core.Action`** + a label **`Core.Message`** + an **`actionRegistry`
127
+ handler** keyed by that action.key.
128
+ - **Record-actions controls** render as **`renderType='BUTTON'`** and dispatch **by
129
+ `config.valueKey`** through `SalesOrderTopBar` (not the action.key/registry path).
130
+ - Therefore you **cannot** duplicate a record-actions BUTTON element onto a row-actions surface
131
+ and expect it to work — a row item must be authored as a MENU_ITEM with its own Action +
132
+ Message + registry handler. Concrete: "Enter PO Details" existed only as the record-actions
133
+ BUTTON; adding it to the `sales-order-listing-row-actions` surface required a new
134
+ `salesOrder.action.enterPo` Message + `salesOrder.enterPo` Action (`OPEN_MODAL`,
135
+ `config {kind:poNumber, valueKey:poNumber}`) + an `actionRegistry["salesOrder.enterPo"]`
136
+ entry (`forwardToDispatch("poNumber")` → `setApprovalModal({modalSelected:'poNumber'})`).
118
137
  - **theme/message resolvers** (`resolve.ts`) — token → CSS and ICU message rendering.
119
138
  - **`SurfaceActionBar`/`SurfaceActions` + `SurfaceSection`** — generic renderers.
120
139
 
@@ -512,6 +531,16 @@ developer asked for the **whole component**, not just badges. The old
512
531
 
513
532
  ## Gotchas
514
533
 
534
+ - **The VIP `/contacts` API call is gated by an approval-details element's `labelSuffix.valueKey`,
535
+ NOT by the element's `isVisible`.** `useSalesOrderVip.ts` decides whether to call `/contacts`
536
+ (which reads `Users.c_isVip`) by scanning the approval-details surface for an element whose
537
+ `labelSuffix.valueKey` **ends in `isVip`**. So for a client that does not have the VIP chip
538
+ configured (e.g. Quad), that call 403s `EZ-2` on `Users.c_isVip` unless the VIP `labelSuffix` is
539
+ stripped from the surface. `Core/2026-08-10a` strips the VIP `labelSuffix` from the Core base
540
+ surface (`JSON_REMOVE config.$.labelSuffix`); Compass + Compass Canada re-add the gold VIP chip
541
+ via a `CONFIG` override. Applying the Core seed turns VIP off for Quad/all-non-Compass while
542
+ keeping it on for Compass/Compass Canada — **no FE change needed**. The gate is the
543
+ `labelSuffix.valueKey`, not the visibility flag.
515
544
  - **A named predicate that FAILS OPEN silently enables a button when its data isn't fetched.**
516
545
  `stepTwoAssigned` returns `true` when `order.currentStage` is `null` (a fail-OPEN default). So if the
517
546
  surface's approval-stage data is not plumbed in (e.g. `approvalsEnabled=false`, so
@@ -634,6 +663,14 @@ developer asked for the **whole component**, not just badges. The old
634
663
  treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
635
664
 
636
665
  ## Change history
666
+ - 2026-08-18 — Documented that a listing **row-menu `MENU_ITEM`** dispatches by
667
+ `action.key`→`actionRegistry` (label from `Core.Messages`), a **different seam** than a
668
+ record-actions **`BUTTON`** (dispatched by `config.valueKey` via `SalesOrderTopBar`) — so a
669
+ BUTTON element cannot be cloned onto a row-actions surface; a row action needs its own Action +
670
+ Message + registry handler (built "Enter PO Details" as `salesOrder.enterPo`). Also recorded that
671
+ `useSalesOrderVip` gates the `/contacts` (`Users.c_isVip`) call by an approval-details element's
672
+ `labelSuffix.valueKey` ending in `isVip` — stripping the VIP `labelSuffix` (Core/2026-08-10a)
673
+ turns the call off for non-Compass clients (Quad) with no FE change. (apeterson)
637
674
  - 2026-08-14 — Moved the whole Sales Order **Admin Notes** component into Surface (badge labels/colors,
638
675
  chrome labels, layout toggles — not just badges), retiring the hardcoded
639
676
  `addNotesFields.json → fieldsConfig → model → view` plumbing. New **`buildNotesComponent()`**
@@ -12,7 +12,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
12
12
  - **togaview** (TOGa View) — 7 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
13
13
  - **webhook** (Webhook) — 1 doc(s) → [1.0/apps/webhook/INDEX.md](1.0/apps/webhook/INDEX.md)
14
14
  - **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
15
- - **test** (Test) — 14 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
15
+ - **test** (Test) — 15 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
16
16
  - **toga** (TOGa) — 3 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
17
17
  - **tools** (Tools) — 15 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
18
18
 
@@ -5,10 +5,11 @@ project: _Underscore
5
5
  client: compass-usa
6
6
  type: workflow
7
7
  status: active
8
- updated: 2026-08-04
8
+ updated: 2026-08-18
9
9
  owners: ["jcardinal", "bala", "dfranks"]
10
10
  files: []
11
11
  related:
12
+ - 1.0/apps/test/features/compass-retrofix2-sql-generator.md
12
13
  - clients/compass-usa/profile.md
13
14
  - clients/compass-usa/features/asn-to-item-fulfillment.md
14
15
  - clients/compass-usa/features/mits-po-to-so-item-linking.md
@@ -71,6 +72,27 @@ back-link chain climbs **Agilant → ODP → Compass**. Item back-links use
71
72
  `ItemFulfillmentItems.upstreamItemFulfillmentItemId` the same direction and may be many-downstream →
72
73
  one-upstream for bundles.
73
74
 
75
+ > **Mirror-gap root cause — a missing item bridge stalls the climb (verified 2026-08-18).** The
76
+ > recursive IF mirror climbs Agilant → ODP → Compass through **BOTH** bridge tables. If the
77
+ > `PurchaseOrderItems_SalesOrderItems` rung linking the Compass/MITS PO item → the ODP SO item is
78
+ > **missing**, the mirror **cannot map past the ODP SO** and never creates the ODP/Compass mirror IFs
79
+ > — the Compass SO shows "nothing fulfilled" even though the Agilant SO has an ItemFulfillment
80
+ > (`c_netsuiteInternalItemFulfillmentId` set, created by NetSuite sync). Orders SA130530, SA130334,
81
+ > SA130430 all had an intact header chain and every other item bridge, but were missing **only** that
82
+ > one rung (confirmed against a healthy order that had it). When diagnosing a Flow-B order that
83
+ > fulfilled upstream but shows empty on Compass, check that specific bridge first.
84
+
85
+ ### Asset tags arrive as separate CG "units" (Flow B)
86
+ NetSuite delivers asset tags on **separate service-item lines** whose part number contains
87
+ `ASSETTAG` (e.g. `ODP-COMPASS-ASSETTAG1`). Older Compass imports landed each asset tag as its **own
88
+ `Units` row** whose `serialNumber` holds the CG value (e.g. `CG2000027730`), attached via
89
+ `ItemFulfillmentItemUnits` alongside the real serialized unit on the **same** fulfillment. The
90
+ forward fix (`library/app/api/toga2.php` ~lines 3688–3965) applies those asset tags to the serialized
91
+ items **in order** and sets `Units.assetTag` on the real device. Retroactively this is Phase 0 of the
92
+ [retrofix2 tool](../../../1.0/apps/test/features/compass-retrofix2-sql-generator.md); because units
93
+ are **shared up the chain** (same `Units.id` at every level), the correction must land before the
94
+ fulfillment mirror climbs.
95
+
74
96
  ## Expected raw-data invariants (for detect-and-repair)
75
97
 
76
98
  1. **Links:** every SOI that should be procured has exactly one PURE bridge to the matching POI on the
@@ -141,12 +163,18 @@ column is `ON UPDATE CURRENT_TIMESTAMP`), which can look like a fresh modificati
141
163
  `api2` cXML ShipNotice handler; the `_underscore` recursive item-fulfillment engine.
142
164
 
143
165
  ## Retrofix tooling
144
- A standalone repair script lives in the worker 1.0 test area (`test/@jeff/compass/retrofix.php`,
145
- machine-local path tracked in dev memory). It sweeps Compass SOs newest-first, detects deviations from
146
- the invariants above, and repairs via direct SQL on `db_prod2_compass`. Safety rules baked in: rehearsal
147
- mode (logs intended writes, rolls back), per-phase gating, a hard block on any write to the
148
- `TrackingNumbers` table, and the never-over-fulfill cap. It is a diagnostic/repair tool, not part of the
149
- runtime workflow.
166
+ The current repair script is **`test/@jeff/compass/retrofix2.php`** (1.0 `App_`), which
167
+ **supersedes** the older `retrofix.php` sibling in the same folder. Unlike the description below of the
168
+ earlier tool, retrofix2 **emits review-first `.sql`** rather than writing the DB directly. It runs
169
+ three phases newest-first per SO — **Phase 0** (CG-unit asset-tag merge; must run first because units
170
+ are shared up the chain), **Phase 1** (header/link/bridge repairs, levels L1–L4, where L3 fixes the
171
+ `PurchaseOrderItems_SalesOrderItems` rung the mirror needs), **Phase 2** (fulfillment mirror climb).
172
+ Each level is wrapped in `runLevel()` so one level's exception (notably L1's unconditional NetSuite
173
+ call failing) is logged as a skip and swallowed, letting the independent NetSuite-free repairs still
174
+ emit. Write guards: `emit()` hard-blocks `Units` and `TrackingNumbers` writes; the Phase-0-only
175
+ `emitUnit()` permits `Units` UPDATE/DELETE but still blocks `TrackingNumbers`. Full structure in the
176
+ [retrofix2 feature doc](../../../1.0/apps/test/features/compass-retrofix2-sql-generator.md). It is a
177
+ diagnostic/repair tool, not part of the runtime workflow.
150
178
 
151
179
  ## Edge cases & escalation
152
180
  - Incomplete orders (no PO/ASN/Agilant leg yet) are normal — repair only the layers that exist.
@@ -155,6 +183,12 @@ runtime workflow.
155
183
  - High-multiplier over-fulfillment (5×–20×) does not fit the split-PO spurious-link pattern — separate cause.
156
184
 
157
185
  ## Change history
186
+ - 2026-08-18 — Recorded the **Flow-B mirror-gap root cause** (a missing `PurchaseOrderItems_SalesOrderItems`
187
+ rung stalls the recursive IF mirror past the ODP SO, so a fulfilled Agilant order shows empty on
188
+ Compass — orders SA130530/SA130334/SA130430), the **CG asset-tag "unit" data shape** (asset tags land
189
+ as their own CG-serial `Units` rows), and repointed the Retrofix tooling section to the new
190
+ **retrofix2.php** (review-first `.sql`; Phase 0/1/2; `runLevel()` level isolation; `emit()`/`emitUnit()`
191
+ write guards). (jcardinal)
158
192
  - 2026-08-04 — Added the **"empty shell" order signature** — the ODP importer writes the SO header
159
193
  and its items in two separate API requests, so a fatal on the second leaves a committed,
160
194
  zero-item order — plus the three orphan sweeps that detect it (zero-item SOs, zero-item POs, ODP
@@ -82,4 +82,13 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
82
82
  - Quad is one of only **two** clients (with Compass) whose fulfillment views are free of the
83
83
  dangling `joinRecordId` 41 / `parentRecordFieldId` 211+321 config, because both were migrated to
84
84
  base record 29. Ten other clients still carry it.
85
+ - **Single-stage approval (2026-08-18).** Quad's approval flow has only the `pendingApproval`
86
+ stage (no `pendingInitialApproval`), so its PO-details visibility/enable gates on the
87
+ sales-order surfaces use `order._status in [pendingApproval]` — contrast Compass/Compass Canada,
88
+ which widen the same gate to include `pendingInitialApproval`. Quad opted into the "Enter PO
89
+ Details" **row-action** menu item (`sales-order-listing-row-actions`) client-wide, enabled for
90
+ roles 7/8/9. Seed side: [Surface Layer Schema](../../2.0/apps/dbchanges2/features/surface-layer-schema.md).
91
+ - **Sales-orders listing default sort = `dateOrder DESC` (2026-08-18)** — most recent first
92
+ (was `number DESC`), set on Quad's `TableViews` row. See
93
+ [TableView field/column metadata](../../2.0/apps/api2/features/tableview-field-metadata.md).
85
94
  - This profile is a starting point; expand as more Quad-specific behavior is captured.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.605",
3
+ "version": "1.0.607",
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",