toga-ai 1.0.644 → 1.0.646
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/library/features/cron-execution-monitoring.md +25 -3
- package/knowledge/1.0/apps/worker/INDEX.md +2 -1
- package/knowledge/1.0/apps/worker/architecture.md +35 -2
- package/knowledge/1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md +40 -3
- package/knowledge/1.0/apps/worker/workflows/tracing-a-worker-cron-run-in-production.md +124 -0
- package/knowledge/2.0/apps/_underscore/INDEX.md +3 -2
- package/knowledge/2.0/apps/_underscore/features/email-template-sending.md +40 -3
- package/knowledge/2.0/apps/_underscore/features/per-client-database-connections.md +17 -2
- package/knowledge/2.0/apps/_underscore/features/sales-order-status-filter-surface.md +148 -0
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +37 -1
- package/knowledge/2.0/apps/api2/features/environment-variable-drives-underscore-branch.md +67 -1
- package/knowledge/2.0/apps/api2/features/v2-api-error-codes.md +15 -2
- package/knowledge/2.0/apps/api2/workflows/codepipeline-codeconnections-deploy.md +42 -2
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +87 -1
- package/knowledge/2.0/apps/toga-blox/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga-blox/features/table.md +23 -2
- package/knowledge/2.0/apps/worker2/INDEX.md +2 -1
- package/knowledge/2.0/apps/worker2/features/cross-account-aws-access.md +42 -3
- package/knowledge/2.0/apps/worker2/features/oneuptime-worker2-monitoring.md +39 -0
- package/knowledge/2.0/apps/worker2/features/worker-fleet-role-assignment-monitor.md +175 -0
- package/knowledge/INDEX.md +3 -3
- package/knowledge/clients/compass-usa/INDEX.md +1 -0
- package/knowledge/clients/compass-usa/features/odp-edi-850-item-resolution.md +179 -0
- package/knowledge/clients/compass-usa/features/odp-edi-855-acknowledgement-and-overquantity-guard.md +29 -2
- package/knowledge/clients/compass-usa/profile.md +10 -1
- package/knowledge/clients/compass-usa/workflows/odp-order-pipeline-to-netsuite.md +39 -2
- package/knowledge/clients/quad/profile.md +2 -1
- package/package.json +1 -1
|
@@ -6,12 +6,13 @@ project: Library
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [dfranks]
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: [dfranks, bala]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/framework.php
|
|
13
13
|
related:
|
|
14
14
|
- ../../worker/architecture.md
|
|
15
|
+
- ../../worker/workflows/tracing-a-worker-cron-run-in-production.md
|
|
15
16
|
---
|
|
16
17
|
|
|
17
18
|
## Summary
|
|
@@ -38,13 +39,28 @@ row in `db_log` and of the per-job Sentry check-in monitors.
|
|
|
38
39
|
cast silently discards the sub-second precision even when the schema column is `decimal(10,3)`.
|
|
39
40
|
- **Schema:** the table is provisioned by the dbchanges migration
|
|
40
41
|
`dbchanges/Common/DF/2026-5-7 Cron Checkin.sql` (table `CronJobExecutions`,
|
|
41
|
-
`executionTimeSeconds decimal(10,3)
|
|
42
|
+
`executionTimeSeconds decimal(10,3)`). **Correction (2026-08-25): prod `Common.CronJobExecutions`
|
|
43
|
+
does have a `note` TEXT column** — verified by writing to it and reading it back. `App_Framework`
|
|
44
|
+
never populates it, which makes it the practical place to park **temporary** step tracing for a
|
|
45
|
+
cron you are diagnosing (worker crons have no readable stdout — see
|
|
46
|
+
[Tracing a worker cron run in production](../../worker/workflows/tracing-a-worker-cron-run-in-production.md)).
|
|
47
|
+
Remove the tracing when you are done, and never write payloads or credentials into it.
|
|
48
|
+
- **Don't confuse the two `note`s.** `cronInitialization()` writes `note = 'Started execution'` to
|
|
49
|
+
the **`Log`** table on `db_log` (the older `CRON`/`recordType` row), not to `CronJobExecutions`.
|
|
42
50
|
- **Design decision:** 1.0 records executions by writing **directly to the shared DB**
|
|
43
51
|
(`db_common`), *not* by POSTing from 1.0 to a 2.0 API endpoint. Per Jeff Cardinal, 1.0 code
|
|
44
52
|
must not POST to 2.0 code; the direct shared-DB write is the sanctioned path (TRUE-78182).
|
|
45
53
|
|
|
46
54
|
## Gotchas
|
|
47
55
|
|
|
56
|
+
- **⚠ A run skipped by the overlap guard leaves NO ROW — absence is ambiguous.**
|
|
57
|
+
`cronInitialization()` calls `exitIfProcessRunning()` **before** the `CronJobExecutions` INSERT,
|
|
58
|
+
and the `cronFinished(true)` that guard then calls does nothing because `cronLogId` was never
|
|
59
|
+
set. So a blocked run is indistinguishable from a run that never launched: **a missing row for
|
|
60
|
+
an expected slot means "skipped", not "hung"**, and a hung run looks different (it has
|
|
61
|
+
`dtCheckIn` with no `dtCheckOut`). `isProcessRunning()` matches **any** `ps -ef` line containing
|
|
62
|
+
the script path (it only excludes `/bin/sh` and its own pid), so a stray `tail -f` or editor on
|
|
63
|
+
that path blocks the cron indefinitely.
|
|
48
64
|
- **`db_log` has no `CronJobExecutions` table.** A `cronFinished()` UPDATE that runs against the
|
|
49
65
|
`db_log` connection throws on **every** 1.0 cron job. Both the INSERT and the UPDATE must
|
|
50
66
|
target `db_common`.
|
|
@@ -60,6 +76,12 @@ row in `db_log` and of the per-job Sentry check-in monitors.
|
|
|
60
76
|
logic. (Fixed 2026-07-06: consolidated back to one `db_common` INSERT/UPDATE pair.)
|
|
61
77
|
|
|
62
78
|
## Change history
|
|
79
|
+
- 2026-08-25 — Corrected the schema note (prod `CronJobExecutions` **does** carry a `note` TEXT
|
|
80
|
+
column, unused by `App_Framework`, usable for temporary cron tracing) and separated it from the
|
|
81
|
+
`note = 'Started execution'` row `cronInitialization()` writes to `Log` on `db_log`. Recorded
|
|
82
|
+
that the overlap guard runs **before** the INSERT, so a skipped run writes no row at all and
|
|
83
|
+
cannot be told apart from "never launched" — and that `isProcessRunning()`'s `ps -ef` substring
|
|
84
|
+
match lets any process holding the script path block the cron. No code change. (bala)
|
|
63
85
|
- 2026-07-06 — Fixed a merge (`origin/_production`) that reintroduced duplicate
|
|
64
86
|
`CronJobExecutions` writes and pointed one `cronFinished()` UPDATE at `db_log` (where the
|
|
65
87
|
table doesn't exist), which would have thrown on every 1.0 cron. Consolidated to one
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
| [NetSuite Sales Order Sales Rep Sourcing (Staples & ODP EDI orders)](features/netsuite-sales-order-sales-rep-sourcing.md) | How the **sales rep** on a NetSuite Sales Order is determined for the two 1.0 `worker` EDI order-creation integrations (Staples cXML and Compass/ODP EDI). | worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/crons/sync/staples/sync_staples_cxml.php, test/@Mark/NetSuite/TRUE_80451_customer_salesrep_diag.php |
|
|
12
12
|
| [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, library/app/api/toga2.php, library/app/api/netsuite/rest.php, library/app/framework.php, library/app/systemmonitor/netsuiteintegration.php, test/@srija/Elite Testing/Service Requests/test_sync_togasupply_elite_section.php, test/@srija/Elite Testing/Service Requests/test_diagnose_togasupply_elite.php |
|
|
13
13
|
| [OneUptime Server monitor + disk/memory hygiene on the 1.0 worker EB host](features/oneuptime-server-monitor-host-hygiene.md) | The 1.0 `agilant-worker` EB environment runs on the **legacy Amazon Linux 1 PHP 7.2 platform** (Apache httpd/prefork, s3fs mounts, cron) and repeatedly went dow | worker/.ebextensions/040_disk_memory_hygiene.config, worker/.ebextensions/045_oneuptime_agent.config, worker/ebs/cron.worker.php, worker/ebs/mount-s3fs-folders.php, worker/ebs/apache_settings.php, worker/ebs/setup_phpini.php |
|
|
14
|
-
| [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php |
|
|
14
|
+
| [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php, worker/ebs/cron.worker.php, worker/.ebextensions/045_oneuptime_agent.config |
|
|
15
15
|
| [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
|
|
16
16
|
| [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 |
|
|
17
17
|
| [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 |
|
|
18
18
|
| [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 |
|
|
19
19
|
| [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 |
|
|
20
|
+
| [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, library/app/framework.php |
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [jcardinal, sking]
|
|
9
|
+
updated: 2026-08-24
|
|
10
|
+
owners: [jcardinal, sking, bala]
|
|
11
11
|
files:
|
|
12
12
|
- worker/index.php
|
|
13
13
|
- worker/_/app/framework.php
|
|
@@ -20,6 +20,8 @@ files:
|
|
|
20
20
|
related:
|
|
21
21
|
- ../library/architecture.md
|
|
22
22
|
- ../dbchanges/workflows/authoring-and-shipping-sql-files.md
|
|
23
|
+
- ./features/oneuptime-worker-uptime-monitoring.md
|
|
24
|
+
- ../../2.0/apps/worker2/features/worker-fleet-role-assignment-monitor.md
|
|
23
25
|
---
|
|
24
26
|
|
|
25
27
|
## Summary
|
|
@@ -44,6 +46,7 @@ which **self-elects a distinct role** (`notification`, `database`, `infrastructu
|
|
|
44
46
|
under `crons/`; the schedule (cron registration) is the source of truth for what runs — a script
|
|
45
47
|
that isn't scheduled never executes. Client integrations live under `crons/toga2/<client>/`.
|
|
46
48
|
Use prepared statements for all SQL; never interpolate input.
|
|
49
|
+
A worker that fails deploy-time role election runs NO crons while EB still reads healthy.
|
|
47
50
|
|
|
48
51
|
## How a job becomes a cron (the dispatch pipeline)
|
|
49
52
|
|
|
@@ -108,6 +111,32 @@ is the most important and least obvious part of the architecture.
|
|
|
108
111
|
> Net effect: roles are a **claim-the-first-free-slot pool**, and a dead worker's role is
|
|
109
112
|
> reclaimed by its replacement within minutes. There is no static instance→role mapping to edit.
|
|
110
113
|
|
|
114
|
+
### The failure mode: NO NAME = NO CRONTAB (and it is silent)
|
|
115
|
+
|
|
116
|
+
The election is the **single point of failure for an entire instance**, and the architecture gives
|
|
117
|
+
it no second chance:
|
|
118
|
+
|
|
119
|
+
- The claim runs **only at DEPLOY time** — `ebs/cron.worker.php` is written as the EB `appdeploy`
|
|
120
|
+
enact hook. **It never retries.** An instance that fails to claim stays nameless until the next
|
|
121
|
+
deploy or until something terminates it.
|
|
122
|
+
- The claim writes `/etc/worker-role` and **only then** appends `cron.worker.<role>.json` to the
|
|
123
|
+
crontab. So a nameless instance has **no role crontab at all** — it runs *nothing*, not even
|
|
124
|
+
`worker_heartbeat.php`, while **EB reports the environment healthy**. The box is up; it just
|
|
125
|
+
does no work.
|
|
126
|
+
- **The claim's DB connect is `@mysqli_connect(...)` — error-suppressed.** A boot-time connect
|
|
127
|
+
failure to `Vision_Log` therefore leaves the instance nameless **with no log line anywhere**.
|
|
128
|
+
This is the prime suspect for the 2026-08-24 incident (2 of 7 workers roleless for ~3 hours),
|
|
129
|
+
and it violates the no-`@`-suppression rule in `rules/toga/common/coding-style.md`. Removing the
|
|
130
|
+
`@` and logging the failure is the recommended fix.
|
|
131
|
+
- `Workers` **only ever holds instances that SUCCEEDED**, so a failed claim leaves no row —
|
|
132
|
+
**absence is the only evidence**, and detecting it requires the AWS instance list.
|
|
133
|
+
|
|
134
|
+
Because every OneUptime monitor on this tier is keyed by **role** rather than by instance, none of
|
|
135
|
+
them can see this (see
|
|
136
|
+
[the blind spot](./features/oneuptime-worker-uptime-monitoring.md)). External coverage now comes
|
|
137
|
+
from the 2.0
|
|
138
|
+
[Worker Fleet Role-Assignment Monitor](../../2.0/apps/worker2/features/worker-fleet-role-assignment-monitor.md).
|
|
139
|
+
|
|
111
140
|
## Anatomy of a cron script
|
|
112
141
|
|
|
113
142
|
Every script is self-contained and follows this boilerplate:
|
|
@@ -208,6 +237,10 @@ web face (`mvc/` GET routes for login/logout/404); the tier's real work is the c
|
|
|
208
237
|
|
|
209
238
|
## Conventions & gotchas
|
|
210
239
|
|
|
240
|
+
- **A worker that fails role election is silently dead, not degraded.** No name → no crontab →
|
|
241
|
+
the instance runs nothing while EB reads healthy, and the `@`-suppressed `mysqli_connect` in
|
|
242
|
+
`ebs/cron.worker.php` logs nothing. Never treat "EB is green" as evidence the fleet is working;
|
|
243
|
+
check `Vision_Log.Workers` against the actual EB instance list.
|
|
211
244
|
- **`dbchanges` auto-apply is branch-gated, and `_production` is NOT auto-applied.**
|
|
212
245
|
`crons/infrastructure/execute_dbchanges.php` runs every 2 minutes but only for branches matching
|
|
213
246
|
`_%`, deriving the env by stripping the leading `_` and requiring `config.<env>.ini` in the
|
|
@@ -6,12 +6,17 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["jcardinal"]
|
|
9
|
+
updated: 2026-08-24
|
|
10
|
+
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- library/app/worker.php
|
|
13
13
|
- worker/crons/worker/worker_heartbeat.php
|
|
14
|
-
|
|
14
|
+
- worker/ebs/cron.worker.php
|
|
15
|
+
- worker/.ebextensions/045_oneuptime_agent.config
|
|
16
|
+
related:
|
|
17
|
+
- ./oneuptime-server-monitor-host-hygiene.md
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- ../../../2.0/apps/worker2/features/worker-fleet-role-assignment-monitor.md
|
|
15
20
|
---
|
|
16
21
|
|
|
17
22
|
## Summary
|
|
@@ -57,6 +62,31 @@ Database, Infrastructure, TOGa, TOGa Desk, and Catalog, then importing each into
|
|
|
57
62
|
manually. To add a new worker's monitor, clone an existing monitor export the same way and
|
|
58
63
|
register the resulting heartbeat URL in `App_Worker::$oneUptimeEndpoints`.
|
|
59
64
|
|
|
65
|
+
## THE BLIND SPOT — every monitor here is keyed by ROLE, so a ROLELESS instance is invisible
|
|
66
|
+
|
|
67
|
+
Load-bearing limitation, learned the hard way (2026-08-24: **2 of the 7 production workers ran
|
|
68
|
+
roleless for ~3 hours undetected**). This layer, and the Server monitors in
|
|
69
|
+
[host hygiene](./oneuptime-server-monitor-host-hygiene.md), are keyed by `workerName` — **14
|
|
70
|
+
monitors, none keyed by instance**:
|
|
71
|
+
|
|
72
|
+
- `worker_heartbeat.php` only pings `App_Worker::$oneUptimeEndpoints[$row['workerName']]` for
|
|
73
|
+
the row matching **its own** `instanceId`. No row → no ping target → it pushes nothing.
|
|
74
|
+
- The `045_oneuptime_agent.config` Server agent reads `/etc/worker-role` and **exits clean when
|
|
75
|
+
it is empty.**
|
|
76
|
+
|
|
77
|
+
So an instance that never claimed a role reports to nothing — and because every role is still
|
|
78
|
+
owned by *someone*, **no role-keyed monitor is missing a ping either.** The fleet reads 100%
|
|
79
|
+
healthy while a box does zero work (role assignment happens **only at deploy time**, and no role
|
|
80
|
+
means the role's `cron.worker.<role>.json` is never appended to the crontab, so the instance runs
|
|
81
|
+
**nothing**).
|
|
82
|
+
|
|
83
|
+
`Vision_Log.Workers` only ever holds instances that **succeeded** in claiming a role, so the
|
|
84
|
+
failure leaves no row and no log line — **absence is the only evidence**, and detecting an
|
|
85
|
+
absence requires the AWS instance list, which nothing on this tier consults. That gap is what
|
|
86
|
+
the 2.0
|
|
87
|
+
[Worker Fleet Role-Assignment Monitor](../../../2.0/apps/worker2/features/worker-fleet-role-assignment-monitor.md)
|
|
88
|
+
now closes from outside the fleet. **Do not try to close it with another role-keyed monitor.**
|
|
89
|
+
|
|
60
90
|
## Gotchas
|
|
61
91
|
|
|
62
92
|
- Do **not** hardcode the heartbeat URLs/UUIDs anywhere but `App_Worker::$oneUptimeEndpoints`
|
|
@@ -67,6 +97,13 @@ register the resulting heartbeat URL in `App_Worker::$oneUptimeEndpoints`.
|
|
|
67
97
|
pending cleanup — the live source of truth is the `App_Worker::$oneUptimeEndpoints` map.
|
|
68
98
|
|
|
69
99
|
## Change history
|
|
100
|
+
- 2026-08-24 — Documented **the blind spot**: all 14 monitors on this tier are keyed by
|
|
101
|
+
`workerName`, so an instance that claimed **no** role pings nothing and leaves every role-keyed
|
|
102
|
+
monitor still green — which is how 2 of 7 production workers ran roleless for ~3 hours
|
|
103
|
+
undetected. Recorded that `Vision_Log.Workers` only holds successful claims (absence is the
|
|
104
|
+
only evidence) and that the gap is now covered from outside the fleet by the 2.0
|
|
105
|
+
[Worker Fleet Role-Assignment Monitor](../../../2.0/apps/worker2/features/worker-fleet-role-assignment-monitor.md).
|
|
106
|
+
(bala)
|
|
70
107
|
- 2026-07-13 — Added external OneUptime heartbeat push for all 1.0 workers on top of the
|
|
71
108
|
existing internal DB-heartbeat mechanism; endpoints keyed by workerName in
|
|
72
109
|
`App_Worker::$oneUptimeEndpoints`. (jcardinal)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Tracing a 1.0 worker cron run in production (no stdout, silent skips)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: worker
|
|
5
|
+
project: Worker
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-25
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- worker/ebs/cron.worker.php
|
|
13
|
+
- worker/schedules/cron.worker.sync.json
|
|
14
|
+
- library/app/framework.php
|
|
15
|
+
related:
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ./diagnosing-frozen-cron-checkins.md
|
|
18
|
+
- ../../library/features/cron-execution-monitoring.md
|
|
19
|
+
- ../../../1.0/standards/backend-php.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
How to answer *"did this cron actually run, and what did it do?"* on the 1.0 `worker` tier, where
|
|
24
|
+
**there is no usable stdout** and **a skipped run leaves no trace at all**. Written from a
|
|
25
|
+
production diagnostic session on
|
|
26
|
+
`worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php`; the mechanics
|
|
27
|
+
apply to every cron on the tier.
|
|
28
|
+
|
|
29
|
+
Use this before you conclude a cron "never fired" — three different situations look identical
|
|
30
|
+
from the outside.
|
|
31
|
+
|
|
32
|
+
## Step 1 — get the real schedule from `schedules/*.json`, never from the file
|
|
33
|
+
The `.php` file's own header comment is not authoritative and is routinely stale. So are skills,
|
|
34
|
+
docs and tickets. `worker/schedules/cron.<env>[.<role>].json` is the only source of truth.
|
|
35
|
+
|
|
36
|
+
Worked example: the entry *"Download EDI From S3 Files & Create PO - EDI Office Depot"* in
|
|
37
|
+
`cron.worker.sync.json` is `*/5 * * * *` — **every 5 minutes**. The cron file's header said
|
|
38
|
+
"EVERY HOUR" (corrected 2026-08-25) and at least one skill still says hourly. A wrong assumed
|
|
39
|
+
frequency makes you read a normal gap as an outage.
|
|
40
|
+
|
|
41
|
+
Also check whether the job is registered in the env you are testing in at all: this one is **not**
|
|
42
|
+
in `cron.beta.json`, so beta never runs it on a schedule. Nothing on beta is evidence about it.
|
|
43
|
+
|
|
44
|
+
## Step 2 — echo/print goes nowhere useful, so do not debug with it
|
|
45
|
+
The crontab line is assembled in `worker/ebs/cron.worker.php`, and the two blocks that build it
|
|
46
|
+
differ:
|
|
47
|
+
|
|
48
|
+
| Schedule file | Command written | Where output goes |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| base `cron.<env>.json` | `php /var/www/html/crons/<script>` | **nowhere** — the ` > /var/www/cache/WORKER_ERROR_$RANDOM` redirect is **commented out** |
|
|
51
|
+
| role `cron.worker.<role>.json` | `php /var/www/html/crons/<script> > /var/www/cache/WORKER_ERROR_$RANDOM` | a **randomly named** file under `/var/www/cache/` on whichever of the 7 instances ran it |
|
|
52
|
+
|
|
53
|
+
Either way you cannot go and read it: base-schedule output is discarded outright, and a
|
|
54
|
+
role-schedule run leaves an unpredictable filename on a box in an autoscaled fleet. **Treat
|
|
55
|
+
worker cron stdout as write-only.**
|
|
56
|
+
|
|
57
|
+
## Step 3 — put temporary tracing somewhere you can query
|
|
58
|
+
What worked: write timestamped step markers into the **`note`** column of the run's
|
|
59
|
+
`Common.CronJobExecutions` row (the row `App_Framework::cronInitialization()` already inserted),
|
|
60
|
+
then read them back over the DB tooling from your desk. `dtCheckIn`, `dtCheckOut` and
|
|
61
|
+
`executionTimeSeconds` on the same row give you the shape of the run for free.
|
|
62
|
+
|
|
63
|
+
Rules: keep it to a few markers, and **remove the tracing once diagnosed** (it was removed in
|
|
64
|
+
this session). Never write payloads or credentials into `note`.
|
|
65
|
+
|
|
66
|
+
## Step 4 — read `Common.CronJobExecutions` to tell "ran", "hung" and "skipped" apart
|
|
67
|
+
Query the legacy env's `Common.CronJobExecutions` filtering `job LIKE` the script path:
|
|
68
|
+
|
|
69
|
+
- **`dtCheckIn` + `dtCheckOut` + `executionTimeSeconds`** — it ran and finished; the duration
|
|
70
|
+
tells you whether it fits inside its schedule slot.
|
|
71
|
+
- **`dtCheckIn` with no `dtCheckOut`** — it started and never completed (crash, timeout, or the
|
|
72
|
+
instance went away).
|
|
73
|
+
- **no row for a slot you expected** — the run was **skipped by the overlap guard**, not "never
|
|
74
|
+
fired". A blocked run writes **nothing**: `cronInitialization()` calls
|
|
75
|
+
`exitIfProcessRunning()` **before** the INSERT, and the `cronFinished(true)` it then calls is a
|
|
76
|
+
no-op because `cronLogId` is unset. See
|
|
77
|
+
[Cron Execution Monitoring](../../library/features/cron-execution-monitoring.md).
|
|
78
|
+
|
|
79
|
+
### The overlap guard is a `ps -ef` substring match — anything can block it
|
|
80
|
+
`App_Framework::isProcessRunning()` returns true for **any** `ps -ef` line containing the script
|
|
81
|
+
path (it only excludes `/bin/sh` lines and its own pid). So a developer's `tail -f`, an editor, or
|
|
82
|
+
any shell holding that path in its command line **blocks the cron indefinitely** while looking
|
|
83
|
+
like a legitimate overlap. Check `ps -ef` for what is actually holding the name before assuming a
|
|
84
|
+
long-running instance of the job itself.
|
|
85
|
+
|
|
86
|
+
## Step 5 — if runs overlap, look at what the job does before its real work
|
|
87
|
+
A job whose own runtime exceeds its schedule interval silently loses most of its slots to the
|
|
88
|
+
guard. Measure the phases, not the total: in the ODP case an instrumented run showed the S3
|
|
89
|
+
listing alone returning **138,472 objects and taking 32 seconds** before any PO was touched, with
|
|
90
|
+
each PO then costing roughly 40 seconds — comfortably past a 5-minute schedule. Prefixes the job
|
|
91
|
+
skips while processing are still listed, so accumulated `SENT/` and `OUTBOX/` objects inflate
|
|
92
|
+
every run.
|
|
93
|
+
|
|
94
|
+
## Deploy-time trap: a PHP 8-only syntax kills the whole cron with no visible error
|
|
95
|
+
This tier runs **PHP 7.2**, so a **PHP 8 named argument** (`someFunction(items: $x)`) is a
|
|
96
|
+
**parse error**, not a runtime warning:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
PHP Parse error: syntax error, unexpected ':', expecting ',' or ')'
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The script then does nothing at all on every tick — and per Steps 2 and 4 you will see no output
|
|
103
|
+
and no `CronJobExecutions` row, i.e. it looks exactly like "the cron never fired". Typed
|
|
104
|
+
parameters and return types (`: void`, `: array`, `: bool`, `?array`, `?object`, `object $x`) are
|
|
105
|
+
PHP 7.0/7.1 and **are** safe here; they are already used in
|
|
106
|
+
`worker/crons/toga2/compass/backfill_items_isfulfillable.php` and
|
|
107
|
+
`update_item_fulfillments_in_netsuite.php`.
|
|
108
|
+
|
|
109
|
+
`1.0/standards/backend-php.md` already forbids named arguments in 1.0 — but a workspace
|
|
110
|
+
`CLAUDE.md` / `.github/copilot-instructions.md` that says *"use PHP 8 Named Parameters"* applies
|
|
111
|
+
to **2.0 only**. The 1.0 standard wins in `library/`, `worker/` and every other 1.0 app.
|
|
112
|
+
|
|
113
|
+
**Do not read a PHP 8 function call in existing code as proof the tier is PHP 8.**
|
|
114
|
+
`library/app/systemmonitor/500error.php` uses `str_contains()`; that file is not a version
|
|
115
|
+
signal, it is a latent bug.
|
|
116
|
+
|
|
117
|
+
## Change history
|
|
118
|
+
- 2026-08-25 — Documented from a prod diagnostic session on the Compass ODP 850 importer: the
|
|
119
|
+
schedule JSON (not the file header) is authoritative, worker cron stdout is unreadable (base
|
|
120
|
+
block's `WORKER_ERROR` redirect commented out, role block writes a random filename),
|
|
121
|
+
`Common.CronJobExecutions.note` is the practical place for temporary tracing, a run skipped by
|
|
122
|
+
the `ps -ef` overlap guard writes **no row at all** (guard runs before the INSERT), the guard
|
|
123
|
+
matches any process holding the script path, and a PHP 8 named argument parse-errors the whole
|
|
124
|
+
cron silently on this PHP 7.2 tier. (bala)
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
| [Core.Domains — the app host registry (and why you cannot derive a host)](features/core-domains-app-host-registry.md) | `Core.Domains` is the **authoritative registry of which host serves which client's app in which environment**. | _underscore/Model/Core/Domain.php, api2/Component/Api/CrossClient/CrossClient.php, api2/Component/Api/V2/V2.php, worker/config.beta.ini |
|
|
20
20
|
| [Re-pointing a DB alias mid-request (_Database::register park/restore)](features/database-alias-repointing.md) | `_Database` keys **all live per-database runtime state by the connection ALIAS** (`Client` / `_underscore::DB_CLIENT`, `ClientLogs`, `Archive`), **not** by the | _underscore/Database.php, _underscore/Query.php, api2/Component/Api/V2/V2.php, api2/Component/Api/CrossClient/CrossClient.php |
|
|
21
21
|
| [2.0 Email Send Pipeline (queue + Send worker)](features/email-send-pipeline.md) | In 2.0, `_Email::send()` **does not transmit** — it queues the message. | _underscore/Email.php, _underscore/String.php, worker2/Worker/Infrastructure/Email/Send.php, _underscore/Model/Client/Logs/Email.php, _underscore/Model/Client/Logs/EmailAttachment.php |
|
|
22
|
-
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
22
|
+
| [Client Email Template Sending](features/email-template-sending.md) | `_Model_Client_EmailTemplate` sends a stored, client-defined email template by UUID. | _underscore/Model/Client/EmailTemplate.php, _underscore/Model/Quad/ApprovalDecision.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php, _underscore/Email.php |
|
|
23
23
|
| [Error Reporting — Issue/Event Capture, Fingerprinting & Aggregation](features/error-reporting-issue-event.md) | Platform-wide error reporting for TOGA 2.0, built on an **Issue / Event** aggregation model in the **shared Core Logs DB**. | _underscore/Error.php, _underscore/Database.php, _underscore/Exception/Business.php, api2/Controller/Index.php, worker2/Controller/Index.php, _underscore/Model/Core/Logs/Issue.php, _underscore/Model/Core/Logs/Event.php, _underscore/Model/Core/Logs/IssueFingerprint.php, _underscore/Model/Core/Logs/IssueClickupTask.php, _underscore/Model/Core/Logs/IssueEmailAddress.php, _underscore/Model/Core/Logs/IssueAreaOwner.php, worker2/Worker/Infrastructure/Errors.php, tools/mvc/errors/issue/get.php, tools/mvc/errors/post.php, dbchanges2/Logs/2026-07-30a - Error reporting Issues and Events.sql, dbchanges2/Logs/2026-08-01a - Rename tables to singular.sql, dbchanges2/Logs_Client/2026-08-01a - Drop Error table.sql, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql |
|
|
24
24
|
| [Record-Changed Event Publishing (_Event::publish to SQS)](features/event-publish-sqs.md) | `_Event::publish()` (in `_underscore/Event.php`) is the PHP side of the real-time event pipeline. | _underscore/Event.php |
|
|
25
25
|
| [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). | _underscore/Model.php, _underscore/Model/Client/TrackingNumber.php, _underscore/Model/Client/Invoice.php, dbchanges2/Core/HISTORIC/2024/2024-11b - item-fulfillments.sql |
|
|
@@ -37,8 +37,9 @@
|
|
|
37
37
|
| [Persona Name Translation (PersonaTranslations sidecar)](features/persona-name-translation.md) | Serves Persona **names** in multiple languages by adding a per-language **sidecar** table `PersonaTranslations`, reusing the platform's existing metadata-driven | _underscore/Model/Client/PersonaTranslation.php, dbchanges2/Client/2026-07-22b - PersonaTranslations.sql, dbchanges2/Core/2026-07-22a - PersonaTranslationsRecord.sql, dbchanges2/Client/2026-07-22c - PersonaTranslationsAcl.sql, dbchanges2/Client_CompassCanada/2026-07-22 - PersonaTranslationsFrench.sql, toga2-commerce/src/pages/Account/view/MySettingsView.tsx |
|
|
38
38
|
| [Record Change Audit Log (Logs_<Client>.Record / RecordField) — reading a field's history](features/record-change-audit-log.md) | Every 2.0 client schema has a sibling **logs** schema `Logs_<Tenant>` (e.g. | _underscore/Model/Client/Logs/Record.php, _underscore/Model/Client/Logs/RecordField.php, _underscore/Model/Client/Logs/CustomRecordField.php |
|
|
39
39
|
| [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql, dbchanges2/Client_Compass/2026-07-02a - FixSA133377TrackingSerialAndDuplicateIF.sql, dbchanges2/Client_Compass/2026-08-18 - FixSA135471HeroItemHalfQuantityFulfillment.sql |
|
|
40
|
+
| [Per-client sales-order status filter (Surface FILTER_SET → table meta `filterOptions`)](features/sales-order-status-filter-surface.md) | The status-filter dropdown on the sales-orders table is **per client**, driven by a Surface `FILTER_SET` rather than by the raw contents of the client's `SalesO | _underscore/Model/Client/TableView.php, _underscore/Model/Core/Surface.php, _underscore/Model/Client/SalesOrder.php, _underscore/Model/Quad/SalesOrder.php, _underscore/Model/Prudential/SalesOrder.php, toga-blox/src/components/Table/hooks/useFetchTablePageMeta.ts, toga-blox/src/api/types.ts, dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql |
|
|
40
41
|
| [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` is the 2.0 framework's static string utility class. | _underscore/String.php |
|
|
41
|
-
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, toga25-supply/src/App.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/contexts/AuthContext.tsx, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Core/2026-08-21a - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Compass/2026-08-04a - ApprovalDetailsAssignedManagerPreferredStage.sql, _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
|
|
42
|
+
| [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Client/Language.php, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, toga25-supply/src/App.tsx, toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/contexts/AuthContext.tsx, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Core/2026-08-21a - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_Compass/2026-08-04a - ApprovalDetailsAssignedManagerPreferredStage.sql, _underscore/Model/Core/Surface.php, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Client_Compass/2026-07-15f - SalesOrderRecordActionsRemoveDeadConfigRuleOverrides.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php, api2/Component/Api/V2/V2.php |
|
|
42
43
|
| [Table-View Hyperlink Columns (meta → ACL → computed URL → render)](features/tableview-hyperlink-columns.md) | Any 2.0 table-view column can render its value as a clickable link instead of plain text. | _underscore/Model/Client/TableView.php, _underscore/Model/Client/TrackingNumber.php, api2/Component/Api/V2/V2.php, toga2-supply/src/api/toga.ts, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/formatTableData.tsx, toga2-supply/src/components/ui/Tables/PrimaryTable/helperFunctions/convertData.tsx, toga2-supply/src/components/ui/Tables/hooks/useDataTableState.tsx, dbchanges2/Client/2026-07-20 - TrackingNumberHyperlinkAndFieldPermission.sql |
|
|
43
44
|
| [TableView joins (TableViewJoins → SQL) — aliasing, chained multi-hop joins, ACL](features/tableview-joins.md) | `Client_*.TableViewJoins` rows are what let a table view show a column from a table other than its base record. | _underscore/Model/Client/TableView.php, dbchanges2/Client/2026-08-11a - ServiceRequestsInventoryUnitsTableViews.sql, dbchanges2/Client_Elite/2026-08-18 - InventoryUnitsItemColumns.sql, dbchanges2/Client_Nychh/2026-08-18 - InventoryUnitsItemColumns.sql |
|
|
44
45
|
| [TogaIQ Gateway Client (_Component_Api_Togaiq) — AI generate/translate from 2.0](features/togaiq-gateway-client.md) | `_Component_Api_Togaiq` is the 2.0 framework's client for the **TogaIQ** (Talos) AI gateway. | _underscore/Component/Api/Togaiq/Togaiq.php, _underscore/ApiRequest.php |
|
|
@@ -6,10 +6,12 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-25
|
|
10
10
|
owners: ["jcardinal", "bala", "mhammontree", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/EmailTemplate.php
|
|
13
|
+
- _underscore/Model/Quad/ApprovalDecision.php
|
|
14
|
+
- _underscore/Model/Quad/SalesOrder.php
|
|
13
15
|
- _underscore/Model/Client/EmailTemplateOutgoingEmailAddress.php
|
|
14
16
|
- _underscore/Email.php
|
|
15
17
|
related:
|
|
@@ -105,9 +107,31 @@ can keep using `sendEmail($api, ...)`.
|
|
|
105
107
|
→ `$args = ['userName'=>$name, ...]`). Passing the map as a single positional argument
|
|
106
108
|
(`sendEmail($api, $uuid, $to, [], [], $vars)`) makes `$args = [0 => [...]]` — a numeric key
|
|
107
109
|
whose value is an *array*. `replaceTemplateVariables()` then runs `str_replace('{0}', <array>, …)`
|
|
108
|
-
→ `TypeError: str_replace(): Argument #2 must be of type string
|
|
109
|
-
production 500 / **EO-1
|
|
110
|
+
→ `TypeError: str_replace(): Argument #2 ($replace) must be of type string when argument #1
|
|
111
|
+
($search) is a string`, surfacing as a production 500 / **EO-1** — it fails the **whole request**,
|
|
112
|
+
not just one placeholder. `Model/Compass/*` gets this right (spread); the four `Model/Quad/*`
|
|
110
113
|
call sites were copied from Compass but dropped the `...` — fixed 2026-08-18.
|
|
114
|
+
- **This contract has regressed TWICE.** `c22999d9` (2026-05-19) **removed** the spread and broke
|
|
115
|
+
Quad order-approval emails; `2b0c1470` (2026-08-18) restored it. `sendEmail` has been variadic
|
|
116
|
+
since **2024-12-24**, so the spread has **always** been the correct form and the May commit was
|
|
117
|
+
the regression, not a style change. The contract and both regressions are now recorded on
|
|
118
|
+
`sendEmail()`'s docblock, with a marker comment at each of the four Quad call sites — because
|
|
119
|
+
that is where the mistake actually gets made.
|
|
120
|
+
- **Hardened 2026-08-25 — a mis-spread caller now degrades instead of 500-ing.**
|
|
121
|
+
`replaceTemplateVariables()` (now typed) **skips non-scalar values**, logging the key and the
|
|
122
|
+
**type only — never the value** (template vars carry user names and order numbers, so logging
|
|
123
|
+
them would be a PII violation). Result: a bad caller leaves an **unreplaced placeholder** in the
|
|
124
|
+
email rather than taking down order approval. Verified behaviourally: the old code fatals on the
|
|
125
|
+
no-spread shape, the new code degrades, and the correct spread path is **byte-identical**. The
|
|
126
|
+
spread is still the contract — the guard is a safety net, not a licence to pass a bare array.
|
|
127
|
+
- **⚠ Still seeing this error in an environment does NOT mean the code is wrong — check the deploy
|
|
128
|
+
FIRST.** Verified 2026-08-25: Quad order placement was still 500-ing in the **commerce
|
|
129
|
+
sandbox-client** environment (`EmailTemplate.php:82` via `_Model_Quad_SalesOrder::postPost`)
|
|
130
|
+
even though the fix `2b0c1470` was already committed on `_sandbox-client`. `_underscore` is
|
|
131
|
+
**cloned at build time from a moving per-environment branch**, so the running artifact simply
|
|
132
|
+
predated the commit. That is a **deploy gap, not a code bug** — a redeploy fixes it, and no code
|
|
133
|
+
change should be made. Same known-issue as
|
|
134
|
+
[api2 environment-variable-drives-underscore-branch](../../api2/features/environment-variable-drives-underscore-branch.md).
|
|
111
135
|
- **`sendEmail()`'s signature is load-bearing for scripted APIs** — the Record Script engine
|
|
112
136
|
(`api2/Component/Api/V2/V2.php`, ~line 3594) calls the method with `api` as a named
|
|
113
137
|
argument, so the first param must stay `&$api`. Do not "clean it up" by removing it.
|
|
@@ -161,6 +185,19 @@ worker method) in-process instead.
|
|
|
161
185
|
|
|
162
186
|
## Change history
|
|
163
187
|
|
|
188
|
+
- 2026-08-25 — Added the **deploy-gap** caveat to the spread gotcha: Quad order placement was still
|
|
189
|
+
500-ing in the commerce **sandbox-client** environment after `2b0c1470` landed, because
|
|
190
|
+
`_underscore` is cloned at build from a moving per-environment branch and the running artifact
|
|
191
|
+
predated the commit. Diagnose the deploy before touching the code. (apeterson)
|
|
192
|
+
- 2026-08-25 — Hardened `replaceTemplateVariables()` (signature typed) to **skip non-scalar values**,
|
|
193
|
+
logging key + **type only** (never the value — template vars carry user names and order numbers), so
|
|
194
|
+
a mis-spread caller degrades to an unreplaced placeholder instead of a 500 that kills the whole
|
|
195
|
+
request. Verified the old code fatals on the no-spread shape, the new code degrades, and the correct
|
|
196
|
+
spread path is byte-identical. Recorded that the spread contract has regressed **twice** —
|
|
197
|
+
`c22999d9` (2026-05-19) removed it and broke Quad order-approval emails, `2b0c1470` (2026-08-18)
|
|
198
|
+
restored it — and that `sendEmail` has been variadic since 2024-12-24, so the spread was always
|
|
199
|
+
correct and May was the regression. Documented the contract on `sendEmail()`'s docblock plus a marker
|
|
200
|
+
comment at each of the four Quad call sites. (apeterson)
|
|
164
201
|
- 2026-08-18 — Fixed the Quad order-approval/rejection emails' production 500
|
|
165
202
|
(`str_replace(): Argument #2 must be string`, EO-1): the four `Model/Quad/ApprovalDecision.php`
|
|
166
203
|
+ `Model/Quad/SalesOrder.php` call sites passed the template-vars map as a bare positional
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi"]
|
|
9
|
+
updated: 2026-08-24
|
|
10
|
+
owners: ["dfranks", "jcardinal", "mhammontree", "apeterson", "kyalamarthi", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Database.php
|
|
13
13
|
- _underscore/Model.php
|
|
@@ -177,6 +177,16 @@ here — they live in `Config/*.ini`.)
|
|
|
177
177
|
- Related 1.0 analogue: the legacy `App_` worker has the same hazard writing to `Logs.API`
|
|
178
178
|
(`db_logs`) — the laptop trap there is documented separately in the worker NetSuite bootstrap
|
|
179
179
|
notes.
|
|
180
|
+
- **`_Database::register()` is LAZY — it stores connection config and never opens a socket.**
|
|
181
|
+
The connect happens on first use. So adding a static alias registration to an app's `_.php`
|
|
182
|
+
bootstrap is **safe across every environment**, even one whose config group points at a
|
|
183
|
+
localhost that lacks the schema: nothing fails until something actually queries that alias.
|
|
184
|
+
This is what makes "register the alias globally, use it in one cron" a two-line change rather
|
|
185
|
+
than a per-environment config exercise (worked example: `DB_VISION_LOGS` in
|
|
186
|
+
[the Worker Fleet Role-Assignment Monitor](../../worker2/features/worker-fleet-role-assignment-monitor.md)).
|
|
187
|
+
The corollary is the trap the rest of this doc describes: because registration is free and
|
|
188
|
+
silent, a **bad** registration also stays silent until the request that needs it blows up with
|
|
189
|
+
`Unknown database`.
|
|
180
190
|
|
|
181
191
|
## WITHDRAWN — the "alias-keyed `$_modelCache` cross-tenant leak" hypothesis (2026-08-17)
|
|
182
192
|
|
|
@@ -210,6 +220,11 @@ also hits `_modules/<module>/`** — is recorded in
|
|
|
210
220
|
|
|
211
221
|
## Change history
|
|
212
222
|
|
|
223
|
+
- 2026-08-24 — Recorded that **`_Database::register()` is lazy** (stores config, opens no
|
|
224
|
+
socket; the connect happens on first use), so a static alias registration in an app's `_.php`
|
|
225
|
+
is safe in every environment including a localhost config that lacks the schema — and that
|
|
226
|
+
this is also why a *bad* registration stays silent until first query. Verified while adding
|
|
227
|
+
`DB_VISION_LOGS` to worker2. (bala)
|
|
213
228
|
- 2026-08-17 (later pass) — **WITHDRAWN, supersedes the entry below.** The alias-keyed
|
|
214
229
|
`$_modelCache` cross-tenant-leak hypothesis is **not supported** and is no longer a live security
|
|
215
230
|
concern. The six `c_` columns are **AIG's own**: `_modules/netsuite/2026-07-10a -
|