toga-ai 1.0.951 → 1.0.952

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,8 +6,8 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-09-16
10
- owners: [apeterson]
9
+ updated: 2026-10-05
10
+ owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga-blox/package.json
13
13
  - toga-blox/src/components/index.ts
@@ -29,7 +29,7 @@ Shared React+TS component/template/API library (`@agilant/toga-blox`) reused by
29
29
 
30
30
  `toga-blox` (npm package **`@agilant/toga-blox`**) is the shared React + TypeScript component / template / utility library reused across the 2.0 frontends: `toga2-view`, `toga2-supply`, `toga25-supply`, `toga2-commerce`, `toga2-hub`. Not a PHP framework repo — no `_underscore` backend. Filed under `2.0/` because every consumer is a 2.0 app, but structurally a standalone front-end package (`dependsOn: []`).
31
31
 
32
- **New-platform apps (toga25-desk, toga25-iq, toga2-template, future apps) do not use this repo — they use [toga25-blox](../toga25-blox/architecture.md), a source-shared (git, not npm) replacement.**
32
+ **Legacy — fixes only.** No new components and no Storybook work here. [toga25-blox](../toga25-blox/architecture.md) (`@toga/blox`, source-shared) replaces this library for **all** apps, eventually: new platform apps first, then these consumers. When an app moves, rewrite onto toga25-blox pieces — do not port this code wholesale. New shared UI goes to toga25-blox.
33
33
 
34
34
  Ships: presentational + data-aware **components** (`src/components/`), **templates** wiring components into ready screens (`src/templates/`, e.g. `PrimaryTable`, `Login`, `AddNotes`), a thin **API client** over axios + React Query (`src/api/`, `src/reactQuery/`), and **utilities** (`src/utils/`). Builds with `tsc` to `dist/` (not bundled), develops via Storybook, tests with Vitest.
35
35
 
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-05
10
10
  owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga-blox/src/components/BaseButton
@@ -74,6 +74,21 @@ Defect 4 is about swapping variants; this is the shape of the same problem for a
74
74
  - **Consumer symptom.** One page toolbar renders its buttons at three different heights, and the developer cannot fix it from `themeConfig.json` — the number they need is not exposed. Nothing in the theme layer hints geometry is off-limits, so time is lost hunting the app's own CSS.
75
75
  - **Fix direction.** Same as defect 5: convert both to `var(--baseButton-height, <existing literal>)` (and give `.primaryBtn` the same base height), non-breaking because the fallback preserves today's look. Until then a consumer must own the chrome: `toga25-supply` built its own `ToolbarButton` rather than retheme tokens shared by ~all buttons in the app — see [toolbar button chrome](../../toga25-supply/features/toolbar-button-chrome.md).
76
76
 
77
+ ### Tooling defects (found 2026-09-28, Storybook/Docker audit)
78
+
79
+ **7. Storybook addons in `dependencies` — they install into every consumer**
80
+ - **Mechanism.** `@storybook/addon-docs`, `@storybook/addon-viewport`, `storybook-addon-react-router-v6` sit in `dependencies`, not `devDependencies`.
81
+ - **Consumer symptom.** Every app installing `@agilant/toga-blox` pulls Storybook packages it never uses (install weight, peer noise).
82
+ - **Fix direction.** Move them to `devDependencies`. Non-breaking.
83
+
84
+ **8. Storybook (7.6.24) is local-only and renders unstyled**
85
+ - Never deployed. Defines no host tokens (frontend §13e), so components render unstyled. Many exported components and all 3 templates have no story. `addon-onboarding` is configured but not installed.
86
+ - **Fix direction.** None planned — Storybook investment goes to [toga25-blox](../../toga25-blox/architecture.md), not here. To verify a CSS change, see the Storybook gotcha in [table](table.md).
87
+
88
+ **9. Docker files call scripts that do not exist**
89
+ - `docker-compose` runs `npm run storybook` (the script is `npm run blox`); the `Dockerfile` `CMD` runs `npm run start` (no such script). Neither container works as written.
90
+ - **Fix direction.** Point them at real scripts, or delete them.
91
+
77
92
  ### Already-known authoring items (carried over from §22)
78
93
 
79
94
  Long-standing, previously recorded only as consumer-side advice:
@@ -89,6 +104,9 @@ Long-standing, previously recorded only as consumer-side advice:
89
104
  - **Do not "fix" these in an app.** Working around them in a consumer is §22's job and is legitimate; changing app code to *imitate* the defect is not.
90
105
  - **A fix here is a fleet-wide change.** blox is unbundled and consumed by ~40 call sites; anything touching auth or `localStorage` keys changes live session behavior on the next channel head a consumer picks up — and `toga2-commerce` picks up the channel head **automatically at build time**, with no PR. Sequence auth fixes deliberately.
91
106
 
107
+ ## Change history
108
+ - 2026-10-05 — added tooling defects 7–9 (Storybook addons in `dependencies`, unstyled local-only Storybook, broken Docker scripts) (tcox)
109
+
92
110
  ## Related
93
111
 
94
112
  - [frontend standard §22](../../../standards/frontend.md) — the app-side workarounds this backlog is the counterpart to.
@@ -6,8 +6,8 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-23
10
- owners: [apeterson]
9
+ updated: 2026-10-05
10
+ owners: [apeterson, tcox]
11
11
  files:
12
12
  - toga-blox/src/components/EnvironmentBadge/EnvironmentBadge.tsx
13
13
  - toga-blox/src/components/EnvironmentBadge/EnvironmentBadge.css
@@ -64,7 +64,9 @@ Classifies `VITE_API` into `production` | `local` | `unknown` (plus the existing
64
64
  - **A CSS class typo silently unstyles the badge.** `.env-localost` (missing `h`) shipped for a long time; the localhost badge rendered as plain text and nobody noticed. Class names must match the endpoint strings the classifier returns — add both together.
65
65
  - **Testing `import.meta.env`:** use `vi.stubEnv("MODE", …)` / `vi.stubEnv("VITE_API", …)`. To set the hostname without navigating jsdom, redefine `window.location` with `Object.defineProperty`.
66
66
  - **Consumers import it in ways that matter.** A barrel `import { EnvironmentBadge } from "@agilant/toga-blox"` drags in unrelated peers at build time; older published versions exported it as **default only**. See [toga2-commerce Amplify build & deploy](../../toga2-commerce/workflows/amplify-build-and-deploy.md).
67
+ - **toga2-supply does NOT use this badge.** It mounts its own copy (`toga2-supply/src/components/layout/Header/EnvironmentBadge.tsx` + `src/utils/getEndpointFromEnvironment.ts`), written because older blox mis-labelled unknown API hosts. A change to the hide rule or classifier here does not reach toga2-supply — change both, or move toga2-supply back to blox's badge.
67
68
  - Do not remove the badge from an app to "clean up" — it is a deliberate QA tool.
68
69
 
69
70
  ## Change history
71
+ - 2026-10-05 — noted toga2-supply's app-local copy of the badge + classifier (tcox)
70
72
  - 2026-09-23 — Hostname now overrides the hide rule (`isNonProductionHost`); endpoint classifier matches `production` explicitly and adds `local`/`unknown`; fixed `.env-localost` typo, added `.env-local`/`.env-unknown`; 24 tests added (apeterson)
@@ -6,7 +6,7 @@ project: TOGa Blox
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-28
9
+ updated: 2026-10-05
10
10
  owners: [apeterson, tcox, bala]
11
11
  files:
12
12
  - toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts
@@ -121,7 +121,7 @@ This exists because an unscoped `GET /{recordRoute}` returns *every* row of a lo
121
121
  - **A filter popover's "Clear" must call `onClear()` — resetting local state only leaves the table filtered.** `HeaderFilterMultiselect.handleClear` did just `setSelectedOptions([])`, so the URL param (owned by the consuming app) and the applied filter survived: the popover showed nothing selected while the table stayed filtered. Fixed 2026-09-28 — it now calls `onClear()` and closes, **guarded by `if (filterValue)`**. The guard matters: `onClear` routes through `PrimaryTableHeaderCell` → `table.options.onColumnFiltersChange` → toga25-supply `useServerTableUrlState`, which resets to **page 1 on any filter change** — so an unguarded Clear with nothing applied would bounce the user off their page.
122
122
  - **⚠ OPEN — `HeaderFilterRange.handleClear` has the same bug** (~line 162): it resets `singleValue`/`minValue`/`maxValue` but never calls `onClear()` and never closes the popover. Same fix shape as the multi-select. Not fixed as of 2026-09-28.
123
123
  - **⚠ OPEN — `SortIcon` has no `aria-*` at all.** Sort direction and position are conveyed by shape only — no `aria-label`, no `aria-sort`. Pre-existing; needs its own ticket.
124
- - **A unit test cannot prove a CSS change — verify in blox's own Storybook.** JSDOM applies no real layout, so a Vitest test can only assert the modifier class is present; it can never show the triangle actually flipped or that the digit stayed upright. Add a co-located `*.stories.tsx` rendering the states side by side, run `npm run blox` (port 6006), and look at it.
124
+ - **A unit test cannot prove a CSS change — verify in blox's own Storybook.** JSDOM applies no real layout, so a Vitest test can only assert the modifier class is present; it can never show the triangle actually flipped or that the digit stayed upright. Add a co-located `*.stories.tsx` rendering the states side by side, run `npm run blox` (port 6006), and look at it. **⚠ blox's Storybook defines no host tokens** (frontend §13e), so components render unstyled there — set the `--*` tokens the component reads in the story/decorator, or check the change in a consuming app.
125
125
  - **⚠ OPEN — the multi-select filter matches by SUBSTRING, not exactly.** `HeaderFilterMultiselect` comma-joins the picked slugs and `src/api/tableData/getDataTableData.ts` turns that into `where: { or: [ { field: { contains: value } }, … ] }`. Picking "Jon Smith" also matches "Jon Smithson" — it over-includes rows.
126
126
  - **⚠ OPEN — an option value containing a comma silently returns zero rows.** It splits into two bogus filter terms, with no error. Both bugs are pre-existing and affect **every** multi-select column including Order Status; both fix in one place — send a list / exact-equals operator instead of a comma-joined `contains`. Not fixed as of 2026-09-15.
127
127
  - **⚠ `useTableData` APPENDS `.uuid` to `additionalData.slug` — unless the slug is on a hardcoded allow-list.** The list is exactly `catalogId`, `customerId`, `Items.catalogId`, `SalesOrderItems.uuid`, `SalesOrders.uuid` (verified in `@agilant/toga-blox` **1.1.2-production.144**, the version toga25-supply pins). Consequences for anyone writing a filter slug into config or a `TableLevel` callback:
@@ -144,6 +144,7 @@ This exists because an unscoped `GET /{recordRoute}` returns *every* row of a lo
144
144
  - **Table/surface meta is cached with `Infinity` staleTime/gcTime** (`useFetchTablePageMeta`), so after a DB metadata change (`TableViewFields`, `Core.RecordFields`) a **hard reload** is required to see it. And the app consumes toga-blox's built **`dist/`**, not `src/` — editing blox source requires `npm run build` before it takes effect at runtime.
145
145
 
146
146
  ## Change history
147
+ - 2026-10-05 — Storybook verify gotcha: blox's Storybook has no host tokens, components render unstyled (tcox)
147
148
  - 2026-09-28 — multi-select filter "Clear" now actually clears (calls `onClear`, page-1 guard); sort badge now points down for `desc` via CSS rotation (apeterson)
148
149
 
149
150
  ## Related
@@ -2,4 +2,4 @@
2
2
 
3
3
  | Doc | Summary |
4
4
  |-----|---------|
5
- | [TOGa Blox 2.5 — Architecture](architecture.md) | Shared React+TS SOURCE library for the new platform apps (toga25-desk, toga25-iq, toga2-template, future apps); read the ADR for full detail, not this stub. |
5
+ | [TOGa Blox 2.5 — Architecture](architecture.md) | Shared React+TS SOURCE library for the new platform apps (new platform apps first; replaces toga-blox for all apps over time); read the ADR for full detail, not |
@@ -6,27 +6,23 @@ project: TOGa Blox 2.5
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-09-26
10
- owners: [jcardinal]
9
+ updated: 2026-10-05
10
+ owners: [jcardinal, tcox]
11
11
  files:
12
12
  - toga25-blox/src
13
- related: []
13
+ related:
14
+ - ../toga-blox/architecture.md
14
15
  ---
15
- Shared React+TS SOURCE library for the new platform apps (toga25-desk, toga25-iq, toga2-template, future apps); read the ADR for full detail, not this stub.
16
+ Shared React+TS SOURCE library for the new platform apps (new platform apps first; replaces toga-blox for all apps over time); read the ADR for full detail, not this stub.
16
17
 
17
18
  ## Summary
18
19
 
19
- `toga25-blox` is a fresh, **source-shared** (git, not npm) React+TS library that replaces
20
- `toga-blox` for **new-platform apps only** (toga25-desk, toga25-iq, toga2-template, future
21
- apps). Apps import blox **source** from a sibling clone — `@toga/blox/<entry>` resolves via
22
- Vite alias to `../toga25-blox/src/<entry>/index.ts`. No build, no publish, no npm version.
23
- Deploy clones `toga25-blox` on the app's own branch at build time. Old npm `toga-blox` stays
24
- on `toga25-supply` / `toga2-commerce` / `toga2-view` / `toga2-hub` — do not port those apps to
25
- this repo. Full decision + rejected alternatives + consequences: `docs/decisions/0001-platform-frontend-architecture.md` in the toga25-blox repo — read that for anything beyond these rules.
20
+ `toga25-blox` is a fresh, **source-shared** (git, not npm) React+TS library that replaces `toga-blox` for **all apps, eventually** (decided 2026-09-30). New platform apps (toga25-desk, toga25-iq, toga2-template, future apps) come first. Old apps (toga25-supply, toga2-supply, toga2-commerce, toga2-view, toga2-hub, BDR, talos, togatech) move over later. Apps import blox **source** from a sibling clone — `@toga/blox/<entry>` resolves via Vite alias to `../toga25-blox/src/<entry>/index.ts`. No build, no publish, no npm version. Deploy clones `toga25-blox` on the app's own branch at build time. Old `@agilant/toga-blox` is fixes-only until each app moves. Full decision + rejected alternatives + consequences: `docs/decisions/0001-platform-frontend-architecture.md` in the toga25-blox repo — read that for anything beyond these rules.
26
21
 
27
22
  **Critical rules:**
28
23
  - **Reuse order:** an existing `toga25-blox` piece → a piece already built in another
29
24
  new-platform app (promote it to blox) → only then build new. Never duplicate.
25
+ - **Moving an old app: rewrite onto toga25-blox pieces — never port old blox code wholesale.** Old blox has 3 table engines and 3 select stacks; copying them carries that over.
30
26
  - **Import boundary:** apps may import only `@toga/blox/<entry>` (the public entry barrel) —
31
27
  lint blocks deeper paths. Blox never imports app code.
32
28
  - **Layer order (nothing imports upward):** `lib` < `core` < `tenancy` < `theme` < `surface`
@@ -44,4 +40,5 @@ this repo. Full decision + rejected alternatives + consequences: `docs/decisions
44
40
  ## Related
45
41
 
46
42
  - ADR: `docs/decisions/0001-platform-frontend-architecture.md` (toga25-blox repo).
43
+ - Component viewer: Storybook (`npm run storybook`, port 6006). Setup and open questions are in `docs/storybook.md` in the toga25-blox repo (prototype on branch TRUE-82044, awaiting Jeff's review).
47
44
  - Old npm lib for legacy 2.0 apps: [toga-blox architecture](../toga-blox/architecture.md).
@@ -5,8 +5,8 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-10-01
9
- owners: [jcardinal, apeterson]
8
+ updated: 2026-10-05
9
+ owners: [jcardinal, apeterson, tcox]
10
10
  files: []
11
11
  related:
12
12
  - ../apps/toga25-supply/architecture.md
@@ -479,6 +479,8 @@ so restoration cannot resurrect a cleared cache.
479
479
  `@agilant/toga-blox` is the front-end shared core — the React analog of `_underscore` / `library`
480
480
  on the back end. A consuming app **MUST** meet every requirement below.
481
481
 
482
+ `@agilant/toga-blox` is **legacy — fixes only**. Build new shared components in `toga25-blox` (`@toga/blox`), which replaces it for all apps over time. See [toga25-blox architecture](../apps/toga25-blox/architecture.md). These rules apply until an app has moved.
483
+
482
484
  **(a) Pin an exact published version — never a caret.** Some published versions ship an **empty
483
485
  `dist`** and break the build; a caret can float you onto one. Both apps pin exact versions. Also
484
486
  audit for **pin drift** between branches. A caret on a **prerelease** is inert (npm matches only the
@@ -637,6 +639,7 @@ The payoff: copy and icons change as a **data edit**, with no front-end deploy.
637
639
  - **Build a new generic capability app-LOCAL first; promote into blox only after a 2nd consumer
638
640
  validates the contract.** Hardening a shared contract from a single call site (n=1) bakes in that
639
641
  app's accidents.
642
+ - **Promote into `toga25-blox`, not `@agilant/toga-blox`** (legacy, fixes only). toga25-blox has its own reuse order — see its architecture doc.
640
643
  - **Mark intent with a per-module header** so a future reader knows which side a module belongs on:
641
644
 
642
645
  ```ts
@@ -16,6 +16,7 @@ files:
16
16
  - dbchanges2/Client_Nychh/2026-09-30a - TransferOrderPackedAssetTagException.sql
17
17
  - dbchanges2/Client_Nychh/2026-09-15b - TransferOrderEmailGoLiveRecipient.sql
18
18
  - dbchanges2/Core/2026-09-16a - NychhTransferOrderEmailsCron.sql
19
+ - dbchanges2/Client_Nychh/2026-10-05a - TransferOrderShippedEmailOnTrackingCutover.sql
19
20
  related:
20
21
  - ./transfer-order-netsuite-push.md
21
22
  - ../../../2.0/apps/_underscore/features/units-for-items-for-purchase-orders.md
@@ -42,7 +43,7 @@ holds the two queued entry points, the backstop cron, every query and the HTML b
42
43
  `_underscore` per-client models only declare the `c_` marker fields and queue the job — they build
43
44
  nothing. That is deliberate: app-level over core, and a slow send must not sit inside an api2 save.
44
45
 
45
- Three rules carry most of the risk:
46
+ Rules that carry most of the risk:
46
47
 
47
48
  1. **Submitted fires on `postPut`, not `postPost`** — on the POST the order still holds the
48
49
  `TA…` autoincrement placeholder, not the NetSuite number.
@@ -51,6 +52,9 @@ Three rules carry most of the risk:
51
52
  3. **A failed send throws — it is never swallowed**, so nothing is ever marked sent that did not go.
52
53
  4. **Packed (and shipped) are HELD while a line is short of asset tags** — ops is warned instead, and
53
54
  the customer email sends itself once the tags land. See below.
55
+ 5. **Shipped fires on the first TRACKING NUMBER, never on the NetSuite shipped stage** (since
56
+ 2026-10-05). The warehouse often marks a fulfillment shipped only after delivery, so the stage is
57
+ unreliable. No tracking → no Shipped email, even at stage shipped. See below.
54
58
 
55
59
  Recipient is `DevTeam@togatech.com` while testing. `2026-09-15b` swaps it to
56
60
  `EndUserDeviceLifecycle@nychhc.org` — run that file only after sign-off.
@@ -61,7 +65,10 @@ Recipient is `DevTeam@togatech.com` while testing. `2026-09-15b` swaps it to
61
65
  - `SendSubmitted(string $transferOrderUuid)` — queued from the TransferOrder `postPut`.
62
66
  - `SendFulfillment(string $itemFulfillmentUuid)` — queued from the ItemFulfillment hooks.
63
67
  - `SendPending(bool $dryRun = true)` — the `*/5` backstop cron.
64
- - `resolveStagesStillToAnnounce()` — which stages are still owed (see the late-shipped case).
68
+ - `resolveStagesStillToAnnounce(stageName, stageAlreadyEmailed, hasTrackingNumber)` — which
69
+ stages are still owed (see *Shipped = tracking number*).
70
+ - `buildHasTrackingNumberSql()` — the one "has tracking" test, shared by
71
+ `readItemFulfillmentForEmail()` (`hasTrackingNumber` column) and the `SendPending` sweep.
65
72
  - `readLinesMissingAssetTags()` / `sendPackedExceptionEmail()` — the asset-tag hold.
66
73
  - `initialize()` registers the client DB before any query runs.
67
74
  - Builders: `buildItemRowsHtml()`, `buildTrackingSectionHtml()`, `buildAssetTagTableHtml()`,
@@ -122,13 +129,11 @@ they must stay in step: the Submitted guard, the order read, and both `SendPendi
122
129
  2. **Packed / Shipped** — the ItemFulfillment `postPost` / `postPut` queue `SendFulfillment` with
123
130
  the fulfillment uuid. The stage reached is recorded on `c_transferOrderEmailStageSent`, with
124
131
  `c_dtTransferOrderEmailSent` as the timestamp, so a fulfillment cannot re-announce a stage.
125
- `resolveStagesStillToAnnounce()` decides what is owed: normally the one stage reached, but when a
126
- fulfillment **arrives already shipped** and packed was never announced it returns
127
- **`[packed, shipped]`** and both send in that order in one run. Before this the packed stage was
128
- skipped outright — and **the asset-tag check lives on packed**, so a late shipped fulfillment
129
- bypassed it completely. Real case: transfer order **291773** got Submitted and Shipped, never
130
- Packed. The NYCHH NetSuite sync is a **daily** cron, so a fulfillment landing fully shipped is
131
- normal, not an edge case — see
132
+ `resolveStagesStillToAnnounce()` decides what is owed (table in *Shipped = tracking number*).
133
+ When packed was never announced and shipped is now owed it returns **`[packed, shipped]`**, both
134
+ sent in that order in one run — the **asset-tag check lives on packed**, so packed must never be
135
+ skipped (transfer order **291773** once got Submitted + Shipped, no Packed). The NYCHH NetSuite
136
+ sync is a **daily** cron, so a fulfillment landing fully shipped/tracked is normal — see
132
137
  [the per-client sync](../../../1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md).
133
138
  3. The **backstop cron** sweeps anything the hooks missed (below).
134
139
  4. All three paths share the same markers, so nothing sends twice.
@@ -152,6 +157,48 @@ PUT follows.** Fulfillment **2704** in production: header row at 20:33:09, lines
152
157
  seconds ago with no lines yet is left for the next run instead of being mailed empty.
153
158
  - It also picks up any send that threw (the marker was never written).
154
159
  - Caps at **25 of each kind per run** (`MAXIMUM_EMAILS_PER_RUN`).
160
+ - **It is the real sender of Shipped** — see below.
161
+
162
+ ## ⚠ Shipped = tracking number, not the NetSuite stage (2026-10-05)
163
+
164
+ "Has tracking" = `ItemFulfillments_TrackingNumbers` **INNER JOIN** `TrackingNumbers` on
165
+ `trackingNumberId` with `TRIM(number) <> ''` — a bridge row can hold only a
166
+ `returnTrackingNumberId`. Built once in `buildHasTrackingNumberSql()`; never re-type it.
167
+
168
+ `resolveStagesStillToAnnounce()`, first match wins:
169
+
170
+ | Marker (`c_transferOrderEmailStageSent`) | Tracking | Stage | Owed |
171
+ |---|---|---|---|
172
+ | `shipped` | any | any | nothing |
173
+ | `packed` | yes | any | `[shipped]` |
174
+ | empty | yes | any (incl. picked — tracking means boxed) | `[packed, shipped]` |
175
+ | empty | no | packed / shipped | `[packed]` |
176
+ | else | | | nothing |
177
+
178
+ The asset-tag hold runs whenever packed is owed and blocks both.
179
+
180
+ - **⚠ `SendPending` WHERE must match that table:** marker NULL or `<> 'shipped'` AND (has tracking OR
181
+ (stage IN packed/shipped AND `COALESCE(marker,'') = ''`)). Drift → rows picked then skipped, eating
182
+ `LIMIT 25`.
183
+ - **Why the cron, not the hook, sends Shipped:** library `syncItemFulfillmentFromNetsuite`
184
+ (`library/app/api/toga2.php`) PUTs the stage first (~L7270), then units/asset tags, and writes
185
+ package tracking **last** (~L8046). The ItemFulfillment `postPut` job runs before tracking exists.
186
+ (Also removes the old race where Shipped read stale tracking/tags.) Tracking imports at picked,
187
+ packed or shipped alike.
188
+ - **Once per fulfillment.** Business rule = 1 tracking number per fulfillment; prod has violations
189
+ (IF 329047 has 2; IFs 329944/330013 share 1). Still sends once — the marker gates it.
190
+ - **Lag:** adding tracking in NetSuite bumps the IF `lastmodifieddate`, so the sync re-reads it, but
191
+ only on the NYCHH sync interval (daily). E.g. IF 330377 (NS 7566823) still Packed (status B) in
192
+ NetSuite, tracking imported 2026-10-05 11:56.
193
+ - **Only NYCHH emails off the NetSuite shipped stage** (checked 2026-10-05): Compass in-transit emails
194
+ are per tracking number from ASN data; Prudential "order shipped" is a transmission; Elite moves to
195
+ shipped when tracking lands.
196
+ - **Cutover:** `Client_Nychh/2026-10-05a` stamps `c_transferOrderEmailStageSent='shipped'` on supply
197
+ fulfillments that already had tracking (`c_dtTransferOrderEmailSent` untouched — no email sent), so
198
+ old fulfillments got no late Shipped. Re-runnable; matched 2 on prod (IF 330377/TO 291765, IF
199
+ 330400/TO 291965). **⚠ Deploy order:** the old code re-sends Packed for a row stamped `shipped`
200
+ whose stage is packed, so: pause `Core.CronJobs` id 78 (`isActive=0`) → run SQL → deploy worker2 →
201
+ re-enable id 78.
155
202
 
156
203
  ## ⚠ Missing asset tags HOLD the packed email (and warn ops instead)
157
204
 
@@ -257,7 +304,8 @@ Cost of the split, stated so nobody is surprised later:
257
304
  **1,203** have one number and **292** have two or more, **up to 25**.
258
305
  - **Many shipped fulfillments have no tracking at all** — 1,484 of 2,479 measured 2026-09-17, and
259
306
  322 of the rest have no carrier on the number. `buildTrackingSectionHtml()` drops the whole block
260
- when there is no number and drops the button when there is no URL. Placeholders are
307
+ when there is no number and drops the button when there is no URL (since 2026-10-05 Shipped needs a
308
+ number, so this now matters only for the Packed email). Placeholders are
261
309
  **`{trackingIntroSuffix}`** and **`{trackingSection}`**.
262
310
  - **Gmail clips a message past about 102,400 bytes.** `MAXIMUM_EMAIL_BODY_BYTES = 80000` budgets the
263
311
  **item block only**; the remainder is summarised as *"and N more"*. 80,000 (not the earlier
@@ -348,6 +396,11 @@ loaded out of the dbchanges2 file so an unresolved `{placeholder}` fails a test.
348
396
  unescaped values and both detail-box defects were all found there, not in production.
349
397
 
350
398
  ## Change history
399
+ - 2026-10-05 — **Shipped now fires on the first tracking number, not the NetSuite shipped stage**
400
+ (warehouse marks shipped after delivery). Shared `buildHasTrackingNumberSql()`, new
401
+ `resolveStagesStillToAnnounce()` table, matching `SendPending` WHERE; removed dead
402
+ `resolveFulfillmentEmailTemplateUuid()`. Cutover SQL `2026-10-05a` (2 prod rows). Deployed + verified
403
+ on prod. (jcardinal)
351
404
  - 2026-10-05 — **The asset-tag hold now only counts missing units on `SERIALIZED` items**
352
405
  (TRUE-82111). `readLinesMissingAssetTags()` treated "fewer linked units than quantity" as missing
353
406
  tags for every item, so accessories were reported short: first prod exception was `AC785544`, a
@@ -66,6 +66,7 @@ TOGA 2.0 client on `_underscore`, prod schema `Client_Nychh`. Supply-chain / ful
66
66
  - **Worker tiers:** `worker2` (2.0) runs the NYCHH asset-tag backfill; `worker`/`library` (1.0) run the NetSuite→TOGa Supply sync and the asset-tag verification/backfill diagnostics.
67
67
 
68
68
  ## Key features (this client)
69
+ - **Transfer-order "Order Shipped" email fires on the first tracking number, not the NetSuite shipped stage (2026-10-05, prod).** Warehouse marks shipped after delivery. NYCHH is the only client emailing off the NS shipped stage. See [TO notification emails](./features/transfer-order-notification-emails.md).
69
70
  - **Prod NetSuite feeds (2026-10-02):** only ITEM_FULFILLMENTS + INVENTORY_ADJUSTMENTS run (worker `511a7fd9`, 2026-09-30), cursors rewound to 2022/2023.
70
71
  - **TO destination + own ship-to (2026-10-02, library `8f974d93`, _underscore `6839dd3e`).** NetSuite-entered TOs landed on catch-all Location 2 (text match failed on Queens hyphens + multi-address sites). Destination now = mapped NetSuite address-book entry → text match → Location 2; TOs own `shipToAddressId`; Location auto-create uses default shipping. Mapping: 61 entries → 18 sites. See [TO import](./features/netsuite-transfer-order-import.md), [address-book mapping](../../1.0/apps/worker/workflows/netsuite-addressbook-location-mapping.md).
71
72
  - **TO↔customer-PO line matching fixed (2026-10-02, `1bc4de52`).** Old matcher picked unstocked duplicate PO lines and created empty PO lines; now ranks stocked lines and allows reuse. 161 TOs re-processed; 86 bad links over 62 TOs remained at last measure (re-process result pending). See [reconcile](../../1.0/apps/library/features/transfer-order-purchase-order-link-reconcile.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.951",
3
+ "version": "1.0.952",
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",