toga-ai 1.0.579 → 1.0.581

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.
@@ -2,4 +2,4 @@
2
2
 
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
- | [Authoring & Shipping a 1.0 dbchanges SQL File](workflows/authoring-and-shipping-sql-files.md) | `dbchanges` is the **1.0** (legacy/V1) schema-and-data change repository — the 1.0 sibling of 2.0's `dbchanges2`. | dbchanges/index.php, dbchanges/Core/, worker/crons/infrastructure/execute_dbchanges.php, worker/.ebextensions/030_dbchanges.config |
5
+ | [Authoring & Shipping a 1.0 dbchanges SQL File](workflows/authoring-and-shipping-sql-files.md) | `dbchanges` is the **1.0** (legacy/V1) schema-and-data change repository — the 1.0 sibling of 2.0's `dbchanges2`. | dbchanges/index.php, dbchanges/Core/, dbchanges/TOGaDeskSupport/, worker/crons/infrastructure/execute_dbchanges.php, worker/.ebextensions/030_dbchanges.config |
@@ -6,11 +6,12 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-07-29
9
+ updated: 2026-08-14
10
10
  owners: [sking]
11
11
  files:
12
12
  - dbchanges/index.php
13
13
  - dbchanges/Core/
14
+ - dbchanges/TOGaDeskSupport/
14
15
  - worker/crons/infrastructure/execute_dbchanges.php
15
16
  - worker/.ebextensions/030_dbchanges.config
16
17
  related:
@@ -97,6 +98,27 @@ WHERE t.id IN (<ids>)
97
98
  ) AS preflight) = <expected-row-count>;
98
99
  ```
99
100
 
101
+ ## One top-level folder = one DATABASE (and the silent-skip trap)
102
+
103
+ The repo's top level is a list of **database names**, not modules: `Core`, `TOGaDeskSupport`,
104
+ `Common`, `Logs`, `Bridge_NetSuite`, … The runner walks every top-level directory and treats the
105
+ **folder name as the database to connect to** (`index.php` ~L55–60), then — for each folder — scans
106
+ `config.<env>.ini` and uses the **first `[database*]` group it finds** (`~L76–80`; in
107
+ `config.prod.ini` that is `[database_main]`) for hostname/username/password. So *all* folders are
108
+ attempted with **one** set of credentials against one host, with only the DB name varying.
109
+
110
+ Consequence: **a folder whose database is not reachable from that host is skipped silently.**
111
+ The failure path is a bare `echo "Skipping <db> Database... unable to connect"` (`~L189`) — no
112
+ `Logs.Errors` row, no `exit()`, no `_dbchanges` bookkeeping. Nothing retries it, and nothing tells
113
+ you. A change that spans two databases on two different clusters (e.g. `Core` on **db_core** and
114
+ `TOGaDeskSupport` on **db_togadesk**) therefore needs **two separate runs**, one per cluster, and
115
+ an applier pointed at only one of them **half-applies the change and reports success**.
116
+
117
+ **Rule: a multi-database change is not done until you have verified each database independently.**
118
+ Verify **per-pair** (does *this* key have *this* value?), not by a `COUNT(*) ... IS NOT NULL` total —
119
+ a per-count check passes on the cluster that applied and tells you nothing about the one that
120
+ didn't.
121
+
100
122
  ## Which branches actually auto-apply
101
123
 
102
124
  | Path | Trigger | Timing |
@@ -121,7 +143,11 @@ The repo ships `config.alpha/beta/demo/hotfix/prod/stage/test/worker.ini`, there
121
143
  is a no-op rather than a wrong write.
122
144
  5. Open a PR into `_production` (or the target `_<env>` branch).
123
145
  6. After merge: `_<env>` branches apply within ~2 min; **`_production` waits for the next worker
124
- deploy** — verify with a read-only query afterwards rather than assuming.
146
+ deploy** — verify with a read-only query afterwards rather than assuming. **Merging the PR
147
+ applies nothing by itself** — `dbchanges` has no CI, no workflow, no runner of its own; every
148
+ application is driven from the worker tier.
149
+ 7. If the change spans more than one top-level (database) folder, verify **each database
150
+ separately** — see the silent-skip trap above.
125
151
 
126
152
  ## Gotchas
127
153
 
@@ -130,6 +156,14 @@ The repo ships `config.alpha/beta/demo/hotfix/prod/stage/test/worker.ini`, there
130
156
  local one.
131
157
  - **No `DELIMITER`, no stored routines, no explicit `COMMIT`.** See above.
132
158
  - **Error 1093** is why the preflight subquery must be wrapped in a derived table.
159
+ - **A merged PR is not an applied change.** Confirmed live 2026-08-13 (PR dbchanges#265): after the
160
+ merge into `_production`, both target tables were still 100% unchanged. Never tell a customer or a
161
+ ticket "applied" off a merge — always re-read the data.
162
+ - **A cross-database change half-applies without warning.** Same incident: the `TOGaDeskSupport`
163
+ file landed and the `Core` file did not, leaving `assets.tag` 87/87 correct while
164
+ `Core.AdvanceShippingNoticeUnits.assetTag` was 87/87 still empty — a state that *looks* fixed in
165
+ the UI. Cause is the one-credential-set / folder-is-a-database model above. Verify every database
166
+ in the change, per-pair.
133
167
  - **Retry semantics are a feature**: because the data change and the `_dbchanges` row share a
134
168
  transaction, a failing file re-runs next cycle. Do not "help" it with a manual COMMIT.
135
169
  - **⚠ SECURITY — hardcoded GitHub token.** `worker/crons/infrastructure/execute_dbchanges.php`
@@ -140,6 +174,13 @@ The repo ships `config.alpha/beta/demo/hotfix/prod/stage/test/worker.ini`, there
140
174
  argv string. No credential value is recorded here by design.
141
175
 
142
176
  ## Change history
177
+ - 2026-08-14 — Documented that each **top-level folder is a database name** and that the runner uses
178
+ the **first `[database*]` config group** for every folder, so a folder on another cluster is
179
+ **silently skipped** (bare echo, no `Logs.Errors`, no `exit()`, no retry). Confirmed live during
180
+ the DOE asset-tag backfill (PR dbchanges#265): merging the PR applied nothing, and then a
181
+ `TOGaDeskSupport`-only run left `Core` untouched — a half-fix that looked complete in the UI.
182
+ Added the rule that a multi-database change needs one run per cluster and **per-pair**
183
+ verification, not a non-null count. (sking)
143
184
  - 2026-07-29 — Repo onboarded to the KB (1.0 `dbchanges`, framework core sibling of `dbchanges2`).
144
185
  Documented the `index.php` `";\n"` split + `mysqli_query()` model and the comment-only-chunk /
145
186
  error-1065 queue abort, the per-file transaction & retry semantics, the derived-table preflight
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-21
9
+ updated: 2026-08-14
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - _underscore/Model/Core/Surface.php
@@ -322,6 +322,18 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
322
322
 
323
323
  ## Gotchas
324
324
 
325
+ - **An empty bundle (`{surface:null, elements:[], vocabularies:{}, messages:{}, theme:{}}`) means the
326
+ Core.Surfaces row for that slug isn't seeded — NOT an ACL denial and NOT a client/scoping mismatch.**
327
+ `_buildBundle()` (`Surface.php` ~L516-532) sets `surface=null` and short-circuits when `$surface->load()`
328
+ fails (`metaGroup` ~L191 calls `resolve` ~L50 per slug). The lookup is **SLUG-ONLY** — it does NOT
329
+ filter by `appId`, `isVisible`, client, role, persona, or language, and `Surfaces` is a Core/shared
330
+ (not client-partitioned) table. Consequences when diagnosing an empty/blank surface: (a) a null surface
331
+ is **not** an ACL denial (that returns **403 EZ-1**, not a 200 with a null bundle) and **not** a
332
+ client/scoping mismatch (those only filter *overrides* on an already-found surface); it **always** means
333
+ the `Core.Surfaces` row for that slug isn't seeded **in that environment**; (b) `isVisible=0` does NOT
334
+ hide a surface at this layer. Verify with `SELECT slug FROM Core.Surfaces WHERE slug LIKE '...'`. (For
335
+ the FE-side crash when a summary surface IS seeded but its action siblings are NOT, see the partial-seed
336
+ gotcha in [surface-frontend](../../toga25-supply/features/surface-frontend.md).)
325
337
  - **Per-client surface message overrides silently never apply when `languageId` resolves to `0`.**
326
338
  `_Model_Core_Surface::_loadMessages` overlays `Core.Messages.defaultValue` with
327
339
  `Client_<X>.MessageTranslations` rows **only when `languageId > 0`**. The JWT carries the language as
@@ -420,6 +432,12 @@ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The
420
432
  match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
421
433
 
422
434
  ## Change history
435
+ - 2026-08-14 — **Discovered/documented** why a surface meta request returns an empty bundle
436
+ (`surface:null`, empty elements/vocabularies/messages/theme): `_buildBundle()` short-circuits when the
437
+ slug-only `Surfaces` load fails, and the lookup filters by slug ONLY (no appId/isVisible/client/role/
438
+ persona/language; `Surfaces` is Core/shared). So a null bundle always = the `Core.Surfaces` row for
439
+ that slug isn't seeded in that environment — it is NOT a 403 ACL denial and NOT a client/scoping
440
+ mismatch, and `isVisible=0` does not hide a surface at this layer. Added as a gotcha. (apeterson)
423
441
  - 2026-07-21 — Recorded **Option B**: the Approve step-two/PO gate is now evaluated **live on the FE**
424
442
  (FE `evaluateSurfaceRule` gained a `SURFACE_NAMED_RULES` escape hatch) rather than via the no-op
425
443
  backend Tier-2 — an intentional deviation from the frozen-grammar/escalate note (updated the
@@ -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/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 |
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/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/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/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 |
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,9 +6,10 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-05
9
+ updated: 2026-08-14
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
+ - toga25-supply/src/surface/applyColSpan.ts
12
13
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx
13
14
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts
14
15
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx
@@ -458,6 +459,29 @@ machinery for "this box is two columns wide", which is exactly the signal that i
458
459
  FE. Use this as the config/logic line for surface geometry questions: **presentation geometry →
459
460
  FE/CSS; content/visibility/order/ACL → Surface.**
460
461
 
462
+ #### Where per-field `colSpan` lives in the FE (CTO-reviewed, AGREE 2026-08-14)
463
+ Once colSpan is actually assigned FE-side, the durable rule is: **span VALUES stay per-consumer/adapter,
464
+ keyed by `valueKey` only — do NOT centralize colSpan as a universal per-element value, and do NOT store
465
+ it as SurfaceElement DB metadata (both rejected).** The reasoning is that colSpan is inherently
466
+ **(field × grid)-scoped**: a span number only means something relative to a *specific* grid's column
467
+ count, and the SAME surface element (e.g. `valueKey "reasonForRequest"`) is rendered by multiple
468
+ consumers into different grids — the SalesOrders modal `.approvalFieldsGrid` is 2-col, while
469
+ `RecordApprovalModal`'s `.approvalDetailsGrid` / `.approvalWorkflowDetailsGrid` differ. Storing one
470
+ per-element span is therefore semantically impossible across differing column counts (and violates the
471
+ "layout is FE presentation, not surface metadata" boundary above).
472
+ - Each adapter renders into exactly one grid, so **the adapter IS the grid scope** — no surface-slug
473
+ layer is introduced (slug ≠ grid). Value tables stay **local to each adapter**
474
+ (`surfaceBundleToTenantFields.ts`, `surfaceBundlesToDecisionFields.ts`).
475
+ - Share ONLY the trivial assignment plumbing: **`applyColSpan(field, span)`** helper in `@/surface`
476
+ (`src/surface/applyColSpan.ts`, exported from `src/surface/index.ts`). `BaseDetailField` (in
477
+ `@agilant/toga-blox`) reads `field.colSpan` → CSS var `--field-col-span`; it is grid-agnostic.
478
+ - This deliberately prevents a future "consolidate the span maps into one" regression (which would be a
479
+ bug generator).
480
+ - **Caveat (in code):** the decision adapter feeds BOTH the single-action and workflow grids from the
481
+ same field array (`overlayDecisionSurfaceFields` points `orderContentDetails` and
482
+ `orderContentDetailsWorkflow` at one array), so those two grids currently **share one span value**;
483
+ splitting them requires two arrays.
484
+
461
485
  ## Gotchas
462
486
 
463
487
  - **A named predicate that FAILS OPEN silently enables a button when its data isn't fetched.**
@@ -548,6 +572,17 @@ FE/CSS; content/visibility/order/ACL → Surface.**
548
572
  `extractBundle()` by structural fingerprint (the `elements` array). Consumers (`SurfaceSection`,
549
573
  `SurfaceActionBar`) are hardened to bail to `null` unless `elements` is a real array, so a malformed
550
574
  payload never crashes the page. (Root cause of the "s.elements is not iterable" runtime crash.)
575
+ - **A PARTIAL decision-surface seed CRASHES the approval modal (a TOTAL miss falls back cleanly).**
576
+ The SalesOrders approve/deny decision modal gates on the **SUMMARY surface only**
577
+ (`isDecisionSurfaceEnabled` = the summary bundle has any visible element). If the summary surface
578
+ (dbchanges2 seed `Core/2026-07-21a`) is seeded but the approve/deny **ACTION** surfaces
579
+ (`Core/2026-07-21b`) are NOT, the gate flips `true` while the action bundles are empty →
580
+ `buildActionChrome` yields an undefined icon/label → runtime crash `"Cannot read properties of
581
+ undefined (reading 'charAt')"` (undefined Font Awesome icon), then `reading 'label'`. A **total**
582
+ seed miss falls back cleanly to JSON; a **partial** miss crashes. Fix = ensure the whole
583
+ `2026-07-21a`+`b` seed set ran in that environment. (Both seeds are additive + `NOT EXISTS`-guarded
584
+ and inherit `appId`/`recordId` from the existing `order-details` sibling surface — so if
585
+ `order-details` isn't seeded the surface INSERT silently inserts nothing.)
551
586
  - **No feature flag, no fallback.** Surface is the enforced path; a missing/broken surface bundle
552
587
  does not silently fall back to legacy JSON — that path is deleted per screen as it migrates.
553
588
  - **Endpoint is query-string form** — `GET /v2/surfaces/meta?slug=...`, not `/surfaces/<slug>/meta`
@@ -571,6 +606,19 @@ FE/CSS; content/visibility/order/ACL → Surface.**
571
606
  treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
572
607
 
573
608
  ## Change history
609
+ - 2026-08-14 — **DECIDED (CTO-reviewed, AGREE)** where per-field grid `colSpan` lives in the FE Surface
610
+ layer: span VALUES stay per-consumer/adapter keyed by `valueKey` (the adapter IS the grid scope, since
611
+ each adapter renders into exactly one grid), NOT centralized as a universal per-element value and NOT
612
+ stored as SurfaceElement DB metadata (both rejected — colSpan is (field × grid)-scoped, and the same
613
+ element reuses across grids of differing column counts). Shared only the trivial `applyColSpan(field,
614
+ span)` plumbing in `@/surface` (`src/surface/applyColSpan.ts`); `BaseDetailField` reads
615
+ `field.colSpan`→`--field-col-span`. Prevents a future "consolidate the span maps" regression. Caveat:
616
+ the decision adapter feeds both the single-action and workflow grids from one field array
617
+ (`overlayDecisionSurfaceFields`), so they currently share one span value until split into two arrays.
618
+ Also documented a FE gotcha: a **PARTIAL** decision-surface seed (summary `2026-07-21a` seeded but
619
+ action `2026-07-21b` not) flips `isDecisionSurfaceEnabled` true with empty action bundles →
620
+ `buildActionChrome` crash (`charAt`/`label` of undefined); a total miss falls back cleanly to JSON.
621
+ (apeterson)
574
622
  - 2026-08-05 — **Built/Fixed** staged approval-chain value resolution on the record-modal
575
623
  **approval-details** card. Ported the decision adapter's staged resolver (`resolvePathInsensitive`
576
624
  + `makePreferredStageResolver`) into `surfaceBundleToTenantFields.ts`: a surface `config.preferredStage`
@@ -3,5 +3,5 @@
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
5
  | [NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)](features/hold-status-sync.md) | 1.0 | DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and **stay held** — holds are SLA-bearing in both systems. | worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/send_request_item_updates.php, library/app/model/togadesk/repairorder.php, library/app/api/nycdoev2.php, togadesk/desk/includes/classes/class.repair.php |
6
- | [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/test_multi_po_receipt_resolution.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php, library/app/netsuite.php, dbchanges/Core/SK/ |
6
+ | [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/test_multi_po_receipt_resolution.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/nycdoe.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php, library/app/netsuite.php, dbchanges/Core/SK/, dbchanges/TOGaDeskSupport/SK/ |
7
7
  | [New York City Department of Education](profile.md) | 1.0 | NYC DOE (New York City Department of Education) is a TOGA client whose entire integration runs in the **1.0 worker tier** (~30 cron scripts under `worker/crons/ | |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: nycdoe
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-11
9
+ updated: 2026-08-14
10
10
  owners: [mhammontree, sking]
11
11
  files:
12
12
  - worker/crons/sync/nycdoe/import_asn.php
@@ -29,6 +29,7 @@ files:
29
29
  - worker/schedules/cron.worker.notification.json
30
30
  - library/app/api/nycdoe.php
31
31
  - library/app/api/nycdoev2.php
32
+ - library/app/nycdoe.php
32
33
  - library/app/asnprocessor/manufacturer.php
33
34
  - library/app/asnprocessor/apple.php
34
35
  - library/app/asnprocessor/lenovo.php
@@ -37,6 +38,7 @@ files:
37
38
  - library/app/edi.php
38
39
  - library/app/netsuite.php
39
40
  - dbchanges/Core/SK/
41
+ - dbchanges/TOGaDeskSupport/SK/
40
42
  related:
41
43
  - ../profile.md
42
44
  - hold-status-sync.md
@@ -250,10 +252,29 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
250
252
  item/unit lookup keys (built from the truncated DB value, compared against the full
251
253
  in-memory string), and two long descriptions sharing their first 64 characters collapse
252
254
  into the same item.
255
+ - **It is also the root cause of the DOE missing-asset-tag class** (traced 2026-08-13). Because
256
+ the shift is +1, `assetTag ← $cols[23]` picks up the **serial**, and the **real asset-tag
257
+ column `$cols[24]` is never read at all** — so no unit from an off-layout file can ever get a
258
+ correct tag. Further collateral on the same rows: `workPhone` holds a date and `shipZipCode`
259
+ holds a person's name. Treat any Lenovo missing-tag report as this bug until proven otherwise,
260
+ and fix the parser rather than only backfilling.
253
261
  - **Remediation is MANUAL data repair — there is no automated recovery.** Correct
254
262
  `Core.AdvanceShippingNoticeItems.partNumber` on the affected items to the real NetSuite
255
263
  OEM SKU (replacing the description the malformed file supplied); the pipeline then
256
264
  proceeds normally. Done this way for ASN `26720` on 2026-08-11.
265
+ - **Asset-tag repair is a SEPARATE, TWO-STORE backfill** — see the derived-`assets.tag` gotcha
266
+ below. Generate the SQL **mechanically** from the customer's serial→tag CSV (never hand-
267
+ transcribe), match serials through `App_NYCDOE::serialKey()` (the leading-`S` gotcha below),
268
+ scope the statements to the ASN/MSO, and fill **only still-empty** tags so the file is
269
+ re-runnable. Done this way for ASN `26720` / PO `S202648137` / MSO `329924` (87 units) on
270
+ 2026-08-13, PR dbchanges#265.
271
+ - **Known remaining damage from this defect** (each needs its own customer serial→tag list):
272
+ ASN `26641` / PO `S202668066` — **162 of 363** units still tagless; ASN `26706` /
273
+ PO `WR270003276` — **1 of 7**.
274
+ - **A fixture-based regression test is owed with the parser fix**: assert that serial, quantity
275
+ and assetTag each land in the right field for **both** layouts (do not detect by column count
276
+ alone). ⚠ Do not quote constants out of `asnprocessor/lenovo.php` into a doc or ticket — that
277
+ file contains hardcoded FTP credentials.
257
278
  - **Recommended future code fixes (proposed, NOT approved, NOT implemented):** map columns
258
279
  by **header name** and hard-reject an unknown layout with a loud alert; stop building the
259
280
  dedupe key from a field that can be a constant; widen
@@ -266,6 +287,30 @@ Vendor SFTP ───(legacy_import_asn.php, ser+non-ser)─┘ [UNIQUE ded
266
287
  `worker/crons/toga2/**` against the toga2 `AdvanceShippingNoticeItemUnits` schema, while
267
288
  the DOE path is `Core.AdvanceShippingNoticeUnits` via `asnprocessor/lenovo.php`.
268
289
 
290
+ - **⚠ `TOGaDeskSupport.assets.tag` is DERIVED from `Core.AdvanceShippingNoticeUnits.assetTag` — an
291
+ asset-tag repair MUST touch BOTH stores.** The TOGa Desk UI renders `assets.tag`, but that column
292
+ is *populated from* the ASN unit's `assetTag` at install-ticket creation
293
+ (`3_create_installation_ticket.php` ~L150). Fixing only `assets.tag` makes the UI look correct
294
+ while the ASN record stays blank, so every ASN-driven consumer — reports, NetSuite / ServiceNow
295
+ transmissions, proof of delivery — still sees no tag. This is the trap that makes a half-fix look
296
+ like a full fix, and it compounds with the `dbchanges` **cross-database silent skip**: `Core/` and
297
+ `TOGaDeskSupport/` are different databases on different clusters, so the repair is **two separate
298
+ runs** and an applier that picks up one directory reports success on a 50% fix (confirmed live
299
+ 2026-08-13: `assets.tag` 87/87 correct while `Core…Units.assetTag` was 87/87 empty). Always
300
+ verify **per-pair** (serial ↔ tag) in **both** stores, never a non-null count. See the
301
+ [1.0 dbchanges authoring workflow](../../../1.0/apps/dbchanges/workflows/authoring-and-shipping-sql-files.md).
302
+
303
+ - **The leading `S` on Lenovo/Lexmark serials is stripped ON PURPOSE — do not "correct" it.**
304
+ Customer and vendor lists show ThinkPad serials as `SA00HZ20` while the DB stores `A00HZ20`: the
305
+ importer deliberately strips **one** leading capital `S` for Lenovo and Lexmark
306
+ (`legacy_process_asn_queue.php` ~L269-274). This is intended normalization, **not corruption** —
307
+ re-adding the `S` to stored serials would break the dedupe lookups that rely on the stripped form.
308
+ - Always compare via **`App_NYCDOE::serialKey()`** (`library/app/nycdoe.php` ~L51 =
309
+ `normalizeSerial()` then `stripLeadingSerialS()`), never a raw string compare.
310
+ - **Never** `REPLACE(serial,'S','')` — that strips *every* `S`, not just the leading one.
311
+ - Concretely: 25 of the 87 serials in the 2026-08-13 asset-tag backfill would have silently
312
+ failed to match without this.
313
+
269
314
  - **The "Outstanding Sales Orders - PO Reconciliation Required" email is FLEET-WIDE, not a
270
315
  per-ASN alert** (`2_send_serials_to_netsuite.php:270-357`). It runs at the end of every
271
316
  5-minute run and re-queries **all** `AdvanceShippingNoticeItems` with
@@ -518,6 +563,22 @@ use the toga DB MCP + `Logs.API` instead of running prod code locally.
518
563
  of the consumer query; `php -l` every touched file.
519
564
 
520
565
  ## Change history
566
+ - 2026-08-14 — Tied the DOE **missing-asset-tag** class to the existing "Lenovo Off-Layout ASN File"
567
+ bug: the +1 shift puts the serial in `assetTag` and means the real asset-tag column (`$cols[24]`)
568
+ is **never read**, so no unit from an off-layout file can get a tag (still **open** — needs the
569
+ parser fix plus a fixture regression test covering both layouts). Backfilled 87 tags for ASN
570
+ `26720` / PO `S202648137` / MSO `329924` (client 16, P.S. 235 Lenox School) in **both** stores
571
+ (`dbchanges/Core/SK/2026-08-13-backfill-asn-asset-tags-S202648137.sql`,
572
+ `dbchanges/TOGaDeskSupport/SK/2026-08-13-backfill-asset-tags-S202648137.sql`, PR dbchanges#265,
573
+ branch `fix/backfill-asset-tags-s202648137` → `_production`); SQL generated mechanically from the
574
+ customer serial→tag CSV, 87/87 verified against the live order before generation and round-trip
575
+ verified byte-identical, scoped to the ASN/MSO and filling only still-empty tags, so re-runnable.
576
+ Documented that `TOGaDeskSupport.assets.tag` is **derived** from
577
+ `Core.AdvanceShippingNoticeUnits.assetTag` (both must be repaired; confirmed live half-fix where
578
+ the UI was 87/87 right and the ASN 87/87 empty) and that the leading `S` on Lenovo/Lexmark serials
579
+ is stripped **on purpose** (compare via `App_NYCDOE::serialKey()`; 25/87 depended on it).
580
+ Remaining damage from the same defect: ASN `26641`/PO `S202668066` (162/363) and ASN `26706`/PO
581
+ `WR270003276` (1/7). No code changes in `worker`/`library`/`togadesk`. (sking)
521
582
  - 2026-08-11 — Investigated (read-only, **no code change**) ASN `26720` / PO `S202648137` never completing: root-caused the **"Lenovo Off-Layout ASN File"** bug — `asnprocessor/lenovo.php` maps columns by COUNT with no header validation, so the 31-column `DOE-Open-Lenovo-*.csv` shifts every field +1, making `serialNumber` the constant `"1"`, which collapses 87 source lines into 3 `completed` queue rows and loses units with no error/alert. Documented the diagnostic fingerprint, the 13-row systemic blast radius, the `varchar(64)` partNumber truncation amplifier, and the **manual** partNumber data repair as the only remediation. Also documented that the "Outstanding Sales Orders - PO Reconciliation Required" email is fleet-wide (and its count inflated), ruled out the Quad ASN work, and left the ASN `26720` items `54672`/`54673` correct-serial units as UNRESOLVED. (mhammontree)
522
583
  - 2026-08-06 — Named and cross-referenced the umbrella failure pattern **"Pre-ASN Manual
523
584
  Receipt Mismatch"** (aka "Shipment Received Notice") tying together the existing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.579",
3
+ "version": "1.0.581",
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",