toga-ai 1.0.1039 → 1.0.1040

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.
@@ -24,6 +24,7 @@ related:
24
24
  - ../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md
25
25
  - ../../../clients/compass-usa/features/contact-email-resolution.md
26
26
  - ../../../2.0/apps/worker2/features/tracking-status-refresh.md
27
+ - ../../../2.0/apps/worker2/features/all-client-email-queue-monitor.md
27
28
  - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
28
29
  - ../../../clients/compass-canada/features/french-order-email-localization.md
29
30
  - ../../../clients/compass-usa/features/order-fulfillment-status-per-line.md
@@ -375,9 +376,15 @@ the tracking number as emailed (it retries next run).
375
376
  as is (decided 2026-10-15, TRUE-82847).
376
377
  - Delivered template = prod `emailTemplateId` 26 in `Logs_Compass.Email`; baseline 2026-09-15..10-15:
377
378
  959 SENT, 1 FAILED.
379
+ - **⚠ The delivered cron never checks the send result.** It calls `App_Api_Toga2::send(..., false)`
380
+ (`$throwExceptionOnApiError = false`) and ignores the response, so it marks
381
+ `TrackingNumbers.status = 'DELIVERED'` and inserts notifications even when api2 rejects the email.
382
+ Its "only mark DELIVERED after confirmed send" comment is wrong. "Sent" only means queued to
383
+ `Logs_Compass.Email`; worker2 `Infrastructure/Email/Send` does the real send. Not changed.
378
384
 
379
385
  ## Change history
380
386
  - 2026-10-15 — Delivered crons (USA + Canada) pick one active `Users` row with email + Contacts fallback; documented the 2-active-users duplicate. (jcardinal)
387
+ - 2026-10-15 — Recorded that the delivered cron ignores the send result (TRUE-82846). (bala)
381
388
  - 2026-10-13 — Recorded that the USA delivered cron has no date window, so bulk ASN loads must suppress first. (bala)
382
389
 
383
390
  ## Related
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-15
10
10
  owners: ["jcardinal", "bala"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Operations.php
@@ -19,6 +19,7 @@ related:
19
19
  - ./monitoring-framework.md
20
20
  - ../../_underscore/features/email-send-pipeline.md
21
21
  - ../../_underscore/features/per-client-database-connections.md
22
+ - ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
22
23
  ---
23
24
 
24
25
  Cross-client health check for the 2.0 outbound email queue (`Logs_<Client>.Email`); open when working on email-queue alerting or an all-client DB fan-out.
@@ -49,7 +50,9 @@ Steps:
49
50
 
50
51
  ### Alarm rule (explicit product decision)
51
52
 
52
- Alarm on **`FAILED > 0` only**. `PENDING` is reported as context and **never** alarms — a queue with work in it is normal, and the sender cron runs every minute.
53
+ Alarm on **`FAILED > 0` only** (`ALARM_FAILED_COUNT = 0`). `PENDING` is reported as context and **never** alarms — a queue with work in it is normal, and the sender cron runs every minute.
54
+
55
+ **No time window:** the count is every `FAILED` row ever, so **one** failed email holds the alarm `HIGH` until the 90-day `Email` cleanup removes it or someone changes its `status` by hand.
53
56
 
54
57
  ### Payload contract — `alarm` and `probe` are SEPARATE tokens
55
58
 
@@ -75,6 +78,7 @@ OneUptime can only string-match a request body (no numeric comparison, no JSON k
75
78
 
76
79
  ## Gotchas
77
80
 
81
+ - **⚠ Triage: the OneUptime alarm "Compass email queue backlog is too high" IS this monitor** (the UI name was never renamed; there is no threshold of 20/25 and no `worker/monitor/compass/` code). It almost always means `FAILED` rows, not a backlog. First step: `SELECT id, subject, failureReason FROM Email WHERE status = 'FAILED'` on the named client's logs DB, then fix the **producer** that queued the bad row. 2026-10-15 (TRUE-82846): one row, `Logs_Compass.Email` id 146380, `failureReason` "You must provide at least one recipient email address.", 0 pending — caused by the Compass delivered cron, see [Compass in-transit & delivered emails](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
78
82
  - **⚠ This monitor is blind to an email that was never queued.** It reads `Logs_<Client>.Email`, so it can only see a send that already produced a row. If the producing cron dies, stalls or never selects the work, this monitor reports a **healthy empty queue**. That is exactly what happened during the Compass USA in-transit outage (2026-08-27 → 2026-09-03): the 1.0 sending cron timed out before creating a single email row and this monitor stayed green all week. Pair any email-queue monitor with a **backlog monitor on the producer's input** — first instance `Monitor/Compass/InTransitEmailQueue`, see [OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md).
79
83
  - **A client's database name is NOT derivable from `clientIdentifier`.** `Compass_Usa` → `Logs_Compass`. Always resolve `Clients.logDatabaseId` → `Databases.name`. Building `'Logs_' . $clientIdentifier` silently targets a nonexistent database. See [per-client database connections](../../_underscore/features/per-client-database-connections.md).
80
84
  - **Do not use `_Database::registerClientDatabases()` for an all-client fan-out.** It INNER JOINs the client and archive databases alongside the log database, so a client missing either row drops out and the method then dereferences a null row and throws. It also opens three connections when only the log one is needed. Run a log-database-only variant with LEFT OUTER JOINs instead.
@@ -84,7 +88,10 @@ OneUptime can only string-match a request body (no numeric comparison, no JSON k
84
88
  - **An unconditional `SUM(status = 'X')` cannot use an index.** `SELECT SUM(status='FAILED') FROM Email` full-scans, reading every historical `SENT` row despite `idx_email_status`. Use `WHERE status IN (...) GROUP BY status`. This runs per client on a cron, so the difference compounds.
85
89
  - **There is no `_Query::setTimeout()` in the 2.0 framework** (only `_ApiRequest::setTimeout()`). A 2.0 worker therefore has **no per-query timeout**, so a hung database cluster blocks the fan-out until the connection itself times out. Relevant to any loop over all clients.
86
90
  - **A passing dev run does not prove cross-cluster correctness.** Non-prod is a single all-in-one database endpoint; production splits Core / `Client_*` / `Logs_*` / `Archive_*` / `Cache` across **separate clusters**.
87
- - **Verification status of this capture:** `php -l` passes; php-reviewer and sql-reviewer both returned 0 critical and every actionable finding was fixed. It has **not** been executed against any environment and **no database was queried** — the `Core` join shape is verified against framework source only, not against real rows.
91
+ - **Live in prod** — it fired on a real Compass `FAILED` row on 2026-10-15.
92
+
93
+ ## Change history
94
+ - 2026-10-15 — Recorded no-time-window alarm behavior and the "Compass email queue backlog" triage path; confirmed live in prod (TRUE-82846). (bala)
88
95
 
89
96
  ## Related
90
97
 
@@ -144,6 +144,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
144
144
  instead of **12 / 8 / 33**. Removing the hack gives them *more* access; whether Canada should get
145
145
  it is an **open decision**. See
146
146
  [Storefront persona gating](../../2.0/apps/toga2-commerce/features/storefront-persona-gating-and-home-counts.md).
147
+ - **⚠ "Admin Canada" persona 31 grants only 2 categories (found 2026-10-15, TRUE-82848).** Categories
148
+ come from `Personas_Assortments`, not bundles/items. Persona 31 has every bundle + item but only
149
+ assortments 17 + 18, so a user with persona 31 alone (James Fuller, CA user 31583) sees 2 of 16
150
+ categories. Fix `dbchanges2/Client_CompassCanada/2026-10-15a - AdminCanadaPersonaAllAssortments.sql`
151
+ is **not yet run** in any env. See
152
+ [Copying a US user into Canada](workflows/copying-a-us-user-into-the-canada-tenant.md).
147
153
  - **⚠ "Blank page at `https://compasscanada.togacommerce.com/`" (reported by Compass Canada IT,
148
154
  Sept 2026) is a shared auth-gating bug, not a Canada or caching problem.** A stale **public**
149
155
  `accessToken` made the app think the visitor was signed in, so `/` rendered nothing until the
@@ -6,10 +6,11 @@ project: Database Changes
6
6
  client: compass-canada
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-28
9
+ updated: 2026-10-15
10
10
  owners: [bala]
11
11
  files:
12
12
  - dbchanges2/Client_CompassCanada/2026-09-28a - AddJamesFullerSuperUser.sql
13
+ - dbchanges2/Client_CompassCanada/2026-10-15a - AdminCanadaPersonaAllAssortments.sql
13
14
  - worker2/Worker/Client/Compass/PeopleFile.php
14
15
  related:
15
16
  - ../profile.md
@@ -17,6 +18,7 @@ related:
17
18
  - ../../compass-usa/workflows/granting-persona-bundle-access.md
18
19
  - ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
19
20
  - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
21
+ - ../../../2.0/apps/toga2-commerce/features/storefront-persona-gating-and-home-counts.md
20
22
  ---
21
23
  Giving a Compass **USA** person a login on the Canada TechHub portal: the guarded insert chain, the uuid role/persona mapping, and the worker2 guard that stops the next PEOPLE run switching them off.
22
24
 
@@ -62,7 +64,7 @@ Role ids differ between the two tenants. Look up `(SELECT r.id FROM Roles r WHER
62
64
 
63
65
  `PeopleFile.php` already keeps `ROLE_UUID_BASE` / `ROLE_UUID_COMPASS_BASE` / `ROLE_UUID_MANAGER` constants for the same reason — reuse those values.
64
66
 
65
- **Personas do not map by name either.** The two Canada admin personas: **Admin Canada** id 31, uuid `94eb162c-7adc-bc2e-66fa-c19fed1d1288` (widest catalogue — 213 items, all 35 bundles; this is Gina Prince's) and **Managers/VIPs** id 33, uuid `7961068f-8b31-1daf-5f52-0e45a09beb98` (168 items, 21 bundles). Persona **All** is id 1. Persona grants resolve through three sources, not just `Users_Personas` — see [Granting persona/bundle access](../../compass-usa/workflows/granting-persona-bundle-access.md).
67
+ **Personas do not map by name either.** The two Canada admin personas: **Admin Canada** id 31, uuid `94eb162c-7adc-bc2e-66fa-c19fed1d1288` (widest for bundles + items: 213 items, all active bundles; this is Gina Prince's) and **Managers/VIPs** id 33, uuid `7961068f-8b31-1daf-5f52-0e45a09beb98` (168 items, 21 bundles, 13 categories). Persona **All** is id 1. **Persona 31 was NOT widest for categories** (see Gotchas) until `2026-10-15a` runs. Persona grants resolve through three sources, not just `Users_Personas` — see [Granting persona/bundle access](../../compass-usa/workflows/granting-persona-bundle-access.md).
66
68
 
67
69
  ### 4. Add the address to `ALWAYS_ACTIVE_EMAILS_CA`
68
70
  In `worker2/Worker/Client/Compass/PeopleFile.php`, **lowercase**, in `ALWAYS_ACTIVE_EMAILS_CA` — **not** `ALWAYS_ACTIVE_EMAILS_BOTH`. The BOTH list's `ensureCrossRegionUsersActive()` also forces `isActive = 1` on the **US** row every run, which would override a real US termination. `isEmailAlwaysActive()` lowercases its input, so a mixed-case constant entry never matches. Both layers of the guard are described in [PEOPLE-File User Lifecycle](../../compass-usa/features/people-file-user-lifecycle.md).
@@ -71,6 +73,8 @@ In `worker2/Worker/Client/Compass/PeopleFile.php`, **lowercase**, in `ALWAYS_ACT
71
73
  Run `sql-reviewer` on the SQL file and `php-reviewer` on the PeopleFile change, then give the developer numbered run steps (SQL file + environment order, then the worker2 deploy).
72
74
 
73
75
  ## Gotchas
76
+ - **"Admin Canada" (31) alone showed only 2 categories (found 2026-10-15, TRUE-82848).** Storefront categories = `Personas_Assortments` rows for any of the user's personas, not bundles/items. Persona 31 had all bundles + items but only assortments 17 (APC Battery Back-UPs) and 18 (Webcams), so James Fuller (CA user 31583, persona 31 only) saw 2 of 16 categories. Gina Prince / Caleb Moore never noticed because their persona 33 adds 13 categories. Fix: `dbchanges2/Client_CompassCanada/2026-10-15a - AdminCanadaPersonaAllAssortments.sql` links persona 31 (by uuid) to every active assortment and drops a duplicate 18 row; verify expects 16 links. **Not yet run in any env** (beta had the same gap). Until it runs, give a hand-added admin persona 33 too, or check `Personas_Assortments` for the chosen persona.
77
+ - **A NULL `Users.locationId` does not hide catalogue.** Bundle/item ACL expressions (`AclRecordExpressions` 251 items, 253 bundles) still pass through `Users_Personas`.
74
78
  - **`Client_CompassCanada.Users` has no `username`, `isSuperUser`, `personaId` or `roleId` column.** Access is entirely `Users_Roles` + `Users_Personas`. The login/personnel username is `c_hrEmpUsername`, stored lowercase.
75
79
  - **Leave `Users.password` NULL.** Login is SSO on the email address: only **45 of 28,788** `Client_CompassCanada` users hold a password, and the US rows for these people are NULL too.
76
80
  - **`Users.email` uses a case-insensitive collation**, so the stored case does not affect matching — `Client_Compass` often stores these addresses uppercase while Canada stores them lowercase.
@@ -86,3 +90,4 @@ Run `sql-reviewer` on the SQL file and `php-reviewer` on the PeopleFile change,
86
90
 
87
91
  ## Change history
88
92
  - 2026-09-28 — Created from the James Fuller hand-add; records the uuid role/persona map and the `ALWAYS_ACTIVE_EMAILS_CA`-not-BOTH rule (bala)
93
+ - 2026-10-15 — Persona 31 had only 2 categories; corrected the "widest catalogue" claim, added the assortment fix (TRUE-82848) (bala)
@@ -18,6 +18,7 @@ files:
18
18
  - dbchanges2/Client_Compass/2026-09-10b - RegisterApprovalDecisionPrePostInterceptor.sql
19
19
  - dbchanges2/Core/2026-10-09a - ManagerAutoApproveSettings.sql
20
20
  - dbchanges2/Client_Compass/2026-10-09a - ManagerAutoApproveSettingsSeed.sql
21
+ - dbchanges2/Client_Compass/2026-10-15a - VipAutoApproveOptOut.sql
21
22
  related:
22
23
  - mr-ma-order-approval-and-status.md
23
24
  - ../../../2.0/apps/toga25-supply/features/action-button-rule-engine.md
@@ -103,7 +104,12 @@ Data (all ids identical in prod, dev-sandbox, client-sandbox):
103
104
  - `dbchanges2/Client_Compass/2026-10-09a - ManagerAutoApproveSettingsSeed.sql` — `PersonaGlobalSettings` persona **35** (VIP Role) / setting 18 = `'5000'`; `UserGlobalSettings` user 60817 / setting 19 = `'1'`. Literal ids, no Core reference.
104
105
  - **Release order: Core SQL → Client_Compass SQL → deploy `_underscore`.** Code first = ALL VIP auto-approval stops until seeded.
105
106
  - Persona 35 is a safe stand-in for `c_isVip`: PeopleFile grants it to every VIP and removes it when VIP is dropped (prod: 2269/2269 active VIPs hold it, no non-VIP does) — see [Persona model](persona-model-and-levy-gating.md). Replay of the last 5,184 prod manager decisions: old rule 1,663 auto-approved, new 1,662; the only difference is user 60817.
106
- - To change a limit: edit the persona's setting-18 row (or seed one on another persona). To exempt one manager: add their setting-19 = `'1'` row.
107
+ - To change a limit: edit the persona's setting-18 row (or seed one on another persona).
108
+ - **To stop auto-approve for a manager (recurring "remove VIP auto approval" request):** add a setting-19 = `'1'` row; clearing `c_isVip` does nothing. Steps:
109
+ 1. Find the user in **prod** `Client_Compass.Users` by **email** (request names often differ; duplicate/inactive accounts exist, pick the active one).
110
+ 2. New `dbchanges2/Client_Compass/` file: one guarded `INSERT ... SELECT ... FROM DUAL WHERE NOT EXISTS` per user into `UserGlobalSettings` (`settingId 19`, `value '1'`, random v4 uuid literal), verify SELECT at end.
111
+ 3. File is **prod-only**: `Users.id` differs per environment (settings/persona ids do not).
112
+ - Opted out so far (prod, 9 rows): 60817 (`2026-10-09a`); 60814, 60836, 60818, 60835, 60843, 60840, 60839, 43717 (`2026-10-15a - VipAutoApproveOptOut.sql`, TRUE-82852).
107
113
 
108
114
  Evaluated in exactly three places, all calling the shared check:
109
115
  - **(a) Order creation** — `_Model_Compass_SalesOrder::postPost`, inside `elseif (!empty($managerUserUuid))` (manager = delegate or requester's `Users.supervisorUserId`).
@@ -202,6 +208,7 @@ When a step-2 (Manager) decision is reassigned, `_swapManagerEmailAddress()` upd
202
208
  - **The upstream cause of an unassigned step 2 is a user saved with no `supervisorUserId`.** The admin add-user screen allows saving a user without one, and the PEOPLE-file import may or may not backfill it later. That data hole feeds both the `!$hasManager` wipe above and the never-created step-2 decision at order creation. Not fixed — the guard only stops the order being *stranded*, it does not give it a manager.
203
209
 
204
210
  ## Change history
211
+ - 2026-10-15 — Opted 8 more managers out of auto-approve via setting 19; recorded the opt-out procedure (TRUE-82852). (bala)
205
212
  - 2026-10-15 — postPut re-picks the step-2 manager only on a real "order for" contact change, detected in prePut via `$api->passThrough` (TRUE-82727, `_underscore` PR #771). (bala)
206
213
  - 2026-10-09 — Manager auto-approve moved from `c_isVip` + hardcoded 5000 to persona setting 18 (limit) + user setting 19 (opt-out); postPut now compares subtotal (TRUE-82608). (bala)
207
214
  - 2026-10-06 — Recorded that no delegate + no supervisor saves an order with no step-2 decision, and that the commerce cart now blocks it (TRUE-82423). (bala)
@@ -6,11 +6,12 @@ project: _Underscore
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-15
10
10
  owners: ["bala"]
11
11
  files:
12
12
  - _underscore/Model/Compass/PurchaseOrder.php
13
13
  - worker/crons/toga2/compass/update_salesorder_status_from_odp.php
14
+ - worker/crons/toga2/compass/send_delivered_email.php
14
15
  related:
15
16
  - ./mits-po-to-so-item-linking.md
16
17
  - ./people-file-user-lifecycle.md
@@ -81,11 +82,14 @@ The remaining ~42,900 crossed contacts were NOT mass-repaired. Consumers carry t
81
82
 
82
83
  ## Gotchas
83
84
  - **⚠ The pointer is crossed on ~14% of Compass contacts — treat an unguarded read as a bug.** Applies to reports, exports, notification crons, api2 payload interceptors, anything.
84
- - **A `Users` row is a different lookup and does not have this problem.** `Users.email` is keyed by `Users.contactId` directly. Where both exist, prefer `Users.email` and fall back to the guarded `ContactEmailAddresses` read — that is the `IFNULL(Users.email, ContactEmailAddresses.emailAddress)` precedence used by the in-transit cron. EDI-created MR orders have **no** `Users` row at all, which is why the fallback exists.
85
+ - **A `Users` row is a different lookup and does not have this problem.** `Users.email` is keyed by `Users.contactId` directly. Where both exist, prefer `Users.email` and fall back to the guarded `ContactEmailAddresses` read — that is the `IFNULL(Users.email, ContactEmailAddresses.emailAddress)` precedence used by the in-transit cron. EDI-created MR orders have **no** `Users` row at all, which is why the fallback exists. **But one contact can have several `Users` rows, some with NULL email** — a bare `JOIN Users ON Users.contactId = …` fans out to one row per user; pick one active emailable user with a single-user subquery (`MAX(id)` pattern in the [delivered cron gotcha](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md)).
85
86
  - **`_Model_Compass_PurchaseOrder`'s contact lookup still has no ownership guard** (as of 2026-09-04). Fixing it there stops *new* crossings; the existing 42,926 remain either way.
86
87
  - **Compass Canada shares the model** (`Model/Compass/Canada/PurchaseOrder.php` inherits the parent), so the same lookup shape runs against `Client_CompassCanada`. Not measured there.
87
88
  - Duplicate-contact creation and duplicate-**user** creation are separate problems with separate causes — the `Users` duplicates come from the PEOPLE-file reactivation window, see [PEOPLE-file user lifecycle](./people-file-user-lifecycle.md).
88
89
 
90
+ ## Change history
91
+ - 2026-10-15 — Noted duplicate `Users` rows per contact fan out a `Users.contactId` join; pick one emailable user (TRUE-82846). (bala)
92
+
89
93
  ## Related
90
94
  - [MITS PO → SO item linking](./mits-po-to-so-item-linking.md)
91
95
  - [PEOPLE-file user lifecycle](./people-file-user-lifecycle.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1039",
3
+ "version": "1.0.1040",
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",