toga-ai 1.0.596 → 1.0.598

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.
@@ -18,6 +18,7 @@
18
18
  | [Compass VIP Support Importer (worker2)](features/compass-vip-support-importer.md) | A worker2 action that ingests Compass's quarterly VIP spreadsheet and assigns each VIP user's support technician by setting `Users.c_supportedByUserId` in `Clie | worker2/Worker/Client/Compass/VipSupport.php |
19
19
  | [Creating Worker Actions](features/creating-worker-actions.md) | How to add a new callable Worker action — a PHP class whose `public static` methods are invoked as background jobs (via webhook, cron, or `_Worker::runTask()`). | worker2/Worker/, worker2/Controller/Index.php, _underscore/Worker.php |
20
20
  | [Cross-account AWS access for worker2 crons (_Component_Aws_Workloads)](features/cross-account-aws-access.md) | `_Component_Aws_Workloads` is the **standard, and only sanctioned, way any new worker2 cron obtains AWS access** — for any account, any region, any AWS SDK clie | worker2/Component/Aws/Workloads/Workloads.php, worker2/Config/production.ini, worker2/Worker/Infrastructure/CloudWatch.php |
21
+ | [Elastic Beanstalk health monitor → OneUptime (ElasticBeanstalkHealth)](features/elastic-beanstalk-health-monitor.md) | `_Worker_Infrastructure_CloudWatch::ElasticBeanstalkHealth()` is a worker2 cron that reads each Elastic Beanstalk (EB) environment's **enhanced-health** status | worker2/Worker/Infrastructure/CloudWatch.php |
21
22
  | [Elite Freshservice Sync (worker2)](features/elite-freshservice-sync.md) | `_Worker_Elite` processes Freshservice webhook events and syncs them into TOGA 2. | worker2/Worker/Elite.php, worker2/Config/dev-kmaramreddy-laptop.ini |
22
23
  | [Error Escalation Cron (Errors::Escalate → ClickUp / email)](features/error-escalation-cron.md) | `_Worker_Infrastructure_Errors::Escalate` (renamed from `SyncWithClickup`) is the sole owner of **escalation, de-escalation, ClickUp ticketing, reminders, busin | worker2/Worker/Infrastructure/Errors.php, worker2/Worker/Notification/Email.php, worker2/Worker/Notification/EmailTemplate.php, worker2/Worker/Client/True.php, worker2/Worker/Clickup/ErrorTask.php, worker2/Worker/Clickup.php, worker2/Controller/Index.php, worker2/Config/production.ini, _underscore/Model/Core/Logs/Issue.php, dbchanges2/Core/2026-07-30a - Error escalation cron job.sql, dbchanges2/Logs/2026-08-03a - Issue clickupPriority.sql, dbchanges2/Core/2026-08-04a - Error neglect digest cron job.sql |
23
24
  | [Error-Issue Auto-Resolution & Reopen (frequency-decay lifecycle)](features/error-issue-auto-resolution.md) | The error system could escalate and de-escalate an Issue's *urgency* but had no concept of an Issue being **resolved**. | worker2/Worker/Infrastructure/Errors.php, worker2/Worker/Clickup/ErrorTask.php, _underscore/Model/Core/Logs/Issue.php, tools/mvc/errors/get.php, tools/mvc/errors/issue/get.php, dbchanges2/Logs/2026-08-05a - Issue status baseline and auto-resolution.sql |
@@ -33,7 +34,7 @@
33
34
  | [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
34
35
  | [NYCHH Asset-Tag Backfill (worker2)](features/nychh-asset-tag-backfill.md) | Keeps NYC Health & Hospitals (`Client_Nychh`) unit asset tags and MAC addresses synced from NetSuite. | worker2/Worker/Client/Nychh.php, worker2/Worker/Client/Nychh/AssetTagBackfill.php, worker/crons/toga2/netsuite/verify_fulfillment_asset_tag_sync_nychh.php, worker/crons/toga2/netsuite/backfill_all_asset_tags_from_netsuite_nychh.php |
35
36
  | [OneUptime Incident → ClickUp Task Sync (Monitor/Oneuptime/SyncIncidents)](features/oneuptime-incident-clickup-sync.md) | An internal/shared TOGA ops feature: worker2 polls the OneUptime API every 15 minutes for **currently-open** incidents and ensures a ClickUp task exists for eac | worker2/Component/Api/Oneuptime/Oneuptime.php, worker2/Worker/Monitor/Oneuptime.php, worker2/Config/production.ini, dbchanges2/Team/2026-08-10a - OneUptime Incident ClickUp Tasks.sql, dbchanges2/Core/2026-08-10b - OneUptime Incident Sync CronJob.sql, dbchanges2/Team/2026-08-12a - OneUptime Incident Tasks Closed Marker.sql |
36
- | [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, worker2/Worker/Client/Compass.php, worker2/composer.json, _underscore/Cloud.php, worker2/Worker/Monitor/Operations.php |
37
+ | [OneUptime push-metric monitors for 2.0 workers](features/oneuptime-worker2-monitoring.md) | A second, **OneUptime-reporting** monitoring pattern for the 2.0 worker2 tier, ported from the 1.0 `App_SystemMonitor_Compass` monitors. | worker2/Worker/Monitor/Compass.php, worker2/Worker/Client/Compass.php, worker2/composer.json, _underscore/Cloud.php, worker2/Worker/Monitor/Operations.php, worker2/Worker/Monitor/Aig.php, worker2/Worker/Monitor/Prudential.php, worker2/Worker/Monitor/Nycdoe.php, worker2/Config/production.ini, worker2/Config/beta.ini, worker2/Config/sandbox-dev.ini |
37
38
  | [Platform Cache Cleanup (_Worker_Platform_Cache — Clean + Truncate)](features/platform-cache-cleanup.md) | `_Worker_Platform_Cache` owns maintenance of the shared **Cache** cluster that backs api2's [multi-client data retrieval](../../api2/features/cross-client-data- | worker2/Worker/Platform/Cache.php, worker2/Controller/Index.php, worker2/_.php, dbchanges2/Core/2026-07-27a - PlatformCacheCleanCron.sql |
38
39
  | [Service Request → Sales Order → Purchase Order generation (Sync/ServiceRequest)](features/service-request-sales-order-generation.md) | `_Worker_Sync_ServiceRequest` turns a **Service Request into a Sales Order, and then into one Purchase Order per vendor**, for **any** tenant. | worker2/Worker/Sync/ServiceRequest.php, _underscore/Model/Elite/ServiceRequest.php, dbchanges2/Core/2026-08-06a - Service request and sales order payload interceptors.sql |
39
40
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
@@ -17,6 +17,7 @@ related:
17
17
  - ../../_underscore/features/config-group-access.md
18
18
  - ../../_underscore/features/cloud-s3-helpers.md
19
19
  - ./oneuptime-worker2-monitoring.md
20
+ - ./elastic-beanstalk-health-monitor.md
20
21
  ---
21
22
 
22
23
  ## Summary
@@ -0,0 +1,182 @@
1
+ ---
2
+ title: Elastic Beanstalk health monitor → OneUptime (ElasticBeanstalkHealth)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-17
10
+ owners: [jcardinal]
11
+ files:
12
+ - worker2/Worker/Infrastructure/CloudWatch.php
13
+ related:
14
+ - ./cross-account-aws-access.md
15
+ - ./oneuptime-worker2-monitoring.md
16
+ - ./monitoring-framework.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ `_Worker_Infrastructure_CloudWatch::ElasticBeanstalkHealth()` is a worker2 cron that reads
22
+ each Elastic Beanstalk (EB) environment's **enhanced-health** status and reports it to a
23
+ OneUptime **Incoming Request** monitor, which pages the team. It is a **"dumb reporter,
24
+ smart monitor"** heartbeat (the same Pattern-B contract as
25
+ [OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md)): the worker decides
26
+ pass/fail and POSTs decided **string tokens** (`alarm=HIGH/OK`, `probe=DEGRADED/OK`) that
27
+ OneUptime string-matches — OneUptime cannot compare numbers on a pushed body.
28
+
29
+ The load-bearing design decision this doc records is the **paging (alarm) criteria**: page
30
+ on EB `Degraded`/`Severe`, but **suppress the common "an occasional HTTP 500 tripped
31
+ Degraded" false alarm** unless 5xx errors actually dominate the request mix.
32
+
33
+ **Critical behavior:** the method is **non-fatal and never throws** — it always returns a
34
+ string. worker2 has **no DLQ and a 3600s SQS visibility timeout**, so any uncaught 500
35
+ becomes a poison-message storm; an outer `catch(\Throwable)` is the backstop and the push
36
+ itself runs non-fatal.
37
+
38
+ ## Key files / entry points
39
+
40
+ - `worker2/Worker/Infrastructure/CloudWatch.php` — `_Worker_Infrastructure_CloudWatch`,
41
+ action `ElasticBeanstalkHealth`, dispatched via
42
+ `_Worker::runTask('Infrastructure/CloudWatch/ElasticBeanstalkHealth', {awsAccountId,
43
+ environments, oneuptimeUrl})`.
44
+ - AWS access is obtained through `_Component_Aws_Workloads::assumeCredentials()` (STS-assume
45
+ a read-only role per account, one assume reused across regions) — see
46
+ [Cross-account AWS access](./cross-account-aws-access.md), which also documents the
47
+ `environments` region→env-names map parameter shape.
48
+ - `oneuptimeUrl` arrives as a **cron parameter and is a push credential** — never log it and
49
+ never record its value in a doc (see gotchas).
50
+
51
+ ## Alarm / paging criteria (the decision logic)
52
+
53
+ Per environment, `alarm=HIGH` (page) when the EB `HealthStatus` is `Degraded` **or**
54
+ `Severe`. `Suspended` never pages.
55
+
56
+ - **`Severe` always pages.**
57
+ - **`Degraded` for a non-5xx reason** (latency, instances down, …) **always pages.**
58
+ - **`Degraded` attributed to HTTP 5xx errors pages only when 5xx errors dominate** — i.e.
59
+ the 5xx share of requests is at/above `HTTP_5XX_ALARM_RATIO` (default 0.80). An occasional
60
+ 500 that trips Degraded is noise and must **not** page; a flood is a real problem and
61
+ **must** page.
62
+ - **Tiny-sample guard:** below `HTTP_5XX_MIN_REQUEST_COUNT` (default 20) requests in the
63
+ window, do **not** trust the ratio — page rather than let a 2-of-3 blip read as "80%
64
+ failing."
65
+
66
+ ### Fail-safe: a blind read pages, never reads healthy
67
+
68
+ If the health read (`describeEnvironmentHealth`) throws `AwsException` for an environment,
69
+ the monitor sets **`alarm=HIGH`** for that environment (a blind read must never masquerade
70
+ as healthy) **and** sets **`probe=DEGRADED`** (coverage gap, distinct from a measured-bad
71
+ signal — same alarm-vs-probe split as the multi-client monitors in
72
+ [OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md)).
73
+
74
+ ### Configurable constants (top of the class)
75
+
76
+ | Constant | Default | Meaning |
77
+ |---|---|---|
78
+ | `HEALTH_ALARM_STATUSES` | `['Degraded','Severe']` | HealthStatus values that page |
79
+ | `HTTP_5XX_ALARM_RATIO` | `0.80` | 5xx share (0.0–1.0) at/above which a 5xx-driven `Degraded` pages |
80
+ | `HTTP_5XX_MIN_REQUEST_COUNT` | `20` | Below this many requests in the window, page rather than trust the ratio |
81
+ | `HTTP_5XX_CAUSE_MARKERS` | `['5xx','http 5']` | Case-insensitive substrings identifying a 5xx-attributed EB `Cause` |
82
+
83
+ ### Implementation shape
84
+
85
+ - `shouldPageForHealth(healthStatus, causes, applicationMetrics): [bool, ?float]` — returns
86
+ the page decision and the observed 5xx ratio.
87
+ - `causesAttributeTo5xx(causes): bool` — substring-matches `HTTP_5XX_CAUSE_MARKERS` against
88
+ the EB `Causes` strings (case-insensitive).
89
+ - The ratio is computed as `StatusCodes.Status5xx / RequestCount` from `ApplicationMetrics`
90
+ (raw counts — see below), **not** by parsing the percentage out of the `Causes` text.
91
+ - `describeEnvironmentHealth` is called with
92
+ `AttributeNames = ['HealthStatus','Status','Color','Causes','ApplicationMetrics']`.
93
+ - Per-environment report + OneUptime payload now include the per-env `alarm` token and the
94
+ observed `fivexxRatio`.
95
+
96
+ ## How EB enhanced-health is read (durable AWS reference)
97
+
98
+ Facts verified against current AWS docs; they inform why the logic above is shaped as it is.
99
+
100
+ - **HealthStatus severity ladder:** `Ok → Info → Warning → Degraded → Severe` (plus the grey
101
+ states `Pending`/`Unknown`/`Suspended`/`NoData`). `Degraded` is the "high failure" tier
102
+ and is **routinely tripped by benign transients** (Auto Scaling scale-up, a mid-deploy
103
+ dip), which is exactly why `Degraded` alone is noisy. `Severe` = "very high failure /
104
+ environment effectively not serving."
105
+ - **`ApplicationMetrics` returns RAW COUNTS, not percentages** — despite the API-reference
106
+ prose saying "percentage"/"per second". Fields: `Duration` (Integer seconds, usually 10),
107
+ `RequestCount` (Integer, **total** requests over the window),
108
+ `StatusCodes.{Status2xx,Status3xx,Status4xx,Status5xx}` (Integer counts). AWS's own
109
+ example: 2xx 3391 + 5xx 843 = RequestCount 4234. With no traffic, `RequestCount=0` and
110
+ `StatusCodes` may be **absent** → treat as "no data," not "zero failures." The 5xx-ratio
111
+ denominator is `RequestCount` (the authoritative total).
112
+ - **Why an env is Degraded comes from `Causes`:** EB `Causes` strings literally contain e.g.
113
+ `"19.9 % of the requests are failing with HTTP 5xx."` A substring check reliably
114
+ **classifies** 5xx-driven vs. not (fail-safe: a wording change → treated as a normal
115
+ `Degraded` → pages). The **ratio itself must come from `ApplicationMetrics`**, never by
116
+ parsing the number out of `Causes`.
117
+ - **A failed application DEPLOYMENT is NOT reliably reflected in HealthStatus** — it can read
118
+ `Warning`/`Degraded`, or fail before any red request-failure signal. The authoritative
119
+ structured deploy signal is per-instance `describeInstancesHealth` →
120
+ `Deployment.Status ∈ {'In Progress','Deployed','Failed'}`. (This detector was evaluated and
121
+ removed this iteration — see Design history.)
122
+ - **IAM:** `describeEnvironmentHealth` / `describeInstancesHealth` / `describeEvents` are all
123
+ covered by the managed `AWSElasticBeanstalkReadOnly` policy and require enhanced health
124
+ enabled. No per-call charge.
125
+
126
+ ## OneUptime monitor config (external, not code)
127
+
128
+ Paging depends on the OneUptime Incoming-Request monitor's **string-match** criteria on the
129
+ POSTed body — `Contains "alarm":"HIGH"` (and, if desired, `Contains "probe":"DEGRADED"`).
130
+ This is external OneUptime configuration, not worker2 code.
131
+
132
+ ## Design history — rejected direction (this session)
133
+
134
+ An earlier iteration set `HEALTH_ALARM_STATUSES = ['Severe']` (dropping `Degraded` entirely)
135
+ and **added a separate deployment-failure detector** (`describeInstancesHealth` →
136
+ `Deployment.Status = 'Failed'`) to still catch deploy failures. That was **reverted** after a
137
+ scope change: the team decided they **do** want to page on `Degraded` (to catch a flood of
138
+ 500s and non-500 degradations), with only the occasional-500 case suppressed via the 80%
139
+ ratio. The deployment-failure detector was **removed** — deploy failures that surface as
140
+ `Degraded` are now covered by the `Degraded` alarm. **Residual gap accepted for this
141
+ iteration:** a deploy that fails fast *without ever degrading health* is not caught.
142
+
143
+ ## Known gaps / follow-ups (not done)
144
+
145
+ - **Per-region client construction is not individually guarded.** Each
146
+ `new ElasticBeanstalkClient` is not in its own try/catch, so a bad region/creds aborts the
147
+ **whole run** via the outer backstop instead of paging just that region's envs as blind.
148
+ Candidate follow-up now that blind reads page.
149
+ - **No first-party test harness exists in worker2** (all tests are vendor/).
150
+ `shouldPageForHealth()` and `causesAttributeTo5xx()` are pure and ideal to unit-test
151
+ (ratio at exactly 0.80; count 19 vs 20; `Severe` over a 5xx cause; empty causes; null
152
+ metrics) — deferred pending a harness.
153
+
154
+ ## Gotchas / known issues
155
+
156
+ - **Keep the method non-fatal — never let it throw.** worker2 has no DLQ and a 3600s SQS
157
+ visibility timeout, so any uncaught 500 becomes a poison-message storm. The push runs
158
+ non-fatal and an outer `catch(\Throwable)` is the backstop.
159
+ - **A blind health read must page, not read healthy** — an `AwsException` on
160
+ `describeEnvironmentHealth` sets `alarm=HIGH` + `probe=DEGRADED` for that env.
161
+ - **`ApplicationMetrics` is raw counts, not percentages** — divide `Status5xx` by
162
+ `RequestCount`; `RequestCount=0`/absent `StatusCodes` means "no data," not "zero failures."
163
+ - **Classify 5xx-attribution from `Causes` text, but take the ratio from `ApplicationMetrics`
164
+ — never parse the percentage out of `Causes`.**
165
+ - **`oneuptimeUrl` is a push credential** — it arrives as a cron parameter; never log it or
166
+ record its value in a doc.
167
+
168
+ ## Change history
169
+ - 2026-08-17 — Created. Documented the `ElasticBeanstalkHealth` alarm/paging criteria (page
170
+ on `Degraded`/`Severe`; suppress a 5xx-driven `Degraded` unless the 5xx share ≥
171
+ `HTTP_5XX_ALARM_RATIO` 0.80 with an `HTTP_5XX_MIN_REQUEST_COUNT` 20 tiny-sample guard;
172
+ `Severe` and non-5xx `Degraded` always page), the blind-read fail-safe (`alarm=HIGH` +
173
+ `probe=DEGRADED`), the four configurable constants, and the `shouldPageForHealth` /
174
+ `causesAttributeTo5xx` helper shape. Captured durable EB enhanced-health facts (severity
175
+ ladder; `ApplicationMetrics` returns raw counts not percentages; `Causes` classifies but
176
+ `ApplicationMetrics` sets the ratio; failed deploys aren't reliably in HealthStatus —
177
+ `describeInstancesHealth.Deployment.Status` is authoritative; `AWSElasticBeanstalkReadOnly`
178
+ covers the reads). Recorded the reverted `['Severe']`-only + deployment-failure-detector
179
+ direction and the accepted residual gap (a deploy that fails without degrading health).
180
+ (jcardinal)
181
+ </content>
182
+ </invoke>
@@ -6,14 +6,20 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-07-30
10
- owners: ["jcardinal"]
9
+ updated: 2026-08-17
10
+ owners: ["jcardinal", "mhammontree"]
11
11
  files:
12
12
  - worker2/Worker/Monitor/Compass.php
13
13
  - worker2/Worker/Client/Compass.php
14
14
  - worker2/composer.json
15
15
  - _underscore/Cloud.php
16
16
  - worker2/Worker/Monitor/Operations.php
17
+ - worker2/Worker/Monitor/Aig.php
18
+ - worker2/Worker/Monitor/Prudential.php
19
+ - worker2/Worker/Monitor/Nycdoe.php
20
+ - worker2/Config/production.ini
21
+ - worker2/Config/beta.ini
22
+ - worker2/Config/sandbox-dev.ini
17
23
  related:
18
24
  - ./oneuptime-incident-clickup-sync.md
19
25
  - ./monitoring-framework.md
@@ -136,6 +142,69 @@ POSTed JSON body is addressed via the `requestBody` prefix — e.g.
136
142
  The 10-min-Degraded / 15-min-Offline heartbeat thresholds pair with a **5-minute** cron
137
143
  cadence. An initial 3/5-min setting false-alarmed on a single missed ping.
138
144
 
145
+ ### Two kinds of monitor — liveness probes and log-scrapers are complements, not duplicates
146
+
147
+ A monitor that **scrapes log rows** for arrived-and-failed calls (`Monitor/Aig/EntitlementApiFailures`,
148
+ the five 1.0 `App_SystemMonitor_ServiceNow*` classes) is **structurally blind to a fully dead
149
+ endpoint**: a refused connection logs nothing, produces zero failure rows, and reads as healthy.
150
+ Pair every failure-rate monitor on an integration endpoint with a **liveness probe** that actually
151
+ touches it. Rules for the probe:
152
+
153
+ - Use an **unauthenticated GET** so the probe can never create or mutate a record.
154
+ - **Any structured HTTP response below 500 is HEALTHY** — it proves DNS, TLS, the load balancer,
155
+ routing, and the auth layer are all alive. A `401` from an unauthenticated call to a protected
156
+ route is a *passing* result, not a failure. Only ≥500 / connect failure / timeout means dead.
157
+ (Verified 2026-08-17: `api-writer.togahub.com/v2/health` → 200; unauthenticated `/v2/entitlements`
158
+ → 401; both `alarm:"OK"`.)
159
+ - api2 short-circuits its health routes before any DB or Sentry work
160
+ (`api2/Controller/Index.php` `HEALTH_CHECK_ROUTES = ['/health','/v2/health']` → 200), so probing
161
+ them is cheap and does not depend on a client DB being reachable.
162
+ - **Token validity is out of scope for a worker2 probe** when the OAuth client lives in 1.0
163
+ (e.g. NYCDOE ServiceNow's `App_Api_NYCDOEV2` in `library`). Re-implementing it in worker2 would
164
+ duplicate credentials into a second repo and race the shared access token. Accept and record the
165
+ coverage gap rather than forking the client.
166
+
167
+ ### Tunable thresholds live in `Core.CronJobs.parameters`, everything else stays a local
168
+
169
+ The alarm threshold is the **one** setting that goes in the DB, as
170
+ `parameters = '{"alertThreshold":N}'`, so it can be retuned without a deploy. worker2 spreads
171
+ `parameters` as **named PHP arguments**, so the JSON key must match the method's parameter name
172
+ **exactly** or the call fails. Every other monitor setting (URLs, cutoffs, window sizes) stays an
173
+ `ALL_CAPS` in-method local.
174
+
175
+ **Echo `alertThreshold` in the pushed body next to the measured count.** A historical alarm is
176
+ otherwise unreadable once the threshold has been retuned.
177
+
178
+ ### Backlog monitors must exclude known-permanently-stranded rows
179
+
180
+ A backlog count that includes rows which will *never* drain pins the monitor HIGH from its first
181
+ run, and a monitor that is always red trains people to ignore it. Define a `BACKLOG_CUTOFF_DATE`
182
+ local at the date the monitor was written and count only rows on/after it. Report stranded/unmapped
183
+ row counts as **diagnostic fields that deliberately do not drive the alarm**.
184
+
185
+ ### Monitors are safe to deploy inert
186
+
187
+ Ship with the `Core.CronJobs` row `isActive = 0`. Each method must guard on an empty push URL, log
188
+ `no OneUptime push URL configured`, and still record its measurement in `WorkerJobs` — so the code
189
+ can deploy and be hand-run before the monitor exists in OneUptime.
190
+
191
+ ### Calibrate thresholds against the tier that runs the business logic
192
+
193
+ Several monitors live in worker2 but watch a pipeline whose crons run in the **1.0 worker** tier
194
+ against a 2.0 client DB (worker2 only READs it) — Compass and Prudential both work this way.
195
+ Thresholds must be derived from the **1.0 cron cadences and per-run throughput**, not from
196
+ worker2's own `*/5` schedule.
197
+
198
+ ### End-to-end test procedure (no deploy required)
199
+
200
+ Testing **recovery** is the step people skip and the most valuable one:
201
+
202
+ 1. `UPDATE Core.CronJobs SET parameters = '{"alertThreshold":-1}'` for the action.
203
+ 2. Hand-run the action → confirm `alarm:"HIGH"`, the monitor flips **Offline**, an incident opens,
204
+ and the notification arrives.
205
+ 3. Restore the real threshold and hand-run again → confirm the OK-gated criterion returns the
206
+ monitor to **Online** and resolves the incident.
207
+
139
208
  ## Provisioning a monitor (runbook)
140
209
 
141
210
  1. Write the monitor method on `_Worker_Monitor_Compass` (or a new `_Worker_Monitor_<X>`),
@@ -144,9 +213,14 @@ cadence. An initial 3/5-min setting false-alarmed on a single missed ping.
144
213
  **push credential**; never log it and never record its value in a doc.
145
214
  3. Provision the OneUptime monitor by importing the monitor's OneUptime resource-export
146
215
  JSON (authored under a local staging dir, e.g. `d:\STAGE` — these are import artifacts,
147
- not repo files) and wiring the Contains criteria above.
148
- 4. Add the `Core.CronJobs` row: `action = 'Monitor/Compass/<Method>'`, schedule
149
- `*/5 * * * *`, `maxExecutionTime` 120s, `parameters` NULL. Then deploy worker2.
216
+ not repo files) and wiring **only the body-`Contains` criteria** at this point.
217
+ The **push URL does not exist until the monitor is saved** OneUptime generates it, it cannot
218
+ be pre-derived, so the monitor must exist before the worker code can be finished.
219
+ 4. Add the `Core.CronJobs` row: `action = 'Monitor/<Class>/<Method>'`, schedule
220
+ `*/5 * * * *`, `maxExecutionTime` 120s, `parameters` NULL (or
221
+ `{"alertThreshold":N}`), `isActive = 0`. Then deploy worker2.
222
+ 5. Flip `isActive = 1` and **confirm a push has landed**. Only then add the
223
+ **absence criteria** (not-received 10/15 min) — see the activation-ordering trap below.
150
224
 
151
225
  ### CronJob scheduling for these monitors
152
226
 
@@ -200,8 +274,15 @@ check for another client.
200
274
  - OneUptime Incoming Request bodies are matched **as strings only** (Contains/NotContains) —
201
275
  no numeric comparison, no JSON key targeting. Always emit a decided token, never a raw
202
276
  number, for push monitors.
203
- - The OneUptime push URL is a credential keep it as an in-file constant in the method;
204
- never log it or write its value into knowledge docs.
277
+ - **ACTIVATION-ORDERING TRAP.** Creating a monitor with the **absence** criteria already wired
278
+ while its cron is still `isActive = 0` makes it go **Offline and open an incident within 15
279
+ minutes** with nothing pushing. Wire body-match criteria at creation; add absence criteria only
280
+ after `isActive = 1` and a confirmed push — or mute the monitor until then.
281
+ - The OneUptime push URL is a credential — a holder can spoof a healthy heartbeat and thereby
282
+ silence a real outage. Keep it as an in-file `$ONEUPTIME_URL` local in the method (the
283
+ established team pattern — 11 pre-existing instances across `Monitor/Compass.php`,
284
+ `Monitor/Aig.php`, `Monitor/Operations.php`); never log it and never write its value into a doc.
285
+ Moving these to `Config/production.ini` is a recognized future improvement, not yet done.
205
286
  - **`App_Client_Compass` (1.0) is unavailable in worker2.** Referencing it throws a
206
287
  class-not-found `Error`, which `catch (Exception)` does NOT catch — the whole monitor
207
288
  dies uncaught. Use `_Worker_Client_Compass::` constants only.
@@ -212,6 +293,19 @@ check for another client.
212
293
  monitors must pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
213
294
 
214
295
  ## Change history
296
+ - 2026-08-17 — TRUE-80587: added five monitors and generalized this doc beyond Compass —
297
+ `Monitor/Prudential/{SalesOrderGenerationBacklog,DellPurchaseOrderTransmissionBacklog,
298
+ OrderShippedUpdateBacklog}`, `Monitor/Aig/EntitlementApiHealth`,
299
+ `Monitor/Nycdoe/ServiceNowEndpointHealth` (cron rows in
300
+ `dbchanges2/Core/2026-08-07a - Prudential AIG ServiceNow OneUptime monitor crons.sql`).
301
+ New durable sections: **liveness probes vs. log-scrapers are complements** (a scraper cannot see
302
+ a dead endpoint; <500 including 401 is healthy), the **`alertThreshold` DB parameter** convention
303
+ (named-argument spread; echo it in the body), **excluding permanently-stranded rows** via a
304
+ `BACKLOG_CUTOFF_DATE`, **deploy-inert** (`isActive = 0` + empty-URL guard), calibrating against
305
+ the **1.0** tier's cadences, the no-deploy **end-to-end + recovery test procedure**, and the
306
+ **activation-ordering trap** (absence criteria before first push = false incident). See
307
+ [Prudential OneUptime order-pipeline monitors](../../../clients/prudential/features/oneuptime-order-pipeline-monitors.md).
308
+ (mhammontree)
215
309
  - 2026-08-10 — Noted that worker2 gained its **first OneUptime API *reader*** (this doc covers
216
310
  the push-only half; OneUptime was previously push-only, "dumb reporter, smart monitor"). The
217
311
  reader polls open incidents and files ClickUp tasks — see
@@ -5,7 +5,7 @@ project: _Underscore
5
5
  client: shared
6
6
  type: standard
7
7
  status: active
8
- updated: 2026-08-10
8
+ updated: 2026-08-17
9
9
  owners: [jcardinal, mhammontree, dfranks]
10
10
  files: []
11
11
  related:
@@ -269,6 +269,22 @@ WHERE
269
269
 
270
270
  * If necessary, add comments to clarify complex sections of SQL code. Comments should be concise and informative.
271
271
  * Prefer to use the `#` character to begin a comment rather than `--`.
272
+ * **Do not write a prose preamble on a migration file.** Comment each statement with a
273
+ short description of what it does and why. A multi-paragraph narrative over mechanical
274
+ `INSERT`s is not documentation — it rots, it repeats the ticket, and it buries the SQL.
275
+ Rollout/runbook steps belong on the ClickUp ticket; a reusable pattern belongs in the
276
+ knowledge base; the migration file gets one line per statement.
277
+ * **This is not a licence to strip PHP DocBlocks.** DocBlocks on classes and methods remain
278
+ required (see DocBlocks below) and are a different artifact. A docblock that records *why*
279
+ a query is written a certain way — e.g. "`Customers` is `LEFT` joined on purpose so the
280
+ monitor can see rows the transmission crons cannot" — is load-bearing: it prevents a future
281
+ "cleanup" to `INNER JOIN` that would silently blind the monitor. Keep those.
282
+ * Do not comment a migration with a dependency warning about an earlier file **in the same
283
+ folder**. The dbchanges2 executor applies each folder's files in **alphabetical order**, so an
284
+ earlier `YYYY-MM-DD` file in that folder is guaranteed already applied. A **cross-folder**
285
+ dependency (e.g. a `Client_<Name>/` file relying on a `Core/` change) is a different matter —
286
+ those are separate executor passes against separate clusters, so noting the ordering there is
287
+ legitimate and should be kept.
272
288
 
273
289
  ```sql
274
290
  # Bad
@@ -832,5 +848,11 @@ See: 2.0/apps/worker2/features/cross-account-aws-access.md
832
848
 
833
849
  ## Change history
834
850
 
851
+ - 2026-08-17 — SQL Comments: banned prose preambles on migration files (one short description per
852
+ statement; runbooks go on the ticket, patterns in the knowledge base), while explicitly protecting
853
+ PHP DocBlocks that record *why* a query is shaped a certain way. Also scoped the "no
854
+ earlier-file dependency warning" rule to **within one folder** — `Core/` and `Client_<Name>/` are
855
+ separate executor passes against separate clusters, so a cross-folder ordering note is legitimate
856
+ and must be kept. (mhammontree)
835
857
  - 2026-08-10 — Added "AWS access (worker2)" section: new worker2 AWS code must use `_Component_Aws_Workloads` (instance-role → per-account `WorkloadsRuntime` STS assume-role); legacy static key grandfathered for existing paths only. (jcardinal)
836
858
  - 2026-08-01 — Recorded the Logs-cluster singular table-naming exception to the otherwise-plural table convention. (jcardinal)
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
19
19
  ## 2.0 framework
20
20
 
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 58 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
- - **worker2** (Worker) — 49 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
22
+ - **worker2** (Worker) — 50 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 24 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
24
  - **dbchanges2** (Database Changes) _(framework core)_ — 8 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 6 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -3,5 +3,5 @@
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
5
  | [AIG Contract Reconciliation & Dealer Programs (STS_001 / SA_001)](features/contract-reconciliation.md) | 2.0 | How to reconcile an **AIG contract sales sheet** against the `Client_Aig` tenant, and the durable finding that the **Staples Advantage.com (`SA_001`) program do | _underscore/Model/Client/Entitlement.php, _underscore/Model/Aig/Entitlement.php, _underscore/Model/Client/Contact.php, _underscore/Model/Aig/Contact.php |
6
- | [AIG Entitlement Intake & SaleItem Code Resolution](features/entitlement-intake.md) | 2.0 | The `_Model_Aig_Entitlement::prePost` interceptor below is the code path for AIG protection-plan entitlements arriving as `api2` V2 JSON POSTs — and that path * | _underscore/Model/Aig/Entitlement.php, _underscore/Model/Aig/Unit.php, api2/Component/Api/V2/V2.php, api2/Config/sandbox-dev.ini, dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql, dbchanges2/Client_Aig/2026-08-17a - TRUE-80562 AIG API identifier overrides.sql |
6
+ | [AIG Entitlement Intake & SaleItem Code Resolution](features/entitlement-intake.md) | 2.0 | The `_Model_Aig_Entitlement::prePost` interceptor below is the code path for AIG protection-plan entitlements arriving as `api2` V2 JSON POSTs — and that path * | _underscore/Model/Aig/Entitlement.php, _underscore/Model/Aig/Unit.php, api2/Component/Api/V2/V2.php, api2/Config/sandbox-dev.ini, dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql, dbchanges2/Client_Aig/2026-08-17a - TRUE-80562 AIG API identifier overrides.sql, worker2/Worker/Monitor/Aig.php, api2/Controller/Index.php |
7
7
  | [AIG (Staples Protection Plan)](profile.md) | 2.0 | AIG is the warranty underwriter behind the **Staples Protection Plan** retail program. | |
@@ -14,7 +14,10 @@ files:
14
14
  - api2/Config/sandbox-dev.ini
15
15
  - dbchanges2/Client_Aig/2026-06-18a - TRUE-79534 AIG SaleItem codes.sql
16
16
  - dbchanges2/Client_Aig/2026-08-17a - TRUE-80562 AIG API identifier overrides.sql
17
+ - worker2/Worker/Monitor/Aig.php
18
+ - api2/Controller/Index.php
17
19
  related:
20
+ - 2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
18
21
  - clients/aig/features/contract-reconciliation.md
19
22
  - 1.0/apps/library/features/toga2-api-client-and-bridge.md
20
23
  - 2.0/apps/api2/features/request-logging.md
@@ -379,6 +382,23 @@ When AIG sends a new "Active SaleItemID" spreadsheet (columns `SaleItemID`, `Des
379
382
  local pass is not evidence.** See the gotcha and
380
383
  [local vs prod MySQL config parity](../../../2.0/apps/dbchanges2/workflows/local-vs-prod-mysql-config-parity.md).
381
384
 
385
+ ## Monitoring the intake endpoint — the failure-scraper cannot see a dead endpoint
386
+
387
+ `Monitor/Aig/EntitlementApiFailures` (worker2 → OneUptime) counts **arrived-and-failed** intake
388
+ calls from the log tables. It is structurally blind to a **fully dead** endpoint: a refused
389
+ connection logs nothing, yields zero failure rows, and reads as healthy. TRUE-80587 therefore added
390
+ `Monitor/Aig/EntitlementApiHealth`, a **liveness probe** alongside it — an **unauthenticated GET**
391
+ (so it can never create a record) where **any structured HTTP response below 500 is a PASS**.
392
+
393
+ Verified 2026-08-17: `api-writer.togahub.com/v2/health` → **200**; unauthenticated
394
+ `/v2/entitlements` → **401**. The 401 is a *healthy* answer — it proves DNS, TLS, the load balancer,
395
+ routing, and the auth layer are all alive. `api2/Controller/Index.php`
396
+ `HEALTH_CHECK_ROUTES = ['/health','/v2/health']` short-circuits to 200 before any DB or Sentry work,
397
+ so the probe does not depend on `Client_Aig` being reachable.
398
+
399
+ See the shared pattern:
400
+ [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md).
401
+
382
402
  ## Client variations
383
403
 
384
404
  This is AIG-specific behavior (the `_Model_Aig_Entitlement` override); other clients do not run
@@ -485,6 +505,10 @@ this interceptor or use this dual-purpose Items pattern.
485
505
  This is a general dbchanges2 temp-table-staging rule, not AIG-specific.
486
506
 
487
507
  ## Change history
508
+ - 2026-08-17 — TRUE-80587: added `Monitor/Aig/EntitlementApiHealth`, an unauthenticated-GET liveness
509
+ probe on the intake endpoint, because the existing `EntitlementApiFailures` log-scraper cannot see
510
+ a fully dead endpoint. Documented that <500 (including the unauthenticated **401**) is a PASS.
511
+ (mhammontree)
488
512
 
489
513
  - 2026-08-17 (later pass) — **Correction + final verification, supersedes two points in the entry
490
514
  below.** (1) The nested-`Units` "cross-tenant field list" open question is **WITHDRAWN** — the six
@@ -6,11 +6,12 @@ apps:
6
6
  - api2
7
7
  - dbchanges2
8
8
  - library
9
+ - worker2
9
10
  project: API
10
11
  client: aig
11
12
  type: profile
12
13
  status: active
13
- updated: 2026-07-28
14
+ updated: 2026-08-17
14
15
  owners: ["mhammontree"]
15
16
  files: []
16
17
  related:
@@ -36,6 +37,9 @@ go out under the Staples Protection Plan brand and link to `staplesprotection.to
36
37
  NetSuite item/SO/invoice traits).
37
38
  - **`dbchanges2`** — `Client_Aig/` schema + reference-data migrations (e.g. the SaleItem code
38
39
  catalog in `Client_Aig.Items`).
40
+ - **`worker2`** — OneUptime monitors: `Monitor/Aig/EntitlementApiFailures` (log-scraper) plus
41
+ `Monitor/Aig/EntitlementApiHealth` (liveness probe of the intake endpoint). See
42
+ features/entitlement-intake.md and the shared worker2 OneUptime monitoring doc.
39
43
  - **`library` (1.0)** — the `App_Api_Toga2` bridge syncs 2.0 AIG contacts/entitlements down into the
40
44
  legacy 1.0 `TOGA_AIG` DB (`Customers`/`ServiceRequests`), run by a worker cron every ~10 min. Note
41
45
  the two AIG databases: **`TOGA_AIG`** = V1 legacy sync target; **`Client_Aig`** = V2 prod tenant. See
@@ -3,5 +3,5 @@
3
3
  | Doc | Framework | Summary | Files |
4
4
  |-----|-----------|---------|-------|
5
5
  | [NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)](features/hold-status-sync.md) | 1.0 | DOE ticket **hold** status must round-trip between ServiceNow (SNOW) and TOGaDesk and **stay held** — holds are SLA-bearing in both systems. | worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/send_request_item_updates.php, library/app/model/togadesk/repairorder.php, library/app/api/nycdoev2.php, togadesk/desk/includes/classes/class.repair.php |
6
- | [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/test_multi_po_receipt_resolution.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/nycdoe.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php, library/app/netsuite.php, dbchanges/Core/SK/, dbchanges/TOGaDeskSupport/SK/ |
6
+ | [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents + RITMs) into local tables, turns vendor shipment notices into NetSuite Sales Orde | worker/crons/sync/nycdoe/import_asn.php, worker/crons/sync/nycdoe/import_inc.php, worker/crons/sync/nycdoe/legacy_import_asn.php, worker/crons/sync/nycdoe/legacy_process_asn_queue.php, worker/crons/sync/nycdoe/process_tickets.php, worker/crons/sync/nycdoe/1_send_asn_to_netsuite.php, worker/crons/sync/nycdoe/2_send_serials_to_netsuite.php, worker/crons/sync/nycdoe/3_create_installation_ticket.php, worker/crons/sync/nycdoe/test_multi_po_receipt_resolution.php, worker/crons/sync/nycdoe/send_ticket_updates.php, worker/crons/sync/nycdoe/send_request_item_updates.php, worker/crons/sync/nycdoe/send_nycdoe_proof_of_delivery.php, worker/crons/sync/nycdoe/sync_nycdoe_locations.php, worker/crons/sync/nycdoe/receive_edi_purchase_orders.php, worker/crons/sync/nycdoe/send_edi_open_invoices.php, worker/crons/notifications/nycdoe/, worker/schedules/cron.worker.sync.json, worker/schedules/cron.worker.notification.json, library/app/api/nycdoe.php, library/app/api/nycdoev2.php, library/app/nycdoe.php, library/app/asnprocessor/manufacturer.php, library/app/asnprocessor/apple.php, library/app/asnprocessor/lenovo.php, library/app/asnprocessor/lexmark.php, library/app/asnprocessor/acer.php, library/app/edi.php, library/app/netsuite.php, dbchanges/Core/SK/, dbchanges/TOGaDeskSupport/SK/, worker2/Worker/Monitor/Nycdoe.php |
7
7
  | [New York City Department of Education](profile.md) | 1.0 | NYC DOE (New York City Department of Education) is a TOGA client whose entire integration runs in the **1.0 worker tier** (~30 cron scripts under `worker/crons/ | |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: nycdoe
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-14
9
+ updated: 2026-08-17
10
10
  owners: [mhammontree, sking]
11
11
  files:
12
12
  - worker/crons/sync/nycdoe/import_asn.php
@@ -39,8 +39,10 @@ files:
39
39
  - library/app/netsuite.php
40
40
  - dbchanges/Core/SK/
41
41
  - dbchanges/TOGaDeskSupport/SK/
42
+ - worker2/Worker/Monitor/Nycdoe.php
42
43
  related:
43
44
  - ../profile.md
45
+ - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
44
46
  - hold-status-sync.md
45
47
  - ../../../1.0/apps/worker/architecture.md
46
48
  - ../../../1.0/apps/dbchanges/workflows/authoring-and-shipping-sql-files.md
@@ -551,6 +553,28 @@ use the toga DB MCP + `Logs.API` instead of running prod code locally.
551
553
  `__get` throws "No field or quick query defined for key …". Read that column via **raw SQL**,
552
554
  not the model.
553
555
 
556
+ ## Monitoring the ServiceNow endpoint (worker2 liveness probe — TRUE-80587)
557
+
558
+ The five 1.0 `App_SystemMonitor_ServiceNow*` monitors scrape logs for **arrived-and-failed** calls,
559
+ so they are **structurally blind to a fully dead endpoint**: a refused connection logs nothing,
560
+ produces zero failure rows, and reads as healthy. `Monitor/Nycdoe/ServiceNowEndpointHealth`
561
+ (`worker2/Worker/Monitor/Nycdoe.php`, OneUptime push) is the complement — a **liveness probe** that
562
+ actually touches the endpoint.
563
+
564
+ - **Unauthenticated GET**, so it can never create a record.
565
+ - **Any structured HTTP response below 500 is a PASS** — it proves DNS, TLS, the load balancer,
566
+ routing, and the auth layer are alive. A `401` is a *healthy* answer, not a failure.
567
+ - **Accepted coverage gap: token validity is NOT monitored.** The OAuth client
568
+ `App_Api_NYCDOEV2` lives in the 1.0 `library` repo, which worker2 cannot reach. Re-implementing
569
+ it in worker2 would duplicate DOE's credentials into a second repo and race the shared access
570
+ token. An expired/revoked token will surface via the 1.0 failure monitors, not this probe — do
571
+ not "fix" this by forking the client.
572
+
573
+ This probe is the client's **only 2.0 footprint**. DOE is **1.0 by default** — `worker2` appears in
574
+ the client's `apps:` scope solely so this monitor loads, and its presence is **not** the start of a
575
+ 2.0 migration (there is no `Client_Nycdoe` 2.0 tenant). All business logic stays in the 1.0 tier. See
576
+ [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md).
577
+
554
578
  ## Operating rules when changing this integration
555
579
 
556
580
  1. **Trace the full pipeline downstream before fixing** — the constraint that makes an edit
@@ -563,6 +587,10 @@ use the toga DB MCP + `Logs.API` instead of running prod code locally.
563
587
  of the consumer query; `php -l` every touched file.
564
588
 
565
589
  ## Change history
590
+ - 2026-08-17 — TRUE-80587: added `Monitor/Nycdoe/ServiceNowEndpointHealth` (worker2 → OneUptime), an
591
+ unauthenticated-GET liveness probe complementing the log-scraping 1.0 `App_SystemMonitor_ServiceNow*`
592
+ monitors, which cannot see a dead endpoint. Token validity is a deliberately accepted coverage gap
593
+ (`App_Api_NYCDOEV2` is 1.0-only). First 2.0 code for this client. (mhammontree)
566
594
  - 2026-08-14 — Tied the DOE **missing-asset-tag** class to the existing "Lenovo Off-Layout ASN File"
567
595
  bug: the +1 shift puts the serial in `assetTag` and means the real asset-tag column (`$cols[24]`)
568
596
  is **never read**, so no unit from an off-layout file can get a tag (still **open** — needs the
@@ -6,11 +6,12 @@ apps:
6
6
  - library
7
7
  - togadesk
8
8
  - dbchanges
9
+ - worker2
9
10
  project: Worker
10
11
  client: nycdoe
11
12
  type: profile
12
13
  status: active
13
- updated: 2026-07-29
14
+ updated: 2026-08-17
14
15
  owners: [mhammontree, sking]
15
16
  files: []
16
17
  related:
@@ -27,8 +28,13 @@ proof-of-delivery back to ServiceNow. A parallel EDI relationship (850 POs in, i
27
28
  runs over DOE's SFTP.
28
29
 
29
30
  ## Platforms & data
30
- - **1.0 worker tier only** — no 2.0 footprint. Local mirror tables live in `db_common`
31
+ - **1.0 worker tier** — all business logic. Local mirror tables live in `db_common`
31
32
  (`NYCDOETickets`, `NYCDOELocations`, `AdvanceShippingNotice*`).
33
+ - **`worker2` (2.0) — monitoring only.** DOE's *only* 2.0 code is the OneUptime liveness probe
34
+ `Monitor/Nycdoe/ServiceNowEndpointHealth` (`worker2/Worker/Monitor/Nycdoe.php`, TRUE-80587).
35
+ This client remains **1.0 by default** — `worker2` is in `apps:` solely so that monitor loads.
36
+ It is **not** the start of a 2.0 migration and there is no `Client_Nycdoe` 2.0 tenant; do not
37
+ read it as a platform move.
32
38
  - **TOGa Desk:** DOE is client id **16** (`db_togadesk`) — repair orders, managed service
33
39
  orders, and the "#received / #ordered" Receiving Summary UI.
34
40
  - **NetSuite:** Sales Orders, PO reconciliation, item receipts, Item Fulfillments, invoices
@@ -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 | _underscore/Model/Prudential/AdvanceShippingNotice.php, dbchanges2/Client_Prudential/2026-06-10 - AsnUnitsInterceptor.sql |
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. | library/app/api/delllch.php, library/app/apitransaction.php, worker/crons/toga2/prudential/transmissions_to_dell_usa.php, worker/crons/toga2/prudential/transmissions_to_dell_ireland.php, worker/crons/toga2/prudential/transmissions_to_dell_india.php, worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php |
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 | worker/crons/toga2/prudential/import_device_information.php, worker/crons/toga2/prudential/backfill_unit_contacts.php, dbchanges2/Client_Prudential/2026-07-07 - Contact Dedup Merge.sql |
8
+ | [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 | worker2/Worker/Monitor/Prudential.php, worker2/Config/production.ini, dbchanges2/Core/2026-08-07a - Prudential AIG ServiceNow OneUptime monitor crons.sql, dbchanges2/Client_Prudential/2026-04-08 - Insert Customers.sql |
8
9
  | [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 | _underscore/Model/Prudential/ServiceRequest.php, _underscore/Test/Prudential/ServiceRequestTest.php |
9
10
  | [Prudential: Service Request rejection alert email](features/service-request-rejection-alert-email.md) | 2.0 | When a Prudential ServiceNow→TOGa service-request submission (`POST /v2/service-requests`) is **rejected by validation**, TOGa now sends a real-time internal al | worker2/Worker/Client/Prudential/reports/ReqRejectionEmail.php, _underscore/Model/Prudential/ServiceRequest.php |
10
11
  | [Prudential Order Shipped Email — transmit_ordershipped_updates_prudential.php](features/transmit-ordershipped-email.md) | 1.0 | Cron script that transmits "Order Shipped" updates to ServiceNow (RITM) and sends a shipped notification email to the end user. | worker/crons/toga2/prudential/transmit_ordershipped_updates_prudential.php, worker/crons/toga2/prudential/transmit_closecomplete_updates_prudential.php, worker/crons/toga2/prudential/transmit_rejected_cancelled_updates_prudential.php, worker/crons/toga2/prudential/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/toga2/prudential_beta/generate_sales_and_purchase_orders_from_service_requests.php, worker/crons/notifications/reports/prudential_exception_report.php |
@@ -0,0 +1,108 @@
1
+ ---
2
+ title: "Prudential: OneUptime order-pipeline backlog monitors (Monitor/Prudential/*)"
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: prudential
7
+ type: client-feature
8
+ status: active
9
+ updated: 2026-08-17
10
+ owners: ["mhammontree"]
11
+ files:
12
+ - worker2/Worker/Monitor/Prudential.php
13
+ - worker2/Config/production.ini
14
+ - dbchanges2/Core/2026-08-07a - Prudential AIG ServiceNow OneUptime monitor crons.sql
15
+ - dbchanges2/Client_Prudential/2026-04-08 - Insert Customers.sql
16
+ related:
17
+ - ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
18
+ - ../profile.md
19
+ - ./transmit-ordershipped-email.md
20
+ - ./dell-lch-iop-transmissions.md
21
+ - ../../../2.0/apps/worker2/features/service-request-sales-order-generation.md
22
+ ---
23
+
24
+ ## Summary
25
+
26
+ Three worker2 OneUptime push monitors (TRUE-80587) that watch the Prudential/Dell order pipeline
27
+ for **stalls**, which were previously invisible — a stuck pipeline produced no error, no email,
28
+ and no alert. They implement the shared pattern in
29
+ [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md);
30
+ read that first for the payload contract, criteria set, and provisioning runbook.
31
+
32
+ - `Monitor/Prudential/SalesOrderGenerationBacklog` — Service Requests not yet turned into Sales Orders.
33
+ - `Monitor/Prudential/DellPurchaseOrderTransmissionBacklog` — POs not yet transmitted to Dell.
34
+ - `Monitor/Prudential/OrderShippedUpdateBacklog` — order-shipped updates not yet transmitted.
35
+
36
+ ## Key files / entry points
37
+
38
+ - `worker2/Worker/Monitor/Prudential.php` — `_Worker_Monitor_Prudential`; one `public static`
39
+ action method per monitor, each self-contained (in-method `ALL_CAPS` locals + `$push` closure).
40
+ - `dbchanges2/Core/2026-08-07a - Prudential AIG ServiceNow OneUptime monitor crons.sql` — the
41
+ `Core.CronJobs` rows (shipped `isActive = 0`, `parameters = '{"alertThreshold":N}'`).
42
+
43
+ ## How it works
44
+
45
+ ### The monitors read a 2.0 DB but watch 1.0 crons
46
+
47
+ Prudential's order-pipeline **business logic runs in the 1.0 worker tier** against the 2.0
48
+ `Client_Prudential` database. The monitors live in worker2 and only **READ** that database — the
49
+ same split as the Compass monitors. Thresholds are therefore calibrated against the **1.0 cron
50
+ cadences**, not worker2's own `*/5` schedule:
51
+
52
+ | 1.0 cron | Cadence | Throughput |
53
+ |---|---|---|
54
+ | `generate_sales_and_purchase_orders` | every 3 min | 1 request per run (≈20/hour) |
55
+ | `transmissions_to_dell_{usa,ireland,india}` | every 5 min | one region each |
56
+ | `transmit_ordershipped_updates_prudential` | every 2 min | — |
57
+
58
+ ### `BACKLOG_CUTOFF_DATE` — excluding permanently-stranded rows
59
+
60
+ `BACKLOG_CUTOFF_DATE = 2026-08-07`. Rows before it are excluded because they will **never** drain:
61
+
62
+ - ~38 Service Requests orphaned by the **2026-07-17 Dell regional-split cutover**.
63
+ - Two-phase order-shipped stranding — `transmit_ordershipped_updates_prudential` stamps the
64
+ transmitted column in Phase 1, so a Phase-2 crash strands that PO's email forever (see the
65
+ [profile gotchas](../profile.md) and [transmit-ordershipped-email](./transmit-ordershipped-email.md)).
66
+
67
+ Counting them would pin the monitor HIGH on its very first run, which trains people to ignore it.
68
+
69
+ ### Diagnostic fields that deliberately do NOT drive the alarm
70
+
71
+ `DellPurchaseOrderTransmissionBacklog` also reports:
72
+
73
+ - `strandedCount` — `ServiceRequests.customerId IS NULL`.
74
+ - `unmappedCustomerCount` — customer is not one of the three regional customer uuids.
75
+
76
+ Both are informational only. The three regional transmission crons **INNER JOIN `Customers` and
77
+ filter to a single region uuid each**, so these rows are invisible to *every* transmitter — they
78
+ are a data-integrity problem, not a transmission backlog, and alarming on them would misroute the
79
+ response.
80
+
81
+ **The monitor `LEFT JOIN`s `Customers` on purpose** so it can see rows the crons cannot. Changing
82
+ it to an `INNER JOIN` would silently blind the monitor — the DocBlock on the method says so, and
83
+ that docblock is load-bearing; do not "tidy" it away.
84
+
85
+ ## Reference data (verified 2026-08-17)
86
+
87
+ - The USA / Ireland / India regional customer uuids are seeded in
88
+ `dbchanges2/Client_Prudential/2026-04-08 - Insert Customers.sql` — **not** `2024-01-01.sql`, which
89
+ an in-code comment incorrectly claims.
90
+ - `Client_Prudential.Vendors.id = 1` is Dell.
91
+
92
+ ## Gotchas / known issues
93
+
94
+ - Thresholds are DB-tunable (`Core.CronJobs.parameters` → `{"alertThreshold":N}`) — retune there,
95
+ never by editing PHP. The JSON key must match the method's parameter name exactly.
96
+ - The crons ship `isActive = 0`; the monitors do nothing until activated, and each guards on an
97
+ empty push URL. Do not add OneUptime absence criteria until a push has landed.
98
+
99
+ ## Change history
100
+ - 2026-08-17 — TRUE-80587: built the three backlog monitors; set `BACKLOG_CUTOFF_DATE` to exclude
101
+ the ~38 Dell regional-split orphans and the two-phase order-shipped strandings; made
102
+ `strandedCount` / `unmappedCustomerCount` non-alarming diagnostics behind a deliberate
103
+ `LEFT JOIN Customers`; calibrated thresholds against the 1.0 cron cadences; corrected the
104
+ regional-customer-uuid seed-file reference to `2026-04-08 - Insert Customers.sql`. (mhammontree)
105
+
106
+ ## Related docs
107
+ - [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md) — the shared pattern, payload contract, and runbook.
108
+ - [Prudential Financial profile](../profile.md)
@@ -15,9 +15,10 @@ client: prudential
15
15
  type: profile
16
16
  status: active
17
17
  updated: 2026-08-17
18
- owners: ["jcardinal", "rgirish", "bala"]
18
+ owners: ["jcardinal", "rgirish", "bala", "mhammontree"]
19
19
  files: []
20
20
  related:
21
+ - features/oneuptime-order-pipeline-monitors.md
21
22
  - features/dell-asn-units-interceptor.md
22
23
  - features/service-request-address-validation.md
23
24
  - features/service-request-rejection-alert-email.md
@@ -81,7 +82,14 @@ order-status transmissions.
81
82
  (E_ALL→ErrorException), so `!== false` guards do not work — use the `safeFetchPdf()` pattern. See
82
83
  the exception report and the transmit-ordershipped-email feature doc.
83
84
 
85
+ ## Monitoring
86
+ - `Monitor/Prudential/{SalesOrderGenerationBacklog,DellPurchaseOrderTransmissionBacklog,OrderShippedUpdateBacklog}`
87
+ (worker2 → OneUptime) watch the order pipeline for stalls. Thresholds are DB-tunable and the
88
+ monitors deliberately ignore pre-2026-08-07 permanently-stranded rows. See
89
+ features/oneuptime-order-pipeline-monitors.md.
90
+
84
91
  ## Related docs
92
+ - OneUptime order-pipeline backlog monitors (worker2).
85
93
  - Dell ASN units interceptor.
86
94
  - Dell LCH IOP transmissions (LCHRequestV2, outbound 1.0).
87
95
  - 2.0 _underscore: Tracking-Number Bridge Migration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.596",
3
+ "version": "1.0.598",
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",