toga-ai 1.0.654 → 1.0.655
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.
- package/knowledge/2.0/apps/_underscore/features/apirequest-json-content-type.md +28 -2
- package/knowledge/2.0/apps/toga-blox/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga-blox/features/toaster.md +100 -0
- package/knowledge/2.0/apps/toga-blox/workflows/dynamic-publish-pipeline.md +27 -0
- package/knowledge/2.0/apps/toga2-commerce/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga2-commerce/features/order-submit-sync-sequencing.md +15 -2
- package/knowledge/2.0/apps/toga2-commerce/workflows/amplify-build-and-deploy.md +50 -6
- package/knowledge/2.0/apps/worker2/INDEX.md +1 -0
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +17 -2
- package/knowledge/2.0/apps/worker2/features/netsuite-opportunity-client-labels.md +138 -0
- package/knowledge/2.0/apps/worker2/features/netsuite-opportunity-sync.md +66 -7
- package/knowledge/2.0/apps/worker2/features/notification-email.md +19 -2
- package/knowledge/2.0/standards/frontend-deploy.md +8 -3
- package/knowledge/2.0/standards/frontend.md +28 -6
- package/knowledge/INDEX.md +2 -2
- package/knowledge/standalone/apps/claude/workflows/mcp-tool-usage.md +19 -2
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: ["jcardinal", "ajean"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/ApiRequest.php
|
|
13
13
|
related:
|
|
@@ -74,7 +74,33 @@ case-insensitive scan of already-set headers, so a caller-supplied `Content-Type
|
|
|
74
74
|
save) lives **inside `execute()`** and would affect every framework caller, so it is deferred
|
|
75
75
|
pending architecture review. Do **not** work around it per-caller.
|
|
76
76
|
|
|
77
|
+
- **Hand-rolled `Logs.Api` inserts must set `isAuthRequest`, `instanceId` and `hostname` — they are
|
|
78
|
+
NOT NULL with no defaults.** `_ApiRequest`'s own logging sets all three (`ApiRequest.php:260-264`),
|
|
79
|
+
so a helper that writes its own log row while only setting `direction`/`source`/`method`/`route`
|
|
80
|
+
throws `Column 'X' cannot be null` on **every** insert. Because such helpers are usually a
|
|
81
|
+
*wrapper* around the real call, the throw propagates out of the wrapped call and, under a
|
|
82
|
+
`catch(Throwable)`, turns the whole feature into a **silent no-op that still reports success**.
|
|
83
|
+
Two worker2 handlers shipped with exactly this bug and one went unnoticed for months — see
|
|
84
|
+
[netsuite-opportunity-sync](../../worker2/features/netsuite-opportunity-sync.md#gotchas--known-issues).
|
|
85
|
+
Prefer letting `_ApiRequest` log rather than writing `Logs.Api` by hand.
|
|
86
|
+
- **⚠ Auto-logging persists the `Authorization` header in PLAINTEXT.** `ApiRequest.php:267` writes
|
|
87
|
+
the full request headers to `Logs.Api.requestHeaders`, so any component that sends a bearer
|
|
88
|
+
token/API key and does not `setLogging(false)` is depositing that credential into the logs DB on
|
|
89
|
+
every call. Known live instance: `_underscore/Component/Api/Clickup/Clickup.php` (which also
|
|
90
|
+
**hardcodes** its token in committed source — flagged for a rotate-and-move-to-config ticket; 11
|
|
91
|
+
worker2 files use it). When adding a new component, decide deliberately: log and accept the header
|
|
92
|
+
exposure, strip/redact the auth header before the call is logged, or `setLogging(false)`.
|
|
93
|
+
- **`Logs.Api` is 1.4M+ rows and `source` is NOT indexed.** Any diagnostic query must be bounded by
|
|
94
|
+
`dtStamp` (indexed) or it times out — `WHERE source = '…'` alone will not return.
|
|
95
|
+
|
|
77
96
|
## Change history
|
|
97
|
+
- 2026-08-26 — Documented the **manual-insert column contract** (`isAuthRequest`, `instanceId`,
|
|
98
|
+
`hostname` are NOT NULL with no defaults, and `_ApiRequest` sets them at `ApiRequest.php:260-264`),
|
|
99
|
+
after a hand-rolled `logged()` helper in two worker2 handlers threw on every insert and silently
|
|
100
|
+
no-op'd an entire feature behind a `catch(Throwable)`. Also recorded that auto-logging persists the
|
|
101
|
+
**`Authorization` header in plaintext** into `Logs.Api.requestHeaders` (`ApiRequest.php:267`) — the
|
|
102
|
+
reason `_Component_Api_Clickup`'s hardcoded token needs rotation — and that `Logs.Api.source` is
|
|
103
|
+
unindexed at 1.4M+ rows, so queries must be bounded by `dtStamp`. (ajean)
|
|
78
104
|
- 2026-08-05 — Documented (no code change) that the `direction = 'OUT'` api-log row is
|
|
79
105
|
**committed immediately** inside `execute()` (`transactionCommit(DB_CLIENT_LOGS)`), so it
|
|
80
106
|
survives a later rollback and `Logs_<Client>.Api` is definitive evidence of whether an outbound
|
|
@@ -11,4 +11,5 @@
|
|
|
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/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 |
|
|
13
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 |
|
|
14
|
+
| [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 |
|
|
14
15
|
| [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,100 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Toaster — host-token styling contract
|
|
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-26
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- toga-blox/src/components/Toaster/Toaster.module.css
|
|
13
|
+
- toga-blox/src/components/Toaster/Toaster.tsx
|
|
14
|
+
- toga-blox/src/components/Toaster/ToasterContext.tsx
|
|
15
|
+
- toga2-commerce/src/styles/index.css
|
|
16
|
+
- toga2-commerce/src/components/Toasters/ToasterList.tsx
|
|
17
|
+
related:
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- ../../../standards/frontend.md
|
|
20
|
+
- ../../toga2-commerce/architecture.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
The blox `Toaster` is a CSS-Module component whose every visual property reads a
|
|
26
|
+
`--toaster-*` custom property supplied by the **host app**. It is the clearest working example of
|
|
27
|
+
the blox styling contract described in `frontend.md` §13(e) — and of that contract's failure mode:
|
|
28
|
+
a host that defines **zero** tokens gets a component that renders **unstyled**, because every
|
|
29
|
+
declaration is invalid-at-computed-value-time and dropped.
|
|
30
|
+
|
|
31
|
+
As of 2026-08-26 the component ships **default values via `var(--token, fallback)`**, so it renders
|
|
32
|
+
fully styled with no host configuration, while any host-defined `--toaster-*` token still wins.
|
|
33
|
+
*(All Toaster changes below are working-tree on blox `feature-new-table` and commerce `TRUE-80707` —
|
|
34
|
+
not yet merged or published; verify against the installed version before relying on them.)*
|
|
35
|
+
|
|
36
|
+
## How it works
|
|
37
|
+
|
|
38
|
+
### Token layer with fallbacks (the pattern to copy)
|
|
39
|
+
|
|
40
|
+
Every declaration in `Toaster.module.css` is written `var(--toaster-x, <literal default>)`. This was
|
|
41
|
+
chosen over adding a `:root` token block inside blox because:
|
|
42
|
+
|
|
43
|
+
- blox ships **no** `:root` token block anywhere (§13e), and
|
|
44
|
+
- `dist/main.css` is **uncompiled** and must not be imported by consumers (§13f),
|
|
45
|
+
|
|
46
|
+
so a `:root` block would need a stylesheet import that no consumer makes. Fallbacks need nothing
|
|
47
|
+
imported. **This is the generalisable rule for any blox component that reads host tokens: ship
|
|
48
|
+
`var(--token, fallback)`, never a bare `var(--token)`.**
|
|
49
|
+
|
|
50
|
+
### Do NOT declare a property on an element that also receives a host override class
|
|
51
|
+
|
|
52
|
+
`.prefixIcon` and `.closeBtn` deliberately declare **no `color`**. The host's `iconColor` /
|
|
53
|
+
`closeIconColor` classes land on those same elements at **equal specificity**, so a `color`
|
|
54
|
+
declared in the module would be resolved by **stylesheet source order** and could silently beat the
|
|
55
|
+
host's class (supply's mint/crimson icons losing to a grey default). They inherit from `.base`
|
|
56
|
+
instead, which any host class reliably overrides. In-code comments record this so it is not
|
|
57
|
+
"helpfully" re-added.
|
|
58
|
+
|
|
59
|
+
### Host wiring — the variant-remap mechanism
|
|
60
|
+
|
|
61
|
+
The supported way for a host to express toast variants is to define the `--toaster-*` layer once,
|
|
62
|
+
then have each variant class **remap only the `--toaster-default-*` tokens** that blox's `.base`
|
|
63
|
+
already reads. Do **not** style toasts with utility classes on a wrapper — Tailwind utilities and
|
|
64
|
+
blox's `.base` land at equal specificity and compete unpredictably.
|
|
65
|
+
|
|
66
|
+
```css
|
|
67
|
+
:root { --toaster-padding: 1.25rem; /* … full token set … */ }
|
|
68
|
+
.commerceToaster--error { --toaster-default-bg: …; --toaster-default-text: …; }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`toga25-supply` defines the full set (~33 tokens) in `src/themeConfig.json`; `toga2-commerce` now
|
|
72
|
+
defines it in `src/styles/index.css` with `.commerceToaster--{success,error,restricted,default}`
|
|
73
|
+
variant classes consumed by `ToasterList.tsx`. Commerce's result was verified by `tsc -b` / `vite
|
|
74
|
+
build` and by inspecting the emitted CSS for the tokens and all four variant classes — **not
|
|
75
|
+
visually in a browser**.
|
|
76
|
+
|
|
77
|
+
## Gotchas
|
|
78
|
+
|
|
79
|
+
- **A missing `var()` wrapper fails silently.** Two declarations read `min-width: (--toaster-min-width)`
|
|
80
|
+
/ `max-width: (--toaster-max-width)` — no `var()`, so the parser dropped both. `toga25-supply`
|
|
81
|
+
masked this by re-declaring `min-width` on `.supplyToaster`; commerce had nothing to mask it, which
|
|
82
|
+
is how it surfaced. A host override can hide a blox CSS bug indefinitely.
|
|
83
|
+
- **`border-width` + `border-color` with no `border-style` renders no border at all.** `.base`
|
|
84
|
+
previously set the first two only; `border-style` (plus `border-radius`, `font-family`,
|
|
85
|
+
`font-size`, `margin-block`) has been added.
|
|
86
|
+
- **Order placement in commerce does not raise a success toast** — do not use it to test toast
|
|
87
|
+
styling. See `../../toga2-commerce/features/order-submit-sync-sequencing.md`.
|
|
88
|
+
- Commerce's `restricted` (orange) variant is commerce-only; supply has no equivalent. Commerce's
|
|
89
|
+
palette has no `crimson-600`, so supply's error icon color maps to `crimson-500`.
|
|
90
|
+
|
|
91
|
+
## Change history
|
|
92
|
+
|
|
93
|
+
- 2026-08-26 — Toaster now ships **default styling** via `var(--token, fallback)` on every custom
|
|
94
|
+
property (renders styled with zero host config; host tokens still win) and gained the missing
|
|
95
|
+
`border-style`/`border-radius`/`font-family`/`font-size`/`margin-block` on `.base`. Fixed two
|
|
96
|
+
declarations missing their `var()` wrapper (`min-width`/`max-width`), which supply had been
|
|
97
|
+
masking. Recorded the deliberate **no `color` on `.prefixIcon`/`.closeBtn`** decision (equal
|
|
98
|
+
specificity with the host's `iconColor`/`closeIconColor` classes). `toga2-commerce` rewired to
|
|
99
|
+
supply's variant-remap mechanism and given the full `--toaster-*` token layer, replacing the
|
|
100
|
+
competing Tailwind-utility styling. (apeterson)
|
|
@@ -93,6 +93,27 @@ environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
|
|
|
93
93
|
copy on the pushed branch, a new `_<mode>` channel will **not** publish unless the branch you push
|
|
94
94
|
carries the wildcard-trigger version. When standing up a new channel, merge from a branch that has
|
|
95
95
|
the dynamic `publish.yml` (or update it first).
|
|
96
|
+
- **Do NOT judge a blox branch's staleness by its behind-count.** `feature-new-table` reports
|
|
97
|
+
**"0 ahead / 47 behind"** `_sandbox-client` and `git merge-base --is-ancestor` says it is fully
|
|
98
|
+
contained — which reads as "stale, already merged, wrong base". **It is not.** Those 47 commits are
|
|
99
|
+
almost entirely `Merge branch 'feature-new-table' into _sandbox-client` and
|
|
100
|
+
`chore: bump version to 1.0.3xx` — **release plumbing generated ON `_sandbox-client` by the publish
|
|
101
|
+
flow and never merged back**. The actual source difference is **2 files** (`src/global.css`,
|
|
102
|
+
`src/index.ts`); the rest of the 334-file diff is `.claude/knowledge/` docs and `publish.yml`.
|
|
103
|
+
`feature-new-table` is the **active development branch** (tip 2026-08-24, `feat: prefer
|
|
104
|
+
server-supplied filter options in table meta`) and is the correct base. Verified 2026-08-26.
|
|
105
|
+
Corollary: the `publish.yml` divergence noted above is a **non-issue for publishing**, because
|
|
106
|
+
Actions runs the workflow on the **pushed** branch (`_sandbox-client`), which carries the dynamic
|
|
107
|
+
`_*` trigger.
|
|
108
|
+
- **A declared peer can be phantom — check before working around it.** Verified 2026-08-26:
|
|
109
|
+
`react-router-dom` appeared in blox's `peerDependencies` but only three files referenced it, two of
|
|
110
|
+
them tests; the single production reference was a **dead `import { Link }`** on line 1 of
|
|
111
|
+
`src/components/old/BaseButton/BaseButton.tsx` (the component renders an injected `linkComponent`
|
|
112
|
+
prop instead). The peer declaration and the dead import were removed *(working-tree on
|
|
113
|
+
`feature-new-table` — not yet merged or published)*; `react-router-dom` stays a
|
|
114
|
+
**devDependency** (`^6.30.1`) for the tests' `MemoryRouter`. Verified clean build, zero
|
|
115
|
+
`react-router-dom` references in `dist/`, and vitest identical to baseline. This peer was the
|
|
116
|
+
stated reason consumers pass `--legacy-peer-deps` against a router-7 app.
|
|
96
117
|
- **FontAwesome v7 `style`-prop TS2322** (fixed 2026-07-21 in `getFontAwesomeIcon.tsx`, and a
|
|
97
118
|
reusable pattern for any consumer). FA v7 types the `FontAwesomeIcon` `style` prop as
|
|
98
119
|
`CSSProperties & CSSVariables`, where `CSSVariables` requires a `--fa-*` custom-property index
|
|
@@ -122,6 +143,12 @@ environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
|
|
|
122
143
|
the pattern the consuming apps should follow (see the commerce workflow's secret-hygiene note).
|
|
123
144
|
|
|
124
145
|
## Change history
|
|
146
|
+
- 2026-08-26 — Corrected the branch-topology misreading: `feature-new-table`'s "0 ahead / 47 behind"
|
|
147
|
+
is release plumbing generated on `_sandbox-client`, not staleness — it remains the active dev
|
|
148
|
+
branch and correct base (real source delta: 2 files). Noted the `publish.yml` divergence is a
|
|
149
|
+
non-issue because Actions runs the pushed branch's copy. Removed the **phantom
|
|
150
|
+
`react-router-dom` peer** (its only production reference was a dead `Link` import in
|
|
151
|
+
`old/BaseButton`); it is now a devDependency for tests only. Working-tree, uncommitted. (apeterson)
|
|
125
152
|
- 2026-08-04 — Recorded the prod/beta `.scss`-only build gotcha: the `_production`/`_beta`
|
|
126
153
|
`build` copies only `src/**/*.scss`, so a component shipped with a plain `.css` + matching
|
|
127
154
|
`import "./X.css"` publishes JS referencing a stylesheet absent from the tarball, breaking
|
|
@@ -16,6 +16,6 @@
|
|
|
16
16
|
| [Multi-Tenant Resolution & Theming](features/multi-tenant-theming.md) | `toga2-commerce` serves multiple clients from one codebase. | src/themeConfig/themes.json, src/themeConfig/ThemeContext.tsx, src/themeConfig/types.ts, src/components/ThemeSwitcher/ThemeSwitcher.tsx, src/components/AuthLayout/AuthLayout.tsx, src/api/axiosInstance.ts, src/contexts/AuthContext.tsx, tailwind.config.js |
|
|
17
17
|
| [Order-submit sync sequencing (useSubmitOrder) — why these calls must not run in parallel](features/order-submit-sync-sequencing.md) | Submitting an order from the cart fires **two independent sync routines** — one for the sales-order header (`syncSalesOrderData`) and one for the line items (`s | toga2-commerce/src/pages/OrderDetails/hooks/useSubmitOrder.ts, toga2-commerce/src/api/syncSalesOrdersDataFromLocalStorageCartToApi.ts, toga2-commerce/src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
|
|
18
18
|
| [Config-Driven Shipping Cost Waiver (Standard Ground free for computer kits)](features/shipping-cost-waiver-gating.md) | On the toga2-commerce **Cart** page, a shipping option's **cost** can be waived by config using the same `PrimaryItemShippingRule` vocabulary that drives expedi | toga2-commerce/src/pages/Cart/helpers/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/viewModel/FIELDS/shared/shippingOptionGates.ts, toga2-commerce/src/pages/Cart/CartPage.tsx |
|
|
19
|
-
| [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-commerce` (React + Vite, "commerce2-react") builds and deploys on **AWS Amplify**. | toga2-commerce/amplify.yml, toga2-commerce/.gitattributes, toga2-commerce/package.json, toga2-commerce/.github/workflows/sync-stage-environments.yml |
|
|
19
|
+
| [AWS Amplify Build & Deploy (non-prod environments)](workflows/amplify-build-and-deploy.md) | How `toga2-commerce` (React + Vite, "commerce2-react") builds and deploys on **AWS Amplify**. | toga2-commerce/amplify.yml, toga2-commerce/.gitattributes, toga2-commerce/package.json, toga2-commerce/.npmrc, toga2-commerce/vite.config.ts, toga2-commerce/.github/workflows/sync-stage-environments.yml |
|
|
20
20
|
| [Cart e2e — Cypress conventions & harness (toga2-commerce)](workflows/cypress-testing.md) | The Cypress **e2e** convention set for `toga2-commerce`, and the first **active** e2e coverage for the **Cart** page (`cartV2.cy.ts`, slice 1 — 12 tests, verifi | toga2-commerce/cypress/e2e/cartPage/cartV2.cy.ts, toga2-commerce/cypress/fixtures/cart/fetchSingleUserAdmin.json, toga2-commerce/cypress/fixtures/cart/fetchLocations.json, toga2-commerce/cypress/fixtures/cart/fetchUserShippingMethods.json, toga2-commerce/cypress/support/commands.ts, toga2-commerce/cypress/support/e2e.ts, toga2-commerce/src/pages/Cart/CartPage.tsx, toga2-commerce/src/pages/Cart/view/cartForm/CartForm.tsx, toga2-commerce/src/pages/Cart/view/cartForm/CartFormSection.tsx, toga2-commerce/src/pages/Cart/view/cartTable/CartContentsTable.tsx, toga2-commerce/src/pages/Cart/view/cartTable/CartTableItem.tsx, toga2-commerce/src/components/Inputs/AdvancedInput.tsx, toga2-commerce/src/components/BaseButton/BaseButton.tsx |
|
|
21
21
|
| [Diagnosing ERR_HTTP2_PROTOCOL_ERROR (one client fails, everyone else is fine)](workflows/http2-protocol-error-diagnosis.md) | When a Chromium browser (Chrome / Edge) shows **`ERR_HTTP2_PROTOCOL_ERROR`** loading a `*.togacommerce.com` tenant for **one client/network but works for the TO | |
|
|
@@ -6,8 +6,8 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["bala"]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: ["bala", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-commerce/src/pages/OrderDetails/hooks/useSubmitOrder.ts
|
|
13
13
|
- toga2-commerce/src/api/syncSalesOrdersDataFromLocalStorageCartToApi.ts
|
|
@@ -134,9 +134,22 @@ Three changes, in `toga2-commerce`:
|
|
|
134
134
|
- **⚠ Omitting a line from the rebuilt cart DELETES it from a placed order.** `processItems` with
|
|
135
135
|
`getDeleteItems = true` derives the delete set by difference, so "don't re-add this one" and
|
|
136
136
|
"destroy this one" are the same instruction on this path.
|
|
137
|
+
- **There is NO success toaster on order placement — by design, not a bug.** `useSubmitOrder` calls
|
|
138
|
+
`addToaster` **only in its `catch` block** (`status: 'error'`). Both success paths navigate away
|
|
139
|
+
before anything could render: cart → `createNewSalesOrderSubmit()` →
|
|
140
|
+
`navigate("/order-details?uuid=…&type=orderPlaced")` (no `addToaster` anywhere in it); edit-order →
|
|
141
|
+
`window.location.href = getSupplyRedirectLink(orderUuid)`. **Consequence for debugging: placing an
|
|
142
|
+
order is not a valid way to test toaster styling.** Use a path that stays mounted — add/edit
|
|
143
|
+
shipping address (Cart or Account), Account → My Settings, or Cart → add new user; for the error
|
|
144
|
+
variant, force a submit failure to hit the catch block. See
|
|
145
|
+
[blox Toaster](../../toga-blox/features/toaster.md).
|
|
137
146
|
|
|
138
147
|
## Change history
|
|
139
148
|
|
|
149
|
+
- 2026-08-26 — Recorded that order placement raises **no success toaster by design** (`addToaster`
|
|
150
|
+
is only in the `catch`; both success paths navigate away), and listed the mounted-path
|
|
151
|
+
alternatives for testing toaster styling. (apeterson)
|
|
152
|
+
|
|
140
153
|
- 2026-08-14 — Recorded **how order lines are actually written**, learned while adding a
|
|
141
154
|
server-side order guard: the header sync `delete`s `salesOrderItems` from the edit `PUT` (line
|
|
142
155
|
20), and lines are written **one request at a time** — new lines via the nested
|
|
@@ -6,16 +6,19 @@ project: TOGa Commerce
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: ["jcardinal", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- toga2-commerce/amplify.yml
|
|
13
13
|
- toga2-commerce/.gitattributes
|
|
14
14
|
- toga2-commerce/package.json
|
|
15
|
+
- toga2-commerce/.npmrc
|
|
16
|
+
- toga2-commerce/vite.config.ts
|
|
15
17
|
- toga2-commerce/.github/workflows/sync-stage-environments.yml
|
|
16
18
|
related:
|
|
17
19
|
- ../../toga2-supply/workflows/amplify-build-and-deploy.md
|
|
18
20
|
- ../../toga-blox/workflows/dynamic-publish-pipeline.md
|
|
21
|
+
- ../../../standards/frontend.md
|
|
19
22
|
---
|
|
20
23
|
|
|
21
24
|
## Summary
|
|
@@ -69,6 +72,10 @@ full cross-repo publish-then-build model. **The channel must exist before the ap
|
|
|
69
72
|
|
|
70
73
|
## Edge cases & escalation
|
|
71
74
|
|
|
75
|
+
> The 2026-08-26 commerce-side fixes below (`.npmrc` flag, added peers, `resolve.dedupe`, the
|
|
76
|
+
> `--toaster-*` token layer, the `ToasterList` rewiring) are **working-tree on branch `TRUE-80707`** —
|
|
77
|
+
> uncommitted, unmerged, and not in any deployed build.
|
|
78
|
+
|
|
72
79
|
- **All the toga2-supply gotchas apply** (console build spec overriding repo `amplify.yml`;
|
|
73
80
|
CRLF breaking block-scalar parsing; Vite silently falling back to `.env`/beta when a mode's env
|
|
74
81
|
file is missing). See the related doc.
|
|
@@ -77,14 +84,44 @@ full cross-repo publish-then-build model. **The channel must exist before the ap
|
|
|
77
84
|
only triggers on branches listed in `on.push.branches` and only tags branches with an explicit
|
|
78
85
|
`elif` case — creating the branch alone does nothing; the workflow file must be present on that
|
|
79
86
|
branch (GitHub Actions runs the workflow version on the pushed branch).
|
|
80
|
-
- **`--legacy-peer-deps` is required** because blox
|
|
87
|
+
- **`--legacy-peer-deps` is required** because blox declared `react-router-dom@^6` as a peer while
|
|
81
88
|
commerce uses `react-router-dom@7` (npm 7+ `ERESOLVE` otherwise). All non-prod build scripts use it.
|
|
89
|
+
(That peer was **phantom** and was removed from blox on 2026-08-26 — see the blox publish-pipeline
|
|
90
|
+
doc — but the flag stays until a build without it is proven.)
|
|
91
|
+
- **When one app installs a shared lib and a sibling app does not, compare `.npmrc` BEFORE touching
|
|
92
|
+
peer ranges.** A local `npm i` in commerce failed `ERESOLVE` on blox's router peer while
|
|
93
|
+
`toga25-supply` installed cleanly against the **same** blox version and the **same** router major.
|
|
94
|
+
The difference was not the peer range: supply carries `legacy-peer-deps=true` in its committed
|
|
95
|
+
`.npmrc` and commerce did not. Fix was adding `legacy-peer-deps=true` to commerce's `.npmrc` — a
|
|
96
|
+
tracked file, so it also reaches the Amplify build (the amplify.yml flag alone does not cover a
|
|
97
|
+
developer's local install).
|
|
82
98
|
- **`--legacy-peer-deps` disables npm's peer auto-install**, so any blox peer that is NOT in
|
|
83
99
|
commerce's own `dependencies` and NOT in blox's `dependencies` must be added explicitly, or Vite/
|
|
84
100
|
Rollup fails with `failed to resolve import "<pkg>"`. Concretely, commerce had to add the table
|
|
85
|
-
peers: `@tanstack/react-table@^8`, `react-table@^7.8.0`, `react-table-sticky@^1.1.3
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
peers: `@tanstack/react-table@^8`, `react-table@^7.8.0`, `react-table-sticky@^1.1.3`, and on
|
|
102
|
+
2026-08-26 `@fortawesome/free-solid-svg-icons@^6.7.2` + `react-multi-select-component@^4.3.4`.
|
|
103
|
+
Check `npm view @agilant/toga-blox@<tag> peerDependencies` against commerce deps when a resolve
|
|
104
|
+
error appears.
|
|
105
|
+
- **A BARREL import is what drags in peers you never use.** The failure text was
|
|
106
|
+
`Rollup failed to resolve import "@fortawesome/free-solid-svg-icons" from
|
|
107
|
+
node_modules/@agilant/toga-blox/dist/components/Input/Input.js`, triggered by
|
|
108
|
+
`AuthLayout.tsx` doing `import { EnvironmentBadge } from "@agilant/toga-blox"` — the package
|
|
109
|
+
index pulls `Input` and `MultiSelect` along with it. A deep import would not have surfaced it, so
|
|
110
|
+
expect the missing-peer set to be blox's **whole** surface, not the components you reference.
|
|
111
|
+
- **`resolve.dedupe` was missing from commerce's `vite.config.ts`** (a `frontend.md` §11 MUST), which
|
|
112
|
+
made consuming blox via a local symlink hazardous: `toga-blox-npm/node_modules/` contains its own
|
|
113
|
+
`react@18.3.1`/`react-dom@18.3.1`, and because blox ships **unbundled**, imports resolve against
|
|
114
|
+
the nearest `node_modules` — under a symlink that is blox's own, giving a second React copy. This
|
|
115
|
+
is precisely why supply's symlink workflow works (it has always deduped
|
|
116
|
+
`react`/`react-dom`/`@tanstack/react-table`) and commerce's did not. Commerce now dedupes
|
|
117
|
+
`react`, `react-dom`, `react-hook-form`, `react-router-dom`, `framer-motion`,
|
|
118
|
+
`@tanstack/react-query`, `@tanstack/react-table`.
|
|
119
|
+
- **`package.json` pins blox with a CARET** (`"@agilant/toga-blox": "^1.0.322-sandbox-client.117"`),
|
|
120
|
+
violating `frontend.md` §13(a) — some published versions ship an empty `dist`, and a caret can
|
|
121
|
+
float you onto one. Pin exact. Related: the `TRUE-80707` branch pin is **11 versions behind** the
|
|
122
|
+
`_sandbox-client` branch pin (`1.0.333-sandbox-client.134`); reconcile before merging.
|
|
123
|
+
- **Secret hygiene re-confirmed unremediated 2026-08-26** — the live tokens are still in the tracked
|
|
124
|
+
`.npmrc` in **both** `toga2-commerce` and `toga25-supply`. See the remediation below.
|
|
88
125
|
- **Secret hygiene (open remediation):** `toga2-commerce/.npmrc` is **git-tracked** — it is listed
|
|
89
126
|
in `.gitignore` but was committed before that, so git still tracks it, and it holds a live
|
|
90
127
|
FontAwesome registry token and a live npm auth token in plaintext. Remediation: (1) **rotate both
|
|
@@ -95,6 +132,13 @@ full cross-repo publish-then-build model. **The channel must exist before the ap
|
|
|
95
132
|
|
|
96
133
|
## Change history
|
|
97
134
|
|
|
135
|
+
- 2026-08-26 — Dependency-resolution findings from a local commerce build: root-caused an
|
|
136
|
+
`ERESOLVE` to the **missing `legacy-peer-deps=true` in commerce's tracked `.npmrc`** (supply had
|
|
137
|
+
it) rather than blox's peer range; recorded the barrel-import trigger for missing peers (added
|
|
138
|
+
`@fortawesome/free-solid-svg-icons`, `react-multi-select-component`); added the previously absent
|
|
139
|
+
`resolve.dedupe` to `vite.config.ts` and explained why supply can symlink blox safely and commerce
|
|
140
|
+
could not; flagged the caret blox pin and the 11-version pin drift on `TRUE-80707`; re-confirmed
|
|
141
|
+
the committed `.npmrc` tokens are still unremediated. All working-tree, uncommitted. (apeterson)
|
|
98
142
|
- 2026-07-21 — Reworked `amplify.yml` to a **fully dynamic inline build** (no per-branch `case`, no
|
|
99
143
|
`npm run <script>`): derive mode from `$AWS_BRANCH`, dynamic `npm install
|
|
100
144
|
"@agilant/toga-blox@$MODE"`, `npx tsc -b` (composite), `npx vite build --mode "$MODE"`. Re-export
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
| [Etilize Item Translation Import](features/etilize-item-translation-import.md) | The abstract worker class `_Worker_Etilize_ItemTranslations` imports **non-English** item text from Etilize into the client's `ItemTranslations` table. | worker2/Worker/Etilize/ItemTranslations.php |
|
|
27
27
|
| [Monitoring Framework (Orchestrator + Child Monitors)](features/monitoring-framework.md) | A unified, DB-driven monitoring framework for business-critical data flows (Compass POs, Prudential asset imports, AIG closed claims, …). | worker2/Worker/Monitor.php, worker2/Worker/Monitors/, worker2/Worker/Monitors/RateEntitlement.php, worker2/Worker/Notification/Email.php, worker2/Worker/Rate.php, dbchanges2/Core/2026-05-21 - Monitors.sql, dbchanges2/Core/2026-06-29a - Rate Entitlement Contract Monitor.sql |
|
|
28
28
|
| [NetSuite Integrations Monitor (Monitor/Operations/NetsuiteIntegrations)](features/netsuite-integrations-monitor.md) | `_Worker_Monitor_Operations::NetsuiteIntegrations()` is a cross-client health check that detects **stuck NetSuite ↔ 2.0 integrations**. | worker2/Worker/Monitor/Operations.php, dbchanges2/Core/2026-08-10a - Netsuite Integrations Monitor.sql, _underscore/Database.php, _underscore/Query.php |
|
|
29
|
+
| [ClickUp Opportunity Client vs End-Customer Labels (Stakeholders / End Customer)](features/netsuite-opportunity-client-labels.md) | The NetSuite→ClickUp opportunity task carries **two** customer labels, not one (TRUE-81010, PR #142): - **Stakeholders** = the **client** — the consolidated par | worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Netsuite/OpportunityStakeholderDigest.php, worker2/Worker/Netsuite/OpportunityStakeholderBackfill.php |
|
|
29
30
|
| [NetSuite ↔ ClickUp / TOGA Opportunity Sync (API Message Queue + worker2 webhook)](features/netsuite-opportunity-sync.md) | Outbound sync from NetSuite to TOGA for the record types the Forecast2 importer pulls (opportunities first; sales/items/etc. | worker2/Worker/Netsuite.php, worker2/Worker/Netsuite/Opportunity.php, worker2/Worker/Clickup.php, worker2/Worker/Clickup/Opportunity.php, worker2/Controller/Index.php, _underscore/Worker.php, test/@dave/NetSuite/api-message-queue/lib_amq_queue.js, test/@dave/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/NetSuite/api-message-queue/ue_amq_drain.js, test/@dave/NetSuite/api-message-queue/ss_amq_drain.js, test/@dave/NetSuite/api-message-queue/DEPLOY_RUNBOOK.md, test/@dave/clickup/backfill_opportunity_numbers.php, test/@dave/clickup/probe_opportunity_fields.php, test/@dave/probe_clickup_desc_match.php, test/@dave/test_model_load_behavior.php, dbchanges2/Forecast/2026-06-25a - Add unique index on Opportunities netsuiteOpportunityInternalId.sql, _underscore/Model/Forecast/Opportunity.php, test/@dave/approach/TRUE-80044.md, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
30
31
|
| [NetSuite → Forecast Open-Orders Sync (salesOrder webhook → OpenOrderItems)](features/netsuite-salesorder-open-orders-sync.md) | Webhook-driven, single-record port of the legacy open-orders importer (TRUE-79142). | worker2/Worker/Netsuite/SalesOrder.php, worker2/Worker/Netsuite.php, worker2/Component/Forecast/Db/Db.php, worker2/Worker/Netsuite/Location.php, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_api_msg_queue_enqueue.js, test/@dave/Junk Drawer/NetSuite/api-message-queue/ue_amq_invoice_resync_salesorder.js, test/@dave/probe_salesorder_rest_shape.php, test/@dave/probe_open_order_lines.php, test/@dave/check_so_status.php, test/@dave/check_so_history.php, test/@dave/probe_so_rest_lines.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_open_order_gating.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php |
|
|
31
32
|
| [Toga → NetSuite Sales-Order Push (multi-client, mapping-driven worker)](features/netsuite-salesorder-outbound-push.md) | The **outbound** half of `worker2/Worker/Netsuite/SalesOrder.php` (everything from the `OUTBOUND PUSH (REST)` banner down) pushes a Toga sales order **into** Ne | worker2/Worker/Netsuite/SalesOrder.php, test/@Bala/tests/netsuite_salesorder_payload_tests.php, worker/crons/toga2/prudential/transmissions_to_netsuite.php |
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: [jcardinal, dfranks, mhammontree, tcox, bala]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/
|
|
13
13
|
- worker2/Controller/Index.php
|
|
@@ -309,10 +309,25 @@ Rules: rename **both sides in one PR and one deploy**; grep every repo for the o
|
|
|
309
309
|
host. **Production's `[databaseClient]` additionally exposes a `readhost`** (a reader endpoint);
|
|
310
310
|
use it for **bulk reads** rather than the primary. Credential *values* live only in the ini files —
|
|
311
311
|
never copy them anywhere else.
|
|
312
|
+
- **A new action does not run on a schedule until a `Core.CronJobs` row exists.** The schedule
|
|
313
|
+
registry is **data, not code** — shipping the PHP file changes nothing on its own. Ship the code
|
|
314
|
+
deploy and the `dbchanges2` cron-row migration together (see "Adding a new cron job" above).
|
|
315
|
+
- **For a code path gated behind a RARE condition, build a dry-run entry point and fire it
|
|
316
|
+
deliberately — do not wait for the path to fire naturally.** A dry run enqueued against production
|
|
317
|
+
found a fatal bug in ~2 seconds that two weeks of green telemetry had not: the path was reached
|
|
318
|
+
only when a NetSuite opportunity webhook carried a presales lead (83% return
|
|
319
|
+
`skipped (no presales lead)`; ~36 tasks in 14 days), so natural traffic would have hidden the
|
|
320
|
+
failure indefinitely. The dry run must exercise the **same helpers** as the real path — a separate
|
|
321
|
+
test harness that reimplements them proves nothing. See
|
|
322
|
+
[netsuite-opportunity-sync](./netsuite-opportunity-sync.md#gotchas--known-issues) for what it caught.
|
|
312
323
|
- See [architecture.md](../architecture.md) for the always-HTTP-200 rule and the
|
|
313
324
|
commit-before-SQS transaction pattern that the worker relies on.
|
|
314
325
|
|
|
315
326
|
## Change history
|
|
327
|
+
- 2026-08-26 — Added two operational gotchas: a new action **does not run on a schedule until its
|
|
328
|
+
`Core.CronJobs` row exists** (schedule is data, not code), and the practice of building a
|
|
329
|
+
**dry-run entry point for a rare-gated path** rather than waiting for it to fire — one found a
|
|
330
|
+
fatal `Logs.Api` insert bug in seconds after two weeks of green telemetry missed it. (ajean)
|
|
316
331
|
- 2026-08-13 — Added a **database-side test for "the tier stopped consuming"**, which resolves the
|
|
317
332
|
absent-`WorkerJobs`-row ambiguity without SQS metrics: compare created vs. completed
|
|
318
333
|
`Core.WorkerJobs` for the day — on dev-sandbox 2026-08-12, **1,548 created / 1,547 never
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: ClickUp Opportunity Client vs End-Customer Labels (Stakeholders / End Customer)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: ["ajean"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Netsuite/Opportunity.php
|
|
13
|
+
- worker2/Worker/Netsuite/OpportunityStakeholderDigest.php
|
|
14
|
+
- worker2/Worker/Netsuite/OpportunityStakeholderBackfill.php
|
|
15
|
+
related:
|
|
16
|
+
- ./netsuite-opportunity-sync.md
|
|
17
|
+
- ./creating-worker-actions.md
|
|
18
|
+
- ./notification-email.md
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Summary
|
|
22
|
+
|
|
23
|
+
The NetSuite→ClickUp opportunity task carries **two** customer labels, not one (TRUE-81010, PR #142):
|
|
24
|
+
|
|
25
|
+
- **Stakeholders** = the **client** — the consolidated parent company we contract with.
|
|
26
|
+
- **End Customer** = the NetSuite opportunity `entity` — the specific (often sub-)customer the work
|
|
27
|
+
is for.
|
|
28
|
+
|
|
29
|
+
Before this change the sync pushed `entity->refName` into **Stakeholders**, flattening the two
|
|
30
|
+
levels of the NetSuite customer hierarchy into one field. Verified against production that `entity`
|
|
31
|
+
is the **end customer**, and the client is the consolidated parent (NetSuite `custentity10`):
|
|
32
|
+
`Forecast.Customers` row `"6096:6308 Compass Group"` hangs off `Forecast.ConsolidatedCustomers`
|
|
33
|
+
`"Office Depot"` — Office Depot is who we bill, Compass Group is who receives.
|
|
34
|
+
|
|
35
|
+
Two support actions ship with it: a **daily digest** of label options still to create in ClickUp,
|
|
36
|
+
and a **seed-if-empty backfill** for the 761 tasks that already exist.
|
|
37
|
+
|
|
38
|
+
## Key files / entry points
|
|
39
|
+
|
|
40
|
+
- `worker2/Worker/Netsuite/Opportunity.php` — `deriveLabelNames()`, `splitCustomerRefName()`,
|
|
41
|
+
`buildCustomFields()`. The sync itself; see [netsuite-opportunity-sync](./netsuite-opportunity-sync.md).
|
|
42
|
+
- `worker2/Worker/Netsuite/OpportunityStakeholderDigest.php` — action
|
|
43
|
+
`Netsuite/OpportunityStakeholderDigest/Daily` (`dryRun=false`, `to=null`).
|
|
44
|
+
- `worker2/Worker/Netsuite/OpportunityStakeholderBackfill.php` — action
|
|
45
|
+
`Netsuite/OpportunityStakeholderBackfill/Backfill` (`dryRun=true`, `taskId=null`, `exactOnly=true`).
|
|
46
|
+
|
|
47
|
+
**ClickUp field ids** on list `901111987449` (Presales Qualification), both `labels` type with ~169
|
|
48
|
+
options each: `Stakeholders` = `ec15edfd-5920-4c12-a1dc-94ede4a62438`,
|
|
49
|
+
`End Customer` = `67bb359f-fa59-40e1-b94a-0076450bb7c0`.
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
**Resolution is entirely local — no extra NetSuite call.** The opportunity payload carries only a
|
|
54
|
+
flat `entity` ref, and `expandSubResources` does **not** dereference it to the consolidated parent.
|
|
55
|
+
So the client is resolved through Forecast:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
Opportunities.customerId → Customers.consolidatedCustomerId → ConsolidatedCustomers.name
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`deriveLabelNames()` then applies three rules, each sized against production data:
|
|
62
|
+
|
|
63
|
+
1. **No consolidated parent → the entity IS the client.** 186 of 759 task-bearing opportunities
|
|
64
|
+
(24.5%) have no parent. Stakeholders then receives exactly what it received before the change,
|
|
65
|
+
so the swap is not a regression for them.
|
|
66
|
+
2. **Mirror when equal.** For 678 of 761 the parent is the same company as the entity
|
|
67
|
+
(`"4679 Staples, Inc."` → `"Staples, Inc."`). Both fields carry the same value rather than one
|
|
68
|
+
being left blank — which makes an **empty field an unambiguous signal that resolution FAILED**,
|
|
69
|
+
and nothing else.
|
|
70
|
+
3. **Genuinely different parent** in only 83 of 573 parented opportunities (14.5%, 25 distinct
|
|
71
|
+
pairs) — the case the split exists for.
|
|
72
|
+
|
|
73
|
+
Net effect measured on production: Stakeholders exact-match rose from **31.5% to 36.4%** (47.0%
|
|
74
|
+
including fuzzy).
|
|
75
|
+
|
|
76
|
+
### Daily digest (`OpportunityStakeholderDigest/Daily`)
|
|
77
|
+
|
|
78
|
+
The originally-shipped per-opportunity "option missing" email does not survive real data — **402 of
|
|
79
|
+
759** task-bearing opportunities fail to resolve a client label, and a second field roughly doubles
|
|
80
|
+
that, i.e. hundreds of mails a day. It is replaced by **one spreadsheet each morning** to
|
|
81
|
+
`pmteam@togatech.com` listing every label option still to add, sorted by opportunity count.
|
|
82
|
+
|
|
83
|
+
**Stateless by design:** recomputed from live Forecast rows + live ClickUp options on every run. No
|
|
84
|
+
table, no migration, no reconciliation state — a row simply disappears from tomorrow's digest once
|
|
85
|
+
someone adds the option in ClickUp.
|
|
86
|
+
|
|
87
|
+
### Seed-if-empty backfill (`OpportunityStakeholderBackfill/Backfill`)
|
|
88
|
+
|
|
89
|
+
Existing tasks are never reached by the sync (see the claim gotcha below), so a separate backfill is
|
|
90
|
+
the only way to populate them.
|
|
91
|
+
|
|
92
|
+
- **Seed-if-empty, never overwrite.** A ClickUp `labels` write is **add-only and therefore
|
|
93
|
+
accumulates**: re-pointing an opportunity would leave the task carrying two clients with no signal
|
|
94
|
+
which is current. Seeding only empty fields also makes re-runs write nothing.
|
|
95
|
+
- **`exactOnly` defaults TRUE.** On live data `"Lee County Sheriff's Office"` scores **86.8%**
|
|
96
|
+
against the option `"Polk County Sheriff Office"` — over the 85% fuzzy floor, wrong county, 12
|
|
97
|
+
opportunities affected, and nothing would have surfaced it. Fuzzy matching is opt-in only.
|
|
98
|
+
- **Pacing is 700 ms per REQUEST, not per task** (~85/min against ClickUp's 100/min/token). A task
|
|
99
|
+
costs a variable 1–3 requests, so per-task pacing cannot bound the request rate.
|
|
100
|
+
|
|
101
|
+
## Gotchas / known issues
|
|
102
|
+
|
|
103
|
+
- **`splitCustomerRefName()` must handle composite sub-customer numbers.** The original pattern
|
|
104
|
+
`/^(\d+)\s+(.*)$/` (digits then whitespace) could not parse `"6096:6308 Compass Group"` — the colon
|
|
105
|
+
broke the match, the number stayed in the name, and normalization produced
|
|
106
|
+
`"60966308compassgroup"`, which can never match a ClickUp option. **15.4% of `Forecast.Customers`
|
|
107
|
+
use this composite form** (1,418 of 9,211) and they are precisely the hierarchical customers this
|
|
108
|
+
feature exists to distinguish. Now `/^(\d+(?::\d+)*)\s+(.*)$/` — still requiring the whitespace so
|
|
109
|
+
`"3M Company"` is not decapitated.
|
|
110
|
+
- **Option ids ride on `$body`, not on extra positional arguments.** `buildCustomFields()` used to
|
|
111
|
+
take the Stakeholders option id as an optional 3rd argument, but `updateTask()` called it as
|
|
112
|
+
`buildCustomFields($body, $presalesUserId)` — two arguments — so that path could never set the
|
|
113
|
+
field even if `UPDATE_CLICKUP` were true. Passing option ids on `$body` (the pattern
|
|
114
|
+
`opportunityStageOrderIndex` already used) makes the divergence structurally impossible and keeps
|
|
115
|
+
the signature under the 4-positional-parameter limit.
|
|
116
|
+
- **The empty field means "resolution failed" — that is the whole diagnostic.** Because of the
|
|
117
|
+
mirror-when-equal rule, a blank Stakeholders or End Customer is never "same as the other one";
|
|
118
|
+
it is always a missing ClickUp option or a failed lookup, and it will appear in the next digest.
|
|
119
|
+
- **The digest emails INLINE, it does not attach.** See
|
|
120
|
+
[notification-email](./notification-email.md#gotchas--known-issues) — the EmailTemplate action has
|
|
121
|
+
no attachment parameter and `_Email::addAttachment()` takes a **file path**, which an enqueued job
|
|
122
|
+
running later may no longer find.
|
|
123
|
+
- **CSV values beginning `=`, `+`, `-` or `@` are apostrophe-prefixed** against spreadsheet formula
|
|
124
|
+
injection before they go into the digest.
|
|
125
|
+
|
|
126
|
+
## Change history
|
|
127
|
+
- 2026-08-26 — **Built the client vs end-customer split (TRUE-81010, PR #142, merged).** Stakeholders
|
|
128
|
+
now receives the consolidated parent (the client, NS `custentity10`) and a new **End Customer**
|
|
129
|
+
field receives the opportunity `entity`; resolution is local via
|
|
130
|
+
`Opportunities.customerId → Customers.consolidatedCustomerId → ConsolidatedCustomers.name` with no
|
|
131
|
+
extra NetSuite call. Three mapping rules sized against production (no-parent → entity is the
|
|
132
|
+
client, 24.5%; mirror-when-equal, 678/761; genuinely-different parent, 83/573) lifted Stakeholders
|
|
133
|
+
exact-match 31.5% → 36.4%. Fixed `splitCustomerRefName()` to parse composite sub-customer numbers
|
|
134
|
+
(`6096:6308 …`, 15.4% of Forecast.Customers). Moved option ids onto `$body` to kill a
|
|
135
|
+
`buildCustomFields()` optional-argument divergence. Added the daily
|
|
136
|
+
`OpportunityStakeholderDigest` (one spreadsheet to pmteam@ replacing hundreds of per-miss mails;
|
|
137
|
+
stateless) and the `OpportunityStakeholderBackfill` (seed-if-empty, `exactOnly` default true,
|
|
138
|
+
700 ms per-request pacing). (ajean)
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["dfranks", "kyalamarthi"]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: ["dfranks", "kyalamarthi", "ajean"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite.php
|
|
13
13
|
- worker2/Worker/Netsuite/Opportunity.php
|
|
@@ -29,6 +29,7 @@ files:
|
|
|
29
29
|
- test/@dave/approach/TRUE-80044.md
|
|
30
30
|
- worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php
|
|
31
31
|
related:
|
|
32
|
+
- ./netsuite-opportunity-client-labels.md
|
|
32
33
|
- ./netsuite-salesorder-open-orders-sync.md
|
|
33
34
|
- ../architecture.md
|
|
34
35
|
---
|
|
@@ -152,6 +153,10 @@ Shared helpers: `buildCustomFields()` (the field array, used by both create and
|
|
|
152
153
|
`Opportunity #` (`a5529cdc-…`) ← `tranId`; `Customer #` (`170dc118-…`) ← `entity->refName`
|
|
153
154
|
(the customer **name**, deliberately — not the NetSuite customer number). Plus `Sales Rep`,
|
|
154
155
|
`Presales Lead Email`, and `Presales Lead` (users field).
|
|
156
|
+
**Since 2026-08-26 the two customer *labels* fields are a separate subject** — `Stakeholders`
|
|
157
|
+
carries the **client** (the consolidated parent) and `End Customer` carries the opportunity
|
|
158
|
+
`entity`. See [ClickUp Opportunity Client vs End-Customer Labels](./netsuite-opportunity-client-labels.md);
|
|
159
|
+
do not re-derive that mapping here.
|
|
155
160
|
- The ClickUp token comes from `[clickup] token` in the worker2 config (legacy webhook received
|
|
156
161
|
it on the payload; the new contract drops it).
|
|
157
162
|
- Every ClickUp `_ApiRequest` calls `setLogging(self::CLICKUP_API_LOGGING_ENABLED=false)`, so ClickUp
|
|
@@ -168,11 +173,11 @@ Shared helpers: `buildCustomFields()` (the field array, used by both create and
|
|
|
168
173
|
removing that `setLogging(false)` is under review (handle the 20–50 KB payload volume the
|
|
169
174
|
prod-appropriate way — retention/sampling/summary — not by defaulting logging off).
|
|
170
175
|
|
|
171
|
-
## Atomic ClickUp-task claim (TRUE-80044 —
|
|
176
|
+
## Atomic ClickUp-task claim (TRUE-80044 — BUILT, in production)
|
|
172
177
|
|
|
173
178
|
Concurrent NetSuite opportunity webhooks (a real edit racing NetSuite's hourly `OPP` fallback) can both
|
|
174
179
|
pass the non-atomic find-then-create in `maybeCreateClickupTask()` and create **duplicate** ClickUp
|
|
175
|
-
tasks. The fix is a **race-safe DB claim** on the **existing `Forecast.Opportunities` table** — an added
|
|
180
|
+
tasks. The fix — **now built and running in production** — is a **race-safe DB claim** on the **existing `Forecast.Opportunities` table** — an added
|
|
176
181
|
`clickupTaskId` column — **not** a new dedicated table. (A prior plan proposed a new
|
|
177
182
|
`NetsuiteOpportunityClickupTask` table in `Client_True`; reviewer Rohan Girish rejected that and mandated
|
|
178
183
|
reusing `Forecast.Opportunities`.)
|
|
@@ -198,9 +203,24 @@ How the claim is race-safe:
|
|
|
198
203
|
- **Edge case:** if `importOpportunity()` was skipped (no NS status → no Forecast row), there is nothing
|
|
199
204
|
to claim; that rare path falls back to today's unguarded create.
|
|
200
205
|
|
|
201
|
-
|
|
206
|
+
Required a new `dbchanges2/Forecast/` ALTER adding the `clickupTaskId` column, and a
|
|
202
207
|
`_underscore/Model/Forecast/Opportunity.php` field addition. Plan: `test/@dave/approach/TRUE-80044.md`.
|
|
203
208
|
|
|
209
|
+
### Consequence: `updateTask()` is unreachable — `UPDATE_CLICKUP` alone does NOT revive it
|
|
210
|
+
|
|
211
|
+
`maybeCreateClickupTask()` claims the row **before** it ever looks at ClickUp
|
|
212
|
+
(`Opportunity.php:40`): `UPDATE Opportunities SET clickupTaskId='PENDING' WHERE … AND clickupTaskId
|
|
213
|
+
IS NULL`. Once `clickupTaskId` holds a real id, **every later webhook for that opportunity returns
|
|
214
|
+
`skipped (already claimed)` and exits** — it never reaches the find-then-update branch.
|
|
215
|
+
|
|
216
|
+
So `updateTask()` is dead **twice over**: behind `UPDATE_CLICKUP = false` *and* behind the claim.
|
|
217
|
+
Flipping the constant activates ~180 lines of code that have **never run in production** and still
|
|
218
|
+
reaches **none** of the 761 existing tasks — only a separate backfill action reaches those (see
|
|
219
|
+
[client vs end-customer labels](./netsuite-opportunity-client-labels.md#seed-if-empty-backfill-opportunitystakeholderbackfillbackfill)).
|
|
220
|
+
`UPDATE_CLICKUP` stays `false`. **This is a trap for the next developer:** the constant's own comment
|
|
221
|
+
says nothing about the claim interaction, so reading the constant alone gives you the wrong model of
|
|
222
|
+
what enabling it would do.
|
|
223
|
+
|
|
204
224
|
## Data model
|
|
205
225
|
|
|
206
226
|
Writes `Forecast.Opportunities` (header) + `Forecast.OpportunityItems` (children), faithful to
|
|
@@ -340,8 +360,8 @@ None — platform-wide Forecast sync.
|
|
|
340
360
|
(create branch, new task `868k2rfj8`).
|
|
341
361
|
- **ClickUp dedup keys on the `Opportunity #` field, not a DB column.** Historically we did NOT add
|
|
342
362
|
a `clickupTaskId` column — the dedup lookup queried ClickUp itself by `Opportunity #` (= tranId).
|
|
343
|
-
**This
|
|
344
|
-
claim"
|
|
363
|
+
**This has been replaced (TRUE-80044) by the race-safe DB-claim approach — see "Atomic ClickUp-task
|
|
364
|
+
claim" above.** Consequences of the field-lookup approach to know:
|
|
345
365
|
- **Legacy tasks had `Opportunity #` empty** (the old `webhook/` handler never set it), so they
|
|
346
366
|
can't be matched until backfilled — see the backfill tool below. Without backfill, the first
|
|
347
367
|
edit of a pre-existing opp creates a *second* task.
|
|
@@ -422,6 +442,33 @@ None — platform-wide Forecast sync.
|
|
|
422
442
|
(doubled-prefix ids — see the doubled-id gotcha), which are **SuiteQL-queryable** for after-the-fact
|
|
423
443
|
diagnosis even though the calling-script log rolls off.
|
|
424
444
|
|
|
445
|
+
- **`logged()` could never insert a `Logs.Api` row — and it failed the whole feature SILENTLY.**
|
|
446
|
+
`Logs.Api` has NOT-NULL columns with **no defaults**. The `logged()` helper set
|
|
447
|
+
`direction`/`source`/`method`/`route` but never `isAuthRequest`, `instanceId` or `hostname`, so
|
|
448
|
+
**every** insert threw `Column 'X' cannot be null` and the exception propagated out of the wrapped
|
|
449
|
+
call. (`_ApiRequest`'s own auto-logging sets all three — `ApiRequest.php:260-264` — which is why
|
|
450
|
+
the omission is invisible when you compare against normal api-logged traffic.) The blast radius was
|
|
451
|
+
far wider than one failing cron: the NS→CU webhook path calls this inside
|
|
452
|
+
`try/catch(Throwable)`, so the whole TRUE-81010 label feature was a **permanent no-op that reported
|
|
453
|
+
success** — tasks created, both label fields empty, `(label lookup failed)` in the output, every
|
|
454
|
+
job green. **The identical omission existed in `_Worker_Clickup_Opportunity::logged()`**, where the
|
|
455
|
+
helper was ported from — fixed there too (PRs #150 + #151). Evidence it never once worked: since
|
|
456
|
+
1 July `Logs.Api` held exactly two `source` values — `NULL` (1,413,654 rows, the `_ApiRequest`
|
|
457
|
+
auto-log path) and `OPEN_ORDER_IMPORT` (87,476). **Zero** `clickup-opportunity` rows, i.e. the
|
|
458
|
+
CU→NS sync's outbound logging has been dead since it shipped and nobody noticed. See
|
|
459
|
+
[`_ApiRequest`](../../_underscore/features/apirequest-json-content-type.md#gotchas--known-issues)
|
|
460
|
+
for the full manual-insert column contract.
|
|
461
|
+
- **When porting a helper, port its correctness too.** A faithful copy of broken code is still
|
|
462
|
+
broken, and a swallowed exception hides it indefinitely — that is exactly how one missing-column
|
|
463
|
+
bug ended up in two handlers and survived two weeks of green telemetry.
|
|
464
|
+
- **Do NOT reach for `_Component_Api_Clickup` for new work.** A live ClickUp API token is
|
|
465
|
+
**hardcoded in committed source** at `_underscore/Component/Api/Clickup/Clickup.php:5`, and because
|
|
466
|
+
`send()` never disables `_ApiRequest`'s auto-logging, that `Authorization` header has been
|
|
467
|
+
persisted into `Logs.Api.requestHeaders` in **plaintext on every call** (`ApiRequest.php:267`).
|
|
468
|
+
11 worker2 files use the component. The TRUE-81010 work deliberately routed around it with an
|
|
469
|
+
inline `_ApiRequest`. **Needs its own ticket:** rotate the token and move it to config. (Never
|
|
470
|
+
paste the value anywhere, including here.)
|
|
471
|
+
|
|
425
472
|
## CU→NS direction (BUILT 2026-06-30, TRUE-79181): reverse sync
|
|
426
473
|
|
|
427
474
|
The integration is now **bidirectional** for **field + stage**. ClickUp task edits flow back to the
|
|
@@ -502,6 +549,18 @@ deprecated** for production opportunity code.
|
|
|
502
549
|
blocked on the Aaron stakeholder decision noted above.
|
|
503
550
|
|
|
504
551
|
## Change history
|
|
552
|
+
- 2026-08-26 — **Fixed `logged()` (PRs #150 + #151) and confirmed the atomic claim makes
|
|
553
|
+
`updateTask()` unreachable.** `logged()` omitted the NOT-NULL `isAuthRequest`/`instanceId`/
|
|
554
|
+
`hostname` columns, so every `Logs.Api` insert threw and — inside the webhook path's
|
|
555
|
+
`catch(Throwable)` — turned the whole TRUE-81010 label feature into a silent no-op that reported
|
|
556
|
+
success; the same omission was fixed in `_Worker_Clickup_Opportunity::logged()`, whose outbound
|
|
557
|
+
logging had been dead since it shipped (zero `clickup-opportunity` rows in `Logs.Api`). Also
|
|
558
|
+
recorded that `maybeCreateClickupTask()` claims `clickupTaskId` **before** consulting ClickUp
|
|
559
|
+
(`Opportunity.php:40`), so `updateTask()` is dead twice over — flipping `UPDATE_CLICKUP` activates
|
|
560
|
+
never-run code and still reaches none of the 761 existing tasks. Flagged the hardcoded ClickUp
|
|
561
|
+
token in `_underscore/Component/Api/Clickup/Clickup.php` (plaintext `Authorization` persisted to
|
|
562
|
+
`Logs.Api.requestHeaders`) for a dedicated security ticket. The Stakeholders/End Customer label
|
|
563
|
+
mapping moved to its own doc. (ajean)
|
|
505
564
|
- 2026-08-19 — Recorded a second, worse instance of the "`Released` ≠ active" enqueuer-deployment failure
|
|
506
565
|
family: the AMQ enqueuer is deployed **per record type**, and **Customer** (`customdeploy9`, deployment
|
|
507
566
|
internal id 9) was still at `status = TESTING` / `isdeployed = F` while the other 15 types were
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["mhammontree", "jcardinal"]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: ["mhammontree", "jcardinal", "ajean"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Notification/Email.php
|
|
13
13
|
- _underscore/Model/Client/EmailTemplate.php
|
|
@@ -140,10 +140,27 @@ working recipe — verified in classic + new Outlook:
|
|
|
140
140
|
- **Missing wrapper row → unbranded send, logged.** If a client has no active wrapper row the alert
|
|
141
141
|
still goes out (raw body) and an `error_log` line records the missing uuid. Branding is *not*
|
|
142
142
|
guaranteed on this path by design (internal mail). Don't rely on it for client-facing email.
|
|
143
|
+
- **There is no attachment path on the worker email actions — send the payload INLINE.**
|
|
144
|
+
`_Worker_Notification_EmailTemplate::Send` has **no attachment parameter**, and
|
|
145
|
+
`_Email::addAttachment()` takes a **file path**, which an enqueued job running minutes later on a
|
|
146
|
+
different instance may no longer be able to find. A generated report (e.g. a CSV digest) should be
|
|
147
|
+
rendered into the body via `_Worker_Notification_Email::Send` rather than attached. Proven by
|
|
148
|
+
`Worker/Netsuite/OpportunityStakeholderDigest.php` — see
|
|
149
|
+
[ClickUp opportunity client labels](./netsuite-opportunity-client-labels.md).
|
|
150
|
+
- **Commit your own DB work BEFORE calling `Send()`.** `_Email` registers the client-log DB and
|
|
151
|
+
toggles the read host as a side effect of sending; doing that inside your still-open transaction
|
|
152
|
+
is asking for trouble. A worker that has been writing to, say, `DB_FORECAST` should
|
|
153
|
+
`_Database::transactionCommit(DB_FORECAST)` first, then send.
|
|
143
154
|
- **`{subject}`/`{body}` substitution is `strtr`, not sequential `str_replace`.** All placeholders
|
|
144
155
|
are replaced simultaneously, so a `{body}` literal inside the subject can't be re-expanded.
|
|
145
156
|
|
|
146
157
|
## Change history
|
|
158
|
+
- 2026-08-26 — Documented (no code change) that **neither worker email action can attach a file** —
|
|
159
|
+
`EmailTemplate::Send` has no attachment parameter and `_Email::addAttachment()` takes a file path
|
|
160
|
+
an enqueued job may not still have — so generated reports go **inline in the body**; and that a
|
|
161
|
+
caller must **commit its own transaction before `Send()`**, because `_Email` registers the
|
|
162
|
+
client-log DB and toggles the read host mid-send. Surfaced building the daily opportunity
|
|
163
|
+
stakeholder digest. (ajean)
|
|
147
164
|
- 2026-08-04 (**uncommitted/undeployed** at time of writing) — Added an optional trailing
|
|
148
165
|
`?int $priority = null` to `Send()`, mapping to `_Email::setPriority()`
|
|
149
166
|
(`X-Priority`/`Importance`/`X-MSMail-Priority`). Backward compatible because the dispatcher
|
|
@@ -5,8 +5,8 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-
|
|
9
|
-
owners: [jcardinal]
|
|
8
|
+
updated: 2026-08-26
|
|
9
|
+
owners: [jcardinal, apeterson]
|
|
10
10
|
related:
|
|
11
11
|
- ../apps/toga25-supply/workflows/amplify-deployment.md
|
|
12
12
|
- ../apps/toga-blox/workflows/dynamic-publish-pipeline.md
|
|
@@ -50,7 +50,12 @@ strict 1:1). When an app does this:
|
|
|
50
50
|
hardcoded per-branch list, so one `amplify.yml` serves every environment.
|
|
51
51
|
- `--legacy-peer-deps` may be required when the app's and library's peer ranges diverge; when it
|
|
52
52
|
is used, npm will not auto-install the library's peers, so any peer not already in the app's
|
|
53
|
-
dependencies must be added explicitly.
|
|
53
|
+
dependencies must be added explicitly. **Set it in the app's committed `.npmrc`
|
|
54
|
+
(`legacy-peer-deps=true`), not only as a CLI flag in `amplify.yml`** — otherwise a developer's
|
|
55
|
+
local `npm i` fails `ERESOLVE` while CI succeeds. When one app installs the shared lib cleanly and
|
|
56
|
+
a sibling does not at the same lib and peer versions, **diff the two `.npmrc` files before
|
|
57
|
+
touching any peer range** (verified 2026-08-26: `toga25-supply` had the flag, `toga2-commerce` did
|
|
58
|
+
not; commerce's `.npmrc` fix is working-tree on `TRUE-80707`, not yet merged).
|
|
54
59
|
|
|
55
60
|
## Build memory / heap
|
|
56
61
|
|
|
@@ -5,8 +5,8 @@ project: _Underscore
|
|
|
5
5
|
client: shared
|
|
6
6
|
type: standard
|
|
7
7
|
status: active
|
|
8
|
-
updated: 2026-08-
|
|
9
|
-
owners: [jcardinal]
|
|
8
|
+
updated: 2026-08-26
|
|
9
|
+
owners: [jcardinal, apeterson]
|
|
10
10
|
files: []
|
|
11
11
|
related:
|
|
12
12
|
- ../apps/toga25-supply/architecture.md
|
|
@@ -352,6 +352,13 @@ memory.
|
|
|
352
352
|
Supply dedupes the table lib (it consumes blox's table context); desk dedupes react-hook-form (it
|
|
353
353
|
consumes blox's `BaseInput`, which reads `useFormContext()`). See §13(c) for why a second copy
|
|
354
354
|
silently breaks blox's hooks.
|
|
355
|
+
|
|
356
|
+
Verified 2026-08-26: `toga2-commerce` had **no** `dedupe` at all and now declares
|
|
357
|
+
`["react","react-dom","react-hook-form","react-router-dom","framer-motion","@tanstack/react-query","@tanstack/react-table"]`
|
|
358
|
+
(working-tree on branch `TRUE-80707`, not yet merged). The concrete hazard: `toga-blox-npm/node_modules/`
|
|
359
|
+
ships its own `react@18.3.1`/`react-dom@18.3.1`, so consuming blox through a **local symlink**
|
|
360
|
+
resolves React to blox's own copy. Dedupe is what makes the symlink workflow safe — it is not
|
|
361
|
+
optional polish.
|
|
355
362
|
- **MUST: one env-read chokepoint.** Read `VITE_API` in exactly **one** module (see §17). Verified:
|
|
356
363
|
both apps read it (plus a per-host override) only in `src/api/api.ts`.
|
|
357
364
|
- **SHOULD: ESLint flat config**, verified identical in both apps:
|
|
@@ -412,12 +419,18 @@ on the back end. A consuming app **MUST** meet every requirement below.
|
|
|
412
419
|
**(a) Pin an exact published version — never a caret.** Some published versions ship an **empty
|
|
413
420
|
`dist`** and break the build; a caret can float you onto one. Both apps pin exact versions
|
|
414
421
|
(verified 2026-08-18: supply `1.0.330-sandbox-client.128`, desk `1.0.328-sandbox-client.125`).
|
|
422
|
+
Verified 2026-08-26: `toga2-commerce` violates this with `"^1.0.322-sandbox-client.117"`. Also audit
|
|
423
|
+
for **pin drift** between branches (commerce's `TRUE-80707` was 11 versions behind `_sandbox-client`).
|
|
415
424
|
|
|
416
425
|
**(b) Declare blox's peer deps that do not hoist into your app.** blox lists them as peers, so npm
|
|
417
426
|
will not necessarily install them for you: verified 2026-08-18 they include `react-hook-form`,
|
|
418
|
-
`@tanstack/react-query`, `@tanstack/react-table`, `framer-motion`,
|
|
419
|
-
|
|
420
|
-
|
|
427
|
+
`@tanstack/react-query`, `@tanstack/react-table`, `framer-motion`, and `axios` (pinned exactly by
|
|
428
|
+
blox at `1.8.4`). **`react-router-dom` was removed as a peer on 2026-08-26** — it was phantom (its
|
|
429
|
+
only production reference was a dead `Link` import in `old/BaseButton`); *(change staged on blox
|
|
430
|
+
`feature-new-table`, not yet merged/published — verify against the installed version before relying
|
|
431
|
+
on it)*. A missing peer surfaces as a runtime "undefined is not a component," **or**, because a
|
|
432
|
+
**barrel import pulls blox's whole index**, as a Rollup `failed to resolve import` for a component
|
|
433
|
+
you never referenced.
|
|
421
434
|
|
|
422
435
|
**(c) Dedupe `react` / `react-dom` (+ `react-hook-form` / `@tanstack/react-table`) to a single
|
|
423
436
|
copy.** blox is shipped **unbundled** — its `build` is `tsc && copyfiles … && fix-esm-imports`
|
|
@@ -575,7 +588,11 @@ Work **around** these shared-lib defects; do **not** imitate them in app code:
|
|
|
575
588
|
- **Two select stacks** coexist (`react-select` + a custom `AdvancedSelect`) and **two table
|
|
576
589
|
stacks** (`react-table` v7 + `@tanstack/react-table` v8) — verified 2026-08-18 in blox's peer
|
|
577
590
|
deps. Know which one a given component uses before you wire it.
|
|
578
|
-
- Phantom/unused peer deps in blox's manifest
|
|
591
|
+
- **Phantom/unused peer deps in blox's manifest** — confirmed and one now removed:
|
|
592
|
+
`react-router-dom` (dead `Link` import in `old/BaseButton`; removal staged 2026-08-26 on
|
|
593
|
+
`feature-new-table`, not yet merged/published — it stays a devDependency for the tests'
|
|
594
|
+
`MemoryRouter`). Verify a declared peer is actually reachable in `src/` before adding it to your
|
|
595
|
+
app or reaching for `--legacy-peer-deps`.
|
|
579
596
|
- Supply/commerce business logic has leaked into the shared lib.
|
|
580
597
|
- **No custom-validator passthrough on `BaseInput`** — hence the app-side validator layer in §7.
|
|
581
598
|
- A **~85-prop className-string styling contract on `BaseInput`** — verbose by design; drive it from
|
|
@@ -624,6 +641,11 @@ app work.** In a consuming app, guard against them and move on.
|
|
|
624
641
|
|
|
625
642
|
## Change history
|
|
626
643
|
|
|
644
|
+
- 2026-08-26 — Recorded commerce's missing `resolve.dedupe` (+ the blox-symlink second-React
|
|
645
|
+
hazard), its caret blox pin and cross-branch pin drift, the barrel-import missing-peer trigger,
|
|
646
|
+
and the removal of blox's phantom `react-router-dom` peer. All the underlying code changes are
|
|
647
|
+
working-tree only (blox `feature-new-table`, commerce `TRUE-80707`) — unmerged and unpublished.
|
|
648
|
+
(apeterson)
|
|
627
649
|
- 2026-08-18 — Initial front-end (React) coding standard: universal React/TS layer + TOGA 2.0
|
|
628
650
|
platform-conventions layer, mined from @agilant/toga-blox, toga25-supply, and toga25-desk; per-rule
|
|
629
651
|
convergence tagging; testing/a11y/error-reporting/React-19 flagged as open. (jcardinal)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ — 67 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
|
-
- **worker2** (Worker) —
|
|
22
|
+
- **worker2** (Worker) — 57 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)_ — 9 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
25
25
|
- **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -31,7 +31,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
31
31
|
- **ai-bdr** (AI-BDR) — 13 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
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) —
|
|
34
|
+
- **toga-blox** (TOGa Blox) — 11 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
|
|
@@ -6,8 +6,8 @@ project: Claude Harness
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["jcardinal", "tcox"]
|
|
9
|
+
updated: 2026-08-26
|
|
10
|
+
owners: ["jcardinal", "tcox", "ajean"]
|
|
11
11
|
files:
|
|
12
12
|
- claude/.claude/skills/kickoff/SKILL.md
|
|
13
13
|
- claude/.claude/skills/capture/SKILL.md
|
|
@@ -138,6 +138,18 @@ data is missing. Query **`dev-sandbox`** whenever someone says "beta". Any front
|
|
|
138
138
|
onboarding SQL shows beta on a us-west-2 `client-cluster`; that has since been consolidated —
|
|
139
139
|
trust `Core.DatabaseHosts`, not old migrations.)
|
|
140
140
|
|
|
141
|
+
**⚠ The write-blocker is a naive string scan — it rejects `UPDATE`/`REPLACE` even inside a string
|
|
142
|
+
literal.** `toga_query` refuses the whole statement if those words appear anywhere in it, so a
|
|
143
|
+
perfectly read-only query like
|
|
144
|
+
`… WHERE output LIKE '%skipped update%'` fails validation. Work around it by matching on a fragment
|
|
145
|
+
that avoids the keyword (`LIKE '%skipped%'`), or by pulling the rows and filtering client-side —
|
|
146
|
+
don't conclude the data isn't there.
|
|
147
|
+
|
|
148
|
+
**Production sessions render in US/Central (UTC−5); most other timestamps you're comparing are
|
|
149
|
+
UTC.** GitHub/API/webhook timestamps are UTC, so a naive `dtStamp` vs. commit-time comparison is off
|
|
150
|
+
by five hours — enough to make a healthy worker fleet look dead. Anchor both sides to UTC (or
|
|
151
|
+
`CONVERT_TZ`) before drawing a conclusion about "when did this last run".
|
|
152
|
+
|
|
141
153
|
**`local` is not this MCP.** If someone means their own machine's database, use the `mysql` CLI
|
|
142
154
|
via shell, not `toga-db`.
|
|
143
155
|
|
|
@@ -183,5 +195,10 @@ Schema names are **case-sensitive** — copy the exact value from `toga_list_sch
|
|
|
183
195
|
and why. Database → what is actually true right now.
|
|
184
196
|
|
|
185
197
|
## Change history
|
|
198
|
+
- 2026-08-26 — Added two `toga-db` query traps: the write-blocker is a **naive string scan** that
|
|
199
|
+
rejects any statement containing `UPDATE`/`REPLACE` **even inside a string literal** (so
|
|
200
|
+
`LIKE '%skipped update%'` fails validation on a read-only SELECT), and **production DB sessions
|
|
201
|
+
render in US/Central (UTC−5)** while GitHub/API/webhook timestamps are UTC — the mismatch made a
|
|
202
|
+
healthy worker fleet look dead. (ajean)
|
|
186
203
|
- 2026-08-06 — Recorded that **"beta" resolves to the `dev-sandbox` cluster** for client databases (`Core.DatabaseHosts` env `beta` → the sandbox-dev host) while the MCP's `client-beta` environment exposes no schemas — query `dev-sandbox` when someone says beta. Cost real debugging time while onboarding Elite to the supply2 frontend (tcox)
|
|
187
204
|
- 2026-07-30 — Created. Established that "ask/use Talos" means the Internal Knowledge Base MCP and not the `talos` repo; that Claude runs its own SELECTs unprompted while developers execute writes; and that both MCP connections are used proactively without being requested. Recorded the KB↔registry client-name divergence and the `Core.Clients` identifier traps (jcardinal)
|
package/package.json
CHANGED