toga-ai 1.0.397 → 1.0.399

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.
@@ -9,6 +9,6 @@
9
9
  | [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
10
10
  | [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 |
11
11
  | [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 |
12
- | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/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 |
12
+ | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/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 |
13
13
  | [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 |
14
14
  | [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-20
9
+ updated: 2026-07-21
10
10
  owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - toga25-supply/src/surface/useFetchSurfaceMeta.ts
@@ -47,6 +47,8 @@ files:
47
47
  - toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx
48
48
  - toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx
49
49
  - toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx
50
+ - toga25-supply/src/surface/useStatusColors.ts
51
+ - toga25-supply/src/surface/SurfaceHeader.tsx
50
52
  related:
51
53
  - ../../_underscore/features/surface-resolver.md
52
54
  - meta-driven-table-data.md
@@ -344,6 +346,25 @@ shaped `{ order }`, so the predicate reads **`order.currentStage` / `order.stage
344
346
  `clipboardListCheck`, `penToSquare`) and must be used as-is (`element.tokens?.icon ?? "check"`).
345
347
  Running it through `resolveThemeToken` misses on every icon and silently falls back to `"check"` for
346
348
  every button.
349
+ - **Vocabulary color tokens are unresolvable because the resolved bundle's `theme` map is empty —
350
+ resolve status-badge dot colors from the status endpoint's `colorHex` instead.** A resolved bundle
351
+ ships theme-token SLUGS on each vocabulary term (`tokens.color = "color.status.<slug>"`,
352
+ `tokens.bg = "bg.status.<slug>"`) but the bundle's `theme` map arrives as `{}`, so
353
+ `resolveThemeToken(theme, term.tokens?.color)` returns `undefined` and the status dot renders
354
+ colorless in the sales-order record-modal header. Fix: resolve the dot color from the
355
+ `sales-order-statuses` API's `colorHex` field keyed by slug — a `SurfaceVocabularyTerm.value` **IS**
356
+ the status slug, so it maps 1:1 to `sales-order-statuses[].slug`. The **`useStatusColors(vocabularySlug)`**
357
+ hook (`src/surface/useStatusColors.ts`) fetches `GET /v2/sales-order-statuses?fields=slug,colorHex&sort=sortOrder`
358
+ (session-cached via react-query, `staleTime`/`gcTime` Infinity, same pattern as `useFetchSurfaceMeta`)
359
+ and returns a `(slug) => "#RRGGBB" | undefined` resolver, gated by a
360
+ `VOCABULARY_COLOR_ROUTES = { "sales-order-status": "sales-order-statuses" }` map so it is reusable
361
+ for other STATUS vocabularies (walks the V2 envelope to find the row array, no hard-coded
362
+ `data.salesOrderStatuses`). `SurfaceHeader.tsx`'s BADGE branch was extracted into a
363
+ `SurfaceStatusBadge` sub-component (needed because `useStatusColors` is a hook) whose dot color is
364
+ `resolveStatusColor(term.value) ?? resolveThemeToken(theme, term.tokens?.color)` — `colorHex` wins,
365
+ the theme token remains a fallback if the `theme` map is ever populated. Label resolution and the
366
+ "no matching term → render nothing" behavior are unchanged. The legacy table path
367
+ (`PrimaryTable*Layout` via `statusTypes.json`) already used `colorHex` and was untouched.
347
368
  - **Scripted-API GET return values land under `data.<scriptRoute>`, not `data` directly.** V2 nests a
348
369
  scripted-API return value inside the envelope under a route-keyed slot of `data` (e.g. `data.meta`,
349
370
  `data.surfaces`), one level deeper than a normal data fetch. The bundle hook unwraps this with
@@ -373,6 +394,17 @@ shaped `{ order }`, so the predicate reads **`order.currentStage` / `order.stage
373
394
  treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
374
395
 
375
396
  ## Change history
397
+ - 2026-07-21 — Fixed the surface-driven **status badge dot rendering colorless** in the sales-order
398
+ record-modal header: the resolved bundle ships vocabulary color TOKEN SLUGS
399
+ (`color.status.*`/`bg.status.*`) but its `theme` map is empty (`{}`), so `resolveThemeToken` returns
400
+ `undefined`. New **`useStatusColors(vocabularySlug)`** hook (`src/surface/useStatusColors.ts`)
401
+ resolves the dot color from the `sales-order-statuses` API's `colorHex` keyed by slug (a
402
+ `SurfaceVocabularyTerm.value` IS the status slug → 1:1 to `sales-order-statuses[].slug`);
403
+ session-cached via react-query, gated by a `VOCABULARY_COLOR_ROUTES` map for reuse across STATUS
404
+ vocabularies. `SurfaceHeader.tsx`'s BADGE branch extracted into a `SurfaceStatusBadge` sub-component
405
+ (hook use), dot color = `resolveStatusColor(term.value) ?? resolveThemeToken(...)` (colorHex wins,
406
+ theme token remains a fallback). Legacy table path (via `statusTypes.json`) already used `colorHex`,
407
+ untouched. `tsc --noEmit` clean. (apeterson)
376
408
  - 2026-07-20 — Documented the **two FE rule engines** (frozen surface Tier-1 `evaluateSurfaceRule`,
377
409
  which THROWS on `{type}` nodes, vs legacy `evaluateEnableRule` with `{type}` NAMED_RULES incl
378
410
  `stepTwoAssigned` reading `ctx.currentStage`/`ctx.stages`) and why the corrected Approve rule is
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: nycdoe
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-07-07
10
- owners: [mhammontree]
9
+ updated: 2026-07-20
10
+ owners: [mhammontree, sking]
11
11
  files:
12
12
  - worker/crons/sync/nycdoe/send_ticket_updates.php
13
13
  - worker/crons/sync/nycdoe/process_tickets.php
@@ -76,29 +76,55 @@ revert or never reach ServiceNow. It is the status-sync companion to the broader
76
76
  - `Hold` has **statusRank 6**, beating `Work in Progress` (rank 5) in the downgrade-guard
77
77
  added by TRUE-76812 — so a downstream sync cannot silently demote a held order.
78
78
 
79
- ### Status field (ServiceNow side)
80
- - The SNOW **native `state` field** (human-readable: `On Hold`, `In Progress`, `Assigned`,
81
- `Resolved`, …) is the **SLA-bearing, authoritative status**. Read/write this for hold.
82
- - The custom **`u_status_task`** field is a *separate concept* with a different vocabulary;
83
- it can lag/disagree with `state` (observed: native `state="In Progress"` while
84
- `u_status_task="Open"`). **Never use `u_status_task` for hold detection.**
85
- - Numeric `state` codes seen on the wire: INC `On Hold` = **3**, `In Progress` = **2**;
86
- RITM `On Hold` = **8**, the scheduled-update state = **"-14"**.
87
-
88
- ### Outbound (TOGaDesk → ServiceNow)
89
- - `send_ticket_updates.php` (INC) pushes the hold as `state:3`. The "Assigned → In Progress"
90
- auto-start block is **guarded** so it never pushes `state:2` when the local repair order is
91
- in any `HOLD_*` status (this is what caused the self-clobber — see Change history).
92
- - `send_request_item_updates.php` (RITM) pushes `state:"8"` (On Hold) and carries a
93
- defensive `HOLD_*` guard on the scheduled-update path so it can't push `state:"-14"` while
94
- held.
79
+ ### Status fields (ServiceNow side) — CORRECTED 2026-07-20
80
+ > **The earlier "native `state` is authoritative; never use `u_status_task`" model was WRONG**
81
+ > (see Change history 2026-07-20). Both fields exist and both matter, with distinct roles:
82
+ - **`u_status_task` (custom) is the CLIENT-FACING, authoritative incident status** — the field
83
+ DOE's own ServiceNow views/reporting read (values `Open`, `In Progress`, `On Hold`, …). **A hold
84
+ MUST be written here or the client never sees it.** It is **writable from any active state**; a
85
+ controlled test PATCH of `{u_status_task:'On Hold'}` returned success and PERSISTED for days
86
+ (INC2192842 held 3+ days). **The TOGaDesk inbound sync keys off `u_status_task`**
87
+ (`process_tickets.php:206` reads `$data->u_status_task`), and the RITM cron checks
88
+ `u_status_task == 'On Hold'` (`send_request_item_updates.php:224`) — so `u_status_task` **is**
89
+ the correct field for hold detection.
90
+ - The native **`state`** field is ServiceNow's INTERNAL, SLA-bearing state machine. Setting
91
+ `state=3` (On Hold) pauses the SNOW SLA clock but is **invisible to the DOE client**. ServiceNow
92
+ only permits the native On Hold transition **FROM In Progress (`state=2`)** — pushing `state=3`
93
+ from any other state (Assigned, or already Resolved/Closed/Canceled) is silently rejected (HTTP
94
+ 200, empty body — see the empty-body gotcha). `hold_reason=10` is not a valid choice on this
95
+ instance and is stored empty/dropped, but native On Hold still sticks without it.
96
+ - Numeric `state` codes seen on the wire: INC `On Hold`=**3**, `In Progress`=**2**;
97
+ RITM `On Hold`=**8**, the scheduled-update state=**"-14"**.
98
+
99
+ ### Outbound (TOGaDesk → ServiceNow) — INC hold push (worker #1676 + #1677)
100
+ When a DOE repair order is on hold, `send_ticket_updates.php` pushes:
101
+ - **Always set `u_status_task='On Hold'`** — the client-facing field, writable from any active
102
+ state; on release set it back to `'In Progress'`. This is what actually surfaces the hold to the
103
+ client. Until the local status stops reverting (the library `qqStatus()` fix below) the outbound
104
+ never sees `orderIsOnHold=true` and never pushes `u_status_task` at all — hence the deploy
105
+ dependency.
106
+ - **Set native `state=3` (On Hold) only when the incident is currently `In Progress`** — an SLA
107
+ pause. Because ServiceNow rejects the transition from any other state (empty-body 200), the cron
108
+ **defers without advancing `dtSynced`** when the incident is neither On Hold nor In Progress, and
109
+ re-pushes only once it reaches In Progress.
110
+ - **Closed-incident reconcile:** if the incident is already `Resolved`/`Closed`/`Canceled` on the
111
+ SNOW side it can no longer be held — the cron **advances `dtSynced`** to accept the remote
112
+ closure instead of retrying forever. (A canceled incident, INC2190846, had generated **2,392**
113
+ failed empty-body PATCHes before this.)
114
+ - The "Assigned → In Progress" auto-start block is still guarded so it never pushes `state:2` over
115
+ a held order (TRUE-79922). Named constants: `SN_STATE_IN_PROGRESS` / `SN_STATE_ON_HOLD` /
116
+ `SN_HOLD_REASON_DEFAULT` in `send_ticket_updates.php`, plus `SN_TASK_STATUS_ON_HOLD` /
117
+ `SN_TASK_STATUS_IN_PROGRESS` for the `u_status_task` writes (#1677).
118
+ - `send_request_item_updates.php` (RITM) pushes `state:"8"` (On Hold) and carries a defensive
119
+ `HOLD_*` guard on the scheduled-update path so it can't push `state:"-14"` while held; its
120
+ reverse-hold logic keys off `u_status_task == 'On Hold'`.
95
121
 
96
122
  ### Inbound (ServiceNow → TOGaDesk)
97
- - `process_tickets.php` determines local status from the native **`$data->state`** field
98
- (NOT `u_status_task`): after the `u_status_task` switch — before the completion-preserve
99
- step and the `statusRank` downgrade-guard — if native `state` is `On Hold`
100
- (case-insensitive) it sets `$newStatus = STATUS_HOLD`. Both inbound "Assigned-force"
101
- `patchINC(state:2)` blocks are guarded against a local hold.
123
+ - `process_tickets.php` reads the client-facing **`u_status_task`** field
124
+ (`$status = $data->u_status_task`, L206) — **NOT** native `state` — and maps `On Hold` to the
125
+ bare **`STATUS_HOLD`** on the local repair order (this is exactly why the library `qqStatus()`
126
+ fix below had to recognize bare `HOLD`). Both inbound "Assigned-force" `patchINC(state:2)` blocks
127
+ are guarded against a local hold.
102
128
 
103
129
  ### RITM "ETA-in-hold"
104
130
  - `send_request_item_updates.php` syncs `scheduled_date` (ETA) to ServiceNow **inside the
@@ -182,9 +208,19 @@ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets
182
208
  auto-start fought the hold push — `state:3` then `state:2` pushed seconds apart, every run,
183
209
  so SNOW never stayed On Hold. Any future auto-start/auto-status block on these crons MUST
184
210
  short-circuit when the local RO is in a `HOLD_*` status.
185
- - **The wrong-field read (root cause #2 of TRUE-79922):** reading `u_status_task` instead of
186
- native `state` meant a hold set on the SNOW side never reached TOGaDesk. Hold detection is
187
- on native `state` only.
211
+ - **⚠ SUPERSEDED (see Change history 2026-07-20):** TRUE-79922 originally concluded that reading
212
+ `u_status_task` (instead of native `state`) was a bug and that hold detection should be on native
213
+ `state` only. That was **backwards.** `u_status_task` is the client-facing authoritative field
214
+ and **is** the correct hold-detection field — the live inbound cron reads it at
215
+ `process_tickets.php:206`. Native `state` is the internal SLA state, pushed only as a secondary
216
+ pause when the incident is In Progress.
217
+ - **The empty-body 200 (nycd3 `data/tasks` PATCH):** `App_Api_NYCDOEV2::patchINC`/`patchRITM`
218
+ (both hit `/api/nycd3/data/tasks/{number}`) return **HTTP 200 with an EMPTY body (no `result`
219
+ object)** when a requested native `state` transition is illegal — e.g. pushing `state=3` from a
220
+ non-In-Progress state. This is silent: no exception, no error field. A canceled incident
221
+ (INC2190846) generated **2,392** such failed PATCHes before the closed-incident reconcile was
222
+ added. Gate native `state=3` on `snState == 'In Progress'`, and reconcile
223
+ Resolved/Closed/Canceled by advancing `dtSynced`.
188
224
  - Holds are **never auto-released** — if a SNOW user manually changes status, the outbound
189
225
  cron correctly re-asserts On Hold; that is intended, not a bug. Clear the hold in TOGaDesk.
190
226
  - **Diagnosing which writer dropped a hold:** `repair_order_history`
@@ -217,6 +253,25 @@ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets
217
253
  `repair_orders.status` directly cannot distinguish the six hold variants after a recompute.
218
254
 
219
255
  ## Change history
256
+ - 2026-07-20 — **Corrected the authoritative-field model and shipped the INC hold push.**
257
+ Controlled test PATCHes proved the earlier "native `state` is authoritative; never use
258
+ `u_status_task`" conclusion (TRUE-79922) **backwards**: the custom **`u_status_task`** is the
259
+ client-facing authoritative incident status (a `{u_status_task:'On Hold'}` PATCH persisted 3+
260
+ days, INC2192842), the inbound cron reads it (`process_tickets.php:206`), and the RITM cron keys
261
+ off it (L224). Native `state` is the internal SLA state and only accepts On Hold **from In
262
+ Progress** — any other transition returns a silent HTTP-200 empty body (a canceled incident,
263
+ INC2190846, had racked up 2,392 failed PATCHes). Fix: outbound `send_ticket_updates.php` now
264
+ always writes `u_status_task='On Hold'` (client-facing; `'In Progress'` on release), sets native
265
+ `state=3` only when In Progress (SLA pause), and reconciles already-closed incidents by advancing
266
+ `dtSynced` instead of retrying forever; added `SN_STATE_*` / `SN_HOLD_REASON_DEFAULT` /
267
+ `SN_TASK_STATUS_*` constants. worker PRs **#1676** (merged — retry-storm/closed reconcile + native
268
+ state gate) and **#1677** (open — `u_status_task` write). (sking)
269
+ - 2026-07-20 — library **#845**: `App_Model_TogaDesk_RepairOrder::qqStatus()` now recognizes all
270
+ six `HOLD_*` constants in both "on hold" IN-clauses. The inbound sync writes SNOW On Hold as the
271
+ bare `STATUS_HOLD` ('HOLD'), which the prior 2-of-6 match dropped, silently recomputing the order
272
+ to `ORDER_ASSIGNED_AWAITING_SCHEDULING` (the local "hold revert"). Additive only. Must ship with/
273
+ before worker #1677 — until the local status stops reverting, the outbound never sees
274
+ `orderIsOnHold=true`. (sking)
220
275
  - 2026-07-07 — TRUE-80060 (togadesk portion): closed the **third** DOE hold-failure path — a
221
276
  hold set via a note/comment (`Repair::addNotes`) never reaching SNOW. `addNotes` recorded
222
277
  status only on `repair_order_notes.newstatus` (never the `repair_orders.status` cache the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.397",
3
+ "version": "1.0.399",
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",