toga-ai 1.0.710 → 1.0.712
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/1.0/apps/worker/INDEX.md +1 -0
- package/knowledge/1.0/apps/worker/features/netsuite-togasupply-per-client-sync.md +46 -0
- package/knowledge/1.0/apps/worker/workflows/netsuite-duplicate-items-dedup.md +102 -0
- package/knowledge/INDEX.md +1 -1
- package/knowledge/sessions/2026-08-31-transfer-orders-id-collision-apeterson.md +242 -0
- package/package.json +1 -1
|
@@ -17,5 +17,6 @@
|
|
|
17
17
|
| [Staples cXML Order Import (SFTP → NetSuite)](features/staples-cxml-order-import.md) | `sync_staples_cxml.php` is an **hourly** cron (runs at **:45**) that imports Staples cXML purchase orders from SFTP into NetSuite as Sales Orders, then writes a | worker/crons/sync/staples/sync_staples_cxml.php |
|
|
18
18
|
| [Diagnosing frozen 1.0 worker cron check-ins (Sentry "missed" flood)](workflows/diagnosing-frozen-cron-checkins.md) | When 1.0 worker cron timestamps freeze and Sentry project `worker1` fills with **`missed`** check-ins, the intuitive diagnosis — a wedged `App_Framework::isProc | worker/.ebextensions/cron.config, library/app/worker.php |
|
|
19
19
|
| [isFulfillable Multi-Client Backfill (all togasupply clients)](workflows/isfulfillable-multi-client-backfill.md) | One-time backfill that catches up `Items.isFulfillable` on **existing** items across **all 17 togasupply clients** (AIG, Broward Sheriff, Canon, Endeavor Health | worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php, library/app/api/toga2.php |
|
|
20
|
+
| [Deduplicating NetSuite duplicate Items across client DBs (recursive MERGE)](workflows/netsuite-duplicate-items-dedup.md) | A one-time, per-client data cleanup that collapses **duplicate `Items` rows sharing a `partNumber`** in each NetSuite → TOGa Supply client DB down to a single c | library/app/api/toga2.php, worker/crons/toga2/netsuite/common_sync_togasupply.php |
|
|
20
21
|
| [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, dbchanges2/_modules/netsuite/2026-08-05 - CLEAN NETSUITE CLINET.SQL |
|
|
21
22
|
| [Tracing a 1.0 worker cron run in production (no stdout, silent skips)](workflows/tracing-a-worker-cron-run-in-production.md) | How to answer *"did this cron actually run, and what did it do?"* on the 1.0 `worker` tier, where **there is no usable stdout** and **a skipped run leaves no tr | worker/ebs/cron.worker.php, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.infrastructure.json, worker/.ebextensions/009_setup_phpini.config, library/app/framework.php |
|
|
@@ -755,9 +755,55 @@ Parameters are stored **per client DB** but accessed **through the TOGa2 API**,
|
|
|
755
755
|
2**) and scope the `Units` lookup by it (returns exactly 1 → reuse). Latent data problem behind it:
|
|
756
756
|
**duplicate `Items` with the same `partNumber`** should be reconciled later (the fix is robust to it).
|
|
757
757
|
GroWrk ITEM_FULFILLMENTS went from stuck `1-RUNNING` (since Aug 21) to green.
|
|
758
|
+
- **⚠ SUPERSEDED 2026-08-31 — item-scoping ALONE was WRONG for NYCHH.** Where the serial's single
|
|
759
|
+
existing unit sits under a **different** duplicate `Item` sharing the part number, scoping the `Units`
|
|
760
|
+
lookup by `$uuidItemForLine` found nothing and re-created the unit → the SAME 1062 collision. Correct
|
|
761
|
+
rule: **a serial identifies a physical device, so query `Units` by `serialNumber` ONLY**, then *prefer*
|
|
762
|
+
the unit whose item matches `$uuidItemForLine` when several share the serial (GroWrk's 2-unit case),
|
|
763
|
+
else reuse the first; create only when **no** unit carries the serial. Handles the 1-unit (NYCHH),
|
|
764
|
+
2-unit (GroWrk), and new-serial cases. (The duplicate `Items` themselves are the real defect — see the
|
|
765
|
+
Agilant-catalog root cause and the dedup workflow below.)
|
|
766
|
+
|
|
767
|
+
- **⚠ Invoice line numbers are NOT unique on multi-SO invoices — assign a sequential `lineNumber`
|
|
768
|
+
(fixed 2026-08-31).** A NetSuite invoice that bills **multiple sales orders** restarts `line` per source
|
|
769
|
+
SO, so `$nsItem->line` is not unique within one invoice → `InvoiceItems UNIQUE(invoiceId, lineNumber)` →
|
|
770
|
+
`POST /v2/invoices` `EV-10` / MySQL **1062**, and because the invoice POST is **atomic** the whole
|
|
771
|
+
invoice rolls back, freezing `INVOICES` at `1-RUNNING` (NYCHH). **Fix (`syncInvoiceFromNetsuite`):**
|
|
772
|
+
assign a unique **1..N sequential** `lineNumber` per imported line instead of `$nsItem->line` (identical
|
|
773
|
+
for the common single-SO invoice; differs only on multi-SO). Safe because nothing downstream keys off
|
|
774
|
+
invoice `lineNumber` — the SO link uses `orderLine`/`salesOrderItem`.
|
|
775
|
+
- **⚠ Transfer-order invoices are NOT imported (fixed 2026-08-31).** Under NYCHH stocking-flag routing a
|
|
776
|
+
$0 NetSuite Sales Order becomes a `TransferOrder`, not a `SalesOrder`; `Invoices` has only
|
|
777
|
+
`salesOrderId` (no `transferOrderId`), so such an invoice imported with a **null order link**. Team
|
|
778
|
+
decision: **do not import transfer-order invoices at all.** **Fix:** in the "billed order not found in
|
|
779
|
+
SalesOrders" branch, classify the order authoritatively (`App_Api_Netsuite_Rest::fetchSalesOrderByIdFull`
|
|
780
|
+
+ `self::isTransferOrder`) and **return early** when it is a transfer order, gated on
|
|
781
|
+
`IS_ENABLED_INTEGRATION_TRANSFER_ORDERS`. Existing TO-billed invoices were deleted by the developer.
|
|
782
|
+
- **⚠ ROOT CAUSE of the duplicate `Items`: the inventory-adjustment item-create OMITTED the catalog
|
|
783
|
+
(fixed 2026-08-31).** `getCreateItem` (`toga2.php:6233`) and the fulfillment create (~4951) stamp
|
|
784
|
+
`'catalog' => ['name' => 'Agilant']`; `syncInventoryAdjustmentFromNetsuite`'s item-create did **not** →
|
|
785
|
+
it produced **NULL-catalog `Items`**. The shared item lookup (`common_sync_togasupply.php` ~L313) is
|
|
786
|
+
populated **only from Agilant-catalog items**, so those NULL-catalog dupes are **invisible** to it → a
|
|
787
|
+
NEW duplicate item was created **every run** for the same part number — the source of every
|
|
788
|
+
`(itemId, serialNumber)` unit collision seen this session. **Fix:** stamp
|
|
789
|
+
`'catalog' => ['name' => 'Agilant']` on the inventory-adjustment item-create. Stops all new duplicates.
|
|
790
|
+
The one-time cleanup of the accumulated ~4,300 duplicate part numbers across all 17 clients is the
|
|
791
|
+
[NetSuite duplicate-Items dedup workflow](../workflows/netsuite-duplicate-items-dedup.md).
|
|
758
792
|
|
|
759
793
|
## Change history
|
|
760
794
|
|
|
795
|
+
- 2026-08-31 — **Invoice + serial-unit + duplicate-Item root-cause fixes (shared, `library`).**
|
|
796
|
+
`syncInvoiceFromNetsuite` now assigns a **sequential 1..N invoice `lineNumber`** (NetSuite reuses `line`
|
|
797
|
+
across multi-SO invoices → `InvoiceItems UNIQUE(invoiceId,lineNumber)` 1062 → atomic rollback → NYCHH
|
|
798
|
+
INVOICES `1-RUNNING`) and **skips transfer-order invoices** (classify via `fetchSalesOrderByIdFull` +
|
|
799
|
+
`isTransferOrder`, return early, gated on `IS_ENABLED_INTEGRATION_TRANSFER_ORDERS` — `Invoices` has no
|
|
800
|
+
`transferOrderId`; team decision not to import them). **Corrected** the 2026-08-30 item-scoped serial
|
|
801
|
+
unit lookup (which broke NYCHH): a serial identifies a physical device, so query `Units` by
|
|
802
|
+
`serialNumber` ONLY, prefer the unit matching `$uuidItemForLine` when several share it, else reuse first,
|
|
803
|
+
create only when none exists. Found + fixed the **root cause of the duplicate `Items`**: the
|
|
804
|
+
inventory-adjustment item-create omitted `catalog => Agilant`, producing NULL-catalog items invisible to
|
|
805
|
+
the Agilant-only sync lookup → a new dup every run. Companion one-time cleanup across all 17 clients:
|
|
806
|
+
[NetSuite duplicate-Items dedup workflow](../workflows/netsuite-duplicate-items-dedup.md). (jcardinal)
|
|
761
807
|
- 2026-08-30 — **Inventory-adjustment serialized-unit path now auto-creates a missing unit warehouse
|
|
762
808
|
(`library`).** `syncInventoryAdjustmentFromNetsuite` took the unit's location from the inventory
|
|
763
809
|
number's `custitemnumber_location` but never created a missing warehouse (unlike
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Deduplicating NetSuite duplicate Items across client DBs (recursive MERGE)"
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: worker
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-31
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- library/app/api/toga2.php
|
|
13
|
+
- worker/crons/toga2/netsuite/common_sync_togasupply.php
|
|
14
|
+
related:
|
|
15
|
+
- ../features/netsuite-togasupply-per-client-sync.md
|
|
16
|
+
- ../../library/features/netsuite-suiteql-rest-shim.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
A one-time, per-client data cleanup that collapses **duplicate `Items` rows sharing a `partNumber`**
|
|
22
|
+
in each NetSuite → TOGa Supply client DB down to a single canonical item, re-pointing every reference.
|
|
23
|
+
Duplicates accumulated because the NetSuite inventory-adjustment sync created **NULL-catalog `Items`**
|
|
24
|
+
that the sync's own lookup could not see, so a new duplicate was minted every run (root cause below).
|
|
25
|
+
Deleting a duplicate item is a **recursive MERGE**, not a simple `DELETE` — ~23 `itemId` tables plus
|
|
26
|
+
two FK cascades (`VendorItems`, `Units`) reference it. Run **per client, in its own transaction**,
|
|
27
|
+
after the code fix is deployed.
|
|
28
|
+
|
|
29
|
+
**Verified in prod (2026-08-31):** all **17** NetSuite clients now show **0 duplicate part numbers**
|
|
30
|
+
(e.g. `Client_Nychh` 3954→1818 items, `Client_Canon` 913→425; 0 orphans, 0 collisions).
|
|
31
|
+
|
|
32
|
+
## Root cause — why the duplicates existed at all
|
|
33
|
+
|
|
34
|
+
`syncInventoryAdjustmentFromNetsuite`'s item-create **omitted the catalog** that
|
|
35
|
+
`getCreateItem`/`toga2.php:6233` and the fulfillment create both stamp
|
|
36
|
+
(`'catalog' => ['name' => 'Agilant']`), so it produced **NULL-catalog `Items`**. The shared sync item
|
|
37
|
+
lookup in `worker/crons/toga2/netsuite/common_sync_togasupply.php` (~L313) is populated **only from
|
|
38
|
+
Agilant-catalog items**, so a NULL-catalog dupe is **invisible** to it → the sync re-created the item
|
|
39
|
+
every run for the same part number. **The fix (stamp `catalog => Agilant` on that create) stops all NEW
|
|
40
|
+
duplicates** and must be deployed first — see
|
|
41
|
+
[per-client sync](../features/netsuite-togasupply-per-client-sync.md). This cleanup removes the
|
|
42
|
+
**existing** backlog (~4,300 duplicate part numbers / ~15,700 `Items` rows accumulated over time).
|
|
43
|
+
|
|
44
|
+
## When to run
|
|
45
|
+
|
|
46
|
+
- After the Agilant-catalog item-create fix is deployed (otherwise the backlog regrows every sync run).
|
|
47
|
+
- Per client, once. Re-runnable safely (idempotent: a client already at 0 duplicates is a no-op).
|
|
48
|
+
|
|
49
|
+
## Procedure
|
|
50
|
+
|
|
51
|
+
1. **Report first (read-only).** Run `test/team/2.0 deployment/duplicate_items_merge_analysis.sql`
|
|
52
|
+
against the client DB — a per-client duplicate + collision report. Confirm the duplicate count and the
|
|
53
|
+
`(itemId, serialNumber)` / `(vendorId, itemId, vendorPartNumber)` collisions before mutating anything.
|
|
54
|
+
2. **Pick the canonical item per `partNumber`** in this priority order: **Agilant + manufacturer**
|
|
55
|
+
preferred → else **Agilant** → else **has-manufacturer** → else **lowest `id`**.
|
|
56
|
+
3. **Re-point every `itemId` reference** across the ~23 `itemId` tables **plus `Entitlements.saleItemId`**
|
|
57
|
+
from each non-canonical item to the canonical one.
|
|
58
|
+
4. **Handle the two FK cascades** (children must move before their parent duplicate is deleted):
|
|
59
|
+
- **`VendorItems`** is referenced by `PurchaseOrderItems`, `VendorItemCosts`, `Personas_VendorItems`.
|
|
60
|
+
- **`Units`** is referenced by **10** `*ItemUnits` child tables.
|
|
61
|
+
5. **Resolve unique-key collisions with a SURVIVOR-per-key approach**, NOT a plain "`UPDATE IGNORE` +
|
|
62
|
+
delete leftover" (that leaves child FKs dangling on dupe-vs-dupe collisions). For each colliding key
|
|
63
|
+
(`Units.(itemId, serialNumber)`, `VendorItems.(vendorId, itemId, vendorPartNumber)`): choose one
|
|
64
|
+
survivor row, **re-point the non-survivor rows' children onto the survivor**, delete the non-survivors,
|
|
65
|
+
then re-point the rest.
|
|
66
|
+
6. **Delete the duplicate `Items` rows LAST** (after all references and cascades are moved).
|
|
67
|
+
7. **Run each client in its own transaction**, continue-on-error (see the missing-table gotcha).
|
|
68
|
+
|
|
69
|
+
## Validation
|
|
70
|
+
|
|
71
|
+
- Validate against **local copies of the client DBs before prod** (this session: `Client_Nychh`
|
|
72
|
+
3954→1818, `Client_Canon` 913→425, **0 orphans, 0 collisions**).
|
|
73
|
+
- After each prod run, re-run the analysis SQL: the client must report **0 duplicate part numbers**.
|
|
74
|
+
|
|
75
|
+
## Deliverables (live in the repo, not the knowledge base)
|
|
76
|
+
|
|
77
|
+
- `test/team/2.0 deployment/duplicate_items_merge_analysis.sql` — read-only per-client duplicate +
|
|
78
|
+
collision report.
|
|
79
|
+
- The per-client merge migration (the recursive MERGE implementing steps 2–6).
|
|
80
|
+
|
|
81
|
+
## Gotchas / known issues
|
|
82
|
+
|
|
83
|
+
- **The sync item lookup is Agilant-catalog-only.** Any non-Agilant item is **invisible** to
|
|
84
|
+
`common_sync_togasupply.php` and will be **re-created** — this is the whole mechanism behind the
|
|
85
|
+
duplicates. Deploy the catalog-stamp fix before (and independently of) this cleanup.
|
|
86
|
+
- **`Items` has NO unique constraint on `partNumber`.** Duplicates persist silently; nothing at the DB
|
|
87
|
+
level prevents them, which is why this is a MERGE-and-clean rather than a rely-on-the-constraint fix.
|
|
88
|
+
- **Client DBs are schema-similar but NOT identical.** Some lack optional tables (e.g.
|
|
89
|
+
`EntitlementServiceItems`). Run the merge **continue-on-error** so a missing-table MySQL **1146** is a
|
|
90
|
+
**benign skip**, not a failed migration.
|
|
91
|
+
- **A plain `UPDATE IGNORE` + delete-leftover is insufficient.** Dupe-vs-dupe unique-key collisions leave
|
|
92
|
+
orphaned child FKs; only the survivor-per-key re-pointing (step 5) is correct.
|
|
93
|
+
|
|
94
|
+
## Change history
|
|
95
|
+
|
|
96
|
+
- 2026-08-31 — Documented the one-time duplicate-`Items` dedup across all 17 NetSuite clients (recursive
|
|
97
|
+
MERGE over ~23 `itemId` tables + `Entitlements.saleItemId`, `VendorItems`/`Units` FK cascades,
|
|
98
|
+
survivor-per-key collision resolution), its root cause (the inventory-adjustment item-create omitted
|
|
99
|
+
`catalog => Agilant` → NULL-catalog items invisible to the Agilant-only sync lookup → a new dup every
|
|
100
|
+
run), and the durable gotchas (Agilant-only lookup, no `partNumber` unique constraint, per-client schema
|
|
101
|
+
drift → continue-on-error). Validated on local copies then run per client in prod; all 17 clients at 0
|
|
102
|
+
duplicate part numbers. (jcardinal)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
7
|
- **library** (Library) _(framework core)_ — 23 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
|
-
- **worker** (Worker) —
|
|
8
|
+
- **worker** (Worker) — 32 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
|
|
10
10
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
11
11
|
- **togadesk** (TOGa Desk) — 13 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: transfer-orders-id-collision
|
|
4
|
+
title: Transfer Orders Core id collision + blox symlink dev-loop fix
|
|
5
|
+
author: apeterson
|
|
6
|
+
repos: [dbchanges2, toga25-supply, toga-blox, _underscore]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: nychh
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-08-31
|
|
11
|
+
updated: 2026-08-31
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: transfer-orders-id-collision
|
|
15
|
+
**Date:** 2026-08-31
|
|
16
|
+
**Project/Repo:** dbchanges2 + toga25-supply + toga-blox (2.0)
|
|
17
|
+
**Task:** Get the NYCHH Transfer Orders page working on sandbox-client after a reset-from-production, and permanently fix the toga-blox local-symlink dev loop in toga25-supply.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
|
|
23
|
+
- **Published `/capture` round two.** 13 doc updates + 4 regenerated `INDEX.md` pushed as
|
|
24
|
+
`172f85d` `knowledge: capture session updates` on `_main` of `~/toga-tech`, confirmed on
|
|
25
|
+
`origin/_main` (CI stacked `c0fcabc` on top). `validate` passed (32 repos), index rebuilt to
|
|
26
|
+
454 docs, nothing outside `knowledge/` staged.
|
|
27
|
+
|
|
28
|
+
- **Diagnosed the `TalosLauncher` import error correctly on the second pass.**
|
|
29
|
+
`toga25-supply/node_modules/@agilant/toga-blox` was a REAL DIRECTORY holding published
|
|
30
|
+
`1.1.1-sandbox-client.140`, which contains **zero** files matching `Talos`. The local
|
|
31
|
+
checkout (`1.1.0`, branch `TRUE-80692`) had a perfectly intact export chain:
|
|
32
|
+
`dist/index.js` -> `dist/components/index.js:44` -> `dist/components/Talos/index.js` ->
|
|
33
|
+
`export { default as TalosLauncher }`. Evidence: `ls -la node_modules/@agilant/` showed no
|
|
34
|
+
`->`, and `grep -rl Talos node_modules/@agilant/toga-blox/` returned nothing.
|
|
35
|
+
|
|
36
|
+
- **Created the symlink per the team workflow.** `npm install --no-save
|
|
37
|
+
/Users/alexpeterson/Agilant/toga-blox-npm` from toga25-supply produced
|
|
38
|
+
`toga-blox -> ../../../toga-blox-npm` resolving to `1.1.0`, and left `package.json`
|
|
39
|
+
untouched (git status clean).
|
|
40
|
+
|
|
41
|
+
- **Permanently fixed the Vite dev loop** in `toga25-supply/vite.config.ts`. It now detects the
|
|
42
|
+
symlink with `fs.lstatSync(bloxDir).isSymbolicLink()` and picks opposite `optimizeDeps`
|
|
43
|
+
settings: LINKED -> `exclude: ["@agilant/toga-blox"]` + `include: BLOX_EXTERNALS`;
|
|
44
|
+
PUBLISHED -> `include: ["@agilant/toga-blox"]` (previous behaviour, unchanged). Also added
|
|
45
|
+
`server.fs.allow` for the symlink's real path. Verified from the running dev server: the
|
|
46
|
+
transform now emits
|
|
47
|
+
`import { SideNav, EnvironmentBadge, TalosLauncher, ... } from "/@fs/Users/alexpeterson/Agilant/toga-blox-npm/dist/index.js"`
|
|
48
|
+
and `node_modules/.vite/deps/@agilant_toga-blox.js` is absent.
|
|
49
|
+
|
|
50
|
+
- **Enumerated blox's full external surface** (the `include` list needed once blox is excluded),
|
|
51
|
+
generated from its dist:
|
|
52
|
+
`@fortawesome/{free-solid,pro-light,pro-regular,pro-solid}-svg-icons`,
|
|
53
|
+
`@fortawesome/react-fontawesome`, `@sentry/react`,
|
|
54
|
+
`@tanstack/{react-query,react-table,react-virtual}`, `axios`,
|
|
55
|
+
`class-variance-authority`, `classnames`, `clsx`, `dayjs`, `dayjs/plugin/timezone`,
|
|
56
|
+
`dayjs/plugin/utc`, `framer-motion`, `react-hook-form`, `react-multi-select-component`,
|
|
57
|
+
`react-select`, `tailwind-merge`, `uuid`.
|
|
58
|
+
Regeneration command is in a comment above the list in `vite.config.ts`.
|
|
59
|
+
|
|
60
|
+
- **Root-caused why Transfer Orders did not render on sandbox-client.** The two `Core/` seeds
|
|
61
|
+
had never run there, AND `Core/2026-08-28a` could no longer run as written: production had
|
|
62
|
+
shipped a `sales-order-denial-details` feature into the exact reserved blocks
|
|
63
|
+
(`Surfaces 47`, `SurfaceElements 170-176`, `Messages 227-232`). Sandbox had zero rows
|
|
64
|
+
matching `%transfer%` in Surfaces or Messages.
|
|
65
|
+
|
|
66
|
+
- **Confirmed `Core/2026-08-28b` ran cleanly on sandbox with no collision.** Its ids
|
|
67
|
+
(Surface 52, Elements 187-189, Messages 251-260) all sat above the then-MAX of 47/176/232.
|
|
68
|
+
Verified present and correct.
|
|
69
|
+
|
|
70
|
+
- **Proved no damage was done to the other team's feature.** All denial-details rows show
|
|
71
|
+
`dtCreated == dtUpdated == 2026-08-31 09:45:11` (the reset timestamp) — never updated. And
|
|
72
|
+
`_underscore/Model/Core/Surface.php:748` `_loadOverrides()` filters
|
|
73
|
+
`WHERE SurfaceOverrides.surfaceId = $surfaceId`, so the NYCHH override row (surfaceId 39,
|
|
74
|
+
surfaceElementId 170) is only ever loaded when rendering surface 39 and can never reach
|
|
75
|
+
surface 47. It is inert in both directions.
|
|
76
|
+
|
|
77
|
+
- **Renumbered `Core/2026-08-28a` safely.** Shifts applied: Surfaces `47-51 -> 53-57`,
|
|
78
|
+
SurfaceElements `170-186 -> 190-206`, Messages `227-250 -> 261-284`. Verified after the
|
|
79
|
+
rewrite: the md5 of every quoted string in the file is IDENTICAL to the backup (UUIDs and
|
|
80
|
+
slugs untouched), and the occurrence counts of `312`, `351`, `39` and `70` are unchanged.
|
|
81
|
+
FK graph re-derived and correct — nav element 190 on surface 39; 191-194 -> 54;
|
|
82
|
+
195-197 -> 55; 198-200 -> 56; 201-206 -> 57.
|
|
83
|
+
|
|
84
|
+
- **Repointed the cross-DB reference.** `Client_Nychh/2026-08-28b` had 3 occurrences of `170`
|
|
85
|
+
(comment, INSERT value, NOT EXISTS guard) — all now `190`.
|
|
86
|
+
|
|
87
|
+
- **Verified the new blocks are free on BOTH sandbox-client and local** (`53-57`, `190-206`,
|
|
88
|
+
`261-284` all return 0), and that `Core.Surfaces 39` is `navigation` (TAB_STRIP).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
93
|
+
|
|
94
|
+
- **`rm -rf node_modules/.vite` + hard reload did NOT fix the `TalosLauncher` error.** This was
|
|
95
|
+
the first hypothesis and it was wrong. The export genuinely was not in the package Vite had
|
|
96
|
+
pre-bundled, because supply was consuming the published tarball, not the local checkout. Do
|
|
97
|
+
not reach for the cache clear before confirming `ls -la node_modules/@agilant/` shows a `->`.
|
|
98
|
+
|
|
99
|
+
- **After symlinking, the error STILL persisted** and a normal hard reload did not clear it.
|
|
100
|
+
Vite serves `/node_modules/.vite/deps/*` with `Cache-Control: max-age=31536000, immutable`.
|
|
101
|
+
The re-optimize rewrote the file but kept the same `browserHash` (`69916b97`), so the URL
|
|
102
|
+
never changed and Chrome would not revalidate. Proven by `curl`-ing the dev server for that
|
|
103
|
+
exact URL and finding `TalosLauncher_default as TalosLauncher` present in the response while
|
|
104
|
+
the browser still errored. Fix at the time: DevTools -> Network -> **Disable cache**. Fix
|
|
105
|
+
permanently: the `optimizeDeps` change above.
|
|
106
|
+
|
|
107
|
+
- **First renumbering attempt CORRUPTED the file — UUIDs got shuffled.** The Python transform
|
|
108
|
+
protected quoted strings by replacing them with `\x00<index>\x00` placeholders. `\x00` is not
|
|
109
|
+
a word character, so the subsequent bare-integer regex matched the placeholder INDICES and
|
|
110
|
+
shifted them, meaning literals were restored to the wrong positions. Caught by comparing the
|
|
111
|
+
md5 of the extracted UUID list before/after (`CHANGED`). Fixed by restoring from backup and
|
|
112
|
+
re-running with a **digit-free placeholder** (`\x01<a-j encoded index>\x01`). Always verify a
|
|
113
|
+
bulk rewrite by diffing the quoted-string set, not by eyeballing.
|
|
114
|
+
|
|
115
|
+
- **Assuming the hyphenated ranges in the file header would be shifted.** The bare-integer regex
|
|
116
|
+
is `(?<![\w.\-])\d+(?![\w.\-])`, so `227-250`, `47-51` and `170-186` in comments were skipped
|
|
117
|
+
(hyphen guard on both sides). A `str.replace` of the old header block asserted false because
|
|
118
|
+
it still contained the ORIGINAL numbers. This is desirable behaviour — it is why nothing
|
|
119
|
+
unintended shifted — but the header had to be rewritten by hand afterwards.
|
|
120
|
+
|
|
121
|
+
- **macOS/zsh tool gaps hit repeatedly** (all cost a round trip):
|
|
122
|
+
- `timeout` does not exist — use `mysql --connect-timeout=N`.
|
|
123
|
+
- `cat -A` is illegal on macOS — use `od -c` or `sed -e 's/$/<EOL>/'`.
|
|
124
|
+
- Unquoted `--include=*.js` is glob-expanded by zsh and fails — quote it.
|
|
125
|
+
- `grep -oE` does not support lookbehind; `grep -P` is required for `(?<!...)`.
|
|
126
|
+
- The `mysql` alias is a shell alias, so `$LM="mysql ..."` in a variable is not resolved —
|
|
127
|
+
call `/Applications/XAMPP/xamppfiles/mysql/bin/mysql` by full path.
|
|
128
|
+
|
|
129
|
+
- **Do NOT re-run `Client_Nychh/2026-08-28b` on any machine that already has the row.** It now
|
|
130
|
+
carries element `190`, but its `uuid` is a fixed literal
|
|
131
|
+
(`7a8b9c0d-1e2f-4304-8516-6a7b8c9d0e1f`), so the insert would fail on the duplicate UUID. Use
|
|
132
|
+
the `UPDATE ... SET surfaceElementId = 190 WHERE uuid = '...'` instead.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Not tried yet (candidates for next session)
|
|
137
|
+
|
|
138
|
+
- Running the 4 sandbox steps and 3 local steps listed under **Exact next step** — nothing has
|
|
139
|
+
been executed yet.
|
|
140
|
+
- Committing the 7 dbchanges2 files. They are ALL still untracked, which is the direct cause of
|
|
141
|
+
the id collision (no registry, no visibility to the other developer).
|
|
142
|
+
- Production rollout — nothing has been run there at all.
|
|
143
|
+
- Beta — still blocked, it has no `TransferOrderStages` table.
|
|
144
|
+
- Flipping `TRANSFER_ORDER_LINES_ENABLED` to `true` in
|
|
145
|
+
`toga25-supply/src/pages/TransferOrders/view/TransferOrderRecordModalLayout/hooks/useTransferOrderRecord.ts`
|
|
146
|
+
once `Client/2026-08-28b` is applied everywhere.
|
|
147
|
+
- Fixing the blank-client template (`Client/2026-06-03- BLANK_CLIENT_DATABASE.sql` line ~9462)
|
|
148
|
+
so new tenants get `TransferOrderItems.dtCreated`/`dtUpdated`.
|
|
149
|
+
- The Create Transfer Order modal — still a UI shell; its 3 blocking data questions remain
|
|
150
|
+
unanswered (which `UnitDispositions` slug counts as available; whether `Locations` are
|
|
151
|
+
hierarchical; who owns the unit-disposition flip to In Transit).
|
|
152
|
+
- Switching `vite.config.ts`'s `BLOX_EXTERNALS` to something generated at config time rather
|
|
153
|
+
than a hand-maintained list.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Current file state
|
|
158
|
+
|
|
159
|
+
| File | Status | Notes |
|
|
160
|
+
|------|--------|-------|
|
|
161
|
+
| `toga25-supply/vite.config.ts` | MODIFIED (uncommitted) | Symlink-aware `optimizeDeps` + `server.fs.allow`. Backup at `scratchpad/vite.config.ts.bak`. Verified working against the running dev server. |
|
|
162
|
+
| `toga25-supply/node_modules/@agilant/toga-blox` | RELINKED | Now `-> ../../../toga-blox-npm` (v1.1.0). `--no-save`, so `package.json` untouched. Any plain `npm i` will clobber it. |
|
|
163
|
+
| `dbchanges2/Core/2026-08-28a - TransferOrderSurfaceSeed.sql` | RENUMBERED (untracked) | Surfaces 53-57, Elements 190-206, Messages 261-284. Header rewritten with the collision post-mortem + a re-verify warning. NOT RUN ANYWHERE YET. |
|
|
164
|
+
| `dbchanges2/Core/2026-08-28b - TransferOrderListActionsSurfaceSeed.sql` | UNCHANGED (untracked) | Already applied to sandbox-client Core AND local Core. Correct as-is. |
|
|
165
|
+
| `dbchanges2/Client_Nychh/2026-08-28b - TransferOrdersNavigationEnable.sql` | MODIFIED (untracked) | `surfaceElementId` 170 -> 190 in 3 places. Already-applied rows need the UPDATE, not a re-run. |
|
|
166
|
+
| `dbchanges2/Client_Nychh/2026-08-28a - TransferOrdersTableView.sql` | UNCHANGED (untracked) | Applied to sandbox (TableView 22, 4 fields) and local. |
|
|
167
|
+
| `dbchanges2/Client_Nychh/2026-08-28c - TransferOrderRecordAcl.sql` | UNCHANGED (untracked) | Applied to sandbox (4 record perms, 34 field perms). |
|
|
168
|
+
| `dbchanges2/Client/2026-08-28a - TransferOrderStageThemeTokens.sql` | UNCHANGED (untracked) | NOT RUN on sandbox OR local (0 `color.transferStage.*` tokens on both). |
|
|
169
|
+
| `dbchanges2/Client/2026-08-28b - TransferOrderItemsTimestamps.sql` | UNCHANGED (untracked) | No action needed — production already had the columns, so sandbox has them via the reset. |
|
|
170
|
+
| `scratchpad/local-undo-transfer-surface-old-ids.sql` | CREATED | Deletes 24 messages / 5 surfaces / 17 elements / 6 vocabulary rows BY UUID in FK order, then repoints local's override to 190. Leaves `2026-08-28b` alone. |
|
|
171
|
+
| `scratchpad/28a.before.sql`, `nychh28b.before.sql`, `vite.config.ts.bak` | CREATED | Pre-change backups. |
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Decisions made
|
|
176
|
+
|
|
177
|
+
- **Renumber into a contiguous block ABOVE the current MAX (53-57 / 190-206 / 261-284) rather
|
|
178
|
+
than back-filling the gaps** left at Surfaces 48-51, Elements 177-186, Messages 233-250.
|
|
179
|
+
Rationale: a contiguous block above MAX is trivially verifiable and trivially reversible
|
|
180
|
+
(`DELETE WHERE id >= n`), whereas a split block is neither. Ids are not scarce. Rejected:
|
|
181
|
+
filling the gaps to keep numbering compact.
|
|
182
|
+
|
|
183
|
+
- **Fix already-applied override rows with a targeted `UPDATE` by uuid, not by re-running the
|
|
184
|
+
migration.** Rationale: the file's uuid is a fixed literal, so a re-run would fail on the
|
|
185
|
+
duplicate key; and after the UPDATE the file's `NOT EXISTS` guard correctly no-ops. Rejected:
|
|
186
|
+
DELETE-then-rerun (more destructive for no benefit).
|
|
187
|
+
|
|
188
|
+
- **`vite.config.ts` detects the symlink instead of exposing a flag/env var.** Rationale: the
|
|
189
|
+
linked and published cases need OPPOSITE `optimizeDeps` settings, and any flag a developer
|
|
190
|
+
must remember to flip will be wrong half the time — which is the whole class of bug this
|
|
191
|
+
session was spent on. Rejected: an env var, and rejected leaving `include` in place and
|
|
192
|
+
documenting the manual cache-clear ritual.
|
|
193
|
+
|
|
194
|
+
- **Left `BLOX_EXTERNALS` as a hand-maintained list with the regeneration command in a comment.**
|
|
195
|
+
Rationale: generating it at config time means reading blox's dist on every server start.
|
|
196
|
+
Accepted cost: if blox adds a dependency, the symptom is
|
|
197
|
+
`does not provide an export named 'default'` on that new package.
|
|
198
|
+
|
|
199
|
+
- **Declined both ELEVATED knowledge-base edits** during `/capture` (developer's call):
|
|
200
|
+
the dbchanges2 `architecture.md` test-data Critical rule, and the `standards/frontend.md`
|
|
201
|
+
§11/§13(c) dedupe accuracy fix. Consequence: the dedupe + `rm -rf node_modules/.vite`
|
|
202
|
+
detail lives ONLY in `toga-blox/workflows/local-link-into-a-consumer-app.md`, and the
|
|
203
|
+
test-data rule ONLY in `toga25-supply/features/transfer-orders-page.md`.
|
|
204
|
+
|
|
205
|
+
- **Did not commit the `.env.development` change** in toga25-supply. Rationale: its diff
|
|
206
|
+
repoints every developer's dev server.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Blockers
|
|
211
|
+
|
|
212
|
+
- **The 7 dbchanges2 files are all untracked.** This is not incidental — it is the direct cause
|
|
213
|
+
of the id collision. Two developers both computed `MAX(id)+1` against the same Core; the other
|
|
214
|
+
one shipped first. Nothing in git could warn either side.
|
|
215
|
+
- **Beta cannot take these migrations** — it has no `TransferOrderStages` table.
|
|
216
|
+
- **Reserved ids remain structurally unsafe.** Core ids must be literals because Core and Client
|
|
217
|
+
are on separate clusters in production, so a Client file cannot join to Core to resolve one.
|
|
218
|
+
That forces the number to be chosen at authoring time, opening a race for however long the
|
|
219
|
+
feature takes to ship. Nothing systemic was changed this session — only this one file was
|
|
220
|
+
renumbered and a re-verify warning added to its header.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Exact next step
|
|
225
|
+
|
|
226
|
+
> Run, in order, and stop if any verification returns non-zero:
|
|
227
|
+
>
|
|
228
|
+
> **sandbox-client / `Core`:** (1) `SELECT (SELECT COUNT(*) FROM Surfaces WHERE id BETWEEN 53 AND 57) AS surf, (SELECT COUNT(*) FROM SurfaceElements WHERE id BETWEEN 190 AND 206) AS elem, (SELECT COUNT(*) FROM Messages WHERE id BETWEEN 261 AND 284) AS msg;` — all must be 0.
|
|
229
|
+
> (2) run `Core/2026-08-28a - TransferOrderSurfaceSeed.sql`.
|
|
230
|
+
>
|
|
231
|
+
> **sandbox-client / `Client_Nychh`:** (3) `UPDATE SurfaceOverrides SET surfaceElementId = 190 WHERE uuid = '7a8b9c0d-1e2f-4304-8516-6a7b8c9d0e1f';`
|
|
232
|
+
> (4) run `Client/2026-08-28a - TransferOrderStageThemeTokens.sql`. Then reload the app — the
|
|
233
|
+
> Transfer Orders nav item should appear for NYCHH.
|
|
234
|
+
>
|
|
235
|
+
> **local:** (5) run `scratchpad/local-undo-transfer-surface-old-ids.sql` (its closing SELECT
|
|
236
|
+
> must be all zeros), (6) run the renumbered `Core/2026-08-28a`, (7) run
|
|
237
|
+
> `Client/2026-08-28a` against `Client_Nychh`.
|
|
238
|
+
>
|
|
239
|
+
> Then commit all 7 dbchanges2 files so the id blocks are recorded in git.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
_Saved by /session-save on 2026-08-31_
|
package/package.json
CHANGED