toga-ai 1.0.742 → 1.0.744

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.
@@ -6,7 +6,7 @@ project: Library
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-02
10
10
  owners: [jcardinal, mhammontree, bala]
11
11
  files:
12
12
  - library/app/api/toga2.php
@@ -413,6 +413,17 @@ enable flags** and an optional `$monitorTogadeskDepartmentIds[]`:
413
413
 
414
414
  ## Change history
415
415
 
416
+ - 2026-09-02 — `syncPurchaseOrderFromNetsuite`'s cleanup DELETE ("remove dropped items") was a **bare
417
+ throwing** DELETE that froze the whole PURCHASE_ORDERS section whenever a NetSuite-dropped PO line
418
+ still had a RESTRICT child (item receipt `ItemReceiptItems.purchaseOrderItemId`, or an SO/TO link).
419
+ Fixed to **unlink first, then delete**: delete the four PO bridge routes, unhook item receipts with
420
+ `PUT /item-receipt-items/{uuid}` `['purchaseOrderItem' => null]`, then DELETE the PO line kept
421
+ **throwing** as the fail-loud backstop — mirroring the SALES_ORDERS cleanup. Written + reviewed,
422
+ **not yet committed/deployed**; carries a schema deploy dependency (nullable
423
+ `ItemReceiptItems.purchaseOrderItemId`). Full detail, the mirror-NetSuite decision, and the api2
424
+ bridge-route-key mechanic (kebab→camel, no pluralization) live on
425
+ [NetSuite → TOGa Supply per-client sync](../../worker/features/netsuite-togasupply-per-client-sync.md)
426
+ (2026-09-02). (jcardinal)
416
427
  - 2026-09-01 — Recorded an options-DSL gotcha: a `where` with a **single** condition still
417
428
  needs the `'and' =>` (or `'or' =>`) wrapper — a bare list of conditions makes api2 return
418
429
  **HTTP 500 `EO-1` with an empty `transactionId`**, no usable message. Most often hit by
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-02
10
10
  owners: ["dfranks", "bala", "jcardinal", "mhammontree", "snaredla"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/common_sync_togasupply.php
@@ -596,6 +596,53 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
596
596
  `NETSUITE_LAST_SYNC_DATETIME_*` frozen watermark), then read `Logs.Issue` — not `Logs_<Client>.Api`
597
597
  — for the throwing order.
598
598
 
599
+ - **⚠ The PURCHASE_ORDERS cleanup DELETE froze the whole section when NetSuite drops a PO line that
600
+ still has an item receipt or SO/TO link (fixed 2026-09-02, `library` only — WRITTEN + REVIEWED,
601
+ NOT yet committed/deployed).** Same freeze class as the SALES_ORDERS renumber-DELETE above, on a
602
+ different path. `syncPurchaseOrderFromNetsuite`'s cleanup loop ("remove remaining items if
603
+ updating", `library/app/api/toga2.php`) deleted a leftover Toga PO line with a **bare, throwing**
604
+ `send()` DELETE `/purchase-order-items/{uuid}`. When the dropped line still had an item receipt
605
+ (`ItemReceiptItems.purchaseOrderItemId`, `ON DELETE RESTRICT`) or a sales-order link, the DELETE
606
+ returned MySQL **1451** → api2 **EV-11** 400 → `send()` threw → the whole PURCHASE_ORDERS section
607
+ aborted **before** advancing its watermark. Mode stuck at
608
+ `NETSUITE_EXECUTION_MODE_PURCHASE_ORDERS='1-RUNNING'`, `NETSUITE_LAST_SYNC_DATETIME_PURCHASE_ORDERS`
609
+ frozen (Elite: 2025-10-13), window shrank to 1s, the same poison PO re-tried every 5 min. Confirmed
610
+ in prod for Elite.
611
+ - **Fix (mirrors the accepted SALES_ORDERS cleanup pattern in the same file):** before deleting a
612
+ NetSuite-dropped PO line — (a) delete its link/bridge rows via api2 for all **four** bridge routes
613
+ (`/purchase-order-item-sales-order-items`, `/sales-order-item-purchase-order-items`,
614
+ `/purchase-order-items-transfer-order-items`, `/transfer-order-items-purchase-order-items`) —
615
+ removes the **relationship only**, keeps the SO/TO line; (b) unhook item-receipt lines with
616
+ `PUT /item-receipt-items/{uuid}` `['purchaseOrderItem' => null]` — keeps the receipt record, nulls
617
+ its link; (c) DELETE the PO line, **kept THROWING** as the fail-loud backstop so any *uncleared*
618
+ RESTRICT child (`BillItems`, `AdvanceShippingNoticeItems`) fails loud instead of corrupting or
619
+ silently skipping.
620
+ - **Decision — the sync MIRRORS NetSuite (NetSuite is source of truth).** When NetSuite drops a PO
621
+ line, delete our line **and** its link/bridge records but **keep the real records on the other
622
+ side** (units, tracking numbers, the item receipt itself — only the link is removed). **Never
623
+ log-and-skip past a stuck record** — that is data damage and violates the fail-loud rule. This
624
+ session first tried a log-and-skip guard; the developer rejected it and it was reverted. Same
625
+ policy as the 2026-08-27 per-order `try/catch` removal (an application of the universal
626
+ *never catch-and-silently-ignore* coding-style rule).
627
+ - **Schema dependency (must ship first).** The receipt "unhook" only works because
628
+ `ItemReceiptItems.purchaseOrderItemId` is **nullable**, made so by
629
+ `dbchanges2/Client/2026-06-19a - TransferOrderSchemaFixes.sql` (section 7, guarded/re-runnable).
630
+ **Confirm that migration has run on every client DB before deploying the code fix**, or the PUT
631
+ `purchaseOrderItem = null` fails.
632
+ - **api2 route mechanics (verified against `api2/Component/Api/V2/V2.php`).** For these list/bridge
633
+ routes the response collection key is a **plain kebab→camel conversion of the route, no
634
+ pluralization** (`/purchase-order-item-sales-order-items` → `purchaseOrderItemSalesOrderItems`).
635
+ `PUT` accepts `['purchaseOrderItem' => null]` and writes SQL `NULL` (`_underscore` Model
636
+ `FIELD_FOREIGNKEY` preserves null). Bridge Records ids: **289/290** (SO↔PO), **329/330** (PO↔TO).
637
+ - **⚠ Tangled duplicate PO lines must fail loud — do NOT auto-move the receipt.** Elite prod has
638
+ **78** duplicate `(purchaseOrder, vendorItem)` PO line-groups; only **4** carry a receipt (the ones
639
+ that froze the cleanup). Worked example: NetSuite PO **138287** (internal 5734159) once had a
640
+ duplicate `SVC-FS-DEPLOY` line; Toga kept both (PO items 138 @line2, 139 @line3); a real NetSuite
641
+ item receipt (6239473, 38 units) landed on the removed duplicate → `Client_Elite.ItemReceiptItems`
642
+ 51 pinned line 139. In **all 4** cases **both** duplicate lines carried receipts, so a naive
643
+ "move the receipt to the surviving line" is **unsafe** — double-count / `UNIQUE(itemReceiptId,
644
+ purchaseOrderItemId)` collision. Only clean cases self-heal; tangled ones must throw for a human.
645
+
599
646
  - **⚠ Inventory-adjustment sync: two shared-engine bugs + it is still SOAP (2026-08-28).** Both fixed
600
647
  in `syncInventoryAdjustmentFromNetsuite` (`library/app/api/toga2.php`) and reachable by any client
601
648
  running inventory adjustments: (a) the stale-item prune deleted an `InventoryAdjustmentItem` before
@@ -922,6 +969,21 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
922
969
 
923
970
  ## Change history
924
971
 
972
+ - 2026-09-02 — **PURCHASE_ORDERS cleanup DELETE freeze fixed (shared, `library` only — WRITTEN +
973
+ REVIEWED, NOT yet committed/deployed).** `syncPurchaseOrderFromNetsuite`'s "remove dropped items"
974
+ cleanup deleted a NetSuite-dropped PO line with a bare throwing DELETE; a leftover item receipt
975
+ (`ItemReceiptItems.purchaseOrderItemId`, RESTRICT) or SO/TO link → 1451 → EV-11 → threw → froze the
976
+ whole PURCHASE_ORDERS section at `1-RUNNING` (Elite watermark stuck 2025-10-13, window ÷3 to 1s,
977
+ poison PO re-tried every 5 min). Fix mirrors the SALES_ORDERS cleanup: unlink all four PO bridge
978
+ routes, unhook item receipts via `PUT /item-receipt-items/{uuid}` `purchaseOrderItem = null`, then
979
+ DELETE the PO line kept **throwing** as the fail-loud backstop. **Decision:** the sync mirrors
980
+ NetSuite — remove our line + its links but keep the real records on the other side; never
981
+ log-and-skip (a log-and-skip guard was tried this session and rejected as data damage). **Deploy
982
+ dependency:** `ItemReceiptItems.purchaseOrderItemId` must be nullable
983
+ (`dbchanges2/Client/2026-06-19a - TransferOrderSchemaFixes.sql` §7) on every client DB first.
984
+ Recorded the api2 bridge-route key mechanic (plain kebab→camel, no pluralization; PUT writes SQL
985
+ NULL) and Elite's 78 duplicate PO line-groups (4 with receipts, all tangled → must fail loud).
986
+ No prod data was modified (read-only SELECTs + NetSuite reads). (jcardinal)
925
987
  - 2026-09-01 — **Customer-PO `dateOrder`/`cost` fixes + NYCHH inventory-adjustment serial-case unfreeze
926
988
  (shared, `library` only, working-tree — not yet committed/deployed).** `syncSalesOrderFromNetsuite`
927
989
  now sets the customer PO's `dateOrder` from the SO `tranDate` (was always NULL) and its PO lines'
@@ -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, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.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 |
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-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, 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, _underscore/Model/Client/RecordFieldSetting.php, _underscore/Model/Client/ClientRecordFieldSetting.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 |
@@ -6,9 +6,11 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-31
9
+ updated: 2026-09-02
10
10
  owners: [jcardinal, apeterson, tcox]
11
11
  files:
12
+ - dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql
13
+ - dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql
12
14
  - dbchanges2/Core/2026-08-31a - SalesOrderDenialDetailsSurfaceSeed.sql
13
15
  - dbchanges2/Client_Compass/2026-08-31a - SalesOrderDenialDetailsReadOnlyRules.sql
14
16
  - toga25-supply/db-migrations/PLAYBOOK.md
@@ -1089,7 +1091,141 @@ hits `Duplicate entry '251' for key 'messages.PRIMARY'`. The resolution that kee
1089
1091
  Create want. Seed an `Action` when the *behaviour* lives in the registry; seed a bare
1090
1092
  `config.valueKey` when it lives in the page.
1091
1093
 
1094
+ ## Gating a WHOLE FEATURE from the surface layer — the Talos seed (2026-09-02)
1095
+
1096
+ `Core/2026-09-02a - TalosAssistantSurfaceSeed.sql` +
1097
+ `Client_Nychh/2026-09-02a - TalosAssistantEnable.sql` put the Talos AI assistant behind the
1098
+ surface layer. The surface is `talos-assistant`, **type `DRAWER`**, **not record-bound**, with
1099
+ `appId` **inherited from the `navigation` sibling by slug** (never a literal). Five elements,
1100
+ **all seeded `isVisible = 0`** — grant-only:
1101
+
1102
+ | Role | Count | Purpose |
1103
+ |---|---|---|
1104
+ | `config.role = 'featureEnabled'`, `config.key = 'feature'` | 1 | **MASTER SWITCH.** Its `isVisible` turns the launcher, the panel and the whole AI stream on or off. |
1105
+ | `config.role = 'suggestion'` | 4 | one chip each (`pendingApproval`, `blockedApprovals`, `pendingFulfillment`, `statusCounts`): label → `labelMessageId`, the question actually sent → **`config.prompt`**, icon → `iconToken` + `iconWeightToken='solid'`, order → `sortOrder`. |
1106
+
1107
+ It replaced `DEFAULT_SUGGESTIONS` — four hardcoded **TOGa Supply** questions that lived inside
1108
+ `toga-blox`, a library five apps consume, so any host mounting the panel inherited Supply's
1109
+ prompts. FE side:
1110
+ [Talos integration](../../toga25-supply/features/talos-integration.md).
1111
+
1112
+ ### 🚨 A feature switch is a MARKER ELEMENT — never `Surfaces.isVisible`
1113
+
1114
+ The override cascade is keyed by **`surfaceElementId`** and is **never applied to a `Surfaces`
1115
+ row's own `isVisible`** (`_loadOverrides`, `Model/Core/Surface.php` ~L733–748). **Overriding the
1116
+ surface row silently does nothing.** So a whole-feature switch has to be a hidden **marker
1117
+ element** on the surface, exactly like the `sectionVisibility` and `approvalsEnabled` markers.
1118
+
1119
+ ### The prompt lives in `config.prompt`, NOT an `Actions` row
1120
+
1121
+ `RUN_TALOS` **is** a member of the `Actions.type` ENUM, but **nothing in `dbchanges2` has ever
1122
+ seeded an `Actions` row** and there is **no FE action-registry entry** for it. `config` is emitted
1123
+ **verbatim** into the resolved bundle, so a self-contained `config` needs **zero new resolution
1124
+ wiring**. Same trade `Core/2026-08-12a` made choosing `config.path` over `Actions`/`NAVIGATE`, and
1125
+ the same discriminator as the BUTTON_BAR rule above: **seed an `Action` only when the behaviour
1126
+ lives in the registry.**
1127
+
1128
+ ### ⚠ Second authoring pattern: AUTO_INCREMENT + `NOT EXISTS`, for a PURELY ADDITIVE seed
1129
+
1130
+ The reserved-literal-id house style above (2026-08-24) remains the default. **These two files
1131
+ deliberately use `AUTO_INCREMENT` ids with `NOT EXISTS` guards on natural keys instead**, and the
1132
+ discriminator is *what the file does*, not *which table it writes*:
1133
+
1134
+ | Use reserved literal ids when… | Use `AUTO_INCREMENT` + `NOT EXISTS` when… |
1135
+ |---|---|
1136
+ | the file **re-parents** elements onto known surfaces, or any `Client_*` file must inline a Core id | the file is **purely additive** and **id-agnostic** — nothing references an id it did not just create |
1137
+ | reserving is only safe after verifying `MAX(id)` against **every** environment first (the TransferOrder seeds) | there is nothing to verify, because no literal is written anywhere |
1138
+
1139
+ The closest structural analogue is the original **`navigation`** seed, which is also additive and
1140
+ also took auto-increment ids.
1141
+
1142
+ **The cost is that element ids differ per environment**, which is exactly why the client grant
1143
+ file reads them off Core at run time (Section B below) instead of inlining them.
1144
+
1145
+ ### The 3-section shape for a client grant that cannot cross clusters in prod
1146
+
1147
+ **In production Core and `Client_*` are on SEPARATE CLUSTERS**, so a client override file's
1148
+ `JOIN Core.SurfaceElements` **cannot run there**. `Client_Nychh/2026-09-02a` is therefore split
1149
+ into three labelled sections, matching the *Prod ACL procedure* in
1150
+ `toga25-supply/db-migrations/SURFACE-FEATURE-RUNBOOK.md`:
1151
+
1152
+ 1. **Section A** — the single-cluster cross-schema `JOIN` version. **Local / beta only**, marked
1153
+ in the file "do NOT run in production".
1154
+ 2. **Section B** — a **read-only `SELECT` on the CORE cluster** that prints the `surfaceId` + the
1155
+ element ids **and a paste-able `VALUES` body**.
1156
+ 3. **Section C** — the literal-id `INSERT` on the **client** cluster, still `NOT EXISTS`-guarded.
1157
+
1158
+ Rollback and verify queries are given in **both** single-cluster and prod forms.
1159
+
1160
+ ### Qualify or de-qualify a schema name on PURPOSE — the two rules pull opposite ways
1161
+
1162
+ - **A Core-only file must be DE-QUALIFIED** (`Core.Messages` → `Messages`). The Core schema is
1163
+ **not guaranteed to be named `Core`** — api2 can rename it via `[database] core_dbname` — so
1164
+ qualifying breaks exactly the environment that renamed it. A Core-only file crosses nothing and
1165
+ needs no qualification.
1166
+ - **A per-client file KEEPS its `Client_<Tenant>.` qualification** on purpose. Run against the
1167
+ wrong cluster or tenant it then fails **loudly** on a missing schema, instead of **silently
1168
+ granting the feature to another tenant**.
1169
+
1170
+ ### 🚨 `SurfaceOverrides`' unique key gives a client-wide grant NO protection
1171
+
1172
+ The index is `UNIQUE (surfaceElementId, surfaceId, personaId, roleId, languageId, attribute)`.
1173
+ **MySQL treats NULLs in a unique key as DISTINCT**, and a client-wide grant has `personaId`,
1174
+ `roleId` and `languageId` **all NULL** — so **duplicate grant rows are accepted by the index.**
1175
+
1176
+ **The `NOT EXISTS` guard is the only thing making a re-run a no-op. It must never be "simplified"
1177
+ away.**
1178
+
1179
+ ### Confirmed against the real DDL, not assumed
1180
+
1181
+ `SurfaceElements.iconToken` is **nullable**; `NEUTRAL` / `BUTTON` / `TEXT` / `LEFT` are valid enum
1182
+ members; `Surfaces.type` **`DRAWER`** exists; and the `Surfaces` UNIQUE KEY is **`(slug, appId)`**,
1183
+ not `slug` alone.
1184
+
1185
+ ### OFF is the CORRECT default here, not merely the cautious one
1186
+
1187
+ Per the developer: *"in core all clients should have it toggled off. and then we will toggle it on
1188
+ specifically for nychh in supply."* This passes test (b) of the neutral-default rule decisively —
1189
+ Talos answers questions from a tenant's **live supply-chain data through an external AI service**,
1190
+ so a tenant must **not** inherit it from a migration. The FE mirrors it and **fails closed**.
1191
+
1192
+ **NOT built: per-role gating.** The grants are **client-wide** (`roleId` NULL), so every NYCHH
1193
+ user — including devteam — gets Talos. That matches how NYCHH already grants its navigation.
1194
+
1195
+ ### Team convention — publish the next free id after any id-consuming seed
1196
+
1197
+ After a seed that consumes ids, post **"next `<Table>.id` = N"** for teammates. The number to
1198
+ publish is the **highest `MAX(id)+1` across EVERY environment**, not just local.
1199
+
1092
1200
  ## Change history
1201
+ - 2026-09-02 — Added the **Talos feature-gate seed pair** (`Core/2026-09-02a -
1202
+ TalosAssistantSurfaceSeed.sql`: surface `talos-assistant`, type **DRAWER**, not record-bound,
1203
+ `appId` inherited from the `navigation` sibling by slug, 5 elements all `isVisible = 0`;
1204
+ `Client_Nychh/2026-09-02a - TalosAssistantEnable.sql`: 5 client-wide grants). **DECIDED** four
1205
+ things worth reusing: (1) a whole-feature switch must be a hidden **marker element**, because the
1206
+ override cascade is keyed by `surfaceElementId` and is **never** applied to a `Surfaces` row's own
1207
+ `isVisible` (`_loadOverrides`, `Surface.php` ~L733–748) — overriding the surface row silently does
1208
+ nothing; (2) the chip prompt lives in **`config.prompt`**, not an `Actions` row — `RUN_TALOS` is in
1209
+ the ENUM but no `Actions` row has ever been seeded and there is no FE registry entry, while
1210
+ `config` is emitted verbatim, so it needs no new wiring; (3) a **purely additive, id-agnostic**
1211
+ seed may use **`AUTO_INCREMENT` + `NOT EXISTS`** instead of reserved literal ids — the
1212
+ discriminator is whether the file re-parents rows or a `Client_*` file must inline a Core id, and
1213
+ the analogue is the original `navigation` seed; (4) **OFF is correct, not just cautious**, for a
1214
+ feature that sends a tenant's live data to an external AI. **FIXED prod-readiness** (developer
1215
+ caught it): Core and `Client_*` are on **separate clusters** in prod, so the client file's
1216
+ `JOIN Core.SurfaceElements` cannot run there — restructured into **Section A** (single-cluster
1217
+ join, local/beta only), **Section B** (read-only `SELECT` on the Core cluster printing ids + a
1218
+ paste-able `VALUES` body), **Section C** (literal-id guarded `INSERT` on the client cluster), with
1219
+ rollback/verify in both forms. **De-qualified the Core file** (`Core.Messages` → `Messages`) because
1220
+ api2 can rename the Core schema via `[database] core_dbname`, while the per-client file **keeps**
1221
+ its `Client_Nychh.` qualification so a wrong-cluster run fails loudly rather than granting another
1222
+ tenant the feature. DISCOVERED that **`SurfaceOverrides`' unique key protects a client-wide grant
1223
+ not at all** — MySQL treats NULLs in a unique key as distinct, and a client-wide grant is
1224
+ `personaId`/`roleId`/`languageId` all NULL, so the `NOT EXISTS` guard is the *only* thing making a
1225
+ re-run idempotent. Verified against the real DDL: `iconToken` nullable, `NEUTRAL`/`BUTTON`/`TEXT`/
1226
+ `LEFT` valid, `Surfaces.type DRAWER` exists, `Surfaces` UNIQUE KEY is `(slug, appId)`. Local run
1227
+ produced `SurfaceElements` **216–220** (`navigation` = surface 39, appId 1); **beta and prod NOT
1228
+ yet run**. (apeterson)
1093
1229
  - 2026-08-31 — Added the **denial-details** seed pair (`Core/2026-08-31a -
1094
1230
  SalesOrderDenialDetailsSurfaceSeed.sql` — Surface **47**, elements **170–176**, Messages
1095
1231
  **227–232**, plus a `readOnlyRule` marker on surface 41; and `Client_Compass/2026-08-31a -
@@ -6,7 +6,7 @@ project: TOGa IQ
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-02
10
10
  owners: [akhokhani, apeterson]
11
11
  files:
12
12
  - talos/libs/aegra-api/src/aegra_api/main.py
@@ -224,11 +224,57 @@ None — uniform across clients. Per-tenant data isolation is enforced by
224
224
  `Content-Type`** — `fetch` forbids a body on GET and a JSON content-type only adds a preflight.
225
225
  - **An unregistered tenant gets a clean 403 on every call even with a valid token** (control-plane
226
226
  DB / `tenant_router` LRU). Provisioning is explicit; there is no auto-create.
227
+ ### The LIVE OpenAPI spec is PUBLIC — use it as the contract source (2026-09-02)
228
+
229
+ `https://api.togaiq.com/openapi.json` (and `/docs`) returns the **full live spec,
230
+ unauthenticated** — title `Talos 0.1.0`. It is a better contract source than any handoff
231
+ document: it is generated from the running deployment, so it cannot drift.
232
+
233
+ **Probing technique for "does this route exist?" without a token:** auth runs **before**
234
+ routing, so a real route answers **401** unauthenticated while a nonexistent route answers
235
+ **404**. Comparing the two codes proves a route exists with no credentials at all.
236
+
237
+ Confirmed this way (both are real server-side persistence in Aegra's own DB, not client state):
238
+
239
+ | Route | Notes |
240
+ |---|---|
241
+ | `PATCH /threads/{thread_id}` | *Update Thread*; body schema `ThreadUpdate` carries **only `metadata`**. `If-Match` is an **optional** header. This is how a client renames a thread. |
242
+ | `DELETE /threads/{thread_id}` | Deletes the thread. |
243
+
244
+ ### ⚠ Thread auto-titles are a PER-ASSISTANT setting, and they are off/broken today (2026-09-02)
245
+
246
+ Two facts a browser client cannot work around:
247
+
248
+ 1. **Aegra names a thread asynchronously after the first run and emits NO SSE event for it.**
249
+ The title lands in `metadata.thread_name`. A client's only way to see it is to **re-request
250
+ the thread list** — and because the name arrives *after* the run finishes, polling only
251
+ *during* the run usually misses it.
252
+ 2. **Naming is configured per assistant, not globally.** `AssistantConfigResponse` exposes
253
+ **`thread_name_generation_enabled`** (boolean, **default `true`**) and
254
+ **`thread_name_model`** (default a Bedrock Claude Haiku model) on
255
+ `GET` / `PATCH /api/assistants/{assistant_id}/config`.
256
+
257
+ **Observed:** every thread in NYCHH's list reads `"New conversation"`, including threads days
258
+ old and far outside any client poll window. So either `thread_name_generation_enabled` is
259
+ `false` on the `TOGa Supply` assistant, or the naming model is unreachable in that deployment.
260
+ **A consumer app cannot fix this** — toga25-supply reads `metadata.thread_name` correctly.
261
+ Open with `akhokhani`; NYCHH `assistant_id` `c7981164-1fe9-5878-b115-85c38dadad43`.
262
+
227
263
  - *Point-in-time note, may be stale:* on **2026-09-01** `https://api.beta.togaiq.com` returned
228
264
  **502** on `/health`, `/info` and `/assistants` (nginx up, app behind it down) after a clean 401
229
265
  ~10 minutes earlier. Re-probe `/info` before assuming a client bug.
230
266
 
231
267
  ## Change history
268
+ - 2026-09-02 — DISCOVERED that Aegra publishes a **public** OpenAPI spec at
269
+ `https://api.togaiq.com/openapi.json` (`/docs`), which is a better contract source than a handoff
270
+ doc, plus the token-free route-probe technique (auth runs before routing, so a real route 401s and
271
+ a missing one 404s). Confirmed `PATCH /threads/{thread_id}` (`ThreadUpdate`, `metadata` only,
272
+ optional `If-Match`) and `DELETE /threads/{thread_id}` exist, so rename/delete are real
273
+ server-side persistence. Recorded that **thread auto-naming is a per-assistant config**
274
+ (`thread_name_generation_enabled`, default true, + `thread_name_model` on
275
+ `GET`/`PATCH /api/assistants/{id}/config`) and is **off or broken** for NYCHH's `TOGa Supply`
276
+ assistant — every thread reads "New conversation" regardless of age — and that the name lands
277
+ **after** the run with **no SSE event**, so a client must re-poll the thread list. (apeterson)
232
278
  - 2026-09-01 — Added a **browser-client** gotcha set found while wiring toga25-supply: Aegra returns
233
279
  CORS headers **on the OPTIONS preflight only**, so a browser blocks every real response even on a
234
280
  healthy service (server-side callers never see it); `AUTH_TYPE=togahub` **accepts an api2 JWT
@@ -11,7 +11,7 @@
11
11
  | [Primary Table templates (server/client, sizing, virtualization)](features/primary-table-templates.md) | `src/templates/PrimaryTable/` is the **production, wired-up table** built on the [Table component](table.md). | toga-blox/src/components/Table/themeConfig/toga.module.css, toga25-supply/src/themeConfig.json, toga-blox/src/templates/PrimaryTable/PrimaryTable.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableServerTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableClientTemplate.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableHeaderCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableBodyCell.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableRow.tsx, toga-blox/src/templates/PrimaryTable/PrimaryTableExpandableRow.tsx, toga-blox/src/templates/PrimaryTable/types.ts |
12
12
  | [TableRecordModal (record-detail modal shell)](features/table-record-modal.md) | `TableRecordModal` is a **generic, presentational modal shell** for showing a single table record (row) in detail — typically opened from a table row click. | toga-blox/src/components/TableRecordModal/TableRecordModal.tsx, toga-blox/src/components/TableRecordModal/index.ts, toga-blox/src/components/TableRecordModal/tableRecordModal.module.css |
13
13
  | [Table component (cells, action cells, filters & sorts, hooks, theming)](features/table.md) | The `Table` component (`src/components/Table/`) is the **TanStack Table v8** building block behind the [Primary Table templates](primary-table-templates.md). | toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, toga-blox/src/components/Table/index.ts, toga-blox/src/components/Table/types.ts, toga-blox/src/components/Table/utils/buildTanstackColumns.tsx, toga-blox/src/components/Table/utils/resolveCellType.tsx, toga-blox/src/components/Table/components/cellTypes, toga-blox/src/components/Table/components/actionCells, toga-blox/src/components/Table/components/columnFiltersAndSorts, toga-blox/src/components/Table/hooks, toga-blox/src/components/Table/themeConfig |
14
- | [Talos AI-Assistant Component (launcher + docked/drawer chat panel)](features/talos-assistant.md) | `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header launcher button (`TalosLauncher`) plus a chat panel (`TalosPanel`) tha | toga-blox/src/components/Talos/TalosLauncher.tsx, toga-blox/src/components/Talos/TalosPanel.tsx, toga-blox/src/components/Talos/TalosMessage.tsx, toga-blox/src/components/Talos/types.ts, toga-blox/src/components/Talos/theme.ts, toga-blox/src/components/Talos/helpers.tsx, toga-blox/src/components/Talos/stub.ts, toga-blox/src/components/Talos/Talos.module.css, toga-blox/src/components/Talos/index.ts, toga-blox/src/components/Talos/TalosPanel.test.tsx, toga-blox/src/components/index.ts |
14
+ | [Talos AI-Assistant Component (launcher + docked/drawer chat panel)](features/talos-assistant.md) | `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header launcher button (`TalosLauncher`) plus a chat panel (`TalosPanel`) tha | toga-blox/src/components/Talos/TalosLauncher.tsx, toga-blox/src/components/Talos/TalosPanel.tsx, toga-blox/src/components/Talos/TalosMessage.tsx, toga-blox/src/components/Talos/types.ts, toga-blox/src/components/Talos/theme.ts, toga-blox/src/components/Talos/helpers.tsx, toga-blox/src/components/Talos/stub.ts, toga-blox/src/components/Talos/Talos.module.css, toga-blox/src/components/Talos/index.ts, toga-blox/src/components/Talos/TalosPanel.test.tsx, toga-blox/src/components/index.ts, toga-blox/src/components/Talos/README.md |
15
15
  | [Toaster — host-token styling contract](features/toaster.md) | The blox `Toaster` is a CSS-Module component whose every visual property reads a `--toaster-*` custom property supplied by the **host app**. | toga-blox/src/components/Toaster/Toaster.module.css, toga-blox/src/components/Toaster/Toaster.tsx, toga-blox/src/components/Toaster/ToasterContext.tsx, toga2-commerce/src/styles/index.css, toga2-commerce/src/components/Toasters/ToasterList.tsx |
16
16
  | [Dynamic npm Publish Pipeline (branch → channel)](workflows/dynamic-publish-pipeline.md) | How `@agilant/toga-blox` (checkout folder `toga-blox-npm`, registry repo key `toga-blox`) publishes a per-environment npm **channel** (dist-tag) from a `_<mode> | toga-blox/.github/workflows/publish.yml, toga-blox/package.json, toga-blox/src/utils/getFontAwesomeIcon.tsx |
17
17
  | [Landing a Large Long-Lived Feature Branch (merge, never rebase)](workflows/landing-a-large-feature-branch.md) | The procedure for landing a long-lived, very large feature branch (months old, hundreds of commits) into `_production` in `toga-blox-npm`. | toga-blox/package.json, toga-blox/.github/workflows/publish.yml |
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-02
10
10
  owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga-blox/src/components/Talos/TalosLauncher.tsx
@@ -20,6 +20,7 @@ files:
20
20
  - toga-blox/src/components/Talos/index.ts
21
21
  - toga-blox/src/components/Talos/TalosPanel.test.tsx
22
22
  - toga-blox/src/components/index.ts
23
+ - toga-blox/src/components/Talos/README.md
23
24
  related:
24
25
  - ../architecture.md
25
26
  - ../workflows/dynamic-publish-pipeline.md
@@ -211,10 +212,56 @@ so moving `messages` to the host is the same pattern, not a new one. And `connec
211
212
  panel a real "we never got a token to begin with" state — previously a thrown `onSend` was its only
212
213
  failure path.
213
214
 
215
+ ### The loading bubble is CHROME, not conversation (fixed 2026-09-02)
216
+
217
+ The in-flight loading bubble was never a message. The panel **synthesizes** it from its own
218
+ `isStreaming` / `progress` props and appends it to its internal `msgs` array. When the
219
+ `renderMessages` host-render slot landed, its branch **replaced that whole array** — so a
220
+ controlled host saw **zero feedback between submit and answer**.
221
+
222
+ **Fix: the panel renders the loading bubble in BOTH modes.** A host taking over the message
223
+ list must not be able to lose it. Two regression tests pin this.
224
+
225
+ > **General rule for any future host-render slot:** anything the panel *synthesizes* rather than
226
+ > receives (loading state, connection notices, empty/error notes) has to live **outside** the
227
+ > host-replaceable branch. A slot that replaces a list also replaces everything the panel quietly
228
+ > mixed into that list.
229
+
230
+ ### History drawer + new chat — DATA-DRIVEN, works in controlled mode (2026-09-02)
231
+
232
+ The drawer UI already existed, but it was bound to the panel's **internal** conversation list and
233
+ therefore force-disabled whenever the panel was controlled. It is now host-fed:
234
+
235
+ ```ts
236
+ threads?: TalosThreadSummary[]; // { id, title, updated }
237
+ activeThreadId?: string | null;
238
+ onSelectThread?: (id: string) => void;
239
+ onNewChat?: () => void;
240
+ threadsStatus?: "loading" | "error" | "ready";
241
+ ```
242
+
243
+ - **`features.history` re-enables only when `threads` AND `onSelectThread` are BOTH supplied.**
244
+ `features.newChat` re-enables only when `onNewChat` is. A half-wired host gets **no button**
245
+ rather than a dead one — this replaces the blanket "controlled ⇒ history/newChat off" rule.
246
+ - **The panel sorts rows newest-first**, so a host may pass any order.
247
+ - **All three drawer notes (loading / error / empty) are gated on `histRows.length === 0`.**
248
+ Before, the error copy had no empty-list guard and rendered **alongside a full, usable list**
249
+ — the user saw "Couldn't load your chats." on top of their chats. Notes that describe the
250
+ *absence* of rows must be mutually exclusive with rows.
251
+
252
+ ### Suggestion chips carry NO default content any more (2026-09-02)
253
+
254
+ `DEFAULT_SUGGESTIONS` — four hardcoded **TOGa Supply** questions — was **removed**. Tenant
255
+ business copy must not sit inside a library five apps consume: any host that mounted the panel
256
+ silently inherited Supply's prompts. `suggestions` now defaults to an **empty module-level
257
+ constant**, and the host supplies the chips (supply reads them from the surface layer — see
258
+ [Talos integration](../../toga25-supply/features/talos-integration.md)).
259
+
214
260
  ### Tests (first ever for this component)
215
261
 
216
- `TalosPanel.test.tsx` — **8 Vitest + RTL tests**, all passing, including **two that pin
217
- uncontrolled behaviour as unchanged**. (Separate from the ~40 pre-existing unrelated suite failures.)
262
+ `TalosPanel.test.tsx` — **31 Vitest + RTL Talos tests** as of 2026-09-02 (8 at inception), all
263
+ passing, including **two that pin uncontrolled behaviour as unchanged** and two that pin the
264
+ loading bubble surviving `renderMessages`. (Separate from the ~40 pre-existing unrelated suite failures.)
218
265
 
219
266
  **The stream itself does NOT belong in blox.** Per
220
267
  [frontend.md §21](../../../standards/frontend.md) (promote only after a 2nd consumer validates the
@@ -234,6 +281,12 @@ Rollup *"failed to resolve import"* for a component they never referenced). blox
234
281
  { role: "user", text } // ✅ user turn
235
282
  { role: "talos", message } // ✅ assistant turn
236
283
  ```
284
+ - **⚠ The old rule "controlled mode forces `history`/`newChat` OFF" is SUPERSEDED (2026-09-02).**
285
+ They are off only until the host supplies `threads` + `onSelectThread` / `onNewChat`. Do not
286
+ code around the old behaviour.
287
+ - **A `renderMessages`-style slot can silently delete panel-synthesized UI.** The loading bubble
288
+ is not in `messages`; it was lost exactly this way. Check what the panel mixes into a list
289
+ before replacing that list.
237
290
  - **`onSend` cannot stream** — use **controlled mode** (above). A live streaming backend needs the
238
291
  controlled props, not a cleverer adapter.
239
292
  - **Tool calls and HITL interrupts still have no representation** in `TalosMessageModel`. A host
@@ -257,12 +310,37 @@ Rollup *"failed to resolve import"* for a component they never referenced). blox
257
310
  - **Hosts can import the Talos subpath to avoid the barrel's CSS.** `@agilant/toga-blox/dist/components/Talos/index.js`
258
311
  resolves (package `exports` allows `./dist/*`); importing the package barrel pulls **every** blox
259
312
  component's CSS into the consumer bundle.
313
+ - **⚠ `dist/` must be REBUILT for a blox change to reach any browser — and a green consumer
314
+ typecheck does NOT prove it did.** The consumer's `tsc` resolves blox **types from source**
315
+ through the `node_modules` symlink, but Vite loads the package at **runtime from `dist/`**. So
316
+ `tsc --noEmit` can be perfectly clean while the browser runs stale blox code. Every blox change
317
+ needs `npm run build` in the blox repo **plus** `rm -rf node_modules/.vite` in the consumer. See
318
+ [Local-linking blox into a consumer app §4](../workflows/local-link-into-a-consumer-app.md).
319
+ **Correction:** an earlier note claimed blox `npm run build` fails on a missing
320
+ `tsconfig.app.json` — it does not; it builds fine.
260
321
  - blox `dist` is gitignored (shipped via publish, not the branch). Other consumers only get
261
322
  Talos after blox is **published** per the `_sandbox-dev` dist-tag workflow — see
262
323
  [Dynamic npm Publish Pipeline](../workflows/dynamic-publish-pipeline.md). toga25-supply
263
324
  currently consumes it via a local symlink.
264
325
 
265
326
  ## Change history
327
+ - 2026-09-02 — **FIXED the `renderMessages` slot silently killing the in-flight loading UI**: the
328
+ bubble is synthesized from `isStreaming`/`progress` into the panel's internal `msgs` array, and the
329
+ host-render branch replaced that whole array, so a run showed **no feedback between submit and
330
+ answer**. The bubble now renders in **both** modes — it is chrome, not conversation. **BUILT a
331
+ data-driven history drawer + new chat that work in controlled mode**: new `threads`
332
+ (`TalosThreadSummary[]`), `activeThreadId`, `onSelectThread`, `onNewChat`, `threadsStatus` props;
333
+ `features.history` re-enables only when `threads` **and** `onSelectThread` are both passed and
334
+ `features.newChat` only when `onNewChat` is, so a half-wired host gets no button instead of a dead
335
+ one (this **supersedes** the blanket "controlled forces history/newChat off" rule); rows are sorted
336
+ newest-first inside the panel. **FIXED the drawer error note rendering alongside a full list** —
337
+ loading/error/empty are all now gated on `histRows.length === 0`. **REMOVED `DEFAULT_SUGGESTIONS`**
338
+ (four hardcoded TOGa Supply questions inside a shared library — every host inherited them);
339
+ `suggestions` now defaults to an empty constant and the host supplies chips. Suite is at **31 Talos
340
+ tests**. DISCOVERED that `dist/` must be rebuilt for any blox change to reach a browser and that a
341
+ clean consumer `tsc` does not prove it (types resolve from source, Vite loads `dist/`); this also
342
+ CORRECTS the earlier note that blox `npm run build` fails on a missing `tsconfig.app.json`.
343
+ (apeterson)
266
344
  - 2026-09-01 — **BUILT controlled mode** (the TRUE-80692 contract change, previously only decided):
267
345
  optional `messages` / `isStreaming` / `onSubmit` / `onStop` / `connection` on `TalosPanelProps`
268
346
  plus a new exported `TalosConnection` type. Controlled mode keys off **`messages` being passed at
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-02
10
10
  owners: [tcox, apeterson]
11
11
  files:
12
12
  - toga-blox/package.json
@@ -207,6 +207,28 @@ config never reaches a real consumer. Restore the name when you unlink.
207
207
 
208
208
  The consumer reads blox's `dist`, so rebuild blox after every change.
209
209
 
210
+ ```
211
+ # in the blox checkout
212
+ npm run build
213
+ # in the consumer
214
+ rm -rf node_modules/.vite
215
+ ```
216
+
217
+ **⚠ A green consumer typecheck does NOT prove the consumer has your change (2026-09-02).** The
218
+ two toolchains read **different copies of blox**:
219
+
220
+ | Tool | Where it reads blox from |
221
+ |---|---|
222
+ | the consumer's `tsc --noEmit -p tsconfig.app.json` | blox **source**, through the `node_modules` symlink |
223
+ | Vite (dev server and build) | the blox package's **`dist/`** |
224
+
225
+ So `tsc` can be completely clean while the browser runs **stale** blox code — the symptom is "my
226
+ change does nothing" with no error anywhere. Both commands above are required after **every** blox
227
+ edit; clearing `node_modules/.vite` matters because Vite's pre-bundle cache holds the old `dist`.
228
+
229
+ **Correction:** an earlier note said blox `npm run build` fails on a missing `tsconfig.app.json`.
230
+ **It does not** — the build works.
231
+
210
232
  ## Gotchas
211
233
 
212
234
  - **Confirm the link actually exists before you trust it — docs and memory go stale.**
@@ -236,6 +258,11 @@ The consumer reads blox's `dist`, so rebuild blox after every change.
236
258
  drags every component's CSS into the consumer bundle.
237
259
 
238
260
  ## Change history
261
+ - 2026-09-02 — Expanded step 4: the consumer's `tsc` resolves blox **types from source** through the
262
+ symlink while Vite loads the package from **`dist/`**, so a clean `tsc --noEmit` does **not** prove
263
+ the browser has your change. Every blox edit needs `npm run build` in blox **plus**
264
+ `rm -rf node_modules/.vite` in the consumer. CORRECTED the earlier claim that blox `npm run build`
265
+ fails on a missing `tsconfig.app.json` — it builds fine. (apeterson)
239
266
  - 2026-09-01 — Recorded that **any `npm install` in the consumer silently replaces the blox symlink
240
267
  with the registry version** (toga25-supply → `1.1.1-production.139`, which has no Talos), and that
241
268
  the symptom is a misleading `does not provide an export named 'TalosLauncher'`. Added the re-link
@@ -13,7 +13,7 @@
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
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
- | [Talos Integration (AppLayout host, live Aegra streaming, LangGraph→blox mapper)](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/api/talos.ts, toga25-supply/src/hooks/useTalosSession.ts, toga25-supply/src/providers/TalosStreamProvider.tsx, toga25-supply/src/utils/talosMessages.ts, toga25-supply/src/utils/talosMessages.test.ts, 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 |
16
+ | [Talos Integration (AppLayout host, live Aegra streaming, LangGraph→blox mapper)](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/api/talos.ts, toga25-supply/src/hooks/useTalosSession.ts, toga25-supply/src/hooks/useTalosThreads.ts, toga25-supply/src/hooks/useTalosThreads.test.ts, toga25-supply/src/hooks/useTalosSurface.ts, toga25-supply/src/hooks/useTalosSurface.test.ts, toga25-supply/src/App.tsx, toga25-supply/db-migrations/PLAYBOOK.md, dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, toga25-supply/src/providers/TalosStreamProvider.tsx, toga25-supply/src/utils/talosMessages.ts, toga25-supply/src/utils/talosMessages.test.ts, 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/usePurchaseOrderItemRows.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTargetLocationOptions.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useTransferSourceLocation.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useLocationContacts.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateLocationContact.ts, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/AddContactModal.tsx, toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/view/TransferNotesField.tsx, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, 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 |
19
19
  | [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 |
@@ -725,6 +725,20 @@ here is a cross-screen change. Re-check Sales Orders, Items and Vendor Items aft
725
725
  builds the bundle fresh every request (see
726
726
  [surface-resolver](../../_underscore/features/surface-resolver.md); the old advice to clear
727
727
  `~/toga/cache/surface_meta/*.json` is retracted). The FE persisted cache is the only one.
728
+
729
+ **✅ FIXED 2026-09-02 — the surface bundle is no longer persisted to disk.** `App.tsx` had **no
730
+ `dehydrateOptions`** at all, so the persister wrote *every* query — bundle included — to
731
+ `localStorage` and it expired only at the persister's **24h default `maxAge`**. Combined with
732
+ `staleTime: Infinity` that meant a grant, **or worse a REVOKE**, could take up to a day to reach
733
+ an existing browser. Unacceptable once a surface element gates an AI with access to tenant data.
734
+ A `shouldDehydrateQuery` filter now **excludes the `surface-meta` and `surface-meta-group`
735
+ namespaces** (and the `talos` namespace, which holds session data). Cost: **one request per page
736
+ load** — the in-memory cache still covers route changes within a page. The `staleTime: Infinity`
737
+ on the hooks is unchanged and still correct; disk persistence was the wrong half.
738
+
739
+ **Rule that generalizes:** a `PersistQueryClientProvider` with no `shouldDehydrateQuery` filter
740
+ persists **everything**. Any query that carries permissions, feature gates or session identity
741
+ must be excluded explicitly — never assume a reload re-reads it.
728
742
  - **The single-meta envelope can hide the real bundle behind a DECOY `elements: []`.** The V2 envelope
729
743
  can arrive as `{ surfaces: { meta: <real-bundle> }, elements: [] }` — a decoy empty `elements: []`
730
744
  sits at the top level with **no `surface` descriptor**. A `looksLikeBundle` check matching on
@@ -900,6 +914,15 @@ button except **View Log** renders and does nothing on click. Whether the SR mod
900
914
  actions surface is **still an open decision**, not an oversight.
901
915
 
902
916
  ## Change history
917
+ - 2026-09-02 — **FIXED the surface bundle being cached in `localStorage` for up to 24h, so a
918
+ permission or feature change could not take effect.** `App.tsx`'s `PersistQueryClientProvider` had
919
+ **no `dehydrateOptions`**, so every query — including the `staleTime: Infinity` surface bundle —
920
+ was written to disk and expired only at the persister's 24h default `maxAge`; a grant or a
921
+ **revoke** could take a day to reach a running browser. Added a `shouldDehydrateQuery` filter
922
+ excluding `surface-meta` / `surface-meta-group` (and `talos`, which is session data). Costs one
923
+ request per page load; the in-memory cache still covers route changes. Prompted by Talos becoming
924
+ a surface-gated feature — see
925
+ [Talos integration](./talos-integration.md). (apeterson)
903
926
  - 2026-09-02 — Corrected the `cleanOrder.ts` note-slug gotcha: it flattens by the **real** slugs
904
927
  `request` / `shipping` (fixed in `ce47dbd`), so the "latent blank-notes bug" recorded earlier no
905
928
  longer exists. Added where "Reason for Request" actually lives (`SalesOrderNotes` +
@@ -6,11 +6,19 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-02
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/src/api/talos.ts
13
13
  - toga25-supply/src/hooks/useTalosSession.ts
14
+ - toga25-supply/src/hooks/useTalosThreads.ts
15
+ - toga25-supply/src/hooks/useTalosThreads.test.ts
16
+ - toga25-supply/src/hooks/useTalosSurface.ts
17
+ - toga25-supply/src/hooks/useTalosSurface.test.ts
18
+ - toga25-supply/src/App.tsx
19
+ - toga25-supply/db-migrations/PLAYBOOK.md
20
+ - dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql
21
+ - dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql
14
22
  - toga25-supply/src/providers/TalosStreamProvider.tsx
15
23
  - toga25-supply/src/utils/talosMessages.ts
16
24
  - toga25-supply/src/utils/talosMessages.test.ts
@@ -21,6 +29,9 @@ files:
21
29
  - toga25-supply/src/assets/talos-owl.png
22
30
  related:
23
31
  - ../../toga-blox/features/talos-assistant.md
32
+ - ./surface-frontend.md
33
+ - ../../dbchanges2/features/surface-layer-schema.md
34
+ - ../../../clients/nychh/features/talos-assistant-enablement.md
24
35
  - ../../toga-blox/workflows/local-link-into-a-consumer-app.md
25
36
  - ../../talos/features/chat-frontend.md
26
37
  - ../../talos/features/aegra-api.md
@@ -30,8 +41,11 @@ related:
30
41
  ## Summary
31
42
 
32
43
  toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md).
33
- **Enabled for ALL clients (no per-client gating).** `AppLayout` is the host: the launcher renders
34
- in the header, the panel at app root.
44
+ `AppLayout` is the host: the launcher renders in the header, the panel at app root.
45
+
46
+ **⚠ Corrected 2026-09-02 — Talos is NO LONGER enabled for all clients.** It is now gated by the
47
+ **surface layer**, seeded **OFF for every client**, and opted in per tenant. **NYCHH is the only
48
+ tenant granted it today.** The suggestion chips come from the same surface, not from blox.
35
49
 
36
50
  As of **2026-09-01 the live Aegra/LangGraph streaming backend is BUILT** (TRUE-80693). `onSend` is
37
51
  no longer a stub — the panel runs in blox's new **controlled mode** and the stream lives in a
@@ -41,7 +55,9 @@ supply-owned provider. All LangChain code is **app-local**; blox stays pure UI.
41
55
 
42
56
  ```
43
57
  src/api/talos.ts — sole VITE_TALOS_API read chokepoint + assistants lookup
58
+ src/hooks/useTalosSurface.ts — DB feature gate + suggestion chips (surface bundle)
44
59
  src/hooks/useTalosSession.ts — resolves token + assistantId → a `connection` state
60
+ src/hooks/useTalosThreads.ts — Aegra thread list → blox TalosThreadSummary[] + title polling
45
61
  src/providers/TalosStreamProvider.tsx — the single useStream() call, exposed via own context
46
62
  src/utils/talosMessages.ts — LangGraph Message[] → blox TalosMessageModel[]
47
63
  src/layout/AppLayout/AppLayout.tsx — host: provider wrapper + controlled panel props
@@ -120,6 +136,85 @@ navigation side effects (`onOpenRecord` → `/sales-orders?sales-orders=<id>`, `
120
136
  **Maps the app-only `"disabled"` state to `"error"`** when passing `connection` down — blox has no
121
137
  `"disabled"` value.
122
138
 
139
+ ### `useTalosSurface()` — the DB feature gate (2026-09-02)
140
+
141
+ Reads the Core surface `talos-assistant` (type `DRAWER`, not record-bound) and returns
142
+ **whether Talos is on for this tenant** plus the suggestion chips. Two element roles in the
143
+ bundle:
144
+
145
+ | `config.role` | Meaning |
146
+ |---|---|
147
+ | `featureEnabled` (one element, `config.key = 'feature'`) | **master switch** — its `isVisible` turns the launcher, the panel and the Aegra stream on or off |
148
+ | `suggestion` (four elements) | one chip each: `labelMessageId` → label, **`config.prompt`** → the question actually sent, `iconToken`/`iconWeightToken` → icon, `sortOrder` → order |
149
+
150
+ Chip keys: `pendingApproval`, `blockedApprovals`, `pendingFulfillment`, `statusCounts`.
151
+
152
+ **The FE FAILS CLOSED — deliberately, not defensively.** Missing bundle, failed fetch, absent
153
+ marker element, or *still loading* all resolve to **disabled**. Talos answers questions from a
154
+ tenant's live supply-chain data through an external AI service, so an unresolved gate must never
155
+ read as "on".
156
+
157
+ `TalosStreamProvider` gained an **`enabled`** prop off the back of this: a tenant without Talos
158
+ never even calls `GET /assistants`.
159
+
160
+ Seeding side, the marker-element choice, and the prod-cluster procedure:
161
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
162
+
163
+ ### `useTalosThreads()` — the history drawer's data (2026-09-02)
164
+
165
+ Maps Aegra `AegraThread` rows → blox `TalosThreadSummary[]` (`{ id, title, updated }`); the
166
+ provider exposes `threads` / `threadsStatus` / `activeThreadId` / `selectThread` and `AppLayout`
167
+ passes them straight into the panel's new data-driven drawer props.
168
+
169
+ **Refresh fires on two edges, and both are needed:**
170
+
171
+ 1. **On `threadId` assignment.** The server creates the thread at the **START** of the first run,
172
+ so without this the *currently active* chat is missing from its own drawer.
173
+ 2. **On the falling edge of a run** (streaming → idle).
174
+
175
+ #### Opening an existing thread needs NO transcript parser
176
+
177
+ `@langchain/langgraph-sdk`'s **`setThreadId`** (`dist/ui/orchestrator.js`,
178
+ `#fetchHistoryForThread`) **clears the stream and fetches that thread's history itself** when
179
+ `fetchStateHistory: true`. So selecting a history row is just "set the threadId", and the
180
+ existing LangGraph→blox mapper handles the rest.
181
+
182
+ This took `fromTalosTranscript()` **off the critical path for history**. It is still needed for
183
+ the durable message ids that feedback requires — do not delete it.
184
+
185
+ #### Auto-title polling — `titlePollInterval`
186
+
187
+ Aegra names a thread **asynchronously after the first run** and emits **no SSE event**, so the
188
+ only way to see a title is to ask again. `titlePollInterval` feeds React Query's
189
+ `refetchInterval`: **4s while any RECENT thread is unnamed, `false` otherwise.**
190
+
191
+ **Two deliberate deviations from the handoff doc §8.10:**
192
+
193
+ - The doc says poll *"while the first run is active"*. **The title lands AFTER the run**, so
194
+ polling only during it usually misses it entirely.
195
+ - Added a **2-minute age window** the doc does not specify. A thread whose first message was
196
+ never text-bearing **never gets named at all**, so "stop when the title arrives" on its own
197
+ polls every 4s for the whole session.
198
+
199
+ 7 tests cover the stop condition, including a server clock running ahead of the client.
200
+
201
+ > Server-side, auto-naming is currently **off or broken** for NYCHH's assistant, so this polling
202
+ > has nothing to find yet — see
203
+ > [aegra-api](../../talos/features/aegra-api.md). Supply reads `metadata.thread_name` correctly
204
+ > and cannot fix it.
205
+
206
+ ### Gotcha — React Query sets `isError` on a FAILED BACKGROUND REFETCH while KEEPING the data
207
+
208
+ This printed **"Couldn't load your chats." over a full, usable chat list.** The status line was
209
+ `isError ? "error" : …`, so **one** failed 4s title poll flipped a good list into an error state.
210
+
211
+ **Fix:** a pure `threadsStatusFrom` helper that returns `"error"` **only when there is no data to
212
+ show**. Pair it with the blox-side fix (drawer notes gated on an empty row list) — both halves
213
+ were needed.
214
+
215
+ Any hook with a `refetchInterval` inherits this trap: `isError` is not "the data is unusable",
216
+ it is "the last fetch failed".
217
+
123
218
  ### `talosMessages.ts` — the LangGraph → blox mapper
124
219
 
125
220
  - Handles LangGraph's **string-OR-content-blocks** union for `content`.
@@ -161,6 +256,15 @@ registry version, which contains no Talos. The symptom looks nothing like a depe
161
256
  Full symptom + relink recipe:
162
257
  [Local-linking blox into a consumer app §1a](../../toga-blox/workflows/local-link-into-a-consumer-app.md).
163
258
 
259
+ ## The `talos` query namespace is EXCLUDED from the persisted cache (2026-09-02)
260
+
261
+ `App.tsx`'s `PersistQueryClientProvider` used to persist **every** query to `localStorage`. A
262
+ `shouldDehydrateQuery` filter now excludes the **`talos`** namespace alongside `surface-meta` /
263
+ `surface-meta-group`: chat titles and assistant ids are **user/session data that must not outlive
264
+ the session on disk**. The surface half of the same fix is what makes a Talos grant (or a
265
+ **revoke**) actually reach a running browser — see
266
+ [surface-frontend](./surface-frontend.md).
267
+
164
268
  ## Environment config
165
269
 
166
270
  `VITE_TALOS_API` is set to `https://api.beta.togaiq.com` in **7 non-prod env files**. It is left
@@ -206,6 +310,27 @@ user resend. Decide deliberately rather than discovering it in QA.
206
310
  passes the token it already has straight into `defaultHeaders`.
207
311
 
208
312
  ## Change history
313
+ - 2026-09-02 — **Talos is now SURFACE-GATED, and OFF for every client by default** (this
314
+ **corrects** the old "enabled for ALL clients, no per-client gating"): new `useTalosSurface()`
315
+ reads the Core `talos-assistant` surface for a `config.role='featureEnabled'` **marker element**
316
+ (master switch) plus four `config.role='suggestion'` chips whose text lives in `config.prompt` —
317
+ replacing blox's hardcoded `DEFAULT_SUGGESTIONS`. The FE **fails closed**: missing bundle, failed
318
+ fetch, absent marker, or still-loading all resolve to disabled, and `TalosStreamProvider` gained
319
+ an `enabled` prop so an ungranted tenant never calls `/assistants`. **NYCHH is the only granted
320
+ tenant.** **BUILT the history drawer + new chat** (`useTalosThreads` maps Aegra threads → blox
321
+ summaries; refresh on `threadId` assignment — the server creates the thread at the **start** of
322
+ the first run, so the active chat was otherwise missing from its own drawer — and on the falling
323
+ edge of a run). DISCOVERED that opening a thread needs **no transcript parser**: the SDK's
324
+ `setThreadId` clears the stream and fetches that thread's history itself when
325
+ `fetchStateHistory: true`, so `fromTalosTranscript()` leaves the history critical path (still
326
+ needed for durable message ids). **BUILT auto-title polling** (`titlePollInterval` → 4s while any
327
+ thread <2min old is unnamed) with two deliberate deviations from the handoff doc §8.10 — the title
328
+ lands **after** the run, not during it, and an unnamed-forever thread needs the age window to stop
329
+ the poll. **FIXED "Couldn't load your chats." printing over a full list**: React Query sets
330
+ `isError` on a failed **background** refetch while keeping the data, so one bad 4s poll flipped a
331
+ good list into error; new pure `threadsStatusFrom` returns `"error"` only when there is nothing to
332
+ show. Excluded the **`talos`** query namespace from the `localStorage`-persisted cache (chat
333
+ titles + assistant ids are session data). (apeterson)
209
334
  - 2026-09-01 — **BUILT the live streaming integration.** Added `src/api/talos.ts` (env chokepoint +
210
335
  runtime `GET /assistants` lookup — `assistantId` is **per-tenant**, never pinned in env),
211
336
  `useTalosSession` (gates on `isAuthenticated`, not on a token, because blox leaves a **public**
@@ -31,7 +31,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
31
31
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
32
32
  - **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
33
33
  - **toga2-commerce** (TOGa Commerce) — 20 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
34
- - **toga25-supply** (TOGa 2.5 Supply) — 17 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
34
+ - **toga25-supply** (TOGa 2.5 Supply) — 18 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
35
35
  - **toga-blox** (TOGa Blox) — 14 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
36
36
  - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
37
37
 
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: elite
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-01
9
+ updated: 2026-09-02
10
10
  owners: ["snaredla", "jcardinal"]
11
11
  files:
12
12
  - worker/crons/toga2/netsuite/sync_togasupply_elite.php
@@ -125,6 +125,21 @@ the trait declares must exist as a column, a `CustomRecordFields` row **and** an
125
125
 
126
126
  ## Change history
127
127
 
128
+ - 2026-09-02 — **Elite PURCHASE_ORDERS froze on a dropped PO line that still had a receipt** (shared
129
+ fix, `library` only — WRITTEN + REVIEWED, NOT yet committed/deployed). Elite was the confirmed prod
130
+ case: `NETSUITE_EXECUTION_MODE_PURCHASE_ORDERS='1-RUNNING'`, watermark frozen at **2025-10-13**,
131
+ window collapsed to 1s, same poison PO re-tried every 5 min. Root cause is shared, not Elite —
132
+ `syncPurchaseOrderFromNetsuite`'s cleanup DELETE threw on a leftover item receipt / SO link. Elite
133
+ data behind it: **78 duplicate `(purchaseOrder, vendorItem)` PO line-groups**, only **4** carry a
134
+ receipt — the 4 that froze the cleanup. Worked example: NetSuite PO **138287** (internal 5734159)
135
+ kept a duplicate `SVC-FS-DEPLOY` line (PO items 138 @line2, 139 @line3); a real receipt
136
+ (6239473, 38 units) landed on the removed duplicate → `Client_Elite.ItemReceiptItems` 51 pinned line
137
+ 139. In all 4 both duplicate lines carried receipts, so a naive "move the receipt" is unsafe
138
+ (double-count / `UNIQUE(itemReceiptId, purchaseOrderItemId)` collision) — tangled cases fail loud
139
+ for a human, only clean cases self-heal. **Before deploy:** confirm
140
+ `ItemReceiptItems.purchaseOrderItemId` is nullable on `Client_Elite`
141
+ (`dbchanges2/Client/2026-06-19a - TransferOrderSchemaFixes.sql` §7). Full mechanics on the
142
+ [engine doc](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md). (jcardinal)
128
143
  - 2026-09-01 — **Drop-ship / special-order POs were never importing for Elite** (e.g. PO tranid
129
144
  128238). Root cause was in shared code, not Elite: `listPurchaseOrders()` filtered the PO header by
130
145
  the VENDOR `entity` (TD Synnex 20202) instead of the customer, so Elite's 36443-scoped filter matched
@@ -5,5 +5,6 @@
5
5
  | [NYCHH inventory-adjustment → item-fulfillment linking (direct FK columns, REST)](features/netsuite-inventory-adjustment-fulfillment-link.md) | 1.0 | For NYCHH stock adjustments, the NetSuite **inventory adjustment** carries a link to the **Item Fulfillment** it corrects, on the multi-select body custom field | library/app/api/toga2.php, library/app/api/netsuite/rest.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/diagnose_hh_import_inventory_adjustment.php |
6
6
  | [NYCHH NetSuite → TransferOrders import (stocking-flag routing, PO bridges, origin location)](features/netsuite-transfer-order-import.md) | 1.0 | NYCHH's NetSuite **transfer orders are represented as NetSuite sales orders** (same as the GroWrk pattern), so the shared NetSuite → TOGa Supply importer (`App_ | library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/netsuite.php, worker/crons/toga2/netsuite/sync_togasupply_hh.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/diagnose_hh_stuck_transfer_order.php, dbchanges2/Core/2026-08-27a - TransferOrderUuidSearchableIdentifier.sql, dbchanges2/Client/2026-08-28a - TransferOrderItemsTimestampColumns.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestampColumnsReposition.sql, dbchanges2/Client/2026-08-29 - ItemReceiptTransferOrderIdFieldPermission.sql, dbchanges2/Client/2026-08-29b - ItemReceiptItemTransferOrderItemIdFieldPermission.sql |
7
7
  | [NYCHH PO links are UPSTREAM — the downstream SO→PO route returns empty](features/po-number-upstream-direction.md) | 2.0 | NYCHH's sales orders are created **from the customer's purchase order**, so their SO↔PO links live in the **upstream** table `PurchaseOrders_SalesOrders` (route | _underscore/Model/Client/SalesOrder.php, _underscore/Trait/Netsuite/SalesOrder.php, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/hooks/usePurchaseOrderDetails.ts, dbchanges2/Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql, dbchanges2/Client/2026-08-25 - SalesOrderPurchaseOrdersFieldAllClients.sql |
8
+ | [NYCHH Talos AI Assistant Enablement (the only granted tenant)](features/talos-assistant-enablement.md) | 2.0 | **NYCHH is the only tenant granted the Talos AI assistant in TOGa 2.5 Supply.** The feature is seeded **OFF in Core for every client** and turned on per tenant | dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql, toga25-supply/src/hooks/useTalosSurface.ts, toga25-supply/src/providers/TalosStreamProvider.tsx, toga25-supply/db-migrations/PLAYBOOK.md |
8
9
  | [NYCHH transfer-order 2.0 model — inventory quantities + V2 field enablement](features/transfer-order-inventory-quantities.md) | 2.0 | The 2.0 (`_underscore`) side of NYCHH transfer-order support: a **two-branch inventory quantity model** on the NYCHH `Item` override, plus the **client-override | _underscore/Model/Nychh/Item.php, _underscore/Model/Nychh/PurchaseOrderItem.php, _underscore/Model/Nychh/TransferOrder.php, _underscore/Model/Nychh/TransferOrderStage.php, _underscore/Model/Client/TransferOrder.php, _underscore/Model/Client/TransferOrderItem.php, dbchanges2/Client_Nychh/2026-08-27a - TransferOrderCustomFieldWritePermission.sql, dbchanges2/Client_Nychh/2026-08-27b - TransferOrderStageStatusIdentifier.sql, dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, _underscore/Model/Client/PurchaseOrderItem.php |
9
- | [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.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, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql |
10
+ | [NYC Health & Hospitals](profile.md) | 2.0 | NYC Health & Hospitals (NYCHH) is a TOGA 2.0 client on the `_underscore` platform, prod schema `Client_Nychh`. | dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.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, dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql, dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql, dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql |
@@ -0,0 +1,93 @@
1
+ ---
2
+ title: NYCHH Talos AI Assistant Enablement (the only granted tenant)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: nychh
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-09-02
10
+ owners: [apeterson]
11
+ files:
12
+ - dbchanges2/Core/2026-09-02a - TalosAssistantSurfaceSeed.sql
13
+ - dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql
14
+ - toga25-supply/src/hooks/useTalosSurface.ts
15
+ - toga25-supply/src/providers/TalosStreamProvider.tsx
16
+ - toga25-supply/db-migrations/PLAYBOOK.md
17
+ related:
18
+ - ../../../2.0/apps/toga25-supply/features/talos-integration.md
19
+ - ../../../2.0/apps/toga-blox/features/talos-assistant.md
20
+ - ../../../2.0/apps/dbchanges2/features/surface-layer-schema.md
21
+ - ../../../2.0/apps/talos/features/aegra-api.md
22
+ - ../profile.md
23
+ ---
24
+
25
+ ## Summary
26
+
27
+ **NYCHH is the only tenant granted the Talos AI assistant in TOGa 2.5 Supply.** The feature is
28
+ seeded **OFF in Core for every client** and turned on per tenant with `SurfaceOverrides` rows on
29
+ the `talos-assistant` surface. Developer instruction, verbatim: *"in core all clients should have
30
+ it toggled off. and then we will toggle it on specifically for nychh in supply."*
31
+
32
+ Everything shared — the surface, the elements, the blox panel, the supply provider — is
33
+ tenant-neutral. Only the five grant rows are NYCHH's.
34
+
35
+ ## How it works
36
+
37
+ 1. **Core** (`Core/2026-09-02a`) seeds surface `talos-assistant` (type `DRAWER`, not
38
+ record-bound) with 5 elements, all `isVisible = 0`: one `config.role='featureEnabled'` marker
39
+ (the master switch) and four `config.role='suggestion'` chips.
40
+ 2. **NYCHH** (`Client_Nychh/2026-09-02a`) inserts **5 `SurfaceOverrides` rows**, all
41
+ `IS_VISIBLE = '1'`, one per element.
42
+ 3. The supply FE reads the marker via `useTalosSurface()` and **fails closed** — no bundle, failed
43
+ fetch, missing marker, or still-loading all mean disabled. `TalosStreamProvider`'s `enabled`
44
+ prop means an ungranted tenant never even calls Aegra's `/assistants`.
45
+
46
+ Mechanism and seeding rules:
47
+ [surface-layer-schema](../../../2.0/apps/dbchanges2/features/surface-layer-schema.md).
48
+ FE wiring: [Talos integration](../../../2.0/apps/toga25-supply/features/talos-integration.md).
49
+
50
+ ## The grants are CLIENT-WIDE — no role scoping
51
+
52
+ `roleId` / `personaId` / `languageId` are **NULL**, so **every NYCHH user, including internal
53
+ devteam accounts, gets Talos.** That was accepted deliberately: it matches how this tenant already
54
+ grants navigation (NYCHH does not scope navigation by role). **Per-role gating was NOT built** — if
55
+ it is ever needed, the grants have to be widened/narrowed at the *same* tier (see the shadowing rule
56
+ in the schema doc).
57
+
58
+ ## Aegra side
59
+
60
+ - **`assistant_id` `c7981164-1fe9-5878-b115-85c38dadad43`** — name `TOGa Supply`,
61
+ `graph_id: deep_agent`, `read_only: true`, `customer_facing: true`. NYCHH returns exactly **one**
62
+ assistant. It is still **discovered at runtime**, never pinned in env.
63
+ - 🚨 **Thread auto-titles are broken/disabled SERVER-side for this assistant.** Every thread in
64
+ NYCHH's list reads `"New conversation"`, including threads days old. Naming is a per-assistant
65
+ setting (`thread_name_generation_enabled` / `thread_name_model` on
66
+ `GET`/`PATCH /api/assistants/{id}/config`), so either it is `false` on `TOGa Supply` or the naming
67
+ model is unreachable in that deployment. **Supply cannot fix this** — it reads
68
+ `metadata.thread_name` correctly. Open with `akhokhani`; details in
69
+ [aegra-api](../../../2.0/apps/talos/features/aegra-api.md).
70
+
71
+ ## Run state — 🚨 beta and prod are NOT done
72
+
73
+ Verified on **local only** (2026-09-02, developer ran the SQL):
74
+
75
+ - `navigation` surface = **id 39**, `appId` **1**.
76
+ - Talos seed produced `SurfaceElements` **216–220** (feature / pendingApproval / blockedApprovals /
77
+ pendingFulfillment / statusCounts), `sortOrder` 1 / 10 / 20 / 30 / 40, all `isVisible = 0`, all
78
+ labels non-NULL.
79
+ - `Client_Nychh` grants: **5 rows, all `IS_VISIBLE = '1'` on 216–220**.
80
+ - **Beta and production: NOT YET RUN.**
81
+
82
+ **Element ids are per-environment** (`AUTO_INCREMENT`), so local / sandbox / prod will differ. Do
83
+ **not** copy 216–220 into a prod run — that is precisely why the client grant file's Section B reads
84
+ the ids off the Core cluster first.
85
+
86
+ ## Change history
87
+ - 2026-09-02 — First record. NYCHH became the **only** tenant granted Talos: Core seeds the
88
+ `talos-assistant` surface OFF for all clients, `Client_Nychh/2026-09-02a` adds the 5 client-wide
89
+ `IS_VISIBLE=1` grants (`roleId` NULL — every NYCHH user incl. devteam; per-role gating not built).
90
+ Verified on **local only** — surface elements **216–220**, `navigation` surface 39/appId 1; **beta
91
+ and prod not yet run**, and element ids are per-environment so prod must read them off Core.
92
+ Recorded the NYCHH `assistant_id` and that thread auto-titles are broken **server-side** on that
93
+ assistant, not in supply. (apeterson)
@@ -17,7 +17,7 @@ project: _Underscore
17
17
  client: nychh
18
18
  type: profile
19
19
  status: active
20
- updated: 2026-09-01
20
+ updated: 2026-09-02
21
21
  owners: ["jcardinal", "apeterson", "bala", "akhokhani"]
22
22
  files:
23
23
  - dbchanges2/Client_Nychh/2026-08-31a - PurchaseOrdersTableViewRebase.sql
@@ -28,6 +28,7 @@ files:
28
28
  - dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql
29
29
  - dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql
30
30
  - dbchanges2/Client/2026-09-01a - PurchaseOrderItemQtyFieldsApiRoleRead.sql
31
+ - dbchanges2/Client_Nychh/2026-09-02a - TalosAssistantEnable.sql
31
32
  related:
32
33
  - ../../2.0/apps/_underscore/features/tracking-number-bridges.md
33
34
  - ../../2.0/apps/_underscore/features/tableview-joins.md
@@ -35,6 +36,7 @@ related:
35
36
  - ./features/netsuite-transfer-order-import.md
36
37
  - ./features/transfer-order-inventory-quantities.md
37
38
  - ./features/netsuite-inventory-adjustment-fulfillment-link.md
39
+ - ./features/talos-assistant-enablement.md
38
40
  - ../../2.0/apps/_underscore/features/sales-order-purchase-order-bridge-direction.md
39
41
  - ../../2.0/apps/toga25-supply/features/transfer-orders-page.md
40
42
  ---
@@ -64,6 +66,15 @@ table views. Client-specific DB change-sets live in `dbchanges2/Client_Nychh/`.
64
66
  `AclLogicGroups` → `AclLogicGroupExpressions`, reusing record 17's `all` expression) and reads
65
67
  `AclFieldPermissions` for the displayed fields, resolving `roleId` by role **name**. Written
66
68
  idempotent / uuid-keyed; applied to **prod and client-sandbox**.
69
+ - **NYCHH is the ONLY tenant granted the Talos AI assistant (2026-09-02).** Talos is seeded **off
70
+ in Core for every client** and enabled per tenant with 5 `SurfaceOverrides` rows on the
71
+ `talos-assistant` surface; `Client_Nychh/2026-09-02a - TalosAssistantEnable.sql` is the only grant
72
+ file. The grants are **client-wide** (`roleId` NULL), so every NYCHH user including devteam gets
73
+ it — matching how this tenant already grants navigation. **Applied on LOCAL only; beta and prod
74
+ are not run**, and the element ids are per-environment. Their Aegra `assistant_id` is
75
+ `c7981164-1fe9-5878-b115-85c38dadad43` (`TOGa Supply`), and thread auto-titles are broken
76
+ **server-side** on that assistant. See
77
+ [NYCHH Talos enablement](./features/talos-assistant-enablement.md).
67
78
  - **`inventory_units` table view carries item detail as of 2026-08-18** — Part Number, Manufacturer
68
79
  and Item Description sit directly after Asset Tag, reached through a **chained** Units → Items →
69
80
  Manufacturers join (`dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql`, identical
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.742",
3
+ "version": "1.0.744",
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",