toga-ai 1.0.796 → 1.0.798
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/workflows/tracing-a-worker-cron-run-in-production.md +29 -2
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/features/cloud-s3-helpers.md +13 -2
- package/knowledge/2.0/apps/_underscore/features/item-classification-itemclasses.md +127 -0
- package/knowledge/2.0/apps/ai-bdr/features/web-funnel-app.md +35 -2
- package/knowledge/2.0/apps/toga-blox/workflows/dynamic-publish-pipeline.md +57 -2
- package/knowledge/2.0/apps/worker2/features/clickup-task-fluffer-talos.md +15 -2
- package/knowledge/2.0/apps/worker2/features/creating-worker-actions.md +50 -2
- package/knowledge/2.0/apps/worker2/features/netsuite-opportunity-sync.md +31 -2
- package/knowledge/INDEX.md +2 -2
- package/knowledge/clients/prudential/INDEX.md +1 -0
- package/knowledge/clients/prudential/features/itemclasses-device-tier-data.md +108 -0
- package/knowledge/clients/prudential/features/transmit-ordershipped-email.md +19 -9
- package/knowledge/clients/prudential/profile.md +2 -1
- package/package.json +1 -1
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: ["bala", "kyalamarthi"]
|
|
9
|
+
updated: 2026-09-11
|
|
10
|
+
owners: ["bala", "kyalamarthi", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/ebs/cron.worker.php
|
|
13
13
|
- worker/schedules/cron.worker.sync.json
|
|
@@ -23,6 +23,7 @@ related:
|
|
|
23
23
|
- ../../library/features/cron-execution-monitoring.md
|
|
24
24
|
- ../../library/features/email-test-mode-and-write-free-sends.md
|
|
25
25
|
- ../../../1.0/standards/backend-php.md
|
|
26
|
+
- ../../../clients/prudential/features/itemclasses-device-tier-data.md
|
|
26
27
|
---
|
|
27
28
|
|
|
28
29
|
## Summary
|
|
@@ -47,6 +48,27 @@ frequency makes you read a normal gap as an outage.
|
|
|
47
48
|
Also check whether the job is registered in the env you are testing in at all: this one is **not**
|
|
48
49
|
in `cron.beta.json`, so beta never runs it on a schedule. Nothing on beta is evidence about it.
|
|
49
50
|
|
|
51
|
+
### Is the job switched OFF? — read the `active` flag carefully
|
|
52
|
+
|
|
53
|
+
Each schedule entry is `{active, name, schedule, cron}`, and `ebs/cron.worker.php:39` gates it:
|
|
54
|
+
|
|
55
|
+
```php
|
|
56
|
+
if (!property_exists($cron, 'active') || $cron->active) {
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **`"active": 0` genuinely stops the job** — it is never written into the crontab, so it leaves
|
|
60
|
+
no `CronJobExecutions` row and no trace anywhere. Indistinguishable from "never fired" unless
|
|
61
|
+
you read the JSON.
|
|
62
|
+
- **⚠ A MISSING `active` property means the job RUNS.** The default is on, not off. Do not read
|
|
63
|
+
the absence of `active` as "disabled".
|
|
64
|
+
|
|
65
|
+
When a job is off, `git log` the schedule file to find out who turned it off and why — the commit
|
|
66
|
+
message usually carries the reason. Worked example (2026-09-11): "Transmissions to Netsuite"
|
|
67
|
+
(`cron.worker.sync.json:441`) is `"active": 0`, disabled 2026-07-17 in commit `14570def`
|
|
68
|
+
("Add careOf & skip Apple; disable Netsuite cron") on `_production`. That single flag is what made
|
|
69
|
+
Prudential's `ItemClasses` data dead — see
|
|
70
|
+
[Prudential ItemClasses device-tier data](../../../clients/prudential/features/itemclasses-device-tier-data.md).
|
|
71
|
+
|
|
50
72
|
### Which of the 7 boxes runs it — and in which timezone
|
|
51
73
|
|
|
52
74
|
Two facts you need before you go looking for evidence, both easy to get wrong:
|
|
@@ -188,6 +210,11 @@ real script **never executes**, on every tick, with no error anywhere. It looks
|
|
|
188
210
|
contain anything; only the `cron` path is shell-parsed.
|
|
189
211
|
|
|
190
212
|
## Change history
|
|
213
|
+
- 2026-09-11 — Added **"is the job switched OFF?"** to Step 1: `ebs/cron.worker.php:39` gates on
|
|
214
|
+
`!property_exists($cron, 'active') || $cron->active`, so `"active": 0` genuinely stops the job
|
|
215
|
+
and leaves no trace, but a **missing `active` property means it RUNS** (default is on). Also:
|
|
216
|
+
`git log` the schedule file to find who disabled it and why. Found while proving Prudential's
|
|
217
|
+
`ItemClasses` data was dead. (rgirish)
|
|
191
218
|
- 2026-09-04 — Added the **`&`-in-a-cron-filename trap**: `ebs/cron.worker.php:41` concatenates the
|
|
192
219
|
path into the crontab line unquoted, so a shell metacharacter splits the command and the script
|
|
193
220
|
never runs while leaving no trace. Found via the renamed Compass Canada
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
| [FIELD_STORAGE fields — per-row lazy hydration and the platform-wide missing-column 500](features/field-storage-row-hydration.md) | `FIELD_STORAGE` is the 2.0 field type for blob-backed columns (S3 or local folder). |
|
|
28
28
|
| [Forecast.Sales NetSuite import engine (real-time webhook)](features/forecast-sale-import.md) | Real-time importer that takes a NetSuite **sale** record and writes its lines into `Forecast.Sales` (the Forecast2 revenue table). |
|
|
29
29
|
| [isFulfillable Propagation Up the SO↔PO Chain](features/fulfillable-item-propagation.md) | `Items.isFulfillable` is a boolean that gates whether a storefront line's **Qty Fulfilled** cell is actionable. |
|
|
30
|
+
| [ItemClasses / _Model_Client_ItemClass — who actually uses it (and why it is not free)](features/item-classification-itemclasses.md) | `ItemClasses` is an empty-looking, almost-unused lookup table that ships in **every** 2.0 client database. |
|
|
30
31
|
| [Item-Fulfillment Stage Lifecycle (picked/packed/shipped) & Order Status](features/item-fulfillment-stage-lifecycle-and-order-status.md) | Every ItemFulfillment (IF) now carries an explicit **stage** — picked → packed → shipped — resolved through `ItemFulfillmentStages → ItemFulfillmentStatuses` (m |
|
|
31
32
|
| [DB-free unit testing for _underscore model interceptors](features/model-interceptor-unit-testing.md) | `_underscore` shipped with **no** PHPUnit setup (no `composer.json`/`phpunit`; only vendored PhpOffice tests existed). |
|
|
32
33
|
| [_Model magic-field access (__get without __isset)](features/model-magic-field-access.md) | `_Model` exposes DB columns as "magic" properties via `__get()`, but it defines **no** `__isset()`. |
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal", "bala"]
|
|
9
|
+
updated: 2026-09-10
|
|
10
|
+
owners: ["jcardinal", "bala", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Cloud.php
|
|
13
13
|
related:
|
|
@@ -59,6 +59,13 @@ None — shared core helper.
|
|
|
59
59
|
do not add your own `ContinuationToken` loop on top.
|
|
60
60
|
- A bucket outside the config-default us-east-1 (e.g. `agilant-as2` in us-west-2) throws
|
|
61
61
|
`AuthorizationHeaderMalformed` unless you pass `$awsRegion`.
|
|
62
|
+
- **⚠ `copyFileToS3` is a single `putObject` — it CANNOT upload a file larger than 5 GB.**
|
|
63
|
+
Verified 2026-09-10 at `_underscore/Cloud.php:49`: the method issues one `putObject` call, and
|
|
64
|
+
S3 caps a single PUT at **5 GB**. There is **no `MultipartUploader` anywhere in `_underscore`
|
|
65
|
+
or `worker2`**. Anything that could produce a large artifact — a database dump above all — will
|
|
66
|
+
fail on this helper once it crosses the limit. If you need large-object upload, add
|
|
67
|
+
`MultipartUploader` explicitly; do not assume this helper scales.
|
|
68
|
+
|
|
62
69
|
- **`getFileFromS3` (the GET method) is single-region.** It builds the `S3Client` with region
|
|
63
70
|
`_Config::cloud('aws_region')` (the worker's own region) and only swallows HTTP 404. Reading
|
|
64
71
|
an object from a bucket in a **different** region raises a `301 PermanentRedirect`, which it
|
|
@@ -67,6 +74,10 @@ None — shared core helper.
|
|
|
67
74
|
has no region-override parameter.)
|
|
68
75
|
|
|
69
76
|
## Change history
|
|
77
|
+
- 2026-09-10 — Recorded that **`copyFileToS3` (`Cloud.php:49`) is a single `putObject`**, so it
|
|
78
|
+
cannot upload an object larger than S3's 5 GB single-PUT cap, and that **no `MultipartUploader`
|
|
79
|
+
exists anywhere in `_underscore` or `worker2`**. Matters for any large-artifact upload, database
|
|
80
|
+
dumps first. (rgirish)
|
|
70
81
|
- 2026-07-15 — Documented that `getFileFromS3` is single-region (client built with the worker's
|
|
71
82
|
own `aws_region`, only swallows 404, re-throws `301 PermanentRedirect` for cross-region reads);
|
|
72
83
|
use a direct HTTPS download for cross-region/public objects. (bala)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "ItemClasses / _Model_Client_ItemClass — who actually uses it (and why it is not free)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: _underscore
|
|
5
|
+
project: _Underscore
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-11
|
|
10
|
+
owners: ["rgirish"]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/ItemClass.php
|
|
13
|
+
- _underscore/Model/Client/Item.php
|
|
14
|
+
- worker/crons/toga2/prudential/transmissions_to_netsuite.php
|
|
15
|
+
- toga2-supply/src/pages/Inventory/api/InventoryApi.tsx
|
|
16
|
+
related:
|
|
17
|
+
- ./netsuite-rest-client.md
|
|
18
|
+
- ./record-change-audit-log.md
|
|
19
|
+
- ../../../../clients/prudential/features/itemclasses-device-tier-data.md
|
|
20
|
+
- ../../dbchanges2/workflows/client-schema-drift-audit.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
`ItemClasses` is an empty-looking, almost-unused lookup table that ships in **every** 2.0 client
|
|
26
|
+
database. It looks like free real estate for a new item-classification feature. It is not:
|
|
27
|
+
**2 of 36 production clients already have rows in it, and they use it for two different things,
|
|
28
|
+
neither of which is NetSuite's `class` field.**
|
|
29
|
+
|
|
30
|
+
Read this before you plan to reuse, extend, or repurpose `ItemClasses` for any client.
|
|
31
|
+
|
|
32
|
+
Audit done 2026-09-11 across all TOGA codebases and all 36 production client schemas.
|
|
33
|
+
|
|
34
|
+
## Key files / entry points
|
|
35
|
+
|
|
36
|
+
- **`_underscore/Model/Client/ItemClass.php`** — `_Model_Client_ItemClass`,
|
|
37
|
+
`DATABASE = _underscore::DB_CLIENT`, `TABLE = 'ItemClasses'`. Four fields only:
|
|
38
|
+
`id`, `uuid`, `code`, `name`.
|
|
39
|
+
- **`_underscore/Model/Client/Item.php:23`** — the only foreign key:
|
|
40
|
+
`public $itemClassId = [self::FIELD_FOREIGNKEY, ... => '\_Model_Client_ItemClass'];`
|
|
41
|
+
- **`worker/crons/toga2/prudential/transmissions_to_netsuite.php:171-178`** — the **only** code
|
|
42
|
+
anywhere that reads it (see below).
|
|
43
|
+
|
|
44
|
+
## How it works
|
|
45
|
+
|
|
46
|
+
### The table ships blank to everyone
|
|
47
|
+
|
|
48
|
+
`ItemClasses` is part of the blank client DB, so every 2.0 tenant has the table. Having the
|
|
49
|
+
table says nothing about whether the client uses it. **Check row counts, not schema.**
|
|
50
|
+
|
|
51
|
+
### Who has data (prod, verified 2026-09-11)
|
|
52
|
+
|
|
53
|
+
| Client | Rows in `ItemClasses` | Items tagged | What the rows actually mean |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| **Northwell** | 27 | 245 of 247 | ServiceNow **service categories** — "Print", "Computer", "Service Desk" |
|
|
56
|
+
| **Prudential** | 7 | 9 of 765 | Dell/Apple **device tiers** — "LATITUDE 73XX", "MACBOOK PRO" |
|
|
57
|
+
| all other 34 clients | 0 | 0 | unused |
|
|
58
|
+
|
|
59
|
+
Two clients, two unrelated meanings. Neither is an "item class" in the NetSuite sense.
|
|
60
|
+
|
|
61
|
+
**Canon is a false positive.** `dbchanges2/Client_Canon/2024-01-01.sql` contains `ItemClasses`
|
|
62
|
+
schema changes, but production `Client_Canon` has **0 rows**. Schema history is not usage.
|
|
63
|
+
|
|
64
|
+
### There is exactly one reader in the whole platform
|
|
65
|
+
|
|
66
|
+
`worker/crons/toga2/prudential/transmissions_to_netsuite.php:171-178` pulls
|
|
67
|
+
`ItemClasses.name` as `deviceTier` for the line-1 item of a sales order and sends it to NetSuite:
|
|
68
|
+
|
|
69
|
+
```sql
|
|
70
|
+
SELECT ItemClasses.name
|
|
71
|
+
FROM SalesOrderItems
|
|
72
|
+
INNER JOIN Items ON Items.id = SalesOrderItems.itemId
|
|
73
|
+
INNER JOIN ItemClasses ON ItemClasses.id = Items.itemClassId
|
|
74
|
+
WHERE SalesOrderItems.salesOrderId = SalesOrders.id AND SalesOrderItems.lineNumber = 1
|
|
75
|
+
LIMIT 1
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
That cron is **disabled** — see the
|
|
79
|
+
[Prudential ItemClasses data doc](../../../../clients/prudential/features/itemclasses-device-tier-data.md).
|
|
80
|
+
|
|
81
|
+
**Nothing writes `itemClassId`.** No writer exists in `api2`, `worker2`, or `_underscore`.
|
|
82
|
+
|
|
83
|
+
### Frontend: one real reference, and a grep trap
|
|
84
|
+
|
|
85
|
+
`grep itemClass` in the frontend is misleading — most hits in `toga2-supply` are CSS
|
|
86
|
+
variables like `menuItemClassNames`, not this table. The one genuine reference is
|
|
87
|
+
`toga2-supply/src/pages/Inventory/api/InventoryApi.tsx:815`, requesting the column
|
|
88
|
+
`item.itemClass.name`.
|
|
89
|
+
|
|
90
|
+
## NetSuite `classification` — the shape you would have to map from
|
|
91
|
+
|
|
92
|
+
Queried live via SuiteQL (2026-09-11): **311 classifications**, columns
|
|
93
|
+
`id`, `name`, `fullname`, `isinactive`, `parent`.
|
|
94
|
+
|
|
95
|
+
- **It is a tree, not a flat list.** Each row has a `parent` and a colon-separated `fullname`,
|
|
96
|
+
e.g. name `Laptop` / fullname `Technology Sales : Hardware : End User Devices : Laptop` /
|
|
97
|
+
parent `405`.
|
|
98
|
+
- **⚠ `name` is NOT globally unique** — only unique within its parent. Repeats seen:
|
|
99
|
+
"Managed Services" 3x (ids 75, 395, 379), "Software" 2x (ids 3, 419), "AI & Data Sciences" 2x,
|
|
100
|
+
"Data Services" 2x.
|
|
101
|
+
|
|
102
|
+
## Gotchas
|
|
103
|
+
|
|
104
|
+
- **⚠ `ItemClasses.name` has a UNIQUE index (index literally named `name`).** NetSuite
|
|
105
|
+
classification names repeat, so **any attempt to load NetSuite classes into this table fails
|
|
106
|
+
until that UNIQUE is dropped.** This is the first blocker on any NetSuite-classification work.
|
|
107
|
+
- **A hierarchy path column must be `TEXT`, not `varchar(512)`.** NetSuite `fullname` values are
|
|
108
|
+
long, and the 2.0 standard caps varchar at 255.
|
|
109
|
+
- **The table is already `utf8mb4_0900_ai_ci`**, so new columns need no explicit `COLLATE`.
|
|
110
|
+
- **`c_` custom fields are NOT declared as properties on the 2.0 Model class.** The framework
|
|
111
|
+
reads them from `CustomRecordFields` (`ItemClasses` is `Core.Records` id **105**). Confirmed:
|
|
112
|
+
no `public $c_netsuite*` property exists on any `Model/Client` class. Copy the pattern in
|
|
113
|
+
`dbchanges2/_modules/netsuite/2026-08-18 - TransferOrdersNetsuiteStatus.sql`.
|
|
114
|
+
- **Check whether the client has an `ItemClassCustomFields` table before adding `c_` columns.**
|
|
115
|
+
`Client_Prudential` does not, so its `c_` columns would go directly on `ItemClasses`.
|
|
116
|
+
- **"Every client has the table" ≠ "the table is free."** Repurposing `ItemClasses` org-wide
|
|
117
|
+
would break Northwell's 245 tagged items, which hold ServiceNow service categories.
|
|
118
|
+
|
|
119
|
+
## Change history
|
|
120
|
+
- 2026-09-11 — Documented from a full usage audit of `ItemClasses` across every TOGA codebase and
|
|
121
|
+
all 36 prod client schemas: the table ships blank to all clients but **only Northwell (27 rows,
|
|
122
|
+
ServiceNow service categories) and Prudential (7 rows, Dell device tiers) have data**, for two
|
|
123
|
+
different meanings; there is exactly **one reader** (`transmissions_to_netsuite.php:171-178`,
|
|
124
|
+
currently disabled) and **zero writers**; Canon has schema but no rows. Recorded the live
|
|
125
|
+
NetSuite `classification` shape (311 rows, a tree with repeating `name`) and the resulting
|
|
126
|
+
blocker: `ItemClasses.name` is UNIQUE and must be dropped before NetSuite classes can be
|
|
127
|
+
mapped in. (rgirish)
|
|
@@ -6,8 +6,8 @@ project: AI-BDR
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [tcox]
|
|
9
|
+
updated: 2026-09-10
|
|
10
|
+
owners: [tcox, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- bdr/PLAN.md
|
|
13
13
|
- bdr/next.config.ts
|
|
@@ -32,6 +32,9 @@ files:
|
|
|
32
32
|
- bdr/test/fastDial.test.ts
|
|
33
33
|
- bdr/eslint.config.mjs
|
|
34
34
|
- bdr/amplify.yml
|
|
35
|
+
- bdr/package.json
|
|
36
|
+
- bdr/.npmrc
|
|
37
|
+
- bdr/src/components/TalosDrawer.tsx
|
|
35
38
|
- bdr/src/app
|
|
36
39
|
- bdr/src/app/layout.tsx
|
|
37
40
|
- bdr/src/app/page.tsx
|
|
@@ -619,6 +622,28 @@ would be cached by browsers and crawlers and become painful to undo.
|
|
|
619
622
|
|
|
620
623
|
## Gotchas
|
|
621
624
|
|
|
625
|
+
- **⚠ An `import` added without the matching `package.json` dependency only fails in Amplify —
|
|
626
|
+
never locally.** Root cause of the 2026-09-10 build break: PR #5 (`blox-talos-bdr-update`,
|
|
627
|
+
TRUE-81447) added
|
|
628
|
+
`import { TalosPanel } from "@agilant/toga-blox/dist/components/Talos/index.js"` to
|
|
629
|
+
`src/components/TalosDrawer.tsx` but **never added `@agilant/toga-blox` to `package.json`**.
|
|
630
|
+
The author's `node_modules` already had it (linked or left over from an earlier install), so
|
|
631
|
+
local `next build` was green; Amplify installs from the lockfile into a clean tree and failed
|
|
632
|
+
with `Module not found`. **Before merging any PR that adds a new bare import, grep the new
|
|
633
|
+
import specifiers against `package.json`** — a green local build proves nothing about a clean
|
|
634
|
+
install. The `local-link` workflow states the same trap from the blox side: "a link masks
|
|
635
|
+
missing dependency + peer declarations."
|
|
636
|
+
- **BDR's blox dependency set (as fixed 2026-09-10):** `@agilant/toga-blox@1.1.2-production.144`
|
|
637
|
+
(pinned exactly, from the `production` dist-tag), plus
|
|
638
|
+
`@fortawesome/fontawesome-svg-core@^7.3.1` and `@fortawesome/react-fontawesome@^3.5.0`, plus a
|
|
639
|
+
new `bdr/.npmrc`. Verified: clean install exit 0, `next build` compiled, **385/385 tests pass**.
|
|
640
|
+
The `.npmrc` needs the **FontAwesome Pro** registry line and `legacy-peer-deps=true` — blox is a
|
|
641
|
+
**public** package needing no token, and blox peers still cap React at `^18` while BDR runs
|
|
642
|
+
React 19. Full detail:
|
|
643
|
+
[dynamic-publish-pipeline.md](../../toga-blox/workflows/dynamic-publish-pipeline.md#consuming-blox-which-registry-actually-needs-a-token).
|
|
644
|
+
**Never commit the token value** — a live one was pushed in `bdr` commit `88809c3`; treat it as
|
|
645
|
+
compromised and rotate.
|
|
646
|
+
|
|
622
647
|
- **"The funnel is gone / a CRM link doesn't prefill" — check the redirect FIRST.** Since
|
|
623
648
|
2026-08-24 `/` 307s to `/landing`, so every symptom that used to mean "missing env" or
|
|
624
649
|
"rate-limit soft-degrade" can now simply mean the request never reached `page.tsx`. Probe
|
|
@@ -754,6 +779,14 @@ would be cached by browsers and crawlers and become painful to undo.
|
|
|
754
779
|
harness; pull it with `npx toga-ai`.
|
|
755
780
|
|
|
756
781
|
## Change history
|
|
782
|
+
- 2026-09-10 — **Fixed the Amplify `Module not found` build break** and recorded its durable
|
|
783
|
+
lesson: PR #5 added a `@agilant/toga-blox` import to `TalosDrawer.tsx` without adding the
|
|
784
|
+
package to `package.json`, so it built locally (stale `node_modules`) and failed only on
|
|
785
|
+
Amplify's clean install. Added `@agilant/toga-blox@1.1.2-production.144` +
|
|
786
|
+
`@fortawesome/fontawesome-svg-core@^7.3.1` + `@fortawesome/react-fontawesome@^3.5.0` and a
|
|
787
|
+
`bdr/.npmrc` (FontAwesome Pro registry + `legacy-peer-deps=true` for the React 19 host; blox
|
|
788
|
+
itself is public and needs no token). Verified clean install, compiled build, 385/385 tests.
|
|
789
|
+
(rgirish)
|
|
757
790
|
- 2026-08-26 — Env-var accounting only: recorded an **optional 8th deploy var**,
|
|
758
791
|
`TOGA_SECURITY_CAMPAIGN_UUID`, declared in `.env.example` and deliberately **left unset**.
|
|
759
792
|
It gates `/landing`'s new real callback route (`POST /api/landing-call`), which 503s and
|
|
@@ -6,12 +6,14 @@ project: TOGa Blox
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [jcardinal, apeterson, tcox]
|
|
9
|
+
updated: 2026-09-10
|
|
10
|
+
owners: [jcardinal, apeterson, tcox, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- toga-blox/.github/workflows/publish.yml
|
|
13
13
|
- toga-blox/package.json
|
|
14
14
|
- toga-blox/src/utils/getFontAwesomeIcon.tsx
|
|
15
|
+
- bdr/.npmrc
|
|
16
|
+
- toga25-supply/.npmrc
|
|
15
17
|
related:
|
|
16
18
|
- ../architecture.md
|
|
17
19
|
- ./landing-a-large-feature-branch.md
|
|
@@ -83,6 +85,50 @@ blox channel. A channel exists **only** when this repo (`toga-blox-npm`) publish
|
|
|
83
85
|
This publish-before-build ordering is the ongoing cost of the strict 1:1 model: every
|
|
84
86
|
environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
|
|
85
87
|
|
|
88
|
+
## Consuming blox: which registry actually needs a token
|
|
89
|
+
|
|
90
|
+
**`@agilant/toga-blox` is a PUBLIC npm package. It needs NO auth.** Verified 2026-09-10 by an
|
|
91
|
+
unauthenticated request:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
curl -s -o /dev/null -w '%{http_code}' https://registry.npmjs.org/@agilant%2Ftoga-blox
|
|
95
|
+
# 200
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The team had been copying an `.npmrc` carrying **two** tokens into each consumer, believing the
|
|
99
|
+
`@agilant` scope was private. It is not. Only **one** credential is real:
|
|
100
|
+
|
|
101
|
+
| Registry | Auth needed? | Why |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `registry.npmjs.org` (`@agilant/toga-blox`) | **No** | Public package, HTTP 200 unauthenticated |
|
|
104
|
+
| `npm.fontawesome.com` (`@fortawesome/pro-*`) | **Yes** | Paid/private packages |
|
|
105
|
+
|
|
106
|
+
**FontAwesome Pro is the only thing the token is for.** blox's `src/utils/getFontAwesomeIcon.tsx`
|
|
107
|
+
hard-imports `@fortawesome/pro-solid-svg-icons`, `pro-regular-svg-icons` and
|
|
108
|
+
`pro-light-svg-icons`, so any consumer of blox pulls the Pro registry transitively. Proven by
|
|
109
|
+
isolating `HOME` to an empty dir and installing: the failure is **E401 on `npm.fontawesome.com`
|
|
110
|
+
tarballs only** — never on the `@agilant` scope.
|
|
111
|
+
|
|
112
|
+
Practical consequence: a consumer's `.npmrc` needs the FontAwesome registry line + token and
|
|
113
|
+
`legacy-peer-deps=true`. It does **not** need an `@agilant:registry` line or an npmjs token.
|
|
114
|
+
|
|
115
|
+
### `legacy-peer-deps=true` is required on a React 19 host
|
|
116
|
+
|
|
117
|
+
blox `peerDependencies` still declare React `^18`. A React 19 consumer (e.g. the `bdr` Next.js
|
|
118
|
+
app) cannot resolve the install without `legacy-peer-deps=true` in `.npmrc`. The peer range is a
|
|
119
|
+
declaration, not a runtime wall — blox runs fine on React 19 (see
|
|
120
|
+
[local-link-into-a-consumer-app.md](./local-link-into-a-consumer-app.md) §2).
|
|
121
|
+
|
|
122
|
+
### ⚠ Never commit a token VALUE in `.npmrc`
|
|
123
|
+
|
|
124
|
+
Live FontAwesome tokens have been found **committed** in `toga25-supply/.npmrc` and
|
|
125
|
+
`info/.npmrc`, and one was pushed in `bdr` commit `88809c3`. Treat every token found this way as
|
|
126
|
+
compromised and rotate it. The correct shape is an env-var placeholder resolved at build time —
|
|
127
|
+
which is exactly what blox's own `publish.yml` already does (see the "Secret hygiene done right"
|
|
128
|
+
gotcha below). This is the same violation already recorded in
|
|
129
|
+
[`../../../standards/frontend.md`](../../../standards/frontend.md); what is **new here is that
|
|
130
|
+
only the FontAwesome token was ever needed** — the npmjs token in those files bought nothing.
|
|
131
|
+
|
|
86
132
|
## Gotchas
|
|
87
133
|
|
|
88
134
|
- **⚠ There is a STALE UNDERSCORED dist-tag on the registry — `_sandbox-client` (with the
|
|
@@ -212,6 +258,15 @@ environment needs its own maintained `_<mode>` branch in `toga-blox-npm`.
|
|
|
212
258
|
you push to a release branch.
|
|
213
259
|
|
|
214
260
|
## Change history
|
|
261
|
+
- 2026-09-10 — **Corrected a long-standing team assumption: `@agilant/toga-blox` is a PUBLIC npm
|
|
262
|
+
package and needs no auth** (verified by unauthenticated `curl` → HTTP 200). The only registry
|
|
263
|
+
that requires a token is **FontAwesome Pro** (`npm.fontawesome.com`), pulled in transitively
|
|
264
|
+
because `getFontAwesomeIcon.tsx` hard-imports the `@fortawesome/pro-*` packages — proven by
|
|
265
|
+
isolating `HOME` and observing E401 on FontAwesome tarballs only. Consumers had been copying an
|
|
266
|
+
`.npmrc` with both tokens; the npmjs one was never needed. Also recorded that
|
|
267
|
+
`legacy-peer-deps=true` is required on a React 19 host (blox peers still cap at `^18`), and that
|
|
268
|
+
live tokens were found committed in `toga25-supply/.npmrc`, `info/.npmrc` and pushed in `bdr`
|
|
269
|
+
`88809c3` — rotate and replace with a build-time env-var placeholder. (rgirish)
|
|
215
270
|
- 2026-09-02 — Released `1.1.2-sandbox-client.142` (base `1.1.0` → `1.1.1` on `_sandbox-client`,
|
|
216
271
|
commit `543a7186`) and rolled it into `toga25-supply` (pin `1.1.1-sandbox-client.140` →
|
|
217
272
|
`1.1.2-sandbox-client.142`, clean `npm install`, `tsc --noEmit` zero errors). The documented
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [rgirish]
|
|
9
|
+
updated: 2026-09-10
|
|
10
|
+
owners: [rgirish, jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Clickup/Fluffer.php
|
|
13
13
|
- worker2/Worker/Clickup.php
|
|
@@ -121,6 +121,14 @@ never by value.
|
|
|
121
121
|
|
|
122
122
|
## Gotchas / known issues
|
|
123
123
|
|
|
124
|
+
- **⚠ OPEN (found 2026-09-10, NOT fixed): the Talos agent call is failing in prod with HTTP 403.**
|
|
125
|
+
`Clickup/Fluffer/Process` failed **168 times over 7 days** with `Talos agent returned HTTP 403` — an
|
|
126
|
+
auth/permission failure on the `POST /api/ai/agent` call (`callTalosAgent()`,
|
|
127
|
+
`worker2/Worker/Clickup/Fluffer.php` ~L230, from `Process()` ~L94). 403 points at a bad/expired
|
|
128
|
+
`[talos] agent_api_key` (the `dev-core`-scoped key sent as `X-API-Key`) or the agent bridge revoking
|
|
129
|
+
it — **not** a ClickUp problem. Left unfixed by the developer for now; recorded so the next triage
|
|
130
|
+
starts from "check the `agent_api_key` / agent-bridge auth", not from the code. Surfaced by the
|
|
131
|
+
`Core.WorkerJobs` action+outcome census (group by action, isSuccess over 7 days).
|
|
124
132
|
- **Watchdog race.** The fluffer is an **ACTION** job; its watchdog is the Lambda env
|
|
125
133
|
`MAX_EXECUTION_TIME_WORKER_ACTION` (default 300s). The Talos timeout is set to **280** (just
|
|
126
134
|
under 300) so the job doesn't lose the race against its own watchdog out of the box.
|
|
@@ -144,6 +152,11 @@ never by value.
|
|
|
144
152
|
|
|
145
153
|
## Change history
|
|
146
154
|
|
|
155
|
+
- 2026-09-10 — **Discovered (NOT fixed): the fluffer is down in prod on a Talos-agent HTTP 403.**
|
|
156
|
+
`Clickup/Fluffer/Process` failed 168x over 7 days with `Talos agent returned HTTP 403` — an
|
|
157
|
+
auth/permission failure on the `/api/ai/agent` call, most likely an expired/wrong `[talos]
|
|
158
|
+
agent_api_key`. Deferred by the developer; logged as an open known issue for the next triage. Found
|
|
159
|
+
via the `Core.WorkerJobs` action+outcome census (toga-db MCP, no ClickUp/Talos access). (jcardinal)
|
|
147
160
|
- 2026-07-20 — Built the ClickUp task description fluffer: new `_Worker_Clickup_Fluffer`
|
|
148
161
|
(action `Clickup/Fluffer/Process`, `taskCreated` webhook auto-trigger, `force` regenerate)
|
|
149
162
|
calling the Talos `dev-core` KB-investigation agent at `/api/ai/agent` via `_ApiRequest`
|
|
@@ -6,13 +6,15 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean]
|
|
9
|
+
updated: 2026-09-10
|
|
10
|
+
owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean, rgirish]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/
|
|
13
13
|
- worker2/Controller/Index.php
|
|
14
14
|
- worker2/LambdaFunctions/JobScheduler/lambda_function.py
|
|
15
15
|
- _underscore/Worker.php
|
|
16
|
+
- worker2/Worker/Infrastructure/Database.php
|
|
17
|
+
- worker2/.platform/hooks/prebuild/020-install-mysql-client.sh
|
|
16
18
|
related:
|
|
17
19
|
- ../architecture.md
|
|
18
20
|
- ./netsuite-opportunity-client-labels.md
|
|
@@ -190,6 +192,40 @@ The EB worker dispatcher (`Controller/Index.php::worker()`) wraps your action ca
|
|
|
190
192
|
> It now captures **both** success results and failure text. See
|
|
191
193
|
> [architecture.md](../architecture.md) for the multi-producer rename gotcha.
|
|
192
194
|
|
|
195
|
+
### ⚠ A `null` return is logged as SUCCESS — "it ran green" is not evidence it did anything
|
|
196
|
+
|
|
197
|
+
`Controller/Index.php:378` records `isSuccess = 1` for **any** normal return, including `null`.
|
|
198
|
+
The framework has no notion of "the action did no work": an action that silently does nothing —
|
|
199
|
+
or is an unfinished stub — produces a clean `WorkerJobs` row indistinguishable from a real run.
|
|
200
|
+
|
|
201
|
+
Live example: **`_Worker_Infrastructure_Database::Backup()`
|
|
202
|
+
(`worker2/Worker/Infrastructure/Database.php:5-15`) is a stub.** It sends a test email to a
|
|
203
|
+
hardcoded address and returns `null`. Every invocation is logged as a **successful database
|
|
204
|
+
backup**. Nothing is backed up.
|
|
205
|
+
|
|
206
|
+
Two rules follow:
|
|
207
|
+
|
|
208
|
+
1. **When you write an action, return something that proves the work happened** — a count, an
|
|
209
|
+
id list, a summary string. `return null;` on a work-performing action throws away the only
|
|
210
|
+
signal the dispatcher records.
|
|
211
|
+
2. **When you audit an action, never accept a green `WorkerJobs` row as proof.** Read the
|
|
212
|
+
`output` column, and read the method. This applies to anything safety-critical (backups,
|
|
213
|
+
reconciliations, syncs) where "it's been green for months" is the expected — and worthless —
|
|
214
|
+
observation.
|
|
215
|
+
|
|
216
|
+
Two related defects found in the same audit, both worth knowing before you build on this
|
|
217
|
+
infrastructure:
|
|
218
|
+
|
|
219
|
+
- **`_underscore/Cloud.php:49` `copyFileToS3` is a single `putObject`**, so it cannot upload
|
|
220
|
+
anything over S3's **5 GB** single-PUT cap, and there is no `MultipartUploader` anywhere in
|
|
221
|
+
`_underscore` or `worker2` — see
|
|
222
|
+
[_Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md).
|
|
223
|
+
- **`worker2/.platform/hooks/prebuild/020-install-mysql-client.sh` installs the WRONG client for
|
|
224
|
+
Aurora MySQL 3.** It installs **`mariadb105`**, whose `mysqldump` has no `--set-gtid-purged`
|
|
225
|
+
flag and emits MariaDB-versioned `/*!100xxx*/` conditional comments — comments that MySQL
|
|
226
|
+
silently **ignores on reload**, so parts of the dump vanish without any error. Any dump-based
|
|
227
|
+
work on this host needs the **MySQL** client (`mysql-community-client`), not the MariaDB one.
|
|
228
|
+
|
|
193
229
|
Crucially, the worker **always returns HTTP 200, even on a throw**, so SQS deletes the message
|
|
194
230
|
immediately. **There is no DLQ and no automatic retry.** A throw makes the failure *visible* (a
|
|
195
231
|
failed `WorkerJobs` row to alert on) — it does **not** re-run the job.
|
|
@@ -464,6 +500,18 @@ Rules: rename **both sides in one PR and one deploy**; grep every repo for the o
|
|
|
464
500
|
commit-before-SQS transaction pattern that the worker relies on.
|
|
465
501
|
|
|
466
502
|
## Change history
|
|
503
|
+
- 2026-09-10 — Recorded that **a `null` return is stamped `isSuccess = 1`**
|
|
504
|
+
(`Controller/Index.php:378`), so an action that does nothing is logged as a successful run —
|
|
505
|
+
with the live proof that `_Worker_Infrastructure_Database::Backup()`
|
|
506
|
+
(`Worker/Infrastructure/Database.php:5-15`) is a stub that emails a hardcoded address, returns
|
|
507
|
+
`null`, and has therefore been reporting successful database backups while backing up nothing.
|
|
508
|
+
Added the two rules: return a proof-of-work value from every action, and never treat a green
|
|
509
|
+
`WorkerJobs` row as evidence for a safety-critical job. Also recorded two neighbouring defects
|
|
510
|
+
found in the same audit: `_Cloud::copyFileToS3` is a single `putObject` (5 GB ceiling, no
|
|
511
|
+
`MultipartUploader` in the codebase), and the EB prebuild hook
|
|
512
|
+
`020-install-mysql-client.sh` installs **`mariadb105`**, whose `mysqldump` lacks
|
|
513
|
+
`--set-gtid-purged` and writes `/*!100xxx*/` comments that MySQL silently drops on reload —
|
|
514
|
+
the wrong client for Aurora MySQL 3. (rgirish)
|
|
467
515
|
- 2026-09-02 — Added the corollary that an action must **never sleep-wait for a third-party API
|
|
468
516
|
rate budget** (it burns the 300s cap and the failure is unrecoverable — no retry, no DLQ); bulk
|
|
469
517
|
work yields and re-enqueues, event-driven work proceeds over budget counted and alarmed. (ajean)
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: ["dfranks", "kyalamarthi", "ajean"]
|
|
9
|
+
updated: 2026-09-10
|
|
10
|
+
owners: ["dfranks", "kyalamarthi", "ajean", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Netsuite.php
|
|
13
13
|
- worker2/Worker/Netsuite/Opportunity.php
|
|
@@ -387,6 +387,22 @@ the legacy cron's OPPORTUNITIES section:
|
|
|
387
387
|
(`hasEndCustomerColumn()`), since the migration is applied by hand and an unknown column would fail
|
|
388
388
|
the whole query and break every opportunity webhook. See
|
|
389
389
|
[the labels doc](./netsuite-opportunity-client-labels.md).
|
|
390
|
+
- **⚠ `hasEndCustomerColumn()` guards the DB COLUMN, NOT the ORM MODEL field — and that gap took the
|
|
391
|
+
whole opportunity sync down for 8 days (2026-09-02 → 2026-09-10).** The guard does a `SHOW COLUMNS`
|
|
392
|
+
on `Forecast.Opportunities`, so once the migration was applied it passed and the handler ran
|
|
393
|
+
`$opportunity->endCustomerName = …`. But the 2.0 model `_Model_Forecast_Opportunity`
|
|
394
|
+
(`_underscore/Model/Forecast/Opportunity.php`) never **declared** the field, so `_Model.__set`
|
|
395
|
+
(`_underscore/Model.php`) threw *"There is no field called 'endCustomerName' in the
|
|
396
|
+
'_Model_Forecast_Opportunity' model."* on **every** `Netsuite/Opportunity/post|put` — 100% failure,
|
|
397
|
+
every opportunity import dead. `Core.WorkerJobs` census: Sep 1 = 115 ok / 60 fail (last good day),
|
|
398
|
+
Sep 2 → Sep 10 = 0 ok / 100% fail (61 fails on Sep 10 alone). This was a **half-deploy**: the
|
|
399
|
+
migration + the handler write shipped, the model field declaration did not.
|
|
400
|
+
**Durable rule: a new column that the handler writes must ship THREE things together — the
|
|
401
|
+
`dbchanges2` migration, the handler write, AND the `_Model` field declaration.** A column-existence
|
|
402
|
+
guard (`SHOW COLUMNS`) proves nothing about the ORM; both ORMs enumerate *declared* properties, so an
|
|
403
|
+
undeclared field is a hard `__set` throw, not a silent skip. Fixed 2026-09-10 by declaring
|
|
404
|
+
`public $endCustomerName = self::FIELD_CHAR;` (matching sibling char fields clickupTaskId/title/memo
|
|
405
|
+
over the varchar column); deployed to worker2 `_production`, sync recovered.
|
|
390
406
|
- Writes use raw `_Query` against `_underscore::DB_FORECAST` and **must**
|
|
391
407
|
`_Database::transactionCommit(DB_FORECAST)` (lazy-transaction gotcha).
|
|
392
408
|
|
|
@@ -759,6 +775,19 @@ deprecated** for production opportunity code.
|
|
|
759
775
|
blocked on the Aaron stakeholder decision noted above.
|
|
760
776
|
|
|
761
777
|
## Change history
|
|
778
|
+
- 2026-09-10 — **Fixed an 8-day total opportunity-sync outage caused by a half-deploy: a missing ORM
|
|
779
|
+
model field.** `Forecast.Opportunities.endCustomerName` (column added ~Sep 1) and the worker2 handler
|
|
780
|
+
write shipped, but `_Model_Forecast_Opportunity` (`_underscore/Model/Forecast/Opportunity.php`) never
|
|
781
|
+
**declared** the field. `hasEndCustomerColumn()` guards the DB column (`SHOW COLUMNS`), not the ORM
|
|
782
|
+
model, so the guard passed and `_Model.__set` then threw *"There is no field called 'endCustomerName'
|
|
783
|
+
in the '_Model_Forecast_Opportunity' model."* on **every** `Netsuite/Opportunity/post|put` —
|
|
784
|
+
`Core.WorkerJobs` shows Sep 1 = 115 ok / 60 fail (last good day), Sep 2 → Sep 10 = 0 ok / 100% fail.
|
|
785
|
+
Fix: declared `public $endCustomerName = self::FIELD_CHAR;` (php-reviewer clean); deployed to worker2
|
|
786
|
+
`_production`, sync recovered. Durable lesson added to the `endCustomerName` data-model bullet: a
|
|
787
|
+
handler-written column must ship its migration, its handler write, AND its `_Model` field declaration
|
|
788
|
+
together — a `SHOW COLUMNS` guard does not protect against a missing ORM field. Diagnosed with only the
|
|
789
|
+
`Core.WorkerJobs` action+outcome census (group by action, isSuccess over 7 days) via the toga-db MCP —
|
|
790
|
+
no NetSuite/ClickUp access. (jcardinal)
|
|
762
791
|
- 2026-09-04 — **Investigation + design session; no code shipped (two prototype fixes were written and
|
|
763
792
|
deliberately reverted).** Four corrections and four new facts. **Corrected:** the `STAGE_MAP`
|
|
764
793
|
section described a constant that does not exist — the shipped map is **`SALES_STAGE_MAP`** on
|
package/knowledge/INDEX.md
CHANGED
|
@@ -18,10 +18,10 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
18
18
|
|
|
19
19
|
## 2.0 framework
|
|
20
20
|
|
|
21
|
-
- **_underscore** (_Underscore) _(framework core)_ —
|
|
21
|
+
- **_underscore** (_Underscore) _(framework core)_ — 83 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
22
|
- **worker2** (Worker) — 68 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
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
24
|
+
- **dbchanges2** (Database Changes) _(framework core)_ — 18 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)
|
|
26
26
|
- **saml** (SAML SSO Gateway) — 6 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
27
27
|
- **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
| [Prudential: Dell ASN units PRE/POST interceptor (legacy key + flat tracking)](features/dell-asn-units-interceptor.md) | 2.0 | After the tracking-number bridge migration, the ASN unit route was renamed (`advance-shipping-notice-units` → `advance-shipping-notice-item-units`), so the inhe |
|
|
6
6
|
| [Prudential: Dell LCH IOP transmissions (LCHRequestV2)](features/dell-lch-iop-transmissions.md) | 1.0 | Outbound order transmissions from the 1.0 worker tier to Dell's Lifecycle Hub (LCH) ITSM integration. |
|
|
7
7
|
| [Prudential: Device information import + unit→contact linking (import_device_information.php)](features/device-information-import-and-contact-linking.md) | 1.0 | Prudential's **device-sync** cron (`worker/crons/toga2/prudential/import_device_information.php`) pulls device/asset records (from ServiceNow / the Dell CMDB fe |
|
|
8
|
+
| [Prudential: ItemClasses device-tier data is dead (and how that was proved)](features/itemclasses-device-tier-data.md) | 2.0 | `Client_Prudential.ItemClasses` held 7 hand-seeded Dell/Apple **device tiers** ("LATITUDE 73xx", "MACBOOK PRO", …) tagged onto **9 of 765** items. |
|
|
8
9
|
| [Prudential: OneUptime order-pipeline backlog monitors (Monitor/Prudential/*)](features/oneuptime-order-pipeline-monitors.md) | 2.0 | Three worker2 OneUptime push monitors (TRUE-80587) that watch the Prudential/Dell order pipeline for **stalls**, which were previously invisible — a stuck pipel |
|
|
9
10
|
| [Prudential: order email PDF attachments (which guide goes on which email)](features/order-email-pdf-attachments.md) | 1.0 | Every customer-facing Prudential order email carries S3-hosted PDF guides, and **which** guide is attached is decided by the service request type. |
|
|
10
11
|
| [Prudential: Service Request Regional Address Validation](features/service-request-address-validation.md) | 2.0 | The `prePost` interceptor on `_Model_Prudential_ServiceRequest` validates `deliverToAddress` fields differently depending on which Prudential regional customer |
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Prudential: ItemClasses device-tier data is dead (and how that was proved)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: dbchanges2
|
|
5
|
+
project: Database Changes
|
|
6
|
+
client: prudential
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-11
|
|
10
|
+
owners: ["rgirish"]
|
|
11
|
+
files:
|
|
12
|
+
- dbchanges2/Client_Prudential/2024-01-01.sql
|
|
13
|
+
- dbchanges2/Client_Prudential/2026-09-11 - ClearItemClasses.sql
|
|
14
|
+
- worker/crons/toga2/prudential/transmissions_to_netsuite.php
|
|
15
|
+
- worker/schedules/cron.worker.sync.json
|
|
16
|
+
related:
|
|
17
|
+
- ../profile.md
|
|
18
|
+
- ../../../2.0/apps/_underscore/features/item-classification-itemclasses.md
|
|
19
|
+
- ../../../2.0/apps/_underscore/features/record-change-audit-log.md
|
|
20
|
+
- ../../../1.0/apps/worker/workflows/tracing-a-worker-cron-run-in-production.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
`Client_Prudential.ItemClasses` held 7 hand-seeded Dell/Apple **device tiers**
|
|
26
|
+
("LATITUDE 73xx", "MACBOOK PRO", …) tagged onto **9 of 765** items. The data was seeded once by
|
|
27
|
+
SQL in 2023, never maintained, is partly wrong, and **nothing reads or writes it today**. It was
|
|
28
|
+
cleared on 2026-09-11 to free the table for real NetSuite item classification later.
|
|
29
|
+
|
|
30
|
+
The second half of this doc is the **4-source method used to prove "this field is dead"** —
|
|
31
|
+
reusable for any field on any client.
|
|
32
|
+
|
|
33
|
+
## How it works (what the data was)
|
|
34
|
+
|
|
35
|
+
Source is a hardcoded seed block in `dbchanges2/Client_Prudential/2024-01-01.sql:435-449`:
|
|
36
|
+
7 `INSERT INTO ItemClasses`, then a per-`partNumber` `UPDATE` stamping `Items.itemClassId`.
|
|
37
|
+
**No API, no external feed, no sync.** That is the entire origin.
|
|
38
|
+
|
|
39
|
+
The one consumer was `worker/crons/toga2/prudential/transmissions_to_netsuite.php:171-178`,
|
|
40
|
+
which sent `ItemClasses.name` to NetSuite as `deviceTier`.
|
|
41
|
+
|
|
42
|
+
## Proving a field is dead — the 4-source method
|
|
43
|
+
|
|
44
|
+
Run all four. Any one alone is weak; together they are conclusive.
|
|
45
|
+
|
|
46
|
+
**1. Grep for readers and writers in every repo.**
|
|
47
|
+
One reader (`transmissions_to_netsuite.php`), zero writers in `api2`, `worker2`, `_underscore`.
|
|
48
|
+
Watch for frontend grep noise — `itemClass` matches CSS variable names like `menuItemClassNames`.
|
|
49
|
+
|
|
50
|
+
**2. Check the field-change audit log — absence is evidence.**
|
|
51
|
+
Resolve the field id first (`Logs_*` and `Core` are on different clusters, so this is two
|
|
52
|
+
queries — see [Record Change Audit Log](../../../2.0/apps/_underscore/features/record-change-audit-log.md)):
|
|
53
|
+
|
|
54
|
+
- `Core.RecordFields` id **605** = `Items.itemClassId`; `Core.Records` id **21** = `Items`,
|
|
55
|
+
id **105** = `ItemClasses`.
|
|
56
|
+
- `Logs_Prudential.RecordField` has **zero rows** for field 605, ever. Zero log rows on record 105.
|
|
57
|
+
|
|
58
|
+
**Prove the logging itself works, or "no rows" means nothing.** Item logging is demonstrably
|
|
59
|
+
live: 1,963 log rows across 353 items, current through today, tracking `partNumber`,
|
|
60
|
+
`description`, `isFulfillable`, `catalogId`. So `itemClassId` simply never changes.
|
|
61
|
+
|
|
62
|
+
**3. Scan the inbound API payloads.**
|
|
63
|
+
Zero of **3,808** `/v2/items` calls in September carried an `itemClass` key. Items arrive from a
|
|
64
|
+
NetSuite sync cron — `POST /v2/items` from `34.214.204.95` (`apiId` 5) — with payload shape
|
|
65
|
+
`{partNumber, description, inventoryType, catalog:{name}, c_netsuiteInternalItemId, isFulfillable}`.
|
|
66
|
+
**No `itemClass` field.** That is why new items never get a class, and why only 9 of 765 are tagged.
|
|
67
|
+
|
|
68
|
+
**4. Check the consumer cron's `active` flag, then `git log` it.**
|
|
69
|
+
`worker/schedules/cron.worker.sync.json:441` — `"active": 0` on "Transmissions to Netsuite".
|
|
70
|
+
Disabled **2026-07-17** by Jeff Cardinal, commit `14570def` ("Add careOf & skip Apple; disable
|
|
71
|
+
Netsuite cron"), branch `_production`. See
|
|
72
|
+
[tracing a worker cron run](../../../1.0/apps/worker/workflows/tracing-a-worker-cron-run-in-production.md)
|
|
73
|
+
for the `active`-flag semantics.
|
|
74
|
+
|
|
75
|
+
## Gotchas
|
|
76
|
+
|
|
77
|
+
- **⚠ The surviving tags were partly wrong.** 2 of the 9: item 3512 (`MRW23XX/A`,
|
|
78
|
+
"MacBook Pro 16 M3") was tagged **MACBOOK AIR**; item 3513 (`920-008219`, a Logitech keyboard)
|
|
79
|
+
was tagged **MACBOOK PRO**. Stale hand-seeded lookup data degrades silently — do not trust it
|
|
80
|
+
because it exists.
|
|
81
|
+
- **Clear the FK before the parent rows.** `Items_itemClassId` is the only FK referencing
|
|
82
|
+
`ItemClasses` (verified via `information_schema.KEY_COLUMN_USAGE` on `Client_Prudential`), so
|
|
83
|
+
the null-out of `Items.itemClassId` must run **before** the parent rows are removed.
|
|
84
|
+
- **Northwell was NOT touched.** Its 27 ServiceNow service categories and 245 tagged items are
|
|
85
|
+
still live. Open question, not yet decided: whether to move Northwell's rows to a separate
|
|
86
|
+
table if `ItemClasses` gets repurposed org-wide.
|
|
87
|
+
|
|
88
|
+
## The migration
|
|
89
|
+
|
|
90
|
+
`dbchanges2/Client_Prudential/2026-09-11 - ClearItemClasses.sql` — data removal only, re-runnable,
|
|
91
|
+
table and columns untouched. Two statements: null out every non-null `Items.itemClassId`, then
|
|
92
|
+
empty `ItemClasses`. Order matters (see the FK gotcha above).
|
|
93
|
+
|
|
94
|
+
**Deliberately scoped small.** Adding the NetSuite classification columns
|
|
95
|
+
(`c_netsuiteInternalClassificationId`, `c_netsuiteFullName`, `c_netsuiteParentId`), dropping the
|
|
96
|
+
UNIQUE on `name`, and registering the `CustomRecordFields` rows for `recordId` 105 is a
|
|
97
|
+
**separate later task**. The traps already found for it are recorded in
|
|
98
|
+
[ItemClasses / _Model_Client_ItemClass](../../../2.0/apps/_underscore/features/item-classification-itemclasses.md).
|
|
99
|
+
|
|
100
|
+
## Change history
|
|
101
|
+
- 2026-09-11 — Cleared the 7 stale device-tier rows and the 9 item tags via
|
|
102
|
+
`2026-09-11 - ClearItemClasses.sql` (null the FK first — `Items_itemClassId` is the only FK).
|
|
103
|
+
Documented the 4-source proof that the data was dead: one reader / zero writers,
|
|
104
|
+
zero `Logs_Prudential.RecordField` rows for `Core.RecordFields` id 605 against 1,963 live rows
|
|
105
|
+
on other Item fields, zero of 3,808 September `/v2/items` payloads carrying `itemClass`, and the
|
|
106
|
+
sole consumer cron `"active": 0` since 2026-07-17 (commit `14570def`). Also recorded that 2 of
|
|
107
|
+
the 9 surviving tags were plainly wrong. Column additions deliberately deferred to a later task.
|
|
108
|
+
(rgirish)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: prudential
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-09-11
|
|
10
10
|
owners: ["rgirish", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php
|
|
@@ -86,20 +86,26 @@ TrackingNumbers WHERE purchaseOrderId = PurchaseOrders.id AND TrackingNumbers.st
|
|
|
86
86
|
orders that genuinely qualified for a delivery email. Over 2026-07-01..07-09 this dropped 6 false
|
|
87
87
|
positives (25 → 19 flagged) while retaining all 19 genuine discrepancies.
|
|
88
88
|
|
|
89
|
-
## A second cron sends the same shipped email
|
|
89
|
+
## A second cron sends the same shipped email — but it is DISABLED (corrected 2026-09-11)
|
|
90
90
|
|
|
91
|
-
`transmissions_to_netsuite.php`
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
`transmissions_to_netsuite.php` builds and sends the **same** shipped email — same subjects, same
|
|
92
|
+
branch shape — and also stamps `c_dtTransmittedOrderShippedUpdateToPrudential`. It is easy to
|
|
93
|
+
miss: it is not named like the other `transmit_*` crons. Any change to the shipped email's content
|
|
94
|
+
or attachments must be applied to **both** files or the two senders drift.
|
|
95
|
+
|
|
96
|
+
> **⚠ It is `"active": 0`, not `active: 1` as this page said until 2026-09-11.**
|
|
97
|
+
> `worker/schedules/cron.worker.sync.json:441` — disabled **2026-07-17** by Jeff Cardinal, commit
|
|
98
|
+
> `14570def` ("Add careOf & skip Apple; disable Netsuite cron") on `_production`. So **it has not
|
|
99
|
+
> been sending anything since then**, and it is not currently a live duplicate sender. Keep the
|
|
100
|
+
> two files in sync anyway — it can be switched back on with one flag.
|
|
95
101
|
|
|
96
102
|
**And it does not stop at the shipped email — it also sends the two *delivered* emails.**
|
|
97
103
|
`transmissions_to_netsuite.php` carries "Prudential: New hire equipment has been delivered"
|
|
98
104
|
(~L1765) and "Prudential: Your equipment has been delivered" (~L2149), the **same two subjects**
|
|
99
105
|
that `transmit_closecomplete_updates_prudential.php` sends — except the `transmissions_to_netsuite`
|
|
100
|
-
versions make **no `addAttachment()` calls at all**.
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
versions make **no `addAttachment()` calls at all**. The open question from 2026-08-31 — whether
|
|
107
|
+
both crons fire for the same order and send a duplicate delivered email — is **moot while this
|
|
108
|
+
cron is off** (see the box above). It becomes live again the moment anyone sets `"active": 1`.
|
|
103
109
|
|
|
104
110
|
It was also the last Prudential cron still calling `file_get_contents()` directly for its PDFs and
|
|
105
111
|
calling `addAttachment()` unconditionally — so a failed S3 pull threw and killed the batch, and a
|
|
@@ -201,6 +207,10 @@ stamp (crash hits the next iteration). With multiple New-type POs, a crash on PO
|
|
|
201
207
|
all subsequent POs in the same run.
|
|
202
208
|
|
|
203
209
|
## Change history
|
|
210
|
+
- 2026-09-11 — **Correction: `transmissions_to_netsuite.php` is `"active": 0`, not `active: 1`.**
|
|
211
|
+
Disabled 2026-07-17 (commit `14570def`) in `cron.worker.sync.json:441`, so the "second shipped
|
|
212
|
+
sender" and the suspected duplicate *delivered* email are dormant, not live. Found while
|
|
213
|
+
auditing that cron as the only reader of `ItemClasses`. (rgirish)
|
|
204
214
|
- 2026-08-31 — TRUE-81393 / PR #1707 (merged to `_production`): the shipped email's
|
|
205
215
|
`$attachmentText` paragraph was unconditional while each attach was guarded, so a failed S3 pull
|
|
206
216
|
produced copy promising guides that were not attached — and it said "guides" even for a single
|
|
@@ -15,7 +15,7 @@ project: _Underscore
|
|
|
15
15
|
client: prudential
|
|
16
16
|
type: profile
|
|
17
17
|
status: active
|
|
18
|
-
updated: 2026-09-
|
|
18
|
+
updated: 2026-09-11
|
|
19
19
|
owners: ["jcardinal", "rgirish", "bala", "mhammontree"]
|
|
20
20
|
files: []
|
|
21
21
|
related:
|
|
@@ -31,6 +31,7 @@ related:
|
|
|
31
31
|
where each code is logged, and how to tell "we refused it" from "they never sent it".
|
|
32
32
|
- features/supply-orders-csv-export.md
|
|
33
33
|
- features/order-email-pdf-attachments.md
|
|
34
|
+
- features/itemclasses-device-tier-data.md
|
|
34
35
|
---
|
|
35
36
|
|
|
36
37
|
## Summary
|
package/package.json
CHANGED