toga-ai 1.0.625 → 1.0.627
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/2.0/apps/worker2/INDEX.md +1 -1
- package/knowledge/2.0/apps/worker2/features/oneuptime-worker2-monitoring.md +43 -2
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nycdoe/features/hold-status-sync.md +123 -49
- package/knowledge/clients/staples/INDEX.md +1 -0
- package/knowledge/clients/staples/features/oneuptime-sftp-folder-monitors.md +113 -0
- package/knowledge/clients/staples/profile.md +5 -2
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
| [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 |
|
|
36
36
|
| [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 |
|
|
37
37
|
| [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 |
|
|
38
|
-
| [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 |
|
|
38
|
+
| [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/Worker/Monitor/Staples.php, worker2/Config/production.ini, worker2/Config/beta.ini, worker2/Config/sandbox-dev.ini |
|
|
39
39
|
| [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 |
|
|
40
40
|
| [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 |
|
|
41
41
|
| [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 |
|
|
@@ -6,8 +6,8 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["jcardinal", "mhammontree"]
|
|
9
|
+
updated: 2026-08-20
|
|
10
|
+
owners: ["jcardinal", "mhammontree", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Monitor/Compass.php
|
|
13
13
|
- worker2/Worker/Client/Compass.php
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
- worker2/Worker/Monitor/Aig.php
|
|
18
18
|
- worker2/Worker/Monitor/Prudential.php
|
|
19
19
|
- worker2/Worker/Monitor/Nycdoe.php
|
|
20
|
+
- worker2/Worker/Monitor/Staples.php
|
|
20
21
|
- worker2/Config/production.ini
|
|
21
22
|
- worker2/Config/beta.ini
|
|
22
23
|
- worker2/Config/sandbox-dev.ini
|
|
@@ -26,6 +27,7 @@ related:
|
|
|
26
27
|
- ./all-client-email-queue-monitor.md
|
|
27
28
|
- ../../_underscore/features/cloud-s3-helpers.md
|
|
28
29
|
- ../../../1.0/apps/worker/features/oneuptime-worker-uptime-monitoring.md
|
|
30
|
+
- ../../../clients/staples/features/oneuptime-sftp-folder-monitors.md
|
|
29
31
|
---
|
|
30
32
|
|
|
31
33
|
## Summary
|
|
@@ -255,6 +257,38 @@ relies on it) — no new Entra consent.
|
|
|
255
257
|
worker EB tier. The `client_secret` is a credential — it lives in the ini config group,
|
|
256
258
|
never in a doc.
|
|
257
259
|
|
|
260
|
+
## SFTP monitoring (phpseclib3)
|
|
261
|
+
|
|
262
|
+
An SFTP monitor logs into an SFTP with **phpseclib3**, `rawlist`s a folder, counts files
|
|
263
|
+
older than an age cutoff, and pushes a decided alarm token — the same reporter model as the
|
|
264
|
+
S3/DB monitors. A login/list failure pushes `status:"error"`, so the monitor doubles as an
|
|
265
|
+
SFTP connectivity check. First instance:
|
|
266
|
+
[Staples SFTP folder monitors](../../../clients/staples/features/oneuptime-sftp-folder-monitors.md).
|
|
267
|
+
|
|
268
|
+
Two runtime facts about the installed phpseclib3 (v3.0.49) that bite silently — verified at
|
|
269
|
+
runtime against the live SFTP:
|
|
270
|
+
|
|
271
|
+
- **`SFTP::rawlist($dir)` (non-recursive) returns each entry as an associative ARRAY**, not
|
|
272
|
+
a stdClass object — keys `'type'`, `'mtime'`, `'filename'`, `'size'`, … Object access
|
|
273
|
+
(`$entry->mtime`) silently fails. Use array access: `$entry['mtime']`, `$entry['type']`.
|
|
274
|
+
- **The SFTP file-type constants are NOT defined at class-load.** Neither the class const
|
|
275
|
+
`SFTP::TYPE_DIRECTORY` nor the global `NET_SFTP_TYPE_DIRECTORY` exists when you just
|
|
276
|
+
load/use the class (phpseclib defines the globals lazily); `defined()` returned false for
|
|
277
|
+
both. Compare `type` against the SFTP-protocol numeric value via a **self-defined
|
|
278
|
+
constant**: `1` = regular file, `2` = directory. (`rawlist` returned php-type=array,
|
|
279
|
+
`type=1` for files, and `mtime` as a unix epoch matching the filename timestamp.)
|
|
280
|
+
|
|
281
|
+
**Symptom when either is wrong:** reads guarded by `isset()`/`??` silently make the aged-file
|
|
282
|
+
count always **0**, so the monitor never alarms and reads as perfectly healthy. Test the
|
|
283
|
+
count against a folder you know has aged files.
|
|
284
|
+
|
|
285
|
+
### `_Config::<group>('key')` throws on a missing key — pass a falsy fallback
|
|
286
|
+
|
|
287
|
+
`_Config::<group>('key')` **throws** when the key is absent, so a `?:` default after it is
|
|
288
|
+
dead code. To get a safe fallback, pass a **falsy second argument**:
|
|
289
|
+
`_Config::sftp_staples('port', false) ?: 22`. Without the second arg the `?:` never runs
|
|
290
|
+
because the throw happens first.
|
|
291
|
+
|
|
258
292
|
## Composer / deploy dependency
|
|
259
293
|
|
|
260
294
|
`worker2/composer.json` added `javanile/php-imap2` (`^0.1.10`) for the IMAP path. The
|
|
@@ -293,6 +327,13 @@ check for another client.
|
|
|
293
327
|
monitors must pass the region (see [Cloud S3 helpers](../../_underscore/features/cloud-s3-helpers.md)).
|
|
294
328
|
|
|
295
329
|
## Change history
|
|
330
|
+
- 2026-08-20 — Added the **SFTP monitor** shape (phpseclib3 `rawlist` + age-cutoff count +
|
|
331
|
+
decided token; login/list failure → `status:error` connectivity check) and its two
|
|
332
|
+
runtime gotchas: `rawlist` returns **arrays** not objects, and the SFTP type constants are
|
|
333
|
+
**not defined at class-load** (use a self-defined `1`=file/`2`=dir). Also recorded that
|
|
334
|
+
`_Config::<group>('key')` **throws** on a missing key — pass a falsy 2nd arg for a `?:`
|
|
335
|
+
fallback. First instance:
|
|
336
|
+
[Staples SFTP folder monitors](../../../clients/staples/features/oneuptime-sftp-folder-monitors.md). (bala)
|
|
296
337
|
- 2026-08-17 — TRUE-80587: added five monitors and generalized this doc beyond Compass —
|
|
297
338
|
`Monitor/Prudential/{SalesOrderGenerationBacklog,DellPurchaseOrderTransmissionBacklog,
|
|
298
339
|
OrderShippedUpdateBacklog}`, `Monitor/Aig/EntitlementApiHealth`,
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ — 61 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
22
|
-
- **worker2** (Worker) —
|
|
22
|
+
- **worker2** (Worker) — 54 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
23
23
|
- **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
24
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) — 7 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-08-20
|
|
10
10
|
owners: [mhammontree, sking]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/sync/nycdoe/send_ticket_updates.php
|
|
@@ -76,48 +76,87 @@ revert or never reach ServiceNow. It is the status-sync companion to the broader
|
|
|
76
76
|
- `Hold` has **statusRank 6**, beating `Work in Progress` (rank 5) in the downgrade-guard
|
|
77
77
|
added by TRUE-76812 — so a downstream sync cannot silently demote a held order.
|
|
78
78
|
|
|
79
|
-
### Status fields (ServiceNow side) — CORRECTED 2026-
|
|
80
|
-
> **
|
|
81
|
-
>
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- The native **`state`** field is
|
|
91
|
-
|
|
92
|
-
only permits the native On Hold transition
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
79
|
+
### Status fields (ServiceNow side) — CORRECTED 2026-08-20 (VENDOR-STATED, TRUE-81044)
|
|
80
|
+
> **This is the third answer to "which field is authoritative" and the first one that is not an
|
|
81
|
+
> inference.** TRUE-79922 concluded native `state` only; the 2026-07-20 pass reversed that to
|
|
82
|
+
> `u_status_task`; TRUE-81044 re-reverses it. Both earlier conclusions were **inferred from
|
|
83
|
+
> observed rejections**. This one is **stated directly by ServiceNow**, so prefer it.
|
|
84
|
+
|
|
85
|
+
**ServiceNow's stated rule:** to put a DOE Incident On Hold, the mandatory attribute set is
|
|
86
|
+
**`state` + `hold_reason` + `u_eta`, submitted TOGETHER in one payload.** A ServiceNow-side
|
|
87
|
+
**business rule derives the client-facing `u_status_task` FROM `state`** — so we must *not* write
|
|
88
|
+
`u_status_task` ourselves. Doing so was fighting their derivation rule.
|
|
89
|
+
|
|
90
|
+
- The native **`state`** field is what we drive. It is the SLA-bearing state machine *and*, via
|
|
91
|
+
their business rule, the source of the client-facing status. Push it with its two companions.
|
|
92
|
+
- ⚠ **SUPERSEDED:** the claim that ServiceNow "only permits the native On Hold transition FROM
|
|
93
|
+
In Progress (`state=2`)" was **inferred from empty-body 200 rejections and is now doubtful** —
|
|
94
|
+
an *incomplete mandatory attribute set* produces the identical silent rejection, and our
|
|
95
|
+
payload was in fact incomplete (see the empty-body gotcha). **The In-Progress gate has been
|
|
96
|
+
removed** (TRUE-81044). ServiceNow explicitly invited us to report back if holds still fail
|
|
97
|
+
from `Assigned`.
|
|
98
|
+
- **`hold_reason`** is mandatory. ⚠ **SUPERSEDED:** the claim that `hold_reason=10` "is not a valid
|
|
99
|
+
choice on this instance and is stored empty/dropped" was **WRONG** — code **10 ("Other") IS
|
|
100
|
+
valid**, but it additionally **requires the companion free-text field
|
|
101
|
+
`u_other_reason_on_hold`**. We were sending `10` with no companion — an incomplete mandatory set
|
|
102
|
+
— which is why it read as dropped.
|
|
103
|
+
- **`u_other_reason_on_hold`** (string) — required whenever `hold_reason=10`. Must never be empty.
|
|
104
|
+
- **`u_eta`** is mandatory on every hold push, **unconditionally** — not "only when it differs from
|
|
105
|
+
the remote value" (the old conditional silently dropped a mandatory attribute on any re-push with
|
|
106
|
+
an unchanged or null ETA).
|
|
107
|
+
- **`u_status_task` (custom) is the CLIENT-FACING status, but is now READ-ONLY to us** — DOE's own
|
|
108
|
+
ServiceNow views/reporting read it (`Open`, `In Progress`, `On Hold`, …) and their business rule
|
|
109
|
+
populates it from `state`. ⚠ **SUPERSEDED:** the 2026-07-20 instruction that "a hold MUST be
|
|
110
|
+
written here or the client never sees it" no longer holds — we removed both the hold and the
|
|
111
|
+
release writes. It remains the field **three code paths still READ** for hold detection (see the
|
|
112
|
+
gotcha on that outstanding dependency).
|
|
96
113
|
- Numeric `state` codes seen on the wire: INC `On Hold`=**3**, `In Progress`=**2**;
|
|
97
|
-
RITM `On Hold`=**8**, the scheduled-update state=**"-14"**.
|
|
114
|
+
RITM `On Hold`=**8**, the scheduled-update state=**"-14"**. `hold_reason` `Other`=**10**.
|
|
98
115
|
|
|
99
|
-
### Outbound (TOGaDesk → ServiceNow) — INC hold push (
|
|
100
|
-
When a DOE repair order is on hold, `send_ticket_updates.php` pushes
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- **
|
|
116
|
+
### Outbound (TOGaDesk → ServiceNow) — INC hold push (TRUE-81044 current shape)
|
|
117
|
+
When a DOE repair order is on hold, `send_ticket_updates.php` pushes **the full mandatory set
|
|
118
|
+
together, unconditionally**:
|
|
119
|
+
- **`state=3` + `hold_reason=10` + `u_eta` + `u_other_reason_on_hold` in ONE payload, every run.**
|
|
120
|
+
No gate on the remote state, and no "only if the ETA changed" condition — either would drop a
|
|
121
|
+
mandatory attribute and earn a silent empty-body 200.
|
|
122
|
+
- **`u_other_reason_on_hold`** is populated from the analyst's hold note already being read from
|
|
123
|
+
`repair_order_notes` for `work_notes`, falling back to
|
|
124
|
+
`App_Model_TogaDesk_RepairOrder::statusDisplay($repairOrder->status)` so it is **never empty**.
|
|
125
|
+
- **`hold_reason` stays pinned to `SN_HOLD_REASON_DEFAULT` (10, "Other").** See the *decision* on
|
|
126
|
+
not mapping hold reasons, below.
|
|
127
|
+
- **No `u_status_task` write** on either the hold push or the hold release — ServiceNow's own
|
|
128
|
+
business rule derives it from `state`. The `SN_TASK_STATUS_ON_HOLD` / `SN_TASK_STATUS_IN_PROGRESS`
|
|
129
|
+
constants and the `$snTaskStatus` read were removed with it.
|
|
130
|
+
- **Idempotency + release both key off `state`, not `u_status_task`:** the "already held" guard is
|
|
131
|
+
`$snState != 'On Hold'` and the release trigger is `$snState == 'On Hold'`. **Rule: gate a push on
|
|
132
|
+
the field you actually write** — the old `$snTaskStatus` keys gated the push on a field we no
|
|
133
|
+
longer drive.
|
|
134
|
+
- **Closed-incident reconcile (unchanged, still correct):** if the incident is already
|
|
135
|
+
`Resolved`/`Closed`/`Canceled` on the
|
|
111
136
|
SNOW side it can no longer be held — the cron **advances `dtSynced`** to accept the remote
|
|
112
137
|
closure instead of retrying forever. (A canceled incident, INC2190846, had generated **2,392**
|
|
113
138
|
failed empty-body PATCHes before this.)
|
|
114
139
|
- The "Assigned → In Progress" auto-start block is still guarded so it never pushes `state:2` over
|
|
115
140
|
a held order (TRUE-79922). Named constants: `SN_STATE_IN_PROGRESS` / `SN_STATE_ON_HOLD` /
|
|
116
|
-
`SN_HOLD_REASON_DEFAULT` in `send_ticket_updates.php
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
`SN_HOLD_REASON_DEFAULT` in `send_ticket_updates.php`.
|
|
142
|
+
- The existing **empty-body patch-response check** (~L493) still **throws**, so any remaining
|
|
143
|
+
rejection surfaces loudly instead of silently. Keep it.
|
|
144
|
+
|
|
145
|
+
> **DECISION (TRUE-81044) — do NOT map TogaDesk hold reasons onto ServiceNow hold-reason codes.**
|
|
146
|
+
> `hold_reason` stays pinned to `10` ("Other") and the human-readable reason is carried entirely in
|
|
147
|
+
> `u_other_reason_on_hold` / `work_notes`, sourced from TogaDesk. Rationale: ServiceNow's own states
|
|
148
|
+
> are not reliable, so mapping our six `HOLD_*` constants onto their code set buys nothing and adds
|
|
149
|
+
> a second source of truth. **Do not re-propose a reason-code mapping in a future session.**
|
|
150
|
+
|
|
151
|
+
### RITM cron — deliberately unchanged by TRUE-81044
|
|
152
|
+
`send_request_item_updates.php` already pushes native `state:"8"` (On Hold) and **never wrote**
|
|
153
|
+
`u_status_task`, so TRUE-81044's ask was already satisfied there. It carries a defensive `HOLD_*`
|
|
154
|
+
guard on the scheduled-update path so it can't push `state:"-14"` while held; its reverse-hold logic
|
|
155
|
+
still **reads** `u_status_task == 'On Hold'`.
|
|
156
|
+
> **OPEN QUESTION:** ServiceNow's communication was scoped to **Incident** state. We did **not**
|
|
157
|
+
> extend `hold_reason` / `u_eta` / `u_other_reason_on_hold` to the RITM payload without
|
|
158
|
+
> confirmation. Whether the RITM On Hold transition carries the same mandatory-set requirement is
|
|
159
|
+
> unconfirmed — ask before changing the RITM payload.
|
|
121
160
|
|
|
122
161
|
### Inbound (ServiceNow → TOGaDesk)
|
|
123
162
|
- `process_tickets.php` reads the client-facing **`u_status_task`** field
|
|
@@ -125,6 +164,8 @@ When a DOE repair order is on hold, `send_ticket_updates.php` pushes:
|
|
|
125
164
|
bare **`STATUS_HOLD`** on the local repair order (this is exactly why the library `qqStatus()`
|
|
126
165
|
fix below had to recognize bare `HOLD`). Both inbound "Assigned-force" `patchINC(state:2)` blocks
|
|
127
166
|
are guarded against a local hold.
|
|
167
|
+
- **Since TRUE-81044 this read is a DEPENDENCY on ServiceNow's own business rule**, not on anything
|
|
168
|
+
we write — the outbound no longer sets `u_status_task`. See the outstanding-dependency gotcha.
|
|
128
169
|
|
|
129
170
|
### RITM "ETA-in-hold"
|
|
130
171
|
- `send_request_item_updates.php` syncs `scheduled_date` (ETA) to ServiceNow **inside the
|
|
@@ -208,19 +249,32 @@ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets
|
|
|
208
249
|
auto-start fought the hold push — `state:3` then `state:2` pushed seconds apart, every run,
|
|
209
250
|
so SNOW never stayed On Hold. Any future auto-start/auto-status block on these crons MUST
|
|
210
251
|
short-circuit when the local RO is in a `HOLD_*` status.
|
|
211
|
-
- **⚠
|
|
212
|
-
|
|
213
|
-
`
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
-
|
|
218
|
-
(both hit `/api/nycd3/data/tasks/{number}`) return
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
252
|
+
- **⚠ The "which field is authoritative" question has flipped TWICE. Read this before changing it a
|
|
253
|
+
fourth time.** TRUE-79922 said native `state` only (inferred). 2026-07-20 reversed it to
|
|
254
|
+
`u_status_task` (inferred from test PATCHes). **TRUE-81044 re-reversed it on ServiceNow's own
|
|
255
|
+
written instruction:** we write `state` + `hold_reason` + `u_eta` (+ `u_other_reason_on_hold`) and
|
|
256
|
+
**never write `u_status_task`**, because their business rule derives it from `state`. Only the
|
|
257
|
+
current answer is vendor-stated; do not re-litigate it from observed behavior alone.
|
|
258
|
+
- **⚠ An empty-body 200 does NOT tell you WHY (nycd3 `data/tasks` PATCH).**
|
|
259
|
+
`App_Api_NYCDOEV2::patchINC`/`patchRITM` (both hit `/api/nycd3/data/tasks/{number}`) return
|
|
260
|
+
**HTTP 200 with an EMPTY body (no `result` object)** on rejection — no exception, no error field.
|
|
261
|
+
**Critically, this response is IDENTICAL for an illegal state transition and for an INCOMPLETE
|
|
262
|
+
mandatory attribute set.** The previously documented rule "On Hold is only permitted FROM In
|
|
263
|
+
Progress" was *inferred* from this response and is now doubtful: our payload was genuinely
|
|
264
|
+
incomplete in two ways (`u_eta` sent only when it differed from the remote value, so any re-push
|
|
265
|
+
with an unchanged or null ETA dropped a mandatory attribute; and `u_other_reason_on_hold` never
|
|
266
|
+
sent at all alongside `hold_reason=10`). **Diagnostic rule for this integration: never infer a
|
|
267
|
+
state-machine rule from an empty-body 200 until you have verified payload completeness first.**
|
|
268
|
+
The In-Progress gate was removed on that basis. Separately, a canceled incident (INC2190846)
|
|
269
|
+
generated **2,392** such failed PATCHes before the closed-incident `dtSynced` reconcile was added.
|
|
270
|
+
- **⚠ OUTSTANDING DEPENDENCY (open, NOT closed by TRUE-81044) — we stopped WRITING `u_status_task`
|
|
271
|
+
but three paths still READ it.** Hold detection and RITM hold release now depend on ServiceNow's
|
|
272
|
+
business rule actually populating that field from `state`. If it doesn't, these break **silently**:
|
|
273
|
+
- `worker/crons/sync/nycdoe/process_tickets.php:206` — inbound hold detection
|
|
274
|
+
- `worker/crons/sync/nycdoe/send_request_item_updates.php:224` — RITM reverse-hold
|
|
275
|
+
- `worker/crons/sync/nycdoe/send_request_item_updates.php:139` — RITM closed-state check
|
|
276
|
+
**Verification owed on the FIRST hold that syncs after deploy:** read the incident back and confirm
|
|
277
|
+
`u_status_task` returns `On Hold`, via legacy `Logs.API` filtered `endpoint LIKE '%nycd3%'`.
|
|
224
278
|
- Holds are **never auto-released** — if a SNOW user manually changes status, the outbound
|
|
225
279
|
cron correctly re-asserts On Hold; that is intended, not a bug. Clear the hold in TOGaDesk.
|
|
226
280
|
- **Diagnosing which writer dropped a hold:** `repair_order_history`
|
|
@@ -253,6 +307,26 @@ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets
|
|
|
253
307
|
`repair_orders.status` directly cannot distinguish the six hold variants after a recompute.
|
|
254
308
|
|
|
255
309
|
## Change history
|
|
310
|
+
- 2026-08-20 — **TRUE-81044: re-corrected the field-authority model on ServiceNow's own written
|
|
311
|
+
instruction, and fixed the real root cause of the failing INC hold push.** ServiceNow stated the
|
|
312
|
+
mandatory attribute set for an Incident On Hold is **`state` + `hold_reason` + `u_eta` together**,
|
|
313
|
+
and that their business rule derives the client-facing `u_status_task` **from `state`** — so our
|
|
314
|
+
`u_status_task` writes were fighting it. `send_ticket_updates.php`: removed `u_status_task` from
|
|
315
|
+
both the hold push and the release (and the `SN_TASK_STATUS_*` constants / `$snTaskStatus` read);
|
|
316
|
+
now sends `state`+`hold_reason`+`u_eta` **unconditionally together** (the `In Progress` gate and
|
|
317
|
+
the "only if ETA differs" condition are **gone**); added `u_other_reason_on_hold`, required
|
|
318
|
+
alongside `hold_reason=10` ("Other" IS valid — the earlier "invalid/dropped" note was wrong) and
|
|
319
|
+
sourced from the analyst hold note with a `statusDisplay()` fallback so it is never empty;
|
|
320
|
+
re-keyed the idempotency guard and release trigger from `$snTaskStatus` to `$snState`. Root-cause
|
|
321
|
+
correction: the documented "On Hold only from In Progress" rule was **inferred** from empty-body
|
|
322
|
+
200s, which do not distinguish an illegal transition from an **incomplete mandatory set** — ours
|
|
323
|
+
was incomplete. Decision: **no** mapping of the six TogaDesk `HOLD_*` constants onto ServiceNow
|
|
324
|
+
hold-reason codes; reason text comes from TogaDesk only. RITM cron intentionally untouched (already
|
|
325
|
+
compliant); open question whether RITM needs the same mandatory set. New open item: three paths
|
|
326
|
+
still **read** `u_status_task` — verify it comes back `On Hold` on the first post-deploy hold.
|
|
327
|
+
worker-only, single file (commit `ead92ad1e`, branch `TRUE-81044`, 34/-43); `php -l` clean on PHP
|
|
328
|
+
7.2, so the `library`-before-`togadesk` deploy-ordering constraint does not apply here.
|
|
329
|
+
(mhammontree)
|
|
256
330
|
- 2026-07-20 — **Corrected the authoritative-field model and shipped the INC hold push.**
|
|
257
331
|
Controlled test PATCHes proved the earlier "native `state` is authoritative; never use
|
|
258
332
|
`u_status_task`" conclusion (TRUE-79922) **backwards**: the custom **`u_status_task`** is the
|
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
|
|
3
3
|
| Doc | Framework | Summary | Files |
|
|
4
4
|
|-----|-----------|---------|-------|
|
|
5
|
+
| [Staples: OneUptime SFTP folder monitors (Monitor/Staples/*)](features/oneuptime-sftp-folder-monitors.md) | 2.0 | Two worker2 OneUptime push monitors that watch the **Staples SFTP** for stuck files in either direction — a backlog that previously had **no alerting** (the ear | worker2/Worker/Monitor/Staples.php, worker2/Config/beta.ini, worker2/Config/production.ini, dbchanges2/Core/2026-08-20a - Staples SFTP Monitors.sql |
|
|
5
6
|
| [Staples](profile.md) | 1.0 | Staples is a **headless** integration client — no UI, Worker 1.0 only, depending on the `library` (1.0 `App_`) core. | worker/crons/sync/staples/sync_staples_cxml.php, worker/crons/sync/staples/staples_asn_netsuite.php |
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Staples: OneUptime SFTP folder monitors (Monitor/Staples/*)"
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: worker2
|
|
5
|
+
project: Worker
|
|
6
|
+
client: staples
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-20
|
|
10
|
+
owners: ["bala"]
|
|
11
|
+
files:
|
|
12
|
+
- worker2/Worker/Monitor/Staples.php
|
|
13
|
+
- worker2/Config/beta.ini
|
|
14
|
+
- worker2/Config/production.ini
|
|
15
|
+
- dbchanges2/Core/2026-08-20a - Staples SFTP Monitors.sql
|
|
16
|
+
related:
|
|
17
|
+
- ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
|
|
18
|
+
- ../profile.md
|
|
19
|
+
- ../../../1.0/apps/worker/features/staples-cxml-order-import.md
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
Two worker2 OneUptime push monitors that watch the **Staples SFTP** for stuck files in
|
|
25
|
+
either direction — a backlog that previously had **no alerting** (the earlier `/out` pickup
|
|
26
|
+
pileup incident went unnoticed). They implement the shared pattern in
|
|
27
|
+
[OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md);
|
|
28
|
+
read that first for the payload contract, criteria set, and provisioning runbook.
|
|
29
|
+
|
|
30
|
+
- `Monitor/Staples/InboundOrderQueue` — watches **`/in`** (cXML orders Staples sent that
|
|
31
|
+
TOGA has not yet imported).
|
|
32
|
+
- `Monitor/Staples/OutboundPickupQueue` — watches **`/out`** (855 acknowledgements + 856
|
|
33
|
+
ASNs TOGA wrote, waiting for Staples to download).
|
|
34
|
+
|
|
35
|
+
Because each monitor logs into SFTP and lists a folder, it **doubles as an SFTP
|
|
36
|
+
connectivity check**: a login/list failure pushes `status:"error"`, so a dead checker or a
|
|
37
|
+
broken SFTP is distinguishable from a real backlog.
|
|
38
|
+
|
|
39
|
+
## Key files / entry points
|
|
40
|
+
|
|
41
|
+
- `worker2/Worker/Monitor/Staples.php` — `abstract class _Worker_Monitor_Staples`; two
|
|
42
|
+
`public static` action methods (`InboundOrderQueue`, `OutboundPickupQueue`), each
|
|
43
|
+
self-contained (in-method `ALL_CAPS` locals + `$push` closure), following the shared
|
|
44
|
+
"dumb reporter, smart monitor" model.
|
|
45
|
+
- `dbchanges2/Core/2026-08-20a - Staples SFTP Monitors.sql` — the two `Core.CronJobs` rows
|
|
46
|
+
(actions `Monitor/Staples/InboundOrderQueue` and `Monitor/Staples/OutboundPickupQueue`,
|
|
47
|
+
schedule `*/5 * * * *`, `maxExecutionTime` 120), following the sibling monitor
|
|
48
|
+
cron-registration migration pattern.
|
|
49
|
+
- `[sftp_staples]` config group in `worker2/Config/beta.ini` and
|
|
50
|
+
`worker2/Config/production.ini` — SFTP host/port/user/password. **The password is a
|
|
51
|
+
credential; it lives only in the ini group, never in this doc.**
|
|
52
|
+
|
|
53
|
+
## How it works
|
|
54
|
+
|
|
55
|
+
Each monitor logs into the Staples SFTP with **phpseclib3**, `rawlist`s the target folder,
|
|
56
|
+
counts files older than an age threshold, decides an alarm token (`HIGH`/`OK`), and POSTs
|
|
57
|
+
the JSON body to a OneUptime incoming-request monitor. The worker makes the threshold
|
|
58
|
+
decision; OneUptime only string-matches the token (it cannot compare numbers — see the
|
|
59
|
+
shared doc). The OneUptime push URLs are in-file constants (push credentials — never logged
|
|
60
|
+
or recorded).
|
|
61
|
+
|
|
62
|
+
### Staples SFTP folder semantics (background for the thresholds)
|
|
63
|
+
|
|
64
|
+
The Staples pipeline runs in the **1.0 `worker`** tier, not worker2; worker2 only watches
|
|
65
|
+
the folders. The relevant 1.0 crons and folder meanings (schedules are in
|
|
66
|
+
`worker/schedules/cron.worker.sync.json`, in the server's **Central** timezone):
|
|
67
|
+
|
|
68
|
+
| 1.0 cron | Schedule | What it does |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `worker/crons/sync/staples/sync_staples_cxml.php` | `45 * * * *` (hourly) | imports Staples cXML orders from `/in` into NetSuite, writes an 855 ack to `/out`, archives the processed inbound file to `/archive/archive_in` |
|
|
71
|
+
| `worker/crons/sync/staples/staples_asn_netsuite.php` | `50 * * * *` (hourly) | writes 856 ASN ship notices to `/out` |
|
|
72
|
+
|
|
73
|
+
- **`/in`** — inbound orders Staples uploads for TOGA to import.
|
|
74
|
+
- **`/out`** — outbound 855/856 files TOGA writes for Staples to download (drained by
|
|
75
|
+
Staples, moved to `/archive` on pickup).
|
|
76
|
+
|
|
77
|
+
### Threshold tuning (decided)
|
|
78
|
+
|
|
79
|
+
- Aged-file cutoff: **120 min for `/in`** (tied to the hourly import) and **240 min for
|
|
80
|
+
`/out`**.
|
|
81
|
+
- Alarm when **>= 6 aged files** (tunable — can drop to 1 for single-file sensitivity).
|
|
82
|
+
- **5-minute cron cadence** (`*/5 * * * *`), chosen to pair with the OneUptime heartbeat
|
|
83
|
+
criteria (received-in-8-min + `alarm:OK` recovery; not-received-15-min offline;
|
|
84
|
+
not-received-10-min degraded), matching the existing Compass monitor convention rather
|
|
85
|
+
than the source crons' hourly rate. The fast cadence is for a **reliable heartbeat**, not
|
|
86
|
+
because the folders change that fast — the folder cutoffs above are calibrated against the
|
|
87
|
+
**hourly 1.0 crons**, per the shared doc's "calibrate against the tier that runs the
|
|
88
|
+
business logic" rule.
|
|
89
|
+
|
|
90
|
+
## Gotchas / known issues
|
|
91
|
+
|
|
92
|
+
- **phpseclib3 `rawlist` returns arrays, and its SFTP type constants are not loaded** — see
|
|
93
|
+
the shared doc's
|
|
94
|
+
[SFTP monitoring gotchas](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md#sftp-monitoring-phpseclib3).
|
|
95
|
+
Getting this wrong silently makes the aged-file count always 0, so the monitor never
|
|
96
|
+
alarms and looks healthy.
|
|
97
|
+
- The `[sftp_staples]` password and the OneUptime push URLs are credentials — reference
|
|
98
|
+
their location, never their value.
|
|
99
|
+
|
|
100
|
+
## Change history
|
|
101
|
+
- 2026-08-20 — Built the two SFTP folder monitors (`Monitor/Staples/InboundOrderQueue` on
|
|
102
|
+
`/in`, `Monitor/Staples/OutboundPickupQueue` on `/out`), each also acting as an SFTP
|
|
103
|
+
connectivity check via `status:error`; added the `[sftp_staples]` config group and the
|
|
104
|
+
`2026-08-20a` cron-registration migration; set the `/in` 120-min / `/out` 240-min aged-file
|
|
105
|
+
cutoffs and the >=6-file alarm threshold; chose a 5-min cadence for the heartbeat.
|
|
106
|
+
Motivated by the earlier `/out` pickup pileup incident that had no alerting. (bala)
|
|
107
|
+
|
|
108
|
+
## Related docs
|
|
109
|
+
- [OneUptime push-metric monitors for 2.0 workers](../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md) — the shared pattern, payload contract, and runbook.
|
|
110
|
+
- [Staples profile](../profile.md)
|
|
111
|
+
- [Staples cXML order import (1.0)](../../../1.0/apps/worker/features/staples-cxml-order-import.md) — the inbound pipeline these monitors watch.
|
|
112
|
+
</content>
|
|
113
|
+
</invoke>
|
|
@@ -3,17 +3,20 @@ title: Staples
|
|
|
3
3
|
framework: "1.0"
|
|
4
4
|
apps:
|
|
5
5
|
- worker
|
|
6
|
+
- worker2
|
|
7
|
+
- dbchanges2
|
|
6
8
|
project: Worker
|
|
7
9
|
client: staples
|
|
8
10
|
type: profile
|
|
9
11
|
status: active
|
|
10
|
-
updated: 2026-08-
|
|
11
|
-
owners: [jcardinal]
|
|
12
|
+
updated: 2026-08-20
|
|
13
|
+
owners: [jcardinal, bala]
|
|
12
14
|
files:
|
|
13
15
|
- worker/crons/sync/staples/sync_staples_cxml.php
|
|
14
16
|
- worker/crons/sync/staples/staples_asn_netsuite.php
|
|
15
17
|
related:
|
|
16
18
|
- ../../1.0/apps/worker/features/staples-cxml-order-import.md
|
|
19
|
+
- ./features/oneuptime-sftp-folder-monitors.md
|
|
17
20
|
---
|
|
18
21
|
|
|
19
22
|
## Summary
|
package/package.json
CHANGED