toga-ai 1.0.543 → 1.0.545

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.
@@ -27,6 +27,7 @@
27
27
  | [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. | _underscore/Model/Core/Model.php, _underscore/Model.php, _underscore/Model/Rate/Subscription.php |
28
28
  | [_Model::save() vs raw _Query — no atomic conditional update](features/model-save-vs-query-atomic-update.md) | `_Model::save()` is a plain load-then-write ORM primitive and **cannot express an atomic conditional update** (an optimistic-concurrency / row-claim guard such | _underscore/Model.php, _underscore/Query.php, _underscore/Model/Rate/Subscription.php |
29
29
  | [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 |
30
+ | [Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings](features/page-meta-context-field-settings.md) | `_Model_Core_Page::meta()` is the **legacy** page-meta resolver behind `GET /pages/meta?slug=<slug>` — still the live path for `toga2-supply` and other pre-Surf | _underscore/Model/Core/Page.php |
30
31
  | [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/Query.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php, api2/Controller/Index.php |
31
32
  | [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
32
33
  | [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 |
@@ -0,0 +1,75 @@
1
+ ---
2
+ title: Legacy page meta (Page::meta) & context-scoped ClientRecordFieldSettings
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-10
10
+ owners: [tcox]
11
+ files:
12
+ - _underscore/Model/Core/Page.php
13
+ related:
14
+ - surface-resolver.md
15
+ - error-reporting-issue-event.md
16
+ - ../../../clients/elite/features/supply2-tableview-config-drift.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ `_Model_Core_Page::meta()` is the **legacy** page-meta resolver behind `GET /pages/meta?slug=<slug>`
22
+ — still the live path for `toga2-supply` and other pre-Surface frontends. It is being replaced by
23
+ [`_Model_Core_Surface::resolve`](surface-resolver.md), but must not be deleted while consumers
24
+ remain. One part of it — **context-scoped `ClientRecordFieldSettings`** — carries a live framework
25
+ bug that takes down *unrelated* pages for any client that uses the feature.
26
+
27
+ ## What context-scoped field settings are
28
+
29
+ `Client_*.ClientRecordFieldSettings` lets a client override a field's presentation (e.g. its label)
30
+ **in the context of another field**: a row carries the target `recordFieldId` plus a
31
+ `contextRecordFieldId`, meaning "when field X is shown *through* field Y, label it differently."
32
+
33
+ Example (Elite): row id 1 relabels field **58** to **"Total"** in the context of field **190**
34
+ (`ServiceRequests.id`) — authored for the `service-requests` view.
35
+
36
+ ## The bug — "Undefined array key \<contextRecordFieldId\>" 500
37
+
38
+ > **Status: reported to the backend, unfixed as of 2026-08-10.**
39
+
40
+ `Page::meta()` fetches a client's field-settings rows by **target field id only**, so Elite's row is
41
+ returned for **any** page whose field set contains field 58 — not just the page that owns field 190.
42
+ It then resolves `contextRecordFieldId` against a lookup array built **only from the current page's
43
+ fields**. When the context field is not on that page, PHP raises *"Undefined array key 190"* at
44
+ `Model/Core/Page.php:824`.
45
+
46
+ Historically that was a warning and the page still rendered. The **strict Sentry error handler**
47
+ deployed to beta on 2026-08-07 promotes it to a **fatal**, so it now surfaces as a hard 500 — Elite's
48
+ `/pages/meta?slug=inventory` (a page with nothing to do with service requests) began 500ing purely
49
+ because the service-requests row exists.
50
+
51
+ **Fix:** an `isset()` guard around `Page.php` ~L822–829 — skip any context setting whose context
52
+ field is not present on the requested page.
53
+
54
+ **Blast radius:** any client that uses context-scoped field settings can hit this on **every page
55
+ that shares the base field**. The failure looks unrelated to the page you are debugging, and it can
56
+ appear long after the settings row was authored — a redeploy that tightens error handling, not a
57
+ data change, is what makes it fatal.
58
+
59
+ ## Gotchas
60
+
61
+ - **A page-meta 500 is not necessarily about that page.** Trace it to the offending
62
+ `ClientRecordFieldSettings` row via the array key in the message (it is a `Core.RecordFields` id),
63
+ not via the slug you requested.
64
+ - **Error-handler strictness is a deploy-time behavior change.** Warnings that were survivable for
65
+ years become 500s the moment the strict handler ships; expect a cluster of "new" failures in old
66
+ code after such a deploy.
67
+ - Do not "fix" this by deleting the client's settings row — that removes a real client
68
+ customization. The guard belongs in the framework.
69
+
70
+ ## Change history
71
+ - 2026-08-10 — First capture: documented context-scoped `ClientRecordFieldSettings` and the
72
+ `Page.php:824` *"Undefined array key \<contextRecordFieldId\>"* fatal (context field not on the
73
+ requested page), which the 2026-08-07 strict Sentry handler turned from a warning into a 500 on
74
+ Elite's `/pages/meta?slug=inventory`. Reported to the backend; recommended `isset()` guard at
75
+ ~L822–829. (tcox)
@@ -111,9 +111,23 @@ Order matters — dump first so a failed dump never wipes a local schema it can'
111
111
  `Core` change-sets (e.g. the Surface-layer seeds), so a later replay of *committed* changes is
112
112
  largely a no-op. Only ad-hoc changes that were never committed to dbchanges2 need re-applying
113
113
  after a reset.
114
+ - **⚠ Beta itself gets rebuilt from PRODUCTION — treat every beta hand-insert as temporary.** A
115
+ `Client_*` schema on dev-sandbox can be re-imported from prod at any time (e.g. `Client_Elite`,
116
+ 2026-08-07 11:55). Everything hand-inserted on beta and never committed to `dbchanges2` is gone:
117
+ a hand-built TableView, a seeded `devteam@togatech.com` user (it returns as a row with an **empty
118
+ password**, which reads as "wrong password", not "missing user"), ad-hoc config rows. **Row ids
119
+ also shift on re-import**, so notes or scripts that identify a row by id stop matching — key on
120
+ the natural columns instead (for `TableViewJoins`: `tableViewId` + `joinRecordId` +
121
+ `joinOnRecordFieldId`). If a beta-only row matters, ship it as a `dbchanges2` migration. Worked
122
+ example: [Elite TableView config
123
+ drift](../../../clients/elite/features/supply2-tableview-config-drift.md).
114
124
 
115
125
  ## Change history
116
126
 
127
+ - 2026-08-10 — Added the **beta-is-rebuilt-from-prod** gotcha: a `Client_*` schema can be re-imported
128
+ from production at any time, discarding every uncommitted hand-insert (hand-built TableViews, the
129
+ seeded `devteam` user — which returns with an empty password) and **shifting row ids**, so match
130
+ rows by natural columns and ship beta-only rows as `dbchanges2` migrations. (tcox)
117
131
  - 2026-08-06 — Recorded that environment **`beta` resolves to the dev-sandbox cluster** in
118
132
  `Core.DatabaseHosts` for `Client_*` schemas, so an app pointed at `api.beta.togahub.com` is
119
133
  reading dev-sandbox data and the DB MCP environment to query is `dev-sandbox` (not
@@ -5,4 +5,4 @@
5
5
  | [TOGa Supply (toga2-supply) Architecture](architecture.md) | `toga2-supply` is the **React + Vite frontend** for TOGa Supply — warehouse fulfillment tooling (shipment selection, fulfill & ship against carrier APIs, NetSui | toga2-supply/src/api/toga.ts, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx |
6
6
  | [Fulfill & Ship](features/fulfill-and-ship.md) | Fulfill & Ship lets a warehouse user select sales-order line items, enter serials, pick a carrier/method, and in one action: create the Item Fulfillment records | _underscore/Model/Client/Measure.php, _underscore/Model/Client/TrackingNumber.php, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/getEditShipmentFormOptions.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/validateFormOnSubmit.ts, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/checkDimensions.ts, toga2-supply/src/components/ui/Tables/BasicTable/BasicTable.tsx, toga2-supply/src/components/ui/Tables/types.ts, toga2-supply/src/components/ui/GoogleMapsLink.tsx, toga2-supply/src/pages/ShipmentItems/view/forms/ShipmentItemsTable.tsx, toga2-supply/src/pages/ShipmentItems/api/ShipmentItemsApi.ts, toga2-supply/src/pages/ShipmentItems/types.ts, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/RETURNLABELFIELDS.json, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/components/ui/BaseInput/UnitSelect.tsx, toga2-supply/src/pages/EditShipment/view/modals/SerialNumbersModal.tsx, toga2-supply/src/pages/ShipmentItems/view/ShipmentItemsPage.tsx, toga2-supply/src/pages/EditShipment/view/EditShipmentPage.tsx, toga2-supply/src/pages/EditShipment/view/components/forms/EditShipmentForm.tsx, toga2-supply/src/pages/EditShipment/view/components/SelectedShipmentItemsTable.tsx, toga2-supply/src/pages/EditShipment/view/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/src/pages/EditShipment/viewModel/FIELDS/DUMMYUPDATESHIPMENTFIELDS.json, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/renderEditShipmentFormInput.tsx, toga2-supply/tailwind.config.cjs, toga2-supply/src/pages/EditShipment/view/modals/ReturnShippingModal.tsx, toga2-supply/src/styles/index.scss, toga2-supply/src/components/ui/BaseInput/BaseInput.tsx, toga2-supply/src/pages/EditShipment/api/UpdateShipmentApi.ts, toga2-supply/src/pages/EditShipment/viewModel/signatureTypes.ts, toga2-supply/src/pages/EditShipment/view/modals/SelectReturnAddressModal.tsx, toga2-supply/src/pages/EditShipment/helpers/ShipmentDetailsForm/formatShipmentData.ts, toga2-supply/src/pages/EditShipment/types.ts, toga2-supply/src/pages/Shipments/view/ShipmentsPage.tsx, toga2-supply/src/pages/Shipments/view/components/ShipmentsCardTableForm/ShipmentsCardTableForm.tsx, toga2-supply/src/pages/Shipments/api/ShipmentsApi.ts, toga2-supply/src/pages/Shipments/types.ts, toga2-supply/src/pages/FulfilledShipments/view/FulfilledShipmentsPage.tsx, toga2-supply/src/components/ui/CardTable/CardTable.tsx, toga2-supply/src/components/ui/CardTable/types.ts, toga2-supply/src/assets/pen-line.svg, _underscore/Model/Client/ItemFulfillment.php, _underscore/Trait/Netsuite/ItemFulfillment.php, _underscore/Component/Library/Carriers/Ups/Ups.php |
7
7
  | [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-supply` (React + Vite) builds and deploys on **AWS Amplify**. | toga2-supply/amplify.yml, toga2-supply/.gitattributes, toga2-supply/.github/workflows/sync-stage-environments.yml, toga2-supply/.env.qc-security |
8
- | [Onboarding a client to the supply2 frontend (host-scoping)](workflows/client-host-scoping.md) | How a new tenant becomes a "client" in the `toga2-supply` frontend. | toga2-supply/src/stores/useHostNameStore.ts, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/hooks/useHostName.tsx, toga2-supply/src/hooks/usePageDetails.tsx, toga2-supply/src/hooks/usePageListDetails.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/src/utils/handleClientAuthentication.ts, toga2-supply/src/App.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/api/toga.ts, toga2-supply/package.json |
8
+ | [Onboarding a client to the supply2 frontend (host-scoping)](workflows/client-host-scoping.md) | How a new tenant becomes a "client" in the `toga2-supply` frontend. | toga2-supply/src/stores/useHostNameStore.ts, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/hooks/useHostName.tsx, toga2-supply/src/hooks/usePageDetails.tsx, toga2-supply/src/hooks/usePageListDetails.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/src/utils/handleClientAuthentication.ts, toga2-supply/src/App.tsx, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/api/toga.ts, toga2-supply/package.json |
@@ -6,7 +6,7 @@ project: TOGa Supply
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-06
9
+ updated: 2026-08-10
10
10
  owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga2-supply/src/stores/useHostNameStore.ts
@@ -20,7 +20,14 @@ files:
20
20
  - toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts
21
21
  - toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts
22
22
  - toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx
23
+ - toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx
23
24
  - toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx
25
+ - toga2-supply/src/pages/Orders/OrdersPage.tsx
26
+ - toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts
27
+ - toga2-supply/src/pages/Orders/api/OrdersApi.ts
28
+ - toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts
29
+ - toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx
30
+ - toga2-supply/src/utils/convertConstructorColumnTitles.ts
24
31
  - toga2-supply/src/api/toga.ts
25
32
  - toga2-supply/package.json
26
33
  related:
@@ -81,6 +88,52 @@ shared static that shows *only* your additions for other hosts; `npx tsc --noEmi
81
88
  in on the real subdomain and confirm columns render (pageMeta), the group-by switcher is hidden if
82
89
  you locked it, and nested chevrons appear.
83
90
 
91
+ **Two follow-ups the review pass added (2026-08-10) — apply both when host-scoping inventory:**
92
+
93
+ - **Re-seed `localStorage("selectedGrouping")` when the stored uuid isn't in the resolved host's
94
+ array** (`InventoryPage.tsx`). The key is shared across hosts on one origin, so a same-origin host
95
+ switch (or corrupt JSON) otherwise leaves a stale uuid that the non-null-asserted `find` throws on.
96
+ This is a provable no-op for hosts sharing the default array.
97
+ - **Redirect `/inventory_items` → `/inventory` for hosts whose default view has no `subTableParent`**
98
+ (`InventoryRouter.tsx`). Do it **data-driven off the resolved view**, not off a host literal: a
99
+ terminal (non-chained) view otherwise passes `undefined` into `PrimaryTable`.
100
+
101
+ ## Reference implementation 2 — Elite Service Requests: retitling a shared page (2026-08-10)
102
+
103
+ The second Elite page is the **existing Orders page host-configured**, not a new route — the pattern
104
+ to copy when a client wants "their" version of a shared page under a different name. See
105
+ [Elite supply2 scope](../../../../clients/elite/features/supply2-scope.md) for the client detail.
106
+
107
+ The shape: a per-host `FIELDS/<HOST>/BASE.json` supplies `pageTitle`, `tableSlug`, `listingSlug`,
108
+ and feature flags; `useOrdersPageViewModel.ts` gets an **explicit host entry** (`ELITE: {base: …}`,
109
+ following the PRUDENTIAL `base` precedent); and every hardcoded slug literal in the page becomes
110
+ `fields?.<key>` **with the old literal as the fallback**. Elite's page replaced **8** occurrences of
111
+ `"sales-orders"` this way. Because no other host defines the new keys, every default path stays
112
+ literal-identical — that is the property that makes the change zero-risk for other tenants, and it is
113
+ what you should be able to assert before merging.
114
+
115
+ Feature removal is expressed by **omission**, not by a flag check scattered through the page:
116
+ omitting `approvalActionOptions` (plus `approvalsButton.isEnabled: false`) is what drops approvals.
117
+
118
+ **Routing stays centralized.** No route was added for "Service Requests"; the nav label and the page
119
+ title are config. Do not add per-client routes (see above).
120
+
121
+ ## When a client's table view sits on a DIFFERENT record than the detail route
122
+
123
+ Elite's `service-requests` table view is built on the **ServiceRequests** record (joining SalesOrders
124
+ 1:1), while every details-modal route in the page is a `/sales-orders` record route. A retitled page
125
+ is often *also* a re-recorded page — confirm with the backend which record the view is built on
126
+ before wiring the modal. The fix pattern:
127
+
128
+ - add a **resolver** API call that translates the row uuid into the record uuid the modal route
129
+ expects (Elite: `getSalesOrderUuidForServiceRequest()` in `Orders/api/OrdersApi.ts` — `GET
130
+ /sales-orders` with a ServiceRequests join and `where ServiceRequests.uuid = …`);
131
+ - thread the resolved uuid into the details view model as a react-query whose **`enabled` is gated on
132
+ the host**, so other hosts never issue the extra request;
133
+ - expect **columns the other record does not have** — see the `isQuote` gotcha below;
134
+ - expect the join to be **optional**: an unmatched row resolves to `null` and the modal opens empty.
135
+ Decide the empty-state treatment with product; do not silently ship a blank modal.
136
+
84
137
  ## Getting a login for a new host (auth + test user)
85
138
 
86
139
  Onboarding stalls here more often than in the frontend. Two findings from Elite:
@@ -112,6 +165,12 @@ a plaintext credential. Remember [beta = the dev-sandbox
112
165
  cluster](../../_underscore/workflows/local-db-refresh-from-beta.md) — seed the environment the app
113
166
  actually talks to.
114
167
 
168
+ > **The seeded account does not survive a tenant refresh.** When `Client_Elite` was rebuilt from
169
+ > production (2026-08-07), `devteam@togatech.com` came back as a **re-seeded row with an empty
170
+ > password** — the login failure looks like a wrong password, not a missing user. Re-run the same
171
+ > hash copy from `Client_Compass.Users` (id 127046 on dev-sandbox). Anything else you hand-inserted
172
+ > on beta is gone too — see the gotcha below.
173
+
115
174
  ## Onboarding checklist
116
175
 
117
176
  1. Confirm the client's subdomain resolves the tenant (hostname → client → `Client_<slug>` DB), and
@@ -147,8 +206,36 @@ actually talks to.
147
206
  - **View-model host maps must not depend on a fallback.** Orders' `useOrdersPageViewModel.ts`
148
207
  uses `fieldMapping[hostName] || fieldMapping.compass`, but the fallback key is a nonexistent
149
208
  lowercase `compass`, so an unrecognized host **throws**. Always add an explicit host entry.
209
+ (Still true as of 2026-08-10 — the broken fallback was deliberately left in place; fix it only as
210
+ its own change.)
211
+ - **The row-click modal registry is keyed by `pageTitle`.** `renderModalContent.tsx` picks
212
+ `modalConfig[hostName][pageTitle] || modalConfig.DEFAULT[pageTitle]` — so **retitling a page
213
+ silently unhooks its modal**: rows become inert with no error. Elite's modal stayed dead until a
214
+ `"Service Requests"` key was added to `modalConfig.DEFAULT` routing to `OrderDetailsModal`
215
+ (additive). Any host given a custom page title must get a matching modal-config key in the same
216
+ change.
217
+ - **A page slug with no backend page meta used to crash the whole table.**
218
+ `convertConstructorColumnTitles.ts` dereferenced the meta unguarded
219
+ (`Cannot read properties of null (reading 'settings')`) — hit the moment a `listingSlug` pointed at
220
+ a page that does not exist in `Core.Pages`. It is now null-guarded and degrades to `""` labels
221
+ (callers already fall back to the field name). Reusing another page's `listingSlug` is therefore
222
+ survivable, but remember it only affects **ACL and search labels** — column headers come from the
223
+ **table-view** meta, not the page meta.
224
+ - **A record swap costs you columns.** Elite's `service-requests` view is built on ServiceRequests,
225
+ which has **no `isQuote` column**, so Orders' unconditional `isQuote: false` filter 500'd with
226
+ `1054 Unknown column`. Filters that assume the SalesOrders shape must be made conditional on a
227
+ FIELDS flag (`hasQuoteFilter`), not assumed. Backend confirmed this is an FE-side fix.
150
228
 
151
229
  ## Change history
230
+ - 2026-08-10 — Added **reference implementation 2 — retitling a shared page** (per-host
231
+ `FIELDS/<HOST>/BASE.json` supplying `pageTitle`/`tableSlug`/`listingSlug`, explicit host entry in
232
+ the view model, slug literals threaded through `fields?.` with literal fallbacks, feature removal
233
+ by omission) and a section on **table views built on a different record than the detail route**
234
+ (uuid resolver, host-gated query, optional join). New gotchas: the modal registry is keyed by
235
+ `pageTitle` so retitling silently unhooks the modal; `convertConstructorColumnTitles` is now
236
+ null-guarded for a slug with no page meta; a record swap loses columns (`isQuote` → `1054`). Also
237
+ recorded the `selectedGrouping` re-seed + terminal-view `/inventory_items` redirect, and that a
238
+ tenant refresh from prod returns the seeded `devteam` user with an empty password. (tcox)
152
239
  - 2026-08-06 — Added the **Elite Inventory reference implementation** (separate host array +
153
240
  call-time `getGroupOptions()`, the shared `resolveClientHostName.ts` util, host-scoped
154
241
  `getInventoryPageFields()`, additive uuid `else if` branches, `npm run <host>` + hosts-file
@@ -23,6 +23,7 @@
23
23
  | [Etilize Catalog Item Import & Refresh](features/etilize-catalog-item-import.md) | Client-generic catalog onboarding from an S3 CSV plus an Etilize re-pull. | worker2/Worker/Etilize/Items.php |
24
24
  | [Etilize Item Translation Import](features/etilize-item-translation-import.md) | The abstract worker class `_Worker_Etilize_ItemTranslations` imports **non-English** item text from Etilize into the client's `ItemTranslations` table. | worker2/Worker/Etilize/ItemTranslations.php |
25
25
  | [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Monitors/RateEntitlement.php, worker2/Worker/Notification/Email.php, worker2/Worker/Rate.php, dbchanges2/Core/2026-05-21 - Monitors.sql, dbchanges2/Core/2026-06-29a - Rate Entitlement Contract Monitor.sql |
26
+ | [NetSuite Integrations Monitor (Monitor/Operations/NetsuiteIntegrations)](features/netsuite-integrations-monitor.md) | `_Worker_Monitor_Operations::NetsuiteIntegrations()` is a cross-client health check that detects **stuck NetSuite ↔ 2.0 integrations**. | worker2/Worker/Monitor/Operations.php, dbchanges2/Core/2026-08-10a - Netsuite Integrations Monitor.sql, _underscore/Database.php, _underscore/Query.php |
26
27
  | [NetSuite ↔ ClickUp / TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Clickup.php, worker2/Worker/Clickup/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, _underscore/Model/Forecast/Opportunity.php, test/@dave/approach/TRUE-80044.md, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
27
28
  | [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, worker2/Component/Forecast/Db/Db.php, worker2/Worker/Netsuite/Location.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_amq_invoice_resync_salesorder.js, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
28
29
  | [NetSuite Supporting-Record Webhook Importer (the reusable recipe)](features/netsuite-supporting-record-webhook-importer.md) | A single **repeatable recipe** for porting a legacy daily-pull NetSuite *supporting-record* importer (the lookup/dimension tables behind Forecast2 — Employees, | worker2/Worker/Netsuite/Employee.php, worker2/Worker/Netsuite/Account.php, worker2/Worker/Netsuite/Classification.php, worker2/Worker/Netsuite/Customer.php, worker2/Worker/Netsuite/Item.php, worker2/Worker/Netsuite.php, _underscore/Model/Forecast/Employee.php, _underscore/Model/Forecast/Account.php, _underscore/Model/Forecast/Classification.php, _underscore/Component/Forecast/Db/Db.php, test/@dave/test_employee_lifecycle.php, test/@dave/test_account_lifecycle.php, test/@dave/test_classification_lifecycle.php, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, worker/crons/toga2/forecast2/import_supporting_records.php |
@@ -15,6 +15,7 @@ files:
15
15
  - dbchanges2/Logs_Client/2026-05-21 - Email.sql
16
16
  related:
17
17
  - ./oneuptime-worker2-monitoring.md
18
+ - ./netsuite-integrations-monitor.md
18
19
  - ./monitoring-framework.md
19
20
  - ../../_underscore/features/email-send-pipeline.md
20
21
  - ../../_underscore/features/per-client-database-connections.md
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: NetSuite Integrations Monitor (Monitor/Operations/NetsuiteIntegrations)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-10
10
+ owners: ["jcardinal"]
11
+ files:
12
+ - worker2/Worker/Monitor/Operations.php
13
+ - dbchanges2/Core/2026-08-10a - Netsuite Integrations Monitor.sql
14
+ - _underscore/Database.php
15
+ - _underscore/Query.php
16
+ related:
17
+ - ./all-client-email-queue-monitor.md
18
+ - ./oneuptime-worker2-monitoring.md
19
+ - ./monitoring-framework.md
20
+ - ../../_underscore/features/per-client-database-connections.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ `_Worker_Monitor_Operations::NetsuiteIntegrations()` is a cross-client health check that
26
+ detects **stuck NetSuite ↔ 2.0 integrations**. It sweeps **every** client's business
27
+ database (`Client_<X>`), looks in the `Parameters` table for rows where
28
+ `` `key` `` LIKE `NETSUITE_EXECUTION_MODE_%` and `` `value` `` LIKE `1-RUNNING`, and pushes
29
+ one aggregate payload to a single OneUptime Incoming Request monitor with the **offending
30
+ integrations named** in the body. A row parked at the `1-RUNNING` sentinel means an
31
+ integration hung mid-run.
32
+
33
+ This is the second all-client monitor in `_Worker_Monitor_Operations`, modeled exactly on
34
+ its sibling [EmailQueue](./all-client-email-queue-monitor.md) — a "dumb reporter, smart
35
+ monitor": the worker only reports measured values and OneUptime holds the alarm
36
+ thresholds. It is **shared internal operational infrastructure**, not a client feature.
37
+
38
+ ## Key files / entry points
39
+
40
+ - `worker2/Worker/Monitor/Operations.php` — `abstract class _Worker_Monitor_Operations`.
41
+ - `public static function NetsuiteIntegrations(): string` — the action, route
42
+ `Monitor/Operations/NetsuiteIntegrations`.
43
+ - `private static function resolveClientDatabases(): array` — one `DB_CORE` query
44
+ returning every client + its **business** database name (`Client_<X>`) + this
45
+ environment's hosts. Mirrors `resolveClientLogDatabases()` but joins `Core.Clients` on
46
+ `clientDatabaseId` (not `logDatabaseId`).
47
+ - `dbchanges2/Core/2026-08-10a - Netsuite Integrations Monitor.sql` — the `Core.CronJobs`
48
+ seed row (see below).
49
+ - Dispatched by action string, not PHP import — the autoloader + `_Worker::runTask()`
50
+ reach the class via the cron action path.
51
+
52
+ ## How it works
53
+
54
+ 1. **Resolve every client's business database.** `resolveClientDatabases()` joins
55
+ `Clients` → `Databases` (via `Clients.clientDatabaseId`) → `DatabaseHosts` →
56
+ `Environments`/`Regions`, all **LEFT OUTER JOINs** so a misconfigured client is
57
+ reported, not silently dropped (`unresolvedClients`). Closest region first; later rows
58
+ for the same client are skipped.
59
+ 2. **Register each client's business DB under its REAL database name, no alias**, guarded
60
+ by `if (!isset(_Database::$_registers[$dbName]))`, then query via `_Query`. A per-client
61
+ loop cannot reuse a shared alias — every iteration would clobber it.
62
+ 3. **Sweep `Parameters` per client:**
63
+ `` SELECT `key`, `value` FROM Parameters WHERE `key` LIKE 'NETSUITE_EXECUTION_MODE_%' AND `value` LIKE '1-RUNNING' ``.
64
+ The integration **type** is the key with the `NETSUITE_EXECUTION_MODE_` prefix stripped
65
+ via `substr()` (e.g. `NETSUITE_EXECUTION_MODE_SALES_ORDERS` → `SALES_ORDERS`).
66
+ 4. **Bucket each client** into `checked`, `skipped` (no `Parameters` table —
67
+ `isMissingTableError()`), or `unreachable` (any other query/connection failure). A
68
+ per-client `try/catch (Throwable)` keeps one bad client from failing the sweep.
69
+ 5. **Decide the tokens and push once** to OneUptime.
70
+
71
+ ### Payload contract — `alarm` and `probe` are SEPARATE tokens
72
+
73
+ Follows the [OneUptime push-metric pattern](./oneuptime-worker2-monitoring.md): OneUptime
74
+ can only string-match a body, so the worker decides and emits tokens.
75
+
76
+ | Field | Meaning |
77
+ |---|---|
78
+ | `status` | `reporting`, or `error` (reserved for the Core lookup itself failing → `error:client_lookup_failed`) |
79
+ | `alarm` | `DEGRADED` when any stuck integration is found, else `OK` |
80
+ | `probe` | `DEGRADED` when any client DB is unreachable, else `OK` |
81
+ | `stuckCount` | number of stuck integrations found |
82
+ | `offendingIntegrations` | comma-joined `"ClientName: TYPE"` |
83
+ | `clientsChecked`, `clientsSkipped` | coverage counters |
84
+ | `unreachableClients`, `unresolvedClients` | named coverage gaps |
85
+ | `checkedAtUtc` | `gmdate('c')` |
86
+
87
+ ## Cron seed (dbchanges2)
88
+
89
+ `dbchanges2/Core/2026-08-10a - Netsuite Integrations Monitor.sql` — INSERT into
90
+ `Core.CronJobs`: action `Monitor/Operations/NetsuiteIntegrations`, schedule
91
+ `*/10 * * * *` (every 10 min), `maxExecutionTime` 120, `isActive` 1, `parameters` NULL,
92
+ fixed random uuid. Targets **only** `Core` (no cross-cluster refs), INSERT-only and
93
+ unguarded per dbchanges2 conventions. Modeled on
94
+ `Core/2026-07-29a - AIG Entitlement API Failure Monitor.sql`.
95
+
96
+ **Shipped `isActive=1` while the OneUptime URL is still a placeholder** (developer's
97
+ choice): the sweep runs but the push fails soft (`error_log`) until the real URL is pasted
98
+ into the worker method.
99
+
100
+ ## Gotchas / known issues
101
+
102
+ - **`` `key` `` and `` `value` `` are MySQL reserved words** — backtick-quote them in the
103
+ `Parameters` query.
104
+ - **Single source of truth for the key prefix.** `$KEY_PREFIX` builds both the `LIKE`
105
+ pattern (via `_Database::escape`) and the `substr()` that derives the integration type;
106
+ never hardcode the prefix twice.
107
+ - **Never-throw / 200-on-all-paths.** The method always returns a summary string. The
108
+ OneUptime push is `setLogging(false)` + `setThrowExceptionsOnFailure(false)` **and**
109
+ wrapped in its own `try/catch`, so a failed ping never fails the worker job.
110
+ - **OneUptime push URL is a credential** — it lives only as an in-file value in the worker
111
+ method, never committed to dbchanges2 and never recorded in a doc. It is currently a
112
+ placeholder.
113
+ - **Business DB, not logs DB.** Join `Clients.clientDatabaseId` for `Client_<X>`; the
114
+ sibling EmailQueue monitor joins `logDatabaseId` for `Logs_<X>`. See
115
+ [per-client database connections](../../_underscore/features/per-client-database-connections.md).
116
+ - The all-client resolution gotchas from
117
+ [EmailQueue](./all-client-email-queue-monitor.md) apply verbatim: LEFT OUTER JOIN every
118
+ level, null host columns unless the environment join matched, detect missing table by
119
+ MySQL error **number** 1146 (not message text), and remember a passing dev run (single
120
+ all-in-one endpoint) does not prove cross-cluster correctness.
121
+
122
+ ## Change history
123
+
124
+ - 2026-08-10 — Built `NetsuiteIntegrations()` + `resolveClientDatabases()` on
125
+ `_Worker_Monitor_Operations` (all-client `Parameters` sweep for
126
+ `NETSUITE_EXECUTION_MODE_%` = `1-RUNNING`), plus the `Core.CronJobs` seed
127
+ (`*/10 * * * *`, `isActive=1`) and an OneUptime Incoming Request monitor import. OneUptime
128
+ URL still a placeholder; sweep runs and pushes fail-soft until pasted. (jcardinal)
129
+
130
+ ## Related docs
131
+
132
+ - [All-Client Email Queue Monitor](./all-client-email-queue-monitor.md) — the sibling
133
+ all-client monitor this one is modeled on.
134
+ - [OneUptime push-metric monitors for 2.0 workers](./oneuptime-worker2-monitoring.md) — the
135
+ push/token pattern and OneUptime criteria (incl. `{{requestBody.*}}` incident templating).
136
+ - [Per-Client Database Connections](../../_underscore/features/per-client-database-connections.md)
137
+ — business/log database name resolution.
138
+ </content>
139
+ </invoke>
@@ -111,6 +111,25 @@ log dive. `status: "error"` stays reserved for the checker being **fully** blind
111
111
  resolution query failed). Worked example:
112
112
  [All-Client Email Queue Monitor](./all-client-email-queue-monitor.md).
113
113
 
114
+ ### Incident / alert title & description templating (`{{requestBody.*}}`)
115
+
116
+ OneUptime incident and alert **titles and descriptions support `{{variable}}` templating**
117
+ (verified against OneUptime docs 2026-08-10). For an **Incoming Request** monitor, the
118
+ POSTed JSON body is addressed via the `requestBody` prefix — e.g.
119
+ `{{requestBody.offendingIntegrations}}`, `{{requestBody.stuckCount}}`,
120
+ `{{requestBody.checkedAtUtc}}`, `{{requestBody.message}}`. Other available vars:
121
+ `{{requestHeaders}}`, `{{requestMethod}}`, `{{incomingRequestReceivedAt}}`.
122
+
123
+ - **Missing paths resolve to empty string**, so one template covers both a normal degraded
124
+ payload and an edge case like `error:client_lookup_failed` without a second template.
125
+ - Body-content criteria use `checkOn: "Request Body"` / `filterType: "Contains"`; the
126
+ heartbeat criterion uses `"Not Recieved In Minutes"` (note OneUptime's misspelling).
127
+ - A degraded criterion can OR several tokens, e.g. Contains `"alarm":"DEGRADED"` **OR**
128
+ `"error":"client_lookup_failed"`.
129
+ - Reuse the project's existing status/severity ObjectIDs when authoring the import JSON.
130
+ - Docs: https://oneuptime.com/docs/monitor/incident-alert-templating and
131
+ https://oneuptime.com/docs/en/monitor/incoming-request-monitor
132
+
114
133
  ### Heartbeat / cron cadence timing
115
134
 
116
135
  The 10-min-Degraded / 15-min-Offline heartbeat thresholds pair with a **5-minute** cron
@@ -192,6 +211,10 @@ check for another client.
192
211
  monitors must pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
193
212
 
194
213
  ## Change history
214
+ - 2026-08-10 — Documented OneUptime incident/alert `{{requestBody.*}}` templating (missing
215
+ paths → empty string; `Not Recieved In Minutes` heartbeat spelling), discovered while
216
+ wiring the second all-client `_Worker_Monitor_Operations` monitor —
217
+ [NetSuite Integrations Monitor](./netsuite-integrations-monitor.md). (jcardinal)
195
218
  - 2026-07-30 — Added the multi-client refinement of the payload contract: a **separate `probe`
196
219
  token** (`OK`/`DEGRADED`) alongside `alarm`, so a partial logs-cluster outage cannot read as a
197
220
  healthy metric, plus naming the offending clients in the body. Recorded that the previously
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
19
19
  ## 2.0 framework
20
20
 
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 48 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
- - **worker2** (Worker) — 42 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
+ - **worker2** (Worker) — 43 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 22 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
24
  - **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 5 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -2,6 +2,6 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
- | [Elite — supply2 frontend scope (Inventory built, Sales Orders pending)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json |
5
+ | [Elite — supply2 frontend scope (Inventory + Service Requests, both built)](features/supply2-scope.md) | 2.0 | Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). | toga2-supply/ELITE-CLIENT-TASK-NOTES.md, toga2-supply/src/pages/Inventory/viewModel/FIELDS/DUMMYGROUPOPTIONS.ts, toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts, toga2-supply/src/pages/Inventory/viewModel/index.ts, toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx, toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx, toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx, toga2-supply/src/utils/resolveClientHostName.ts, toga2-supply/src/utils/convertConstructorColumnTitles.ts, toga2-supply/src/pages/Orders/OrdersPage.tsx, toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json, toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts, toga2-supply/src/pages/Orders/api/OrdersApi.ts, toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx, toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx, toga2-supply/package.json |
6
6
  | [Elite — stale TableView config (11 dead Core.RecordFields across 9 views)](features/supply2-tableview-config-drift.md) | 2.0 | `Client_Elite`'s `TableViewJoins` predate **two** platform bridge-table migrations and still reference **11 deleted `Core.RecordFields` ids (211, 321, 932, 358, | dbchanges2/Client_Elite/, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
7
7
  | [Elite](profile.md) | 2.0 | Elite is a managed-services client that uses **Freshservice** as their helpdesk platform. | worker2/Worker/Elite.php, library/app/api/toga2.php |
@@ -1,12 +1,12 @@
1
1
  ---
2
- title: Elite — supply2 frontend scope (Inventory built, Sales Orders pending)
2
+ title: Elite — supply2 frontend scope (Inventory + Service Requests, both built)
3
3
  framework: "2.0"
4
4
  repo: toga2-supply
5
5
  project: TOGa Supply
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-06
9
+ updated: 2026-08-10
10
10
  owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga2-supply/ELITE-CLIENT-TASK-NOTES.md
@@ -14,10 +14,14 @@ files:
14
14
  - toga2-supply/src/pages/Inventory/viewModel/FIELDS/INVENTORYPAGEFIELDS.ts
15
15
  - toga2-supply/src/pages/Inventory/viewModel/index.ts
16
16
  - toga2-supply/src/pages/Inventory/listing/InventoryPage.tsx
17
+ - toga2-supply/src/pages/Inventory/listing/InventoryRouter.tsx
17
18
  - toga2-supply/src/pages/Inventory/listing/InventorySubTablePage.tsx
18
19
  - toga2-supply/src/utils/resolveClientHostName.ts
20
+ - toga2-supply/src/utils/convertConstructorColumnTitles.ts
19
21
  - toga2-supply/src/pages/Orders/OrdersPage.tsx
22
+ - toga2-supply/src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json
20
23
  - toga2-supply/src/pages/Orders/viewModel/useOrdersPageViewModel.ts
24
+ - toga2-supply/src/pages/Orders/api/OrdersApi.ts
21
25
  - toga2-supply/src/pages/Orders/view/OrderView/viewModel/useOrderDetailsViewModel.ts
22
26
  - toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/renderModalContent.tsx
23
27
  - toga2-supply/src/components/layout/SlideMenu/SlideMenu.tsx
@@ -32,13 +36,14 @@ related:
32
36
 
33
37
  Scope for onboarding Elite to the `toga2-supply` frontend (host `ELITE`). Elite's supply2 surface
34
38
  is two pages: **Inventory** (a single "Units By Items" group-by view, nested expand, no modal) and
35
- **Sales Orders** (table + details modal, **no approvals**).
39
+ **Service Requests** (the Orders page, host-retitled; table + details modal, **no approvals**).
36
40
 
37
- **Status (2026-08-06):** Part 1 — **Inventory is code-complete** and is the platform's
38
- first-ever host-scoping of the global inventory statics; it follows the shared
39
- [host-scoping playbook](../../../2.0/apps/toga2-supply/workflows/client-host-scoping.md), where the
40
- reusable shape now lives. Part 2 — **Sales Orders is a separate ticket, not started.** The
41
- source-of-truth plan is `toga2-supply/ELITE-CLIENT-TASK-NOTES.md` at the repo root.
41
+ **Status (2026-08-10): both pages are built and live-verified on beta/dev-sandbox.** Part 1 —
42
+ Inventory is the platform's first-ever host-scoping of the global inventory statics and follows the
43
+ shared [host-scoping playbook](../../../2.0/apps/toga2-supply/workflows/client-host-scoping.md),
44
+ where the reusable shape lives. Part 2 — "Sales Orders" was implemented and then **pivoted into
45
+ Service Requests**, iteratively debugged against beta 2026-08-07 → 08-10, and verified end-to-end.
46
+ The source-of-truth plan is `toga2-supply/ELITE-CLIENT-TASK-NOTES.md` at the repo root.
42
47
 
43
48
  ## Part 1 — Inventory (built)
44
49
 
@@ -57,41 +62,101 @@ One host-scoped group-by view only: **"Units By Items"**, no group-by modal.
57
62
  Elite uuid; NYCHH's `94f1e621…` / `904cc7b1…` branches were not touched.
58
63
  - `src/utils/resolveClientHostName.ts` (new, shared) resolves the host from
59
64
  `useHostNameStore.getState()` with a `window.location.hostname` subdomain fallback.
60
- - `SlideMenu.tsx` has an **Inventory-only** Elite branch whose deep-link preselects the uuid.
65
+ - `SlideMenu.tsx` has an Elite branch whose Inventory deep-link preselects the uuid.
61
66
  - Local run: `npm run elite` (`vite --host elite.togasupply`) plus a
62
67
  `127.0.0.1 elite.togasupply` hosts-file entry.
63
68
 
64
- **Verified:** every playbook guardrail holds (one `isDefault` per host; the diff for other hosts is
65
- only the export rename; `npx tsc --noEmit` clean). Live on dev-sandbox/beta: login works, columns
66
- render from pageMeta, the group-by switcher is hidden, chevrons render.
67
-
68
- **Not yet validated: the nested expand.** Expanding the nested table 500s — that is a **backend
69
- data-config** problem in `Client_Elite`, not a frontend one; see
70
- [Elite supply2 TableView config drift](./supply2-tableview-config-drift.md).
71
-
72
- ## Part 2 — Sales Orders (separate ticket, not started)
73
-
74
- - Table columns are backend pageMeta-driven for slug `sales-orders`; ACL fetched under slug
75
- `sales-orders-listing`.
76
- - **Details modal needs no code:** `renderModalContent.tsx` DEFAULT routes unknown hosts to
77
- `OrderDetailsModal` only (no `ApprovalsModal`); `useOrderDetailsViewModel.ts` falls back to
78
- `DEFAULTFIELDS.json`.
79
- - **Required FE change:** add an explicit `ELITE` entry to `useOrdersPageViewModel.ts`
80
- `fieldMapping`. The fallback `fieldMapping[hostName] || fieldMapping.compass` references a
81
- nonexistent lowercase `compass` key, so an unrecognized host **throws**. Elite's FIELDS must
82
- set `approvalsButton.isEnabled=false` and omit `approvalActionOptions` to exclude approvals.
83
- - That ticket will also hit the stale `sales-orders` TableView joins — see the config-drift doc.
84
-
85
- ## Known limitations (current state)
86
-
87
- - **Elite login lands on `/` (Orders) and crashes.** Because Orders has no `ELITE` `fieldMapping`
88
- entry, the landing route throws until the Sales Orders ticket lands. Deliberate: the nav
89
- deliberately exposes **Inventory only** for now, and adding an Orders entry before that ticket
90
- would advertise a broken page.
69
+ **Hardening from the CodeRabbit review (2026-08-10)** — guardrail-preserving variants, not the
70
+ reviewer's literal diffs:
71
+
72
+ - `InventoryPage.tsx` **re-seeds** the persisted `localStorage("selectedGrouping")` whenever the
73
+ stored option's uuid is not present in the resolved host's array. This covers same-origin host
74
+ switches and corrupt JSON, and is a provable no-op for hosts that share the default array (their
75
+ stored uuids always match).
76
+ - `InventoryRouter.tsx` redirects `/inventory_items` → `/inventory` for any host whose default view
77
+ has **no `subTableParent`** — data-driven, no host literal. Without it Elite's terminal nested
78
+ view passes `undefined` into `PrimaryTable`.
79
+
80
+ ## Part 2 — Service Requests (built, verified 2026-08-10)
81
+
82
+ **"Service Requests" is the Orders page host-configured, not a new route.** The supply2.5 mockup's
83
+ own source says so: `elite-app.jsx` has `VIEW_META.Orders.title = "Service Requests"` and a nav
84
+ entry `{label: "Orders", text: "Service Requests"}`, and its "service request details modal" is a
85
+ details-only order modal. The translation principle held throughout: **the 2.5 mockup is the spec
86
+ for WHAT, supply2.0 components are the HOW** — so this shipped as config + slug threading on the
87
+ existing `OrdersPage`, with zero new routes.
88
+
89
+ **Implementation**
90
+
91
+ - `src/pages/Orders/viewModel/FIELDS/ELITE/BASE.json` (new): `pageTitle` `"Service Requests"`,
92
+ `tableSlug` `"service-requests"`, `listingSlug` `"sales-orders-listing"`, `hasQuoteFilter` false,
93
+ `approvalsButton.isEnabled` false, `exportButton` true, and **no `approvalActionOptions`** (that
94
+ omission is what removes approvals).
95
+ - `useOrdersPageViewModel.ts`: an explicit `ELITE: { base: BASE_ELITE }` entry plus the host branch,
96
+ following the **PRUDENTIAL** `base` precedent. The broken `|| fieldMapping.compass` fallback was
97
+ deliberately left untouched — fixing it is out of scope and every host now needs an explicit entry
98
+ anyway (see the playbook).
99
+ - `OrdersPage.tsx`: all **8** hardcoded `"sales-orders"` literals now read `fields?.tableSlug` with a
100
+ `sales-orders` fallback; the page title comes from `fields`; the ACL check is optional-chained; and
101
+ the `isQuote` filter is **conditional** — omitted entirely when `fields?.hasQuoteFilter === false`.
102
+ - `SlideMenu.tsx`: the Elite nav is "Service Requests" + Inventory.
103
+
104
+ **Zero cross-client risk:** no other host defines any of the new FIELDS keys, so every default path
105
+ is literal-identical to before.
106
+
107
+ ### The backend `service-requests` view is built on ServiceRequests, not SalesOrders
108
+
109
+ The backend table view (slug supplied by the backend dev) is built on the **ServiceRequests RECORD**
110
+ — joining Tickets / Customers / ServiceRequestTypes / SalesOrders, where
111
+ `SalesOrders.serviceRequestId = ServiceRequests.id` (1:1). It is **not** a renamed SalesOrders view.
112
+ Three consequences drove the rest of the work:
113
+
114
+ 1. **Row uuids are ServiceRequests uuids, but every detail-modal route is a `/sales-orders` record
115
+ route.** Fixed with `getSalesOrderUuidForServiceRequest()` in `src/pages/Orders/api/OrdersApi.ts`
116
+ (`GET /sales-orders` + a ServiceRequests join, `where ServiceRequests.uuid = …`), whose resolved
117
+ `detailsUuid` is threaded through `useOrderDetailsViewModel.ts` via react-query. The query is
118
+ **enabled only for `hostName === "ELITE"`**, so every other host bypasses it entirely.
119
+ 2. **ServiceRequests has no `isQuote` column.** `OrdersPage`'s unconditional `isQuote: false` filter
120
+ 500'd with `1054 unknown column`; the backend confirmed the fix belongs on the FE, hence
121
+ `hasQuoteFilter`.
122
+ 3. **There is no `service-requests-listing` page in `Core.Pages`.** Elite reuses
123
+ `sales-orders-listing`. Column headers come from the **table-view** meta, not the page meta, so
124
+ the only things affected by the reuse are ACL and search labels.
125
+
126
+ Verified working end-to-end on the one Elite service request that has a linked sales order
127
+ (ticket INC-13982).
128
+
129
+ ## Open question — orphan ServiceRequests (needs backend/product input)
130
+
131
+ **5 of 6** Elite service requests on dev-sandbox have **no linked SalesOrder**, so the details modal
132
+ opens empty — the resolver correctly yields `null` and there is nothing to render. Undecided:
133
+
134
+ - an explicit "no order linked" empty state, or
135
+ - a ServiceRequest-level detail view (not just an order modal), or
136
+ - making order-less rows non-clickable.
137
+
138
+ The prerequisite answer is a data question: **is an order-less service request a legitimate
139
+ lifecycle state**, or is the dev-sandbox data simply incomplete? Do not pick a UI treatment before
140
+ that is answered.
141
+
142
+ ## Known limitations / notes
143
+
91
144
  - `Client_Elite` contains **three pre-existing test Items** (ids 3–5, created 2025-09-18 during
92
145
  Elite onboarding) — not test data from this work.
146
+ - The Elite work still sits on top of the stale `Client_Elite` TableView config — see
147
+ [supply2-tableview-config-drift](./supply2-tableview-config-drift.md), including the
148
+ 2026-08-07 prod→beta refresh that wiped the hand-built `service-requests` view.
93
149
 
94
150
  ## Change history
151
+ - 2026-08-10 — **Service Requests shipped and verified** (INC-13982): implemented as the Orders page
152
+ host-configured, not a new route — new `FIELDS/ELITE/BASE.json` (pageTitle/tableSlug/listingSlug/
153
+ `hasQuoteFilter`/no `approvalActionOptions`), explicit `ELITE` entry in `useOrdersPageViewModel`,
154
+ 8 hardcoded `sales-orders` literals threaded through `fields?.tableSlug`, conditional `isQuote`
155
+ filter, and `getSalesOrderUuidForServiceRequest()` resolving ServiceRequests uuids → SalesOrders
156
+ record routes (ELITE-only). Recorded that the backend view is built on the ServiceRequests record
157
+ (no `isQuote`, no `service-requests-listing` page) and the open orphan-ServiceRequest question.
158
+ Also landed the CodeRabbit Inventory hardening (`selectedGrouping` re-seed, `/inventory_items`
159
+ redirect for terminal views). (tcox)
95
160
  - 2026-08-06 — Elite Inventory implemented (first host-scoping of the global inventory statics):
96
161
  separate `eliteGroupOptions` array + `getGroupOptions()`, `ELITE_UNITS_BY_ITEMS_UUID`, shared
97
162
  `resolveClientHostName.ts`, host-scoped `getInventoryPageFields()`, additive page branches,
@@ -100,4 +165,3 @@ data-config** problem in `Client_Elite`, not a frontend one; see
100
165
  keep the nav Inventory-only until the Sales Orders ticket (Orders throws for unmapped hosts).
101
166
  (tcox)
102
167
  - 2026-08-04 — Captured the planned Elite supply2 scope (Inventory single-view + Sales Orders without approvals) from the ELITE-CLIENT-TASK-NOTES.md scoping session; no code written yet (apeterson)
103
- </content>
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-06
9
+ updated: 2026-08-10
10
10
  owners: [tcox]
11
11
  files:
12
12
  - dbchanges2/Client_Elite/
@@ -15,6 +15,7 @@ related:
15
15
  - ./supply2-scope.md
16
16
  - ../../../2.0/apps/_underscore/features/tracking-number-bridges.md
17
17
  - ../../../2.0/apps/_underscore/features/units-for-items-for-purchase-orders.md
18
+ - ../../../2.0/apps/_underscore/features/page-meta-context-field-settings.md
18
19
  ---
19
20
 
20
21
  ## Summary
@@ -87,6 +88,30 @@ Do **not** hand-patch view by view. Diff `Client_Elite.TableViewJoins` wholesale
87
88
  **production as well as dev-sandbox** — production reproduces the same 500 at go-live otherwise.
88
89
  `sales-order-shipments` (join 10) is the one case a NYCHH diff cannot resolve.
89
90
 
91
+ ## The 2026-08-07 prod→beta refresh — what a rebuild costs you
92
+
93
+ `Client_Elite` on dev-sandbox was **rebuilt from production** at 11:55 on 2026-08-07 (with a beta API
94
+ redeploy). Everything hand-inserted on beta was lost, which is the strongest argument yet for the
95
+ recommendation above. Four distinct breakages followed, all diagnosed by reading the DB:
96
+
97
+ 1. **The hand-built `service-requests` table view was wiped** — production never had it, so the
98
+ rebuild simply did not carry it. The backend dev recreated it by hand *again*. **Every beta
99
+ hand-insert must become a `dbchanges2` migration or the next refresh eats it.**
100
+ 2. **`devteam@togatech.com` came back with an EMPTY password** (re-seeded row). Restored by copying
101
+ the bcrypt hash from `Client_Compass.Users` id 127046 — see the
102
+ [host-scoping playbook](../../../2.0/apps/toga2-supply/workflows/client-host-scoping.md).
103
+ 3. **The Orders `isQuote` filter started 500ing** (`1054 Unknown column`) — ServiceRequests has no
104
+ such column; fixed on the frontend (see [supply2-scope](./supply2-scope.md)).
105
+ 4. **Elite `/pages/meta?slug=inventory` began 500ing** with *"Undefined array key 190"* — a
106
+ `_underscore` framework bug in `Page::meta()` on context-scoped field settings, newly fatal
107
+ because of the strict Sentry error handler that shipped in the same redeploy. See
108
+ [legacy page meta](../../../2.0/apps/_underscore/features/page-meta-context-field-settings.md).
109
+
110
+ **Row ids are not stable across a re-import.** `TableViewJoins` ids shifted with the rebuild, so any
111
+ note, script, or diff that identifies a join by **row id** is worthless after a refresh. Match rows
112
+ by **(`tableViewId`, `joinRecordId`, `joinOnRecordFieldId`)** instead — the id columns in the table
113
+ above are a snapshot for orientation, not a key.
114
+
90
115
  ## Gotchas / known issues
91
116
 
92
117
  - **A `Client_*` DB that predates a platform migration fails silently until a user opens the
@@ -98,10 +123,13 @@ Do **not** hand-patch view by view. Diff `Client_Elite.TableViewJoins` wholesale
98
123
  [error-reporting-issue-event](../../../2.0/apps/_underscore/features/error-reporting-issue-event.md).
99
124
 
100
125
  ## Change history
126
+ - 2026-08-10 — Recorded the **2026-08-07 prod→beta rebuild of `Client_Elite`** and its four
127
+ breakages (wiped hand-built `service-requests` view, empty-password `devteam` user, the `isQuote`
128
+ `1054`, and the `/pages/meta` 500), plus the rule that **`TableViewJoins` row ids shift on
129
+ re-import — match by (`tableViewId`, `joinRecordId`, `joinOnRecordFieldId`), never by id.** (tcox)
101
130
  - 2026-08-06 — Documented Elite's stale TableView config: 11 dead `Core.RecordFields` (211, 321,
102
131
  932, 358, 1431) across 9 views, left behind by the tracking-number-bridge (319) and
103
132
  `SalesOrderItems_PurchaseOrderItems` (289) migrations that NYCHH received and Elite did not.
104
133
  Verified the view-17 fix live, then **reverted it** at the developer's request; recommended a
105
134
  wholesale `Client_Elite` vs `Client_Nychh` diff shipped as a dbchanges2 migration to prod, and
106
135
  flagged `sales-order-shipments` join 10 as broken in NYCHH too. (tcox)
107
- </content>
@@ -10,7 +10,7 @@ project: Worker
10
10
  client: elite
11
11
  type: profile
12
12
  status: active
13
- updated: 2026-08-06
13
+ updated: 2026-08-10
14
14
  owners: [snaredla, apeterson, tcox]
15
15
  files:
16
16
  - worker2/Worker/Elite.php
@@ -28,20 +28,29 @@ Elite is a managed-services client that uses **Freshservice** as their helpdesk
28
28
  TOGA 2 is the source of truth for tickets, notes, and file attachments. The integration
29
29
  keeps Freshservice and TOGA 2 in sync via webhook-driven worker jobs.
30
30
 
31
- **Elite is also being onboarded to TOGa Supply (2026-08).** The frontend scope and its current
32
- state live in [supply2-scope](features/supply2-scope.md); the backend blocker (stale
31
+ **Elite is also being onboarded to TOGa Supply (2026-08).** Both frontend pages — **Inventory** and
32
+ **Service Requests** (the Orders page host-retitled, not a new route) — are built and live-verified
33
+ on beta as of 2026-08-10; see [supply2-scope](features/supply2-scope.md). The backend blocker (stale
33
34
  `Client_Elite` TableView config, a production go-live risk) is in
34
35
  [supply2-tableview-config-drift](features/supply2-tableview-config-drift.md). On the backend,
35
36
  dbchanges2 **PR #454** (TRUE-80499, snaredla24, 2026-08-05) added the **netsuite** module to
36
37
  `Client_Elite/_modules.txt` — the supply-chain schema layer is arriving.
37
38
 
39
+ Elite's service-request data model: `ServiceRequests` is the record the `service-requests` table
40
+ view is built on (joining Tickets / Customers / ServiceRequestTypes / SalesOrders, with
41
+ `SalesOrders.serviceRequestId = ServiceRequests.id`, 1:1). **Most Elite service requests on
42
+ dev-sandbox have no linked sales order** — whether that is a legitimate lifecycle state is an open
43
+ question for backend/product (see supply2-scope).
44
+
38
45
  **Tenant/test-account notes.** `Core.Domains` rows for Elite already exist in **every**
39
46
  environment (dev `http://elite.togasupply`, beta, production, sandbox-dev) with no dbchanges2 Core
40
47
  migration creating them, and Elite has **no `ClientAuthentications` row** — supply2 login is
41
48
  ordinary password auth. The shared `devteam@togatech.com` test user was seeded into dev-sandbox
42
49
  `Client_Elite.Users` (+ `Users_Roles`; Elite roles are 1=Base, 2=Developer, 3=API, 4=Public) from
43
50
  `Client_Compass.Users` — see the
44
- [host-scoping playbook](../../2.0/apps/toga2-supply/workflows/client-host-scoping.md).
51
+ [host-scoping playbook](../../2.0/apps/toga2-supply/workflows/client-host-scoping.md). **Note:**
52
+ `Client_Elite` on dev-sandbox was rebuilt from production on 2026-08-07, which emptied that user's
53
+ password and wiped hand-built config — re-seeding may be needed again after any refresh.
45
54
 
46
55
  ## TOGA 2 API credentials
47
56
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.543",
3
+ "version": "1.0.545",
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",