toga-ai 1.0.626 → 1.0.628

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.
@@ -10,4 +10,5 @@
10
10
  | [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/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 |
11
11
  | [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 |
12
12
  | [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/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 |
13
+ | [Talos AI-Assistant Component (launcher + slide-out 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 slide-out chat panel (`TalosP | 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/index.ts |
13
14
  | [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 |
@@ -0,0 +1,86 @@
1
+ ---
2
+ title: Talos AI-Assistant Component (launcher + slide-out chat panel)
3
+ framework: "2.0"
4
+ repo: toga-blox
5
+ project: TOGa Blox
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-20
10
+ owners: [apeterson]
11
+ files:
12
+ - toga-blox/src/components/Talos/TalosLauncher.tsx
13
+ - toga-blox/src/components/Talos/TalosPanel.tsx
14
+ - toga-blox/src/components/Talos/TalosMessage.tsx
15
+ - toga-blox/src/components/Talos/types.ts
16
+ - toga-blox/src/components/Talos/theme.ts
17
+ - toga-blox/src/components/Talos/helpers.tsx
18
+ - toga-blox/src/components/Talos/stub.ts
19
+ - toga-blox/src/components/Talos/Talos.module.css
20
+ - toga-blox/src/components/Talos/index.ts
21
+ - toga-blox/src/components/index.ts
22
+ related:
23
+ - ../architecture.md
24
+ - ../workflows/dynamic-publish-pipeline.md
25
+ - ../../toga25-supply/features/talos-integration.md
26
+ ---
27
+
28
+ ## Summary
29
+
30
+ `Talos` is a **shared, pure-UI** AI-assistant component in `@agilant/toga-blox`: a header
31
+ launcher button (`TalosLauncher`) plus a slide-out chat panel (`TalosPanel`) with two
32
+ presentations — **docked** (resizable right rail) and **expanded** (centered modal + scrim).
33
+ Ported from a TOGa Desk prototype and intended for reuse across toga25-supply, toga2-desk,
34
+ and toga2-commerce. Build once, retheme per app. (TRUE-80692)
35
+
36
+ The defining constraint: the component holds **no business logic and no `window.*` globals** —
37
+ every side effect flows through an injected adapter, and every visual token is overridable.
38
+ The real chat backend is a **later ticket**; `onSend` currently ships stubbed.
39
+
40
+ ## Architecture (the four load-bearing decisions)
41
+
42
+ - **Pure UI + injected adapter.** All side effects go through an injected `TalosAdapter`:
43
+ `onSend(text) => Promise<TalosResponse>`, `onOpenRecord(id)`, `onAction(key)`,
44
+ `onDockWidthChange(px)`, `onAttach()`. The host owns everything. `createTalosStubAdapter()`
45
+ ships for backend-free/demo use (stubbed `onSend` this ticket).
46
+ - **Controlled state.** `open` / `expanded` / `width` are controlled props owned by the host
47
+ (`onOpenChange` / `onExpandedChange` / `onWidthChange`); the component never manages them
48
+ internally.
49
+ - **Fully tokenized styling.** Every color/font-size/weight/line-height/radius/shadow/dimension
50
+ is a `var(--talos-*, default)` custom property. `Talos.module.css` **is** the default theme
51
+ (the TOGa pink look). No forced Google-Fonts `@import` inside the shared component — the host
52
+ loads the font; default family is "Plus Jakarta Sans".
53
+ - **Theme override model.** A typed `TalosTheme` object (`theme.ts`) + `talosThemeToVars()`
54
+ converter is passed as an optional `theme` prop on `TalosLauncher`/`TalosPanel`; it emits
55
+ `--talos-*` inline overrides on the root elements. Unset keys fall through to defaults.
56
+ Because the CSS is all `var()` fallbacks, **any** `--talos-*` token is also overridable via
57
+ host CSS on an ancestor — not just the typed subset.
58
+
59
+ Reuses blox `BaseToolTip` + `getFontAwesomeIcon` (icon names are camelCase FA names).
60
+
61
+ ## Theming ceiling (known design boundary)
62
+
63
+ **Themeable today:** full palette, font, dock/expanded dimensions, launcher size, radii,
64
+ shadows, orbit/blob/gradient/scrim decoration, launcher icon image (via `branding.avatarSrc`),
65
+ and content props (suggestions / greeting / subtitle / placeholder / disclaimer / statusColors /
66
+ branding name+initials).
67
+
68
+ **NOT themeable yet:** internal **spacing/density** — ~38 padding/gap/margin values remain raw
69
+ literals, so apps can recolor and narrow the panel but cannot make it denser/compact (relevant
70
+ for a small TOGa Desk side chat). Genuinely different **layouts** (e.g. pill launcher vs icon
71
+ button, dropping expanded mode) are variants, not theme overrides.
72
+
73
+ **Proposed future work (NOT done):** a spacing-token pass and/or a `density` preset prop
74
+ (`"comfortable" | "compact"`), and possibly a launcher `variant` + optional panel-mode props.
75
+ Open questions were handed to the UX/UI team.
76
+
77
+ ## Gotchas
78
+
79
+ - The real chat backend is a later ticket — `onSend` is stubbed; do not assume live responses.
80
+ - blox `dist` is gitignored (shipped via publish, not the branch). Other consumers only get
81
+ Talos after blox is **published** per the `_sandbox-dev` dist-tag workflow — see
82
+ [Dynamic npm Publish Pipeline](../workflows/dynamic-publish-pipeline.md). toga25-supply
83
+ currently consumes it via a local symlink.
84
+
85
+ ## Change history
86
+ - 2026-08-20 — Built Talos shared AI-assistant component (launcher + docked/expanded chat panel): pure-UI + injected `TalosAdapter`, controlled open/expanded/width, fully `--talos-*` tokenized styling with typed `TalosTheme` overrides, stub adapter. Two adversarial review passes hardened it (removed a `window.dispatchEvent("resize")` side channel and a hardcoded initials literal; moved resize into an effect with cleanup + rAF throttle + keyboard `role="separator"`; SSR-safe portal guard; memoized message/handlers; restored the attach button + 11px card radius). Documented the spacing/density theming ceiling as future work (apeterson).
@@ -11,5 +11,6 @@
11
11
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
12
12
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx, toga25-supply/src/layout/PrimaryTableServerLayout/types.ts, toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts, toga25-supply/src/layout/RecordApprovalModal/helpers/handleFormatApprovalWorkflowPayload.ts, toga25-supply/src/layout/RecordApprovalModal/api/approvalDecisionsApi.ts |
13
13
  | [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/applyColSpan.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/sectionRenderers.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getVisibleSections.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderApprovalSummaryGrid.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/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 |
14
+ | [Talos Integration (AppLayout host + adapter wiring)](features/talos-integration.md) | toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md). | toga25-supply/src/layout/AppLayout/AppLayout.tsx, toga25-supply/src/components/Header/Header.tsx, toga25-supply/src/components/Header/Header.module.css, toga25-supply/src/index.css, toga25-supply/src/assets/talos-owl.png |
14
15
  | [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json |
15
16
  | [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 |
@@ -0,0 +1,52 @@
1
+ ---
2
+ title: Talos Integration (AppLayout host + adapter wiring)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-20
10
+ owners: [apeterson]
11
+ files:
12
+ - toga25-supply/src/layout/AppLayout/AppLayout.tsx
13
+ - toga25-supply/src/components/Header/Header.tsx
14
+ - toga25-supply/src/components/Header/Header.module.css
15
+ - toga25-supply/src/index.css
16
+ - toga25-supply/src/assets/talos-owl.png
17
+ related:
18
+ - ../../toga-blox/features/talos-assistant.md
19
+ ---
20
+
21
+ ## Summary
22
+
23
+ toga25-supply is the first host of the shared blox [Talos assistant](../../toga-blox/features/talos-assistant.md).
24
+ **Enabled for ALL clients (no per-client gating).** `AppLayout` is the host: it owns Talos
25
+ state and injects the adapter; the launcher renders in the header, the panel at app root.
26
+ (TRUE-80693)
27
+
28
+ ## How it works
29
+
30
+ - **AppLayout owns the state** — `open` / `expanded` / `width` / `dockWidth` — and injects the
31
+ adapter:
32
+ - `onSend`: stub (real backend is a later blox ticket).
33
+ - `onOpenRecord(id)` → navigate `/sales-orders?sales-orders=<id>`.
34
+ - `onAction("approvals")` → navigate `/sales-orders`.
35
+ - `onDockWidthChange(px)` → reserve layout width for the docked panel.
36
+ - **Rendering** — `<TalosLauncher>` renders in the header via a new presentational
37
+ `talosLauncher` slot prop on the blox `Header` component, laid out through a `.headerActions`
38
+ flex wrapper. `<TalosPanel>` renders at app root.
39
+ - **Branding/font** — the owl asset (`src/assets/talos-owl.png`) is passed as
40
+ `branding.avatarSrc`; Plus Jakarta Sans is loaded in `src/index.css` (the shared component
41
+ does not import fonts itself).
42
+
43
+ ## Gotcha — reserve dock width on the OUTER wrapper, not just `<main>`
44
+
45
+ The docked panel is `position: fixed`, full-height on the right. The blox `Header` is in
46
+ **normal document flow** (NOT fixed). Reserving the dock width only on `<main>` leaves the
47
+ header's right edge sitting **under** the panel, hiding the launcher + avatar. **Fix:** apply
48
+ the reserved dock width (`marginRight` = the `onDockWidthChange` value) to the **outer app
49
+ wrapper** so the header shifts along with the content.
50
+
51
+ ## Change history
52
+ - 2026-08-20 — Wired the shared blox Talos assistant into supply for all clients: AppLayout hosts state + injects the adapter (stub `onSend`; `onOpenRecord`/`onAction` navigate to sales orders; `onDockWidthChange` reserves layout width), launcher in a new header `talosLauncher` slot, panel at app root, owl avatar + Plus Jakarta Sans loaded locally. Fixed the docked panel hiding the header launcher by reserving dock width on the outer wrapper (apeterson).
@@ -35,7 +35,7 @@
35
35
  | [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
36
36
  | [NYCHH Asset-Tag Backfill (worker2)](features/nychh-asset-tag-backfill.md) | Keeps NYC Health & Hospitals (`Client_Nychh`) unit asset tags and MAC addresses synced from NetSuite. | worker2/Worker/Client/Nychh.php, worker2/Worker/Client/Nychh/AssetTagBackfill.php, worker/crons/toga2/netsuite/verify_fulfillment_asset_tag_sync_nychh.php, worker/crons/toga2/netsuite/backfill_all_asset_tags_from_netsuite_nychh.php |
37
37
  | [OneUptime Incident → ClickUp Task Sync (Monitor/Oneuptime/SyncIncidents)](features/oneuptime-incident-clickup-sync.md) | An internal/shared TOGA ops feature: worker2 polls the OneUptime API every 15 minutes for **currently-open** incidents and ensures a ClickUp task exists for eac | worker2/Component/Api/Oneuptime/Oneuptime.php, worker2/Worker/Monitor/Oneuptime.php, worker2/Config/production.ini, dbchanges2/Team/2026-08-10a - OneUptime Incident ClickUp Tasks.sql, dbchanges2/Core/2026-08-10b - OneUptime Incident Sync CronJob.sql, dbchanges2/Team/2026-08-12a - OneUptime Incident Tasks Closed Marker.sql |
38
- | [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, worker2/Worker/Client/Compass.php, worker2/composer.json, _underscore/Cloud.php, worker2/Worker/Monitor/Operations.php, worker2/Worker/Monitor/Aig.php, worker2/Worker/Monitor/Prudential.php, worker2/Worker/Monitor/Nycdoe.php, worker2/Config/production.ini, worker2/Config/beta.ini, worker2/Config/sandbox-dev.ini |
38
+ | [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, worker2/Worker/Client/Compass.php, worker2/composer.json, _underscore/Cloud.php, worker2/Worker/Monitor/Operations.php, worker2/Worker/Monitor/Aig.php, worker2/Worker/Monitor/Prudential.php, worker2/Worker/Monitor/Nycdoe.php, worker2/Worker/Monitor/Staples.php, worker2/Config/production.ini, worker2/Config/beta.ini, worker2/Config/sandbox-dev.ini |
39
39
  | [Platform Cache Cleanup (_Worker_Platform_Cache — Clean + Truncate)](features/platform-cache-cleanup.md) | `_Worker_Platform_Cache` owns maintenance of the shared **Cache** cluster that backs api2's [multi-client data retrieval](../../api2/features/cross-client-data- | worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
40
40
  | [Service Request → Sales Order → Purchase Order generation (Sync/ServiceRequest)](features/service-request-sales-order-generation.md) | `_Worker_Sync_ServiceRequest` turns a **Service Request into a Sales Order, and then into one Purchase Order per vendor**, for **any** tenant. | worker2/Worker/Sync/ServiceRequest.php, _underscore/Model/Elite/ServiceRequest.php, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql |
41
41
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
@@ -6,8 +6,8 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-17
10
- owners: ["jcardinal", "mhammontree"]
9
+ updated: 2026-08-20
10
+ owners: ["jcardinal", "mhammontree", "bala"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Compass.php
13
13
  - worker2/Worker/Client/Compass.php
@@ -17,6 +17,7 @@ files:
17
17
  - worker2/Worker/Monitor/Aig.php
18
18
  - worker2/Worker/Monitor/Prudential.php
19
19
  - worker2/Worker/Monitor/Nycdoe.php
20
+ - worker2/Worker/Monitor/Staples.php
20
21
  - worker2/Config/production.ini
21
22
  - worker2/Config/beta.ini
22
23
  - worker2/Config/sandbox-dev.ini
@@ -26,6 +27,7 @@ related:
26
27
  - ./all-client-email-queue-monitor.md
27
28
  - ../../_underscore/features/cloud-s3-helpers.md
28
29
  - ../../../1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md
30
+ - ../../../clients/staples/features/oneuptime-sftp-folder-monitors.md
29
31
  ---
30
32
 
31
33
  ## Summary
@@ -255,6 +257,38 @@ relies on it) — no new Entra consent.
255
257
  worker EB tier. The `client_secret` is a credential — it lives in the ini config group,
256
258
  never in a doc.
257
259
 
260
+ ## SFTP monitoring (phpseclib3)
261
+
262
+ An SFTP monitor logs into an SFTP with **phpseclib3**, `rawlist`s a folder, counts files
263
+ older than an age cutoff, and pushes a decided alarm token — the same reporter model as the
264
+ S3/DB monitors. A login/list failure pushes `status:"error"`, so the monitor doubles as an
265
+ SFTP connectivity check. First instance:
266
+ [Staples SFTP folder monitors](../../../clients/staples/features/oneuptime-sftp-folder-monitors.md).
267
+
268
+ Two runtime facts about the installed phpseclib3 (v3.0.49) that bite silently — verified at
269
+ runtime against the live SFTP:
270
+
271
+ - **`SFTP::rawlist($dir)` (non-recursive) returns each entry as an associative ARRAY**, not
272
+ a stdClass object — keys `'type'`, `'mtime'`, `'filename'`, `'size'`, … Object access
273
+ (`$entry->mtime`) silently fails. Use array access: `$entry['mtime']`, `$entry['type']`.
274
+ - **The SFTP file-type constants are NOT defined at class-load.** Neither the class const
275
+ `SFTP::TYPE_DIRECTORY` nor the global `NET_SFTP_TYPE_DIRECTORY` exists when you just
276
+ load/use the class (phpseclib defines the globals lazily); `defined()` returned false for
277
+ both. Compare `type` against the SFTP-protocol numeric value via a **self-defined
278
+ constant**: `1` = regular file, `2` = directory. (`rawlist` returned php-type=array,
279
+ `type=1` for files, and `mtime` as a unix epoch matching the filename timestamp.)
280
+
281
+ **Symptom when either is wrong:** reads guarded by `isset()`/`??` silently make the aged-file
282
+ count always **0**, so the monitor never alarms and reads as perfectly healthy. Test the
283
+ count against a folder you know has aged files.
284
+
285
+ ### `_Config::<group>('key')` throws on a missing key — pass a falsy fallback
286
+
287
+ `_Config::<group>('key')` **throws** when the key is absent, so a `?:` default after it is
288
+ dead code. To get a safe fallback, pass a **falsy second argument**:
289
+ `_Config::sftp_staples('port', false) ?: 22`. Without the second arg the `?:` never runs
290
+ because the throw happens first.
291
+
258
292
  ## Composer / deploy dependency
259
293
 
260
294
  `worker2/composer.json` added `javanile/php-imap2` (`^0.1.10`) for the IMAP path. The
@@ -293,6 +327,13 @@ check for another client.
293
327
  monitors must pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
294
328
 
295
329
  ## Change history
330
+ - 2026-08-20 — Added the **SFTP monitor** shape (phpseclib3 `rawlist` + age-cutoff count +
331
+ decided token; login/list failure → `status:error` connectivity check) and its two
332
+ runtime gotchas: `rawlist` returns **arrays** not objects, and the SFTP type constants are
333
+ **not defined at class-load** (use a self-defined `1`=file/`2`=dir). Also recorded that
334
+ `_Config::<group>('key')` **throws** on a missing key — pass a falsy 2nd arg for a `?:`
335
+ fallback. First instance:
336
+ [Staples SFTP folder monitors](../../../clients/staples/features/oneuptime-sftp-folder-monitors.md). (bala)
296
337
  - 2026-08-17 — TRUE-80587: added five monitors and generalized this doc beyond Compass —
297
338
  `Monitor/Prudential/{SalesOrderGenerationBacklog,DellPurchaseOrderTransmissionBacklog,
298
339
  OrderShippedUpdateBacklog}`, `Monitor/Aig/EntitlementApiHealth`,
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
19
19
  ## 2.0 framework
20
20
 
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 61 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
- - **worker2** (Worker) — 53 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
+ - **worker2** (Worker) — 54 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
24
  - **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 7 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -30,8 +30,8 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
30
30
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
31
31
  - **ai-bdr** (AI-BDR) — 9 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
32
32
  - **toga2-commerce** (TOGa Commerce) — 19 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
33
- - **toga25-supply** (TOGa 2.5 Supply) — 11 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
34
- - **toga-blox** (TOGa Blox) — 9 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
33
+ - **toga25-supply** (TOGa 2.5 Supply) — 12 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
34
+ - **toga-blox** (TOGa Blox) — 10 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
35
35
  - **bdr** (BDR) — 0 doc(s) → [2.0/apps/bdr/INDEX.md](2.0/apps/bdr/INDEX.md)
36
36
 
37
37
  ## standalone framework
@@ -2,4 +2,5 @@
2
2
 
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
+ | [Staples: OneUptime SFTP folder monitors (Monitor/Staples/*)](features/oneuptime-sftp-folder-monitors.md) | 2.0 | Two worker2 OneUptime push monitors that watch the **Staples SFTP** for stuck files in either direction — a backlog that previously had **no alerting** (the ear | worker2/Worker/Monitor/Staples.php, worker2/Config/beta.ini, worker2/Config/production.ini, dbchanges2/Core/2026-08-20a - Staples SFTP Monitors.sql |
5
6
  | [Staples](profile.md) | 1.0 | Staples is a **headless** integration client — no UI, Worker 1.0 only, depending on the `library` (1.0 `App_`) core. | worker/crons/sync/staples/sync_staples_cxml.php, worker/crons/sync/staples/staples_asn_netsuite.php |
@@ -0,0 +1,113 @@
1
+ ---
2
+ title: "Staples: OneUptime SFTP folder monitors (Monitor/Staples/*)"
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: staples
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-08-20
10
+ owners: ["bala"]
11
+ files:
12
+ - worker2/Worker/Monitor/Staples.php
13
+ - worker2/Config/beta.ini
14
+ - worker2/Config/production.ini
15
+ - dbchanges2/Core/2026-08-20a - Staples SFTP Monitors.sql
16
+ related:
17
+ - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
18
+ - ../profile.md
19
+ - ../../../1.0/apps/worker/features/staples-cxml-order-import.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ Two worker2 OneUptime push monitors that watch the **Staples SFTP** for stuck files in
25
+ either direction — a backlog that previously had **no alerting** (the earlier `/out` pickup
26
+ pileup incident went unnoticed). They implement the shared pattern in
27
+ [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md);
28
+ read that first for the payload contract, criteria set, and provisioning runbook.
29
+
30
+ - `Monitor/Staples/InboundOrderQueue` — watches **`/in`** (cXML orders Staples sent that
31
+ TOGA has not yet imported).
32
+ - `Monitor/Staples/OutboundPickupQueue` — watches **`/out`** (855 acknowledgements + 856
33
+ ASNs TOGA wrote, waiting for Staples to download).
34
+
35
+ Because each monitor logs into SFTP and lists a folder, it **doubles as an SFTP
36
+ connectivity check**: a login/list failure pushes `status:"error"`, so a dead checker or a
37
+ broken SFTP is distinguishable from a real backlog.
38
+
39
+ ## Key files / entry points
40
+
41
+ - `worker2/Worker/Monitor/Staples.php` — `abstract class _Worker_Monitor_Staples`; two
42
+ `public static` action methods (`InboundOrderQueue`, `OutboundPickupQueue`), each
43
+ self-contained (in-method `ALL_CAPS` locals + `$push` closure), following the shared
44
+ "dumb reporter, smart monitor" model.
45
+ - `dbchanges2/Core/2026-08-20a - Staples SFTP Monitors.sql` — the two `Core.CronJobs` rows
46
+ (actions `Monitor/Staples/InboundOrderQueue` and `Monitor/Staples/OutboundPickupQueue`,
47
+ schedule `*/5 * * * *`, `maxExecutionTime` 120), following the sibling monitor
48
+ cron-registration migration pattern.
49
+ - `[sftp_staples]` config group in `worker2/Config/beta.ini` and
50
+ `worker2/Config/production.ini` — SFTP host/port/user/password. **The password is a
51
+ credential; it lives only in the ini group, never in this doc.**
52
+
53
+ ## How it works
54
+
55
+ Each monitor logs into the Staples SFTP with **phpseclib3**, `rawlist`s the target folder,
56
+ counts files older than an age threshold, decides an alarm token (`HIGH`/`OK`), and POSTs
57
+ the JSON body to a OneUptime incoming-request monitor. The worker makes the threshold
58
+ decision; OneUptime only string-matches the token (it cannot compare numbers — see the
59
+ shared doc). The OneUptime push URLs are in-file constants (push credentials — never logged
60
+ or recorded).
61
+
62
+ ### Staples SFTP folder semantics (background for the thresholds)
63
+
64
+ The Staples pipeline runs in the **1.0 `worker`** tier, not worker2; worker2 only watches
65
+ the folders. The relevant 1.0 crons and folder meanings (schedules are in
66
+ `worker/schedules/cron.worker.sync.json`, in the server's **Central** timezone):
67
+
68
+ | 1.0 cron | Schedule | What it does |
69
+ |---|---|---|
70
+ | `worker/crons/sync/staples/sync_staples_cxml.php` | `45 * * * *` (hourly) | imports Staples cXML orders from `/in` into NetSuite, writes an 855 ack to `/out`, archives the processed inbound file to `/archive/archive_in` |
71
+ | `worker/crons/sync/staples/staples_asn_netsuite.php` | `50 * * * *` (hourly) | writes 856 ASN ship notices to `/out` |
72
+
73
+ - **`/in`** — inbound orders Staples uploads for TOGA to import.
74
+ - **`/out`** — outbound 855/856 files TOGA writes for Staples to download (drained by
75
+ Staples, moved to `/archive` on pickup).
76
+
77
+ ### Threshold tuning (decided)
78
+
79
+ - Aged-file cutoff: **120 min for `/in`** (tied to the hourly import) and **240 min for
80
+ `/out`**.
81
+ - Alarm when **>= 6 aged files** (tunable — can drop to 1 for single-file sensitivity).
82
+ - **5-minute cron cadence** (`*/5 * * * *`), chosen to pair with the OneUptime heartbeat
83
+ criteria (received-in-8-min + `alarm:OK` recovery; not-received-15-min offline;
84
+ not-received-10-min degraded), matching the existing Compass monitor convention rather
85
+ than the source crons' hourly rate. The fast cadence is for a **reliable heartbeat**, not
86
+ because the folders change that fast — the folder cutoffs above are calibrated against the
87
+ **hourly 1.0 crons**, per the shared doc's "calibrate against the tier that runs the
88
+ business logic" rule.
89
+
90
+ ## Gotchas / known issues
91
+
92
+ - **phpseclib3 `rawlist` returns arrays, and its SFTP type constants are not loaded** — see
93
+ the shared doc's
94
+ [SFTP monitoring gotchas](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md#sftp-monitoring-phpseclib3).
95
+ Getting this wrong silently makes the aged-file count always 0, so the monitor never
96
+ alarms and looks healthy.
97
+ - The `[sftp_staples]` password and the OneUptime push URLs are credentials — reference
98
+ their location, never their value.
99
+
100
+ ## Change history
101
+ - 2026-08-20 — Built the two SFTP folder monitors (`Monitor/Staples/InboundOrderQueue` on
102
+ `/in`, `Monitor/Staples/OutboundPickupQueue` on `/out`), each also acting as an SFTP
103
+ connectivity check via `status:error`; added the `[sftp_staples]` config group and the
104
+ `2026-08-20a` cron-registration migration; set the `/in` 120-min / `/out` 240-min aged-file
105
+ cutoffs and the >=6-file alarm threshold; chose a 5-min cadence for the heartbeat.
106
+ Motivated by the earlier `/out` pickup pileup incident that had no alerting. (bala)
107
+
108
+ ## Related docs
109
+ - [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md) — the shared pattern, payload contract, and runbook.
110
+ - [Staples profile](../profile.md)
111
+ - [Staples cXML order import (1.0)](../../../1.0/apps/worker/features/staples-cxml-order-import.md) — the inbound pipeline these monitors watch.
112
+ </content>
113
+ </invoke>
@@ -3,17 +3,20 @@ title: Staples
3
3
  framework: "1.0"
4
4
  apps:
5
5
  - worker
6
+ - worker2
7
+ - dbchanges2
6
8
  project: Worker
7
9
  client: staples
8
10
  type: profile
9
11
  status: active
10
- updated: 2026-08-13
11
- owners: [jcardinal]
12
+ updated: 2026-08-20
13
+ owners: [jcardinal, bala]
12
14
  files:
13
15
  - worker/crons/sync/staples/sync_staples_cxml.php
14
16
  - worker/crons/sync/staples/staples_asn_netsuite.php
15
17
  related:
16
18
  - ../../1.0/apps/worker/features/staples-cxml-order-import.md
19
+ - ./features/oneuptime-sftp-folder-monitors.md
17
20
  ---
18
21
 
19
22
  ## Summary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.626",
3
+ "version": "1.0.628",
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",