toga-ai 1.0.713 → 1.0.714

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.
@@ -37,7 +37,7 @@
37
37
  | [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 |
38
38
  | [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
39
39
  | [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, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
40
- | [Sales-order denial reason (`ApprovalDecisions.note`) — captured everywhere, displayed nowhere](features/sales-order-denial-reason.md) | The **denial reason** an approver types when rejecting a sales-order approval stage. | _underscore/Model/Client/ApprovalDecision.php, _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Quad/ApprovalDecision.php, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts |
40
+ | [Sales-order denial reason (`ApprovalDecisions.note`) — capture, the denial-details card, and the read-only workflow mode](features/sales-order-denial-reason.md) | The **denial reason** an approver types when rejecting a sales-order approval stage. | _underscore/Model/Client/ApprovalDecision.php, _underscore/Model/Compass/ApprovalDecision.php, _underscore/Model/Quad/ApprovalDecision.php, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx, toga25-supply/src/utils/formatDateTime.ts, dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts |
41
41
  | [Sales-order \"PO Number\" sourcing — one surface element, `_purchaseOrders`, tenant SQL underneath](features/sales-order-po-number-sourcing.md) | The sales-order modal's **PO Number** field is **not** tenant-split at the display layer. | _underscore/Model/Client/SalesOrder.php, _underscore/Model/Compass/SalesOrder.php, _underscore/Model/Compass/Canada/SalesOrder.php, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Core/2026-08-25a - PoNumberDetailFieldPurchaseOrdersValueKey.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Client_Quad/2026-08-19a - PoDetailsButtonMostRecentPoSelection.sql |
42
42
  | [SO↔PO bridge tables are TWO tables in OPPOSITE directions (upstream vs downstream)](features/sales-order-purchase-order-bridge-direction.md) | There are **two** bridge tables linking sales orders and purchase orders, and they mean **opposite things**. | _underscore/Model/Client/PurchaseOrders/SalesOrder.php, _underscore/Model/Client/SalesOrders/PurchaseOrder.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, library/app/api/toga2.php |
43
43
  | [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | The status-filter dropdown on the sales-orders table is **per client**, driven by a Surface `FILTER_SET` rather than by the raw contents of the client's `SalesO | _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Prudential/SalesOrder.php, toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql |
@@ -1,19 +1,30 @@
1
1
  ---
2
- title: Sales-order denial reason (`ApprovalDecisions.note`) — captured everywhere, displayed nowhere
2
+ title: Sales-order denial reason (`ApprovalDecisions.note`) — capture, the denial-details card, and the read-only workflow mode
3
3
  framework: "2.0"
4
4
  repo: _underscore
5
5
  project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
10
- owners: [apeterson]
9
+ updated: 2026-08-31
10
+ owners: [apeterson, tcox]
11
11
  files:
12
12
  - _underscore/Model/Client/ApprovalDecision.php
13
13
  - _underscore/Model/Compass/ApprovalDecision.php
14
14
  - _underscore/Model/Quad/ApprovalDecision.php
15
15
  - toga25-supply/src/layout/RecordApprovalModal/
16
16
  - toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts
17
+ - toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx
18
+ - toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx
19
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx
20
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx
21
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts
22
+ - toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts
23
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts
24
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx
25
+ - toga25-supply/src/utils/formatDateTime.ts
26
+ - dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql
27
+ - dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql
17
28
  - toga2-commerce/src/pages/OrderDetails/api/OrderDetailsApi.ts
18
29
  related:
19
30
  - ../../../clients/compass-usa/features/approval-decision-flow.md
@@ -28,11 +39,17 @@ related:
28
39
 
29
40
  The **denial reason** an approver types when rejecting a sales-order approval stage. It is a single
30
41
  free-text field — **`ApprovalDecisions.note`** — and this doc is the map of where it is written,
31
- where it is read, and (as of 2026-08-28) where it is **not** read.
42
+ where it is read, and what displays it.
32
43
 
33
- The headline: **every tenant already captures it, and no frontend anywhere displays it.** Its one
34
- and only consumer today is the rejection email. So a "show the denial reason" ticket is a
35
- **read/display** feature — there is no data-capture, no API and no schema work in it.
44
+ **Every tenant already captures it**, so a "show the denial reason" ticket is a pure
45
+ **read/display** feature — no data-capture, no API and no schema work.
46
+
47
+ **Status (2026-08-31).** In **production the rejection email is still the only consumer.** The
48
+ display feature — a **denial banner** on the SO record modal plus a read-only **approval timeline**
49
+ in the workflow modal — is built, committed and **confirmed rendering on `client-sandbox`**, but not
50
+ released: `toga25-supply` branch **`TRUE-80850`** (`ca6678f`, pushed, **no PR**) and `dbchanges2`
51
+ branch **`TRUE-80850`** (`38ef9ff`, **not merged to `_main`**). Seeds have run on `client-sandbox`
52
+ only. See [Built](#built-2026-08-31--denial-banner--read-only-approval-timeline).
36
53
 
37
54
  ## Where the value lives
38
55
 
@@ -77,7 +94,9 @@ Both consumers read `$payload->note` and fall back to
77
94
  it is a separate implementation, so a change to the Compass rejection-note path does not reach
78
95
  Quad and vice versa.
79
96
 
80
- **No other reader exists.** Neither `toga25-supply` nor `toga2-commerce` renders it.
97
+ **No other reader exists in any deployed environment.** `toga2-commerce` still renders nothing; the
98
+ `toga25-supply` banner + timeline exist only on the unmerged `TRUE-80850` branches (+ the
99
+ `client-sandbox` seeds).
81
100
 
82
101
  ### toga2-commerce already fetches it and throws it away
83
102
 
@@ -122,19 +141,73 @@ feature.
122
141
  consistency, but no per-tenant polish is justified by 34 and 3 rows. All figures are point-in-time
123
142
  measurements, not standing facts.
124
143
 
125
- ## Planned display design (decided 2026-08-28 — nothing built yet)
126
-
127
- Three decisions were taken during the investigation. They are recorded here so the build does not
128
- re-litigate them.
144
+ ## Built (2026-08-31) — denial banner + read-only approval timeline
145
+
146
+ The mockup arrived mid-build and **changed the design**: there is **no denial-details card**. The
147
+ denial reason is not surfaced on the record modal at all — it lives one click away, in the workflow
148
+ modal's read-only timeline.
149
+
150
+ **1. Denial banner** (`SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx`) — full
151
+ width at the **top of the modal body**, rendered by `SalesOrderView`, not by the summary grid. Two
152
+ lines plus a **"View approval workflow"** pill (a native `button`; blox `BaseButton` styling
153
+ conflicted):
154
+
155
+ > This order was denied at {stage label} · step {N} of {M}
156
+ > {decider} ({role}) denied the order on {MM/DD/YYYY, h:mm A}.
157
+
158
+ **The reason is deliberately NOT in the banner.** The pill opens the workflow modal, which is
159
+ read-only on a canceled order.
160
+
161
+ **2. Read-only approval timeline** (`src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx`)
162
+ — in read-only mode `RecordApprovalModalLayout` renders the timeline **instead of**
163
+ `ApprovalFlowForm`: per-stage colored disc (green check approved / red x denied / gray step number
164
+ pending) on a connector rail, a status line "Approved by {name} · {role} · {datetime}", and note
165
+ boxes — gray **"Note"** for approvals, pink **"Reason for denial"** for denials. **No footer**; the
166
+ header X closes. Dialog variant `.approvalDialogReadOnly` (780px, auto height).
167
+
168
+ The read-only **summary** above the timeline drops `preferredStage`-bound fields (Assigned Manager —
169
+ "who acts next" is meaningless on a decided order) and strips trailing label colons.
170
+
171
+ **3. Surface 47 is still the gate — it just drives a banner instead of a card.** The
172
+ `salesOrderDenialDetails` section is **excluded from `getVisibleSections`** outright (never a
173
+ summary-grid card); `isDenialDetailsActive(tenantFields, order)` decides the banner: section visible
174
+ **AND** its Tier-1 layout rule passes **AND** `hasDenialDetails(order)` (the ~1% emptiness guard,
175
+ decision 3 below).
176
+
177
+ **4. Deleted as dead code:** `DenialDetailsCard.tsx`, its `denialDetails` renderer in
178
+ `sectionRenderers.tsx`, and the disabled-form read-only plumbing (`isReadOnly` on
179
+ `ApprovalFlowForm` / `ApprovalStageCard` / `ApprovalFlowDetailInputs`). **The timeline is the only
180
+ read-only path** — do not re-add a "disable the form" branch.
181
+
182
+ **5. Adapter** (`surfaceBundleToTenantFields.ts`): `config.role` carried onto fields;
183
+ `config.valueType` used **only when the enum column is NULL**; exports `isDenialDetailsActive`,
184
+ `getDeniedStages`, `hasDenialDetails`, `resolvePathInsensitive`, `DENIAL_DETAILS_KEY`,
185
+ `ApprovalStageRecord`. `SalesOrder` type gained `stages` / `currentStage`.
186
+
187
+ **6. Seeds** (`dbchanges2` branch `TRUE-80850`, `38ef9ff`):
188
+ `Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql` (surface **47**, elements **170–176**,
189
+ Messages **227–232**, plus the `readOnlyRule` marker on surface 41) and
190
+ `Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql` (two-stage widening +
191
+ the Admin **`ENABLED_RULE`** as well as `VISIBILITY_RULE` on element 19). Reserved literal ids, plain
192
+ `INSERT`s, verify-the-block SQL in the header. This feature **kept** 47 / 170–176 / 227–232; the
193
+ transfer-order seeds that had reserved the same snapshot were **renumbered** to 53–57 / 190–206 /
194
+ 261–284 on 2026-08-31 — see
195
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
196
+
197
+ Verified on `client-sandbox` 2026-08-31: banner + read-only timeline render for Compass. Test orders
198
+ SA136508, SA136480, SA136473, and **SA134872 (denial with a NULL note — placeholders render)**.
199
+ Tests: 5 vitest files (`ApprovalTimelineView`, `DenialBanner`, adapter, `getVisibleSections`,
200
+ decision-fields), tsc + eslint clean.
201
+
202
+ ## Design decisions (2026-08-28 plan, confirmed and extended by the 2026-08-31 build)
129
203
 
130
204
  ### 1. The denial-details block gets its OWN surface
131
205
 
132
- **Planned:** `Core.Surfaces` **47** `sales-order-denial-details`, `SurfaceElements` **170–174**, plus
133
- a **175** marker — with **reserved literal ids**, not `AUTO_INCREMENT`, so client override files can
134
- hardcode them and ship prod-ready. (Next free prod ids as of 2026-08-28: Surfaces **47**,
135
- SurfaceElements **170**, Messages **227**, Actions **17** — re-verify before writing the seed; see
206
+ Confirmed as built: `Core.Surfaces` **47** `sales-order-denial-details`, `SurfaceElements`
207
+ **170–176**, `Messages` **227–232** — with **reserved literal ids**, not `AUTO_INCREMENT`, so client
208
+ override files hardcode them and ship prod-ready. Re-verify the block is free before every run; see
136
209
  the verify-the-block-is-free rule in
137
- [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).)
210
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
138
211
 
139
212
  Rejected: adding it to `sales-order-record-header` (surface 11). That surface is **top-bar chrome
140
213
  only** — modal tag / order date / status badge / title, consumed by `SalesOrderTopBar.tsx`. Both
@@ -157,19 +230,23 @@ language) with **no order-status axis**. `VISIBILITY_RULE` and `ENABLED_RULE` ar
157
230
  status-aware attributes in the `SurfaceOverrides` ENUM. Separately, the workflow **button must stay
158
231
  clickable** so the modal can open at all — read-only cannot be expressed by disabling the button.
159
232
 
160
- **Planned home for the marker:** surface **41** (`sales-order-decision-summary`) — the only surface
161
- all three modal modes (approve / deny / approvalWorkflow) already fetch.
233
+ **Home for the marker (built):** element **176** on surface **41**
234
+ (`sales-order-decision-summary`) — the only surface all three modal modes (approve / deny /
235
+ approvalWorkflow) already fetch. Core default rule: `_status nin ["pendingApproval"]`.
236
+
237
+ **Role markers are the convention for this whole family** — `sectionLayoutRule`, `titleSuffix`,
238
+ `readOnlyRule`, and the new **`denialReason`** on the note element. A marker is a `config.role`
239
+ string the FE looks for; it is **never** a magic `valueType`.
162
240
 
163
- **Half the rendering already exists.** `ApprovalFlowDetailInputs.tsx` computes
164
- `areInputsDisabled = !isPreApproved && stageHasApprovalDecision`, and `StageHasDecisionView.tsx`
165
- already renders a decided stage's decision label plus the note in a disabled/readOnly textarea.
166
- Remaining gaps: stages with **no** decision still render editable; `ApprovalStageCard`'s
167
- approve/deny radios are not status-gated; the submit path is not gated.
241
+ **Use the real `valueType` ENUM member whenever one exists** (`DATETIME` for `dtDecision`). The
242
+ `config.valueType` bypass is reserved for genuinely-missing members, and the FE adapter now falls
243
+ back to it **only when the enum column is NULL**.
168
244
 
169
- ### 3. Gate the card with a plain Tier-1 rule + a component emptiness guard
245
+ ### 3. Gate it with a plain Tier-1 rule + a component emptiness guard
170
246
 
171
247
  **Decision:** `{"all":[{"field":"order._status","op":"in","value":["canceled"]}]}` plus a
172
- don't-render-an-empty-card guard in the component. **Not** a new Tier-2 named predicate.
248
+ don't-render-anything-empty guard in the component (now `isDenialDetailsActive` → `hasDenialDetails`).
249
+ **Not** a new Tier-2 named predicate.
173
250
 
174
251
  This **reverses** the initial recommendation. Compass `_status` reaches `canceled` two ways — an
175
252
  explicit `salesOrderStageId` (stage 8, Canceled) *or* any denied `ApprovalDecision` (see
@@ -187,6 +264,71 @@ The frozen rule grammar was confirmed identical on both sides: `eq`/`ne`/`in`/`n
187
264
  `lte` — `RULE_OPS` at `_underscore/Model/Core/Surface.php:233` and `FROZEN_OPS` in
188
265
  `toga25-supply/src/surface/evaluateSurfaceRule.ts`.
189
266
 
267
+ ### 4. Decision dates display ABSOLUTE (2026-08-31)
268
+
269
+ New `src/utils/formatDateTime.ts` (dayjs, `MM/DD/YYYY, h:mm A`). Chosen over the existing
270
+ `formatTimestamp`, which is **relative** ("3 hours ago") and useless on a months-old denial, and over
271
+ native `Date` parsing — MySQL's `YYYY-MM-DD HH:mm:ss` is `Invalid Date` in Safari.
272
+
273
+ ### 5. A read-only widening must target the SAME override scope tier as the existing rows (2026-08-31)
274
+
275
+ Compass needed the read-only rule widened to two stages
276
+ (`nin ["pendingApproval","pendingInitialApproval"]`). For **element 19** (`approvalWorkflow`) the
277
+ widening had to be written as an **Admin role-scoped** override, not client-wide: a client-wide
278
+ (`roleId NULL`) row is **shadowed** by the pre-existing Admin role-scoped rows under the resolver
279
+ cascade (base < client-wide < persona < role). Element 19 also carries an Admin **`ENABLED_RULE`**
280
+ limited to pending statuses, so widening only `VISIBILITY_RULE` leaves the button visible-but-disabled
281
+ and the modal still cannot open — **both attributes** need widening. Caught by sql-reviewer plus a
282
+ sandbox check of the live override rows. Authoring detail:
283
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
284
+
285
+ ### 6. Stage DISPLAY labels come from client config, not the DB (2026-08-31)
286
+
287
+ The banner and every timeline row resolve the stage label from the per-client
288
+ `approvalViewFields.recordActionFields.approvalWorkflow.stages[]` entry matching the **step**
289
+ ("Admin Review" / "Manager Review"), falling back to `ApprovalTemplateStages.name`. Compass's DB
290
+ names are **status-flavored** ("Pending Initial Approval" / "Pending Approval") and read wrong in
291
+ past tense. **Do not rename the DB stages** — those names feed `_status` and the approval emails;
292
+ display naming belongs in config.
293
+
294
+ ### 7. Step totals come from the MAX step NUMBER, not `stages.length` (2026-08-31)
295
+
296
+ The per-stage meta fan-out makes `order.stages[]` **transiently partial**, so array length rendered
297
+ "step 2 of **1**" mid-load. Total = `Math.max(...steps)`.
298
+
299
+ ### 8. The stages fetch is gated on the APPROVALS SURFACE toggle (2026-08-31)
300
+
301
+ `useApprovalModalViewModel` now gates its stages fetch on `sales-order-record-approvals`
302
+ (`isApprovalsSurfaceEnabled`) — the same authoritative per-client gate the record-modal and
303
+ row-state view models use — **not** on `approvalActionStatuses`. The status list excludes canceled,
304
+ so a canceled order fetched no stages and the timeline had nothing to draw; the surface toggle also
305
+ keeps approvals-disabled clients from fetching at all.
306
+
307
+ ### 9. Read-only FAILS CLOSED on a fetch error (2026-08-31)
308
+
309
+ If the decision-surface fetch **errors**, the rule is unknown → the modal opens **read-only**; a
310
+ **successful** response with no marker (unseeded client) stays **editable**. A transient surface
311
+ outage must never unlock edit + submit on a decided order.
312
+
313
+ ### 10. Copy is config-first with FE fallbacks (2026-08-31)
314
+
315
+ `modalFields.timelineLabel` / `noteLabel` / `denialReasonLabel` and
316
+ `approvalWorkflow.viewWorkflowButton.label`. **Composed sentences** (the two banner lines, the
317
+ "Approved by … · … · …" status line) remain **FE English** — Compass Canada localization is an open
318
+ follow-up, not an oversight.
319
+
320
+ ## Open items (2026-08-31)
321
+
322
+ - **Nothing is merged.** `toga25-supply` `TRUE-80850` (`ca6678f`) is pushed but **not PR'd**;
323
+ `dbchanges2` `TRUE-80850` (`38ef9ff`) is **not merged to `_main`**.
324
+ - **Re-verify the id block immediately before any prod run** — a reservation is a snapshot, not a
325
+ lock (see [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md)).
326
+ - **Compass Canada**: override file (same two-stage, role-scoped widening) **and** localization of
327
+ the FE-composed banner/timeline sentences.
328
+ - **Quad** needs an `IS_VISIBLE` opt-in on element 19 first — it cannot open the workflow modal at
329
+ all today.
330
+ - jcardinal still owes the `2.0/standards/framework-rules.md` reserved-id-exemption review.
331
+
190
332
  ## Gotchas
191
333
 
192
334
  - **Quad is not a Compass tenant on this path.** `_Model_Quad_ApprovalDecision` implements its own
@@ -203,6 +345,28 @@ The frozen rule grammar was confirmed identical on both sides: `eq`/`ne`/`in`/`n
203
345
  Anything keyed on `_status in ["canceled"]` must tolerate a missing decision/note.
204
346
 
205
347
  ## Change history
348
+ - 2026-08-31 — **Built the display feature planned on 2026-08-28** (resumed apeterson's
349
+ `denial-reason-surface-plan` session), then **redesigned it mid-build when the mockup arrived**.
350
+ Final shape: a **denial banner** at the top of the record-modal body (stage label · step N of M,
351
+ decider + role + absolute datetime, and a "View approval workflow" pill) with the **reason
352
+ deliberately not in the banner**, plus a read-only **approval timeline**
353
+ (`ApprovalTimelineView.tsx`) that **replaces** the form in the workflow modal — per-stage discs on a
354
+ rail, gray "Note" / pink "Reason for denial" boxes, no footer. The planned `DenialDetailsCard` and
355
+ the disabled-form read-only plumbing were **deleted as dead code**; surface **47** now gates the
356
+ banner (`isDenialDetailsActive`) and is excluded from `getVisibleSections` entirely. Mode still
357
+ comes from the `config.role:'readOnlyRule'` marker (el **176** on surface 41), with the
358
+ decision-surfaces `isLoading` OR'd into `isApprovalLoading` (no editable→read-only flicker) and
359
+ **fail-closed on fetch error**. Other decisions: stage **display labels** come from client config
360
+ (Compass DB names are status-flavored — do not rename them); step totals from the **max step
361
+ number**, not `stages.length` (partial fan-out rendered "step 2 of 1"); the stages fetch is gated on
362
+ the **approvals surface toggle**, not `approvalActionStatuses` (canceled orders were fetching
363
+ nothing); `denialReason` is a **role marker, not a magic valueType**; the enum `valueType` wins over
364
+ `config.valueType`; dates are **absolute** (`formatDateTime`, `MM/DD/YYYY, h:mm A`); and the
365
+ element-19 widening must be **Admin role-scoped** covering **`ENABLED_RULE` as well as
366
+ `VISIBILITY_RULE`**. Seeds renamed `2026-08-28a` → **`2026-08-31a`** and committed
367
+ (`dbchanges2@TRUE-80850` `38ef9ff`); FE committed + pushed (`toga25-supply@TRUE-80850` `ca6678f`,
368
+ no PR). Verified rendering on `client-sandbox` (SA136508 / SA136480 / SA136473 / SA134872
369
+ null-note). Open: nothing merged, Compass Canada override + localization, Quad el-19 opt-in. (tcox)
206
370
  - 2026-08-28 — Documented the denial reason end-to-end for the forthcoming display feature
207
371
  (investigation only; **no code written, no migration run**; all prod figures read-only). Findings:
208
372
  the value is `ApprovalDecisions.note` (Core record **179**, RecordField **1551**); the **write
@@ -6,8 +6,8 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
10
- owners: ["bala", "mhammontree", "apeterson", "jcardinal"]
9
+ updated: 2026-08-31
10
+ owners: ["bala", "mhammontree", "apeterson", "jcardinal", "tcox"]
11
11
  files:
12
12
  - api2/.ebextensions/git.php
13
13
  - api2/.platform/hooks/prebuild/git.sh
@@ -185,6 +185,19 @@ the tier trades a stale-code bug for a total outage. Full procedure:
185
185
  method/class that isn't on the box and fatal. Landing `_underscore` first is **safe on its own**:
186
186
  the old api2 controller simply doesn't use the new framework code yet. Order: merge `_underscore`
187
187
  → (verify it's on the branch) → deploy api2.
188
+ - **🚨 Worked instance — `client-sandbox` was 100% DOWN on this exact violation (2026-08-31,
189
+ since fixed by matching the branches and redeploying).** Every request returned 500 `Call to
190
+ undefined method
191
+ _Model_Core_AclFieldPermission::resolveFieldPermissions()`. `api2@_sandbox-client`
192
+ (`Component/Api/V2/V2.php:6198`, commit `0cc37fe` "Delegate ACL field perms to shared resolver",
193
+ jcardinal 2026-08-14) delegates to a resolver whose `_underscore` half (`a5e79448` "Surface field
194
+ binding and ACL field resolver", same author/date) exists **only on `_qa-alpha` and
195
+ `toga25-desk`** — it was never merged to **`_underscore@_sandbox-client`**. Resolved the same day
196
+ by the team (branches matched + redeployed). **Why a half-shipped ACL refactor is a TOTAL outage,
197
+ not a partial one:** field-ACL
198
+ resolution runs on essentially every authenticated GET, so there is no degraded mode — the whole
199
+ tier 500s and every unrelated feature under test looks broken. When a whole sandbox goes dark,
200
+ check the two-repo pairing before debugging your own feature.
188
201
  - **⚠ A "Could not find required file for `_Model_...`" fatal on prod can be a STALE-BUILD
189
202
  artifact, not a missing file.** `Logs.Issue #476`
190
203
  ("Could not find required file for '_Model_Client_ItemFulfillments_InventoryAdjustment'") fired
@@ -257,6 +270,16 @@ the tier trades a stale-code bug for a total outage. Full procedure:
257
270
 
258
271
  ## Change history
259
272
 
273
+ - 2026-08-31 — Recorded a **worked instance** of the cross-repo ordering rule: **`client-sandbox`
274
+ went entirely down** — every request 500'd with `Call to undefined method
275
+ _Model_Core_AclFieldPermission::resolveFieldPermissions()`. `api2@_sandbox-client` (`V2.php:6198`,
276
+ `0cc37fe`, 2026-08-14) delegates ACL field permissions to an `_underscore` resolver (`a5e79448`)
277
+ that lived only on `_qa-alpha` / `toga25-desk` and was never merged to
278
+ `_underscore@_sandbox-client`; fixed the same day by matching the branches and redeploying.
279
+ Durable point: a
280
+ half-shipped **ACL** refactor has no degraded mode — field-ACL resolution runs on nearly every
281
+ authenticated GET, so the tier is totally dark and unrelated features look broken. Found while
282
+ testing an unrelated toga25-supply feature. (tcox)
260
283
  - 2026-08-28 — Recorded two build-time-clone consequences: (1) **cross-repo deploy ordering** — api2
261
284
  clones `_underscore` (`_<ENVIRONMENT>`) at build via `.platform/hooks/prebuild/git.sh`, so for a
262
285
  change spanning both repos, `_underscore` must land on the deploy branch **before** api2 is
@@ -4,7 +4,7 @@
4
4
  |-----|---------|-------|
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
6
  | [Re-runnable additive migrations (uuid4 in SQL, INSERT guards, the DISTINCT trap, conditional ALTERs)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql, dbchanges2/Core/2026-08-27a - Insert - Netsuite Location SyncAll CronJob.sql |
7
- | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
7
+ | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql, toga25-supply/db-migrations/PLAYBOOK.md, toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md, dbchanges2/Client_Compass/, dbchanges2/Client_CompassCanada/, dbchanges2/Client_Quad/, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Core/2026-08-26a - ServiceRequestRecordHeaderSurfaceSeed.sql, dbchanges2/Core/2026-08-26b - ServiceRequestDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql |
8
8
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
9
9
  | [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
10
10
  | [Framework branch running AHEAD of schema — environment-wide 1054/EO-1 after a deploy](workflows/framework-branch-ahead-of-schema.md) | A **third** kind of 2.0 schema drift, distinct from the two already documented: nobody's database went backwards — **the code went forwards**. | _underscore/Model/Core/RecordField.php, _underscore/Model/Client/TableViewField.php, _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/Urgency.php, api2/Component/Api/V2/V2.php, dbchanges2/Core/2026-08-14c - RecordFieldsValidationMetadata.sql, dbchanges2/Client/2026-08-14a - RecordFieldSettingsClientTable.sql, dbchanges2/Client/2026-08-13a - UserNavPreferencesClientTable.sql, dbchanges2/Client/2026-08-13c - PersonaNavVisibilityClientTable.sql |
@@ -7,8 +7,10 @@ client: shared
7
7
  type: feature
8
8
  status: active
9
9
  updated: 2026-08-31
10
- owners: [jcardinal, apeterson]
10
+ owners: [jcardinal, apeterson, tcox]
11
11
  files:
12
+ - dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql
13
+ - dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql
12
14
  - toga25-supply/db-migrations/PLAYBOOK.md
13
15
  - toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md
14
16
  - dbchanges2/Client_Compass/
@@ -636,6 +638,15 @@ Two durable rules:
636
638
  `INSERT`, a stale block fails loudly on the primary key — which is the designed behavior and the
637
639
  reason `INSERT IGNORE` is banned here (see the 2026-08-24 incident below). Bump to a fresh block
638
640
  above MAX rather than trying to reuse the gap.
641
+ 3. **`SELECT MAX(id)` cannot see a block reserved by a seed that has not RUN.** A DB check only
642
+ reflects applied migrations, so a same-day sibling sitting unmerged in another developer's tree is
643
+ invisible to it. Verifying a block means querying the target Core **and** checking the in-flight
644
+ `Core/*SurfaceSeed.sql` files (this doc's block list is the shared record of them).
645
+
646
+ **The filename suffix is a separate check from the id block.** The denial-details pair was authored
647
+ as `2026-08-28a` and renamed to **`2026-08-31a`** because a committed
648
+ `Core/2026-08-28a - API 2.0 Stability Monitor Cron.sql` already held that date+letter. A free
649
+ date+letter says nothing about a free id block, and vice versa — check both.
639
650
 
640
651
  > These explicit ids are **correct and required**, not a portability bug. `Core.Surfaces` /
641
652
  > `Core.SurfaceElements` / `Core.Messages` are reserved-literal tables precisely so a `Client_*`
@@ -730,6 +741,26 @@ Quad disabled-tooltip statement now does this). Relevant to `Client_Quad/2026-08
730
741
  SalesOrderDecisionSummaryOverride.sql`, whose header wrongly implies a pre-existing row is
731
742
  necessarily the correct row.
732
743
 
744
+ ### 🚨 A client-wide override is SHADOWED by an existing role-scoped row — widen at the SAME tier (2026-08-31)
745
+
746
+ The resolver cascade is **base < client-wide < persona < role** (last write wins — see
747
+ [surface-resolver](../../_underscore/features/surface-resolver.md)). So a `roleId NULL` (client-wide)
748
+ row **does nothing** for any user whose role already has a row for that `(element, attribute)`: the
749
+ role-scoped row is applied after it and wins. Writing the widening client-wide "for everyone" is the
750
+ natural authoring instinct and it is **inert for exactly the users who need it**.
751
+
752
+ Concrete: widening element **19** (`approvalWorkflow`) so Compass admins can open a canceled order
753
+ read-only had to be an **Admin role-scoped** `UPDATE`-then-`INSERT` (role resolved at runtime by
754
+ `Roles.name = 'Admin'`, guarded `IS NOT NULL`), because `Client_Compass` already holds Admin
755
+ role-scoped rows on that element (sandbox: id 3 `IS_VISIBLE`, id 10 `VISIBILITY_RULE`, id 20
756
+ `ENABLED_RULE`). Caught by sql-reviewer before the run.
757
+
758
+ **And widen every attribute that gates the control, not just visibility.** Element 19 also carries an
759
+ Admin **`ENABLED_RULE`** limited to pending statuses, so widening `VISIBILITY_RULE` alone leaves the
760
+ button visible-but-**disabled** and the modal still cannot open. Before authoring a rule widening,
761
+ `SELECT` the element's existing override rows and read **every** row's scope *and* attribute — the
762
+ answer is in the data, not in the base seed.
763
+
733
764
  ## Migration maintenance — fold-back (⚠ the local-only window has CLOSED)
734
765
 
735
766
  > **Superseded 2026-08-21/24.** The Surface layer **has been pushed to production** (a hand-picked
@@ -1016,6 +1047,19 @@ hits `Duplicate entry '251' for key 'messages.PRIMARY'`. The resolution that kee
1016
1047
  `config.valueKey` when it lives in the page.
1017
1048
 
1018
1049
  ## Change history
1050
+ - 2026-08-31 — Added the **denial-details** seed pair (`Core/2026-08-31a -
1051
+ SalesOrderDenialDetailsSurfaceSeed.sql` — Surface **47**, elements **170–176**, Messages
1052
+ **227–232**, plus a `readOnlyRule` marker on surface 41; and `Client_Compass/2026-08-31a -
1053
+ SalesOrderDenialDetailsReadOnlyRules.sql`). Committed on branch **`TRUE-80850`** (`38ef9ff`), **not
1054
+ merged to `_main`**; run on **`client-sandbox` only** and confirmed rendering there. (This is the
1055
+ feature that claimed 47/170–176/227–232 and forced the transfer-order renumber above.) Two
1056
+ authoring rules: **a client-wide (`roleId NULL`) override is shadowed by an existing role-scoped
1057
+ row**, so the Compass read-only widening on element 19 had to be an Admin **role-scoped**
1058
+ `UPDATE`-then-`INSERT` — and it had to widen the Admin **`ENABLED_RULE`** as well as
1059
+ `VISIBILITY_RULE`, or the button stays visible-but-disabled; and **`SELECT MAX(id)` cannot see a
1060
+ block reserved by a seed that has not run**, so a block check must also sweep in-flight seed files.
1061
+ Also noted the filename date+letter is an independent check from the id block (this pair was
1062
+ renamed off `2026-08-28a`, already taken by a committed Core file). (tcox)
1019
1063
  - 2026-08-31 — **Renumbered the transfer-order reserved blocks** to `Messages` **261–284**,
1020
1064
  `Surfaces` **53–57**, `SurfaceElements` **190–206**: the 2026-08-28 blocks were reserved from a
1021
1065
  snapshot and `sales-order-denial-details` claimed `Surfaces` 47 / `SurfaceElements` 170–176 /
@@ -12,7 +12,7 @@
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/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts |
13
13
  | [Side navigation & default route — an empty nav renders a BLANK PAGE and gets reported as "cannot log in"](features/side-navigation-and-default-route.md) | The 2.5 side nav is **100% backend-driven** by the `navigation` surface bundle, and the same list also decides **which routes exist** and **where `/` lands**. | toga25-supply/src/routes.tsx, toga25-supply/src/layout/AppLayout/viewModel/useAppLayoutViewModel.ts, api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
14
14
  | [SSO redirect & public-vs-user session gating (useAuthenticationFlow)](features/sso-redirect-and-session-gating.md) | How 2.5 Supply decides, on every navigation, whether an anonymous visitor should be bounced to their client's SSO IdP instead of the local `/login` form. | toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/src/routes.tsx, toga25-supply/src/contexts/AuthContext.tsx, toga25-supply/src/api/api.ts |
15
- | [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/App.tsx, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, 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/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
15
+ | [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/App.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx, toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx, toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css, toga25-supply/src/utils/formatDateTime.ts, toga25-supply/src/utils/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/layout/RecordApprovalModal/, toga25-supply/src/contexts/AuthContext.tsx, 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/ServiceRequests/view/ServiceRequestRecordModalLayout/ServiceRequestRecordModalLayout.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/view/ServiceRequestsView.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/useServiceRequestRecordModalLayoutModel.tsx, toga25-supply/src/pages/ServiceRequests/view/ServiceRequestRecordModalLayout/viewModel/FIELDS/apiFields.json, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/AdminNotesSection.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getAdminNotes.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/resolveElementState.ts, toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderRowRecordState.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrderVip.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx, toga25-supply/src/surface/useStatusColors.ts, toga25-supply/src/surface/SurfaceHeader.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/helpers/surfaceBundlesToDecisionFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
16
16
  | [Talos Integration (AppLayout host + adapter wiring)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
17
17
  | [Transfer Orders page (TableView → Core surfaces → React page + record modal)](features/transfer-orders-page.md) | The Transfer Orders screen — list + read-only record modal — built end to end on 2026-08-28 from a Claude Design prototype. | toga25-supply/src/pages/TransferOrders/TransferOrders.tsx, toga25-supply/src/pages/TransferOrders/hooks/useTransferOrdersTableState.ts, toga25-supply/src/pages/TransferOrders/viewModel/useTransferOrdersPageViewModel.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrdersTableLayout/TransferOrdersTableLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/TransferOrderRecordModalLayout.tsx, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/viewModel/useTransferOrderRecordModalLayoutModel.ts, toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/view/TransferOrderRecordView.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/CreateTransferOrderModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/index.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferableItemRows.ts, toga25-supply/src/surface/useColumnVisibilityModalConfig.ts, toga25-supply/src/routes.tsx, dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql, dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql, dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql, dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql |
18
18
  | [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, toga25-supply/.env.development |
@@ -6,10 +6,18 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-28
10
- owners: [jcardinal, apeterson]
9
+ updated: 2026-08-31
10
+ owners: [jcardinal, apeterson, tcox]
11
11
  files:
12
12
  - toga25-supply/src/App.tsx
13
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/DenialBanner.tsx
14
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/SalesOrderRecordModal.module.css
15
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/SalesOrderApprovalModalsLayout.tsx
16
+ - toga25-supply/src/layout/RecordApprovalModal/RecordApprovalModalLayout.tsx
17
+ - toga25-supply/src/layout/RecordApprovalModal/view/ApprovalTimelineView.tsx
18
+ - toga25-supply/src/layout/RecordApprovalModal/ApprovalModal.module.css
19
+ - toga25-supply/src/utils/formatDateTime.ts
20
+ - toga25-supply/src/utils/index.ts
13
21
  - toga25-supply/src/surface/evaluateSurfaceRule.ts
14
22
  - toga25-supply/src/layout/RecordApprovalModal/
15
23
  - toga25-supply/src/contexts/AuthContext.tsx
@@ -397,6 +405,91 @@ element (surface-owned binding); the FE owns the *resolution mechanics*.
397
405
  semantics). Quad overrides field sort/visibility on the same card. Core seeds the shared content
398
406
  (shipping-method + currency-charge concat, Order-For inline email path).
399
407
 
408
+ ## A surface section can drive a BANNER instead of a grid card (2026-08-31)
409
+
410
+ `sales-order-denial-details` (surface 47) is the first section surface whose resolved config renders
411
+ **outside the summary grid**: `getVisibleSections` **excludes** `DENIAL_DETAILS_KEY` unconditionally,
412
+ and `SalesOrderView` renders `<DenialBanner>` at the top of the modal body when
413
+ **`isDenialDetailsActive(tenantFields, order)`** — section visible **AND** its Tier-1 layout rule
414
+ passes **AND** `hasDenialDetails(order)`. Same three-part gate shape as `isApprovalDetailsActive`.
415
+
416
+ The takeaway: **a surface section is a config unit, not a placement contract.** Where it renders is
417
+ an FE decision (the layout/geometry boundary below); the surface only says whether it applies and
418
+ what is in it.
419
+
420
+ - **Stage-relative valueKeys.** The section's element valueKeys resolve against `order.stages[]`
421
+ (`DecidedBy._name`, `ApprovalDecisions.note`, `dtDecision`) via `resolvePathInsensitive` — not the
422
+ order root, and not via `config.preferredStage` (which pins *one* stage). A third valueKey
423
+ namespace alongside `order.`-prefixed header keys and bare section keys.
424
+ - **Emptiness guard in the component, not in the rule.** The Tier-1 `order._status in ["canceled"]`
425
+ rule also fires on administratively-canceled orders (~1%) — see "measure the gap" below.
426
+ - **Step totals: use the MAX step NUMBER, never `stages.length`.** The per-stage meta fan-out leaves
427
+ `order.stages[]` transiently partial, which rendered "step 2 of **1**" mid-load.
428
+ - **Stage display labels come from client config** — the `approvalWorkflow.stages[]` entry matching
429
+ the step, falling back to `ApprovalTemplateStages.name` (Compass's DB names are status-flavored:
430
+ "Pending Initial Approval").
431
+
432
+ **Adapter changes** (`surfaceBundleToTenantFields.ts`): `config.role` is carried **onto the field**;
433
+ `config.valueType` is used **only when the element's `valueType` enum column is NULL** — the enum
434
+ wins. New exports: `isDenialDetailsActive`, `getDeniedStages`, `hasDenialDetails`,
435
+ `resolvePathInsensitive`, `DENIAL_DETAILS_KEY`, `ApprovalStageRecord`.
436
+
437
+ **Dates are ABSOLUTE.** New `src/utils/formatDateTime.ts` (dayjs, `MM/DD/YYYY, h:mm A`). The
438
+ pre-existing `formatTimestamp` is **relative** ("3 hours ago") and wrong for a months-old decision,
439
+ and native `new Date(...)` on the API's MySQL `YYYY-MM-DD HH:mm:ss` string is **`Invalid Date` in
440
+ Safari** — always parse these with dayjs.
441
+
442
+ **Design note:** the banner's "View approval workflow" pill is a **native `button`** — blox
443
+ `BaseButton` variants fought the pill styling.
444
+
445
+ Seed side + the Compass override: [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md);
446
+ subject doc: [sales-order-denial-reason](../../_underscore/features/sales-order-denial-reason.md).
447
+
448
+ ## `readOnlyRule` — a marker rule that puts the approval-workflow modal in view-only mode (2026-08-31)
449
+
450
+ The second live use of "a marker's rule selects a MODE" (below). A hidden
451
+ `config.role: 'readOnlyRule'` element (**el 176**) was added to the **existing**
452
+ `sales-order-decision-summary` surface (41) — the only surface all three modal modes fetch — and the
453
+ FE reads its resolved `visibilityRule` as a mode flag, not as visibility.
454
+
455
+ Chain: `surfaceBundlesToDecisionFields` exposes **`workflowReadOnlyRule`** →
456
+ `useApprovalModalViewModel` computes **`isWorkflowReadOnly`** → `RecordApprovalModalLayout`
457
+ `isReadOnly`.
458
+
459
+ **Read-only SWAPS THE VIEW — it does not disable the form.** `RecordApprovalModalLayout` renders
460
+ **`ApprovalTimelineView`** in place of `ApprovalFlowForm`: per-stage colored disc (green check /
461
+ red x / gray step number) on a connector rail, a "Approved by {name} · {role} · {datetime}" status
462
+ line, and note boxes (gray "Note", pink "Reason for denial"). No footer — the header X closes;
463
+ dialog variant `.approvalDialogReadOnly` (780px, auto height). The first implementation *did* thread
464
+ `isReadOnly` through `ApprovalFlowForm` / `ApprovalStageCard` / `ApprovalFlowDetailInputs` to disable
465
+ controls; when the mockup arrived that plumbing was **deleted as dead code**. **Do not re-add a
466
+ "disable the form" branch** — a read-only view of a form is a different component, not the same
467
+ component with everything greyed out, and keeping both means every future form change must be
468
+ correct twice.
469
+
470
+ The read-only summary above the timeline also **drops `preferredStage`-bound fields** (Assigned
471
+ Manager = "who acts next", meaningless once decided) and strips trailing label colons.
472
+
473
+ Rules this establishes:
474
+
475
+ - **No marker seeded → behaves exactly as before.** A mode marker must default to the old behavior,
476
+ so an unseeded client is untouched.
477
+ - **⚠ FAIL CLOSED on a fetch error.** `isWorkflowReadOnly` returns `true` when the decision-surface
478
+ fetch **errors** (the rule is unknown); only a *successful* empty response means editable. A
479
+ transient surface outage must not unlock edit + submit on a decided order.
480
+ - **⚠ Gate the modal's skeleton on the mode fetch.** The decision-surfaces `isLoading` is **OR'd
481
+ into `isApprovalLoading`**, so the skeleton stays up until the mode is known. Without that the
482
+ modal renders **editable and then flips read-only** — a visible flicker on a control that decides
483
+ whether a user can act.
484
+ - **Gate dependent fetches on the SURFACE TOGGLE, not on a status list.** The view model's
485
+ approval-stages fetch now keys off `sales-order-record-approvals`
486
+ (`isApprovalsSurfaceEnabled`) — the same authoritative gate the record-modal and row-state view
487
+ models use — instead of `approvalActionStatuses`, which excludes `canceled` and so starved the
488
+ timeline of stages on exactly the orders it exists for.
489
+ - **Copy is config-first with FE fallbacks** (`modalFields.timelineLabel` / `noteLabel` /
490
+ `denialReasonLabel`, `approvalWorkflow.viewWorkflowButton.label`). Composed sentences stay FE
491
+ English — localization for Compass Canada is an open follow-up.
492
+
400
493
  ## Two FE rule engines (surface Tier-1 vs legacy named-rule) — do not conflate
401
494
 
402
495
  There are **two** rule evaluators in this app. As of 2026-07-21 the surface evaluator now has a
@@ -454,12 +547,10 @@ family. Reach for this whenever the variation is **status-aware**, because `isVi
454
547
  static booleans with no status axis, and whenever the control must stay interactive (a modal's
455
548
  "view only" mode cannot be expressed by disabling the button that opens it).
456
549
 
457
- **Read-only approval-workflow modal — what already exists (2026-08-28).** Roughly half the rendering
458
- is already in place: `ApprovalFlowDetailInputs.tsx` computes
459
- `areInputsDisabled = !isPreApproved && stageHasApprovalDecision`, and `StageHasDecisionView.tsx`
460
- already renders a decided stage's decision label plus its note in a disabled/readOnly textarea. The
461
- gaps are specific: stages with **no** decision still render editable, `ApprovalStageCard`'s
462
- approve/deny radios are not status-gated, and the submit path is not gated.
550
+ **Known role markers:** `sectionVisibility` (section on/off), `sectionLayoutRule` (el 108, layout),
551
+ `approvalsGate` (el 24), `titleSuffix`, `readOnlyRule` (el 176, modal mode — built 2026-08-31, see
552
+ above), `denialReason` (identifies the note field on the denial-details card). A new one is a
553
+ `config.role` string plus the FE that looks for it — **never** a magic `valueType`.
463
554
 
464
555
  ### `resolveElementState` — the flag composition
465
556
  `resolveElementState.ts` computes an element's `isEnabled` as **`staticFlag AND enabledRule AND
@@ -594,6 +685,11 @@ here is a cross-screen change. Re-check Sales Orders, Items and Vendor Items aft
594
685
 
595
686
  ## Gotchas
596
687
 
688
+ - **A vitest file that reaches `@agilant/toga-blox` must stub the whole package:
689
+ `vi.mock("@agilant/toga-blox", () => ({}))`.** blox's `dist` entry imports `global.css`, which
690
+ node's ESM loader rejects, so **any** import chain that transitively pulls blox in fails to load
691
+ the test file (not the assertion — the file). Every surface adapter/component test needs the stub;
692
+ it is the reason a new test can fail before a single expectation runs.
597
693
  - **The VIP `/contacts` API call is gated by an approval-details element's `labelSuffix.valueKey`,
598
694
  NOT by the element's `isVisible`.** `useSalesOrderVip.ts` decides whether to call `/contacts`
599
695
  (which reads `Users.c_isVip`) by scanning the approval-details surface for an element whose
@@ -796,6 +892,28 @@ button except **View Log** renders and does nothing on click. Whether the SR mod
796
892
  actions surface is **still an open decision**, not an oversight.
797
893
 
798
894
  ## Change history
895
+ - 2026-08-31 — **Built** the sales-order **denial banner** + read-only **approval timeline**
896
+ (`toga25-supply@TRUE-80850` `ca6678f`, pushed, no PR; verified rendering on `client-sandbox`).
897
+ Two durable FE patterns. (1) **A surface section can drive a BANNER, not a grid card**:
898
+ `DENIAL_DETAILS_KEY` is excluded from `getVisibleSections` outright and `SalesOrderView` renders
899
+ `DenialBanner` when `isDenialDetailsActive` (visible AND Tier-1 rule AND `hasDenialDetails` — the
900
+ ~1% empty-cancel guard); a surface section is a **config unit, not a placement contract**. Its
901
+ valueKeys are **stage-relative** (`order.stages[]` via `resolvePathInsensitive`, a third namespace
902
+ distinct from `config.preferredStage`); step totals must come from the **max step number** because
903
+ the per-stage fan-out leaves `stages[]` transiently partial ("step 2 of 1"); stage **display
904
+ labels** come from client config, not the status-flavored DB names. (2) **Read-only SWAPS THE
905
+ VIEW**: `RecordApprovalModalLayout` renders `ApprovalTimelineView` instead of `ApprovalFlowForm`
906
+ (discs + rail, gray Note / pink Reason-for-denial boxes, no footer, `.approvalDialogReadOnly`), and
907
+ the earlier `isReadOnly`-disables-the-form plumbing in
908
+ `ApprovalFlowForm`/`ApprovalStageCard`/`ApprovalFlowDetailInputs` was **deleted as dead code** —
909
+ do not re-add it. Mode still comes from the `readOnlyRule` marker (el 176, surface 41) and now
910
+ **fails closed on fetch error**; the skeleton is gated on the mode fetch (no editable→read-only
911
+ flicker); the stages fetch moved onto the **approvals surface toggle** (`approvalActionStatuses`
912
+ excludes `canceled`, starving the timeline). Adapter carries `config.role` onto fields and uses
913
+ `config.valueType` **only when the enum column is NULL**; new absolute `formatDateTime`
914
+ (`MM/DD/YYYY, h:mm A`; `formatTimestamp` is relative and native `Date` parsing of MySQL datetimes
915
+ is `Invalid Date` in Safari). Added the vitest `vi.mock("@agilant/toga-blox")` requirement (blox
916
+ `dist` imports `global.css`). 5 test files, tsc + eslint clean. (tcox)
799
917
  - 2026-08-28 — **`SurfaceActionBar` labeled mode now honours `element.variant`** (shared infra):
800
918
  `renderMode="labeled"` had hardcoded `variant="secondaryAction"` for every button, so a `PRIMARY`
801
919
  seed could never render as primary. Added an `element.variant` → `BaseButton` variant map with a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.713",
3
+ "version": "1.0.714",
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",