toga-ai 1.0.1133 → 1.0.1135

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.
Files changed (21) hide show
  1. package/knowledge/1.0/apps/library/INDEX.md +1 -1
  2. package/knowledge/1.0/apps/library/features/php-8-5-runtime-compatibility.md +26 -1
  3. package/knowledge/1.0/apps/togaview/features/ticket-export.md +4 -3
  4. package/knowledge/1.0/apps/tools/features/legacy-email-notifier.md +2 -1
  5. package/knowledge/1.0/apps/tools/features/worker2-job-bridge.md +4 -3
  6. package/knowledge/1.0/apps/worker/workflows/running-a-worker-cron-locally.md +4 -3
  7. package/knowledge/1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md +6 -2
  8. package/knowledge/2.0/apps/_underscore/INDEX.md +2 -1
  9. package/knowledge/2.0/apps/_underscore/features/acl-capabilities.md +6 -1
  10. package/knowledge/2.0/apps/_underscore/features/customer-locations-files-notes.md +19 -3
  11. package/knowledge/2.0/apps/_underscore/features/record-meta-endpoint.md +5 -2
  12. package/knowledge/2.0/apps/_underscore/features/service-request-status-and-stages.md +3 -1
  13. package/knowledge/2.0/apps/_underscore/features/sla-engine.md +7 -4
  14. package/knowledge/2.0/apps/_underscore/features/step-tasks.md +56 -0
  15. package/knowledge/2.0/apps/_underscore/features/tableview-joins.md +3 -1
  16. package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +3 -1
  17. package/knowledge/2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md +6 -1
  18. package/knowledge/2.0/apps/dbchanges2/workflows/verifying-a-migration-ran.md +6 -2
  19. package/knowledge/INDEX.md +1 -1
  20. package/knowledge/clients/nycdoe/features/servicenow-integration.md +5 -1
  21. package/package.json +1 -1
@@ -34,7 +34,7 @@
34
34
  | [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | Non-obvious NetSuite SuiteQL/REST field semantics behind `App_Api_Netsuite_Rest` shims (signs, `tl.id`==line, `iscogs`, ShipItem cost, `createdFrom`, shim-shape |
35
35
  | [NetSuite Sync Alert Monitor (App_SystemMonitor_NetSuiteIntegration)](features/netsuite-sync-alert-monitor.md) | 1.0 monitor `App_SystemMonitor_NetSuiteIntegration` — flags stale per-client NetSuite sync checkpoints (>48h) to ClickUp; open when a sync-stale alert is missin |
36
36
  | [Unit serial details from NetSuite (date received, wipe, lock, notes, return tracking) — opt-in per client](features/netsuite-unit-serial-details-sync.md) | NetSuite inventory-number (serial) fields synced onto `Units` behind `IS_ENABLED_NETSUITE_UNIT_SERIAL_DETAILS`, plus the new-unit condition constant; open befor |
37
- | [Running library (1.0) on PHP 8.4 / 8.5 — deprecations, guards, what is left](features/php-8-5-runtime-compatibility.md) | How library (1.0) handles PHP 8.4/8.5 deprecations (logged, not fatal), the PHP 7.2-safe guard patterns, IMAP without ext-imap (php-imap2), and what is still op |
37
+ | [Running library (1.0) on PHP 8.4 / 8.5 — deprecations, guards, what is left](features/php-8-5-runtime-compatibility.md) | How library (1.0) handles PHP 8.4/8.5 deprecations (logged, not fatal), the PHP 7.2-safe guard patterns, IMAP without ext-imap (php-imap2), the patched vendored |
38
38
  | [Startech PC Matic B2B Sync (library)](features/startech-pcmaticb2b-sync.md) | PC Matic B2B bidirectional ticket sync between TOGaDesk 1.0 and TOGA 2.0 (`App_Api_Toga2`); open for ticket-type maps, StarTech client onboarding, the per-clien |
39
39
  | [App_Api_Toga2 — TOGa2 API Client & 1.0↔2.0 Sync Bridge](features/toga2-api-client-and-bridge.md) | `App_Api_Toga2` — the 1.0-side client for the TOGa2/api2 REST API + cross-framework sync routines (`send`/`authenticate`, options DSL, `syncWithToga`/`syncWithT |
40
40
  | [Transfer order ↔ PO link reconcile (both directions, header + line, from NetSuite)](features/transfer-order-purchase-order-link-reconcile.md) | `App_Api_Toga2::reconcileTransferOrderPurchaseOrderLinks()` makes one TO's PO links (upstream + downstream, header + line) match NetSuite; open before touching |
@@ -32,6 +32,11 @@ files:
32
32
  - library/app/framework.php
33
33
  - library/app/edi.php
34
34
  - library/netsuitetoolkit/NSPHPClient.php
35
+ - library/PhpOffice/PhpSpreadsheet/PATCHES.md
36
+ - library/PhpOffice/PhpSpreadsheet/Shared/StringHelper.php
37
+ - library/phpmailer/class.smtp.php
38
+ - library/phpmailer/class.pop3.php
39
+ - library/phpexcel/
35
40
  related:
36
41
  - error-capture-1-0.md
37
42
  - ../../walmart-activate/workflows/deploy-to-elastic-beanstalk-al2023.md
@@ -44,7 +49,7 @@ related:
44
49
  - ../../worker1.5/architecture.md
45
50
  ---
46
51
 
47
- How library (1.0) handles PHP 8.4/8.5 deprecations (logged, not fatal), the PHP 7.2-safe guard patterns, IMAP without ext-imap (php-imap2), and what is still open; open before moving any 1.0 app to PHP 8.4+, touching `App_Error::handleError`, or editing `php-imap2/`.
52
+ How library (1.0) handles PHP 8.4/8.5 deprecations (logged, not fatal), the PHP 7.2-safe guard patterns, IMAP without ext-imap (php-imap2), the patched vendored PhpSpreadsheet/phpmailer, and what is still open (phpexcel); open before moving any 1.0 app to PHP 8.4+, touching `App_Error::handleError`, or editing `php-imap2/`, `PhpOffice/` or `phpexcel/`.
48
53
 
49
54
  ## Summary
50
55
 
@@ -91,11 +96,30 @@ ext-imap is gone from PHP 8.4+, so on 8.5 hosts (worker1.5) every `imap2_*` call
91
96
 
92
97
  `library/php-imap2/TOGA-PATCHES.md` lists every patch — re-apply after any re-vendor.
93
98
 
99
+ ### Vendored PhpSpreadsheet + PHPMailer (TRUE-83159)
100
+
101
+ `PhpOffice/PhpSpreadsheet/` (early 1.x, no Composer) and `phpmailer/` 5.x are patched by hand to run on 8.5 with no deprecations or PHP 8 warnings, still valid on 7.2 (library `cc5705e6`). Decided (CTO agreed): patch in place, do not upgrade. New PhpSpreadsheet needs PHP 8.1+ (breaks the 7.2 apps); a second composer copy in worker1.5 clashes on the namespace. `PhpOffice/PhpSpreadsheet/PATCHES.md` lists every edit — re-apply after any re-vendor.
102
+
103
+ Why it mattered: a few PHP 8 warnings in formula/chart code exit a 1.0 cron (App_Error), not just log.
104
+
105
+ Patterns worth reusing (7.2 safe):
106
+ - `#[\ReturnTypeWillChange]` on its own line (a comment on 7.x).
107
+ - `['self','m']` / `'self::m'` callables → `[self::class,'m']`.
108
+ - Column letters: `Shared/StringHelper::increment()` = same result as `++$s`, but `str_increment()` on 8.3+ (8.3 deprecates `++` on a non-numeric string).
109
+ - `auto_detect_line_endings` is still needed for CR-only CSVs: mute only its deprecation with a scoped `set_error_handler` for `E_DEPRECATED`.
110
+ - `HTML-ENTITIES` → `mb_encode_numericentity` on 8.2+ (`Reader/Html.php`).
111
+
112
+ Real PHP 8 breaks fixed (any vendored code can hide these): `implode($arr, "\n")` arg order (fatal, `Reader/Ods`); DATEVALUE `"Jan" > 29` is true on 8, so `+= 1900` threw a TypeError; `'' == 0` is false on 8, so `Reader/Xls` skipped the first cell style XF; `$stack->last()` null and `$condition[0]` on a number (offset-on-null warnings).
113
+
114
+ Verified: both NYC DOE reports + SMTP to a fake server on 7.2, 8.1, 8.5 gave byte-identical xlsx; PhpSpreadsheet's ~90 samples went from 373 distinct deprecations/warnings to 1 (iconv on broken UTF-8, already `@`-suppressed).
115
+
94
116
  **Find new ones:** `php -l -d display_errors=1` with an 8.4+ binary per file (compile-time). Runtime-only ones (`curl_close`, `setAccessible`, `utf8_*`) need a grep. On a live 8.5 app, read `error_log` — each deprecation is logged once per call site.
95
117
 
96
118
  ## Gotchas
97
119
 
98
120
  - **Still open:** vendored `phpseclib/`, `LightSaml/`, `simplesaml/`, `jpgraph/`, `mobile_detect/`, `invoiceprocessor/` raise 8.4 compile-time deprecations (implicit nullable, optional-before-required, `${var}`); `app/string.php:277` `mb_convert_encoding(..., 'HTML-ENTITIES')` (8.2) — no byte-identical 7.2-safe replacement found. Logged, not fatal.
121
+ - **⚠ OPEN — `library/phpexcel/` (old PHPExcel) does not PARSE on PHP 8** (19 files use `$str{0}` string offsets). 7 active worker1.5 crons use it and die on every run: `send_parts_report`, `send_parts_invoice_report`, `send_officedepot_refunds_report`, `send_newegg_invoice_report`, `send_tds_customer_list`, `send_toga_report`, `send_toga_walmart_report` (`Common.CronJobExecutions`: `send_parts_report` started 6 times since 2026-10-15, 0 finished). Needs its own ticket (port to PhpSpreadsheet or patch the syntax).
122
+ - `phpmailer/class.phpmailer.php:1659` still calls `each()` (removed in 8.0), only in a fallback that never runs.
99
123
  - **⚠ Never "fix" the `App_String` encrypt key loop with real modular math.** It changes the key for long strings, so every issued link (Walmart unsubscribe/register) stops decrypting. `keyByteModulo()` must reproduce the old 64-bit wraparound exactly.
100
124
  - **Warnings and notices still exit.** Only deprecations were softened — an "offset on null" or "undefined index" still kills the request on any PHP version.
101
125
  - **`file_get_contents` to IMDS or any optional endpoint:** the `ignore_errors` stream option covers only HTTP status — a refused or slow connection still warns, and App_Error exits. Wrap the call in a local `set_error_handler` / `restore_error_handler` and check for a `200` status line (togaview TRUE-83060).
@@ -110,6 +134,7 @@ ext-imap is gone from PHP 8.4+, so on 8.5 hosts (worker1.5) every `imap2_*` call
110
134
  - **A library fix reaches worker1.5 only after a worker1.5 redeploy** ([worker1.5 architecture](../../worker1.5/architecture.md)).
111
135
 
112
136
  ## Change history
137
+ - 2026-10-23 — Vendored PhpSpreadsheet + phpmailer cleared for 8.5 (patched in place, PATCHES.md); phpexcel parse failure added to open list (TRUE-83159) (bala)
113
138
  - 2026-10-23 — `App_String` key float overflow fixed with exact old result; more deprecations cleared; open list updated (TRUE-83163) (jcardinal)
114
139
  - 2026-10-23 — php-imap2 works without ext-imap: Polyfill loaded + filled in; OAuth/Roundcube gotcha; open gaps (TRUE-83148, `4a2c363f`) (jcardinal)
115
140
  - 2026-10-23 — `instance-data` does not resolve on AL2023 (empty instance id, log noise); deprecations-continue behavior confirmed in the worker1.5 cron log (TRUE-83107). (bala)
@@ -6,8 +6,8 @@ project: TOGa View
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-18
10
- owners: ["mhammontree"]
9
+ updated: 2026-10-23
10
+ owners: ["mhammontree", "bala"]
11
11
  files:
12
12
  - _/app/export/ticketexport.php
13
13
  - mvc/msp_dashboard/export/get.php
@@ -49,7 +49,7 @@ The original export was two requests: request 1 wrote the .xlsx to `$config['int
49
49
  `library/PhpOffice/PhpSpreadsheet/Writer/Xlsx.php::save($pFilename)` (:173) calls `strtolower($pFilename)` (:181) — a stream resource fails with "strtolower() expects parameter 1 to be string, resource given". Stream support landed in a later release. Use `tempnam()` + `file_get_contents()` + `unlink()`; the temp file lives inside one request, so it cannot recreate the load-balancer bug.
50
50
 
51
51
  ### Lint and run togaview with PHP 7.2
52
- Use `xampp_7_2_33`'s `php.exe`. Linting under PHP 8 passes, but a CLI run dies on a vendored PhpSpreadsheet deprecation (`Worksheet\Iterator::current()` return type), which is fatal under `App_Error` — a misleading failure.
52
+ Use `xampp_7_2_33`'s `php.exe`. Older library builds died on PHP 8 with a vendored PhpSpreadsheet deprecation (`Worksheet\Iterator::current()` return type); library TRUE-83159 cleared all of them ([PHP 8.5 compatibility](../../library/features/php-8-5-runtime-compatibility.md)), so an 8.x run is now valid too.
53
53
 
54
54
  ### Don't assume one userType across the forks
55
55
  `common/togaview/msp_dashboard.php:16` requires CUSTOMER or MSP; `common/towfoundation/msp_dashboard.php:234` and `common/newcenturyholdingsllc/msp_dashboard.php:235` require **ADMIN**. All three also require `togadeskClientId`. A shared gate must be the **union**, or every towfoundation/newcentury admin loses the button they can see.
@@ -58,4 +58,5 @@ Use `xampp_7_2_33`'s `php.exe`. Linting under PHP 8 passes, but a CLI run dies o
58
58
  The old `mvc/msp_dashboard/export/get.php` gated only on `isset($_GET['fileId'])` — no login check — then `readfile()`d **and** `unlink()`d `$cacheDir . '/' . $fileName`, validated only by `substr($fileName, -5) === '.xlsx'` and `file_exists()`. `$_GET` passes through `App_String::xssProtect()` (= `htmlspecialchars()`), which touches neither `.` nor `/`, so `../` traversal worked: unauthenticated arbitrary read **and** arbitrary delete. Filenames were also `Tickets_Export_<unix timestamp>.xlsx` in one shared folder — a guessable cross-tenant read with no traversal at all. **An extension check plus `file_exists()` is not a traversal defense, and `htmlspecialchars()` is not path sanitization.** Same family as the TRUE-78835 / TRUE-80671 togaview IDORs.
59
59
 
60
60
  ## Change history
61
+ - 2026-10-23 — PhpSpreadsheet PHP 8 deprecation no longer kills a CLI run (library TRUE-83159) (bala)
61
62
  - 2026-09-18 — Export collapsed to one request (was broken by the shared ALB), extracted to `App_Export_TicketExport`, identity-gated, and the unauthenticated traversal/delete endpoint removed (TRUE-81955) (mhammontree)
@@ -59,7 +59,7 @@ Prod `tools` runs **multiple Elastic Beanstalk instances behind a load balancer*
59
59
  Every send failed (`sent:0`, all "Send failed") because `App_Email::send()` also does a `Defaults` lookup (Vision / `db_main`), a `Logs.Emails` write (`db_logs`), and an `EmailSuppressionList` check (Common / `db_common`) — that 1.0 email-stack DB wiring is **not** present in the tools app, so it threw for every recipient. Fix: send with **PHPMailer directly** using `App_Email`'s SES SMTP **constants** (`SMTP_HOSTNAME` / `SMTP_PORT` / `SMTP_SECURE` / `SMTP_USERNAME` / `SMTP_PASSWORD` — the same proven SES SMTP creds, `us-west-2`) and **no DB calls**; From `no-reply@togatech.com` / "TOGa IT". **Operational prerequisite:** the `togatech.com` sender identity/domain must be **verified in that us-west-2 SES account** or delivery is rejected. Any future tools-app feature that needs to send email hits the same wall — bypass `App_Email::send()` and go straight to SES.
60
60
 
61
61
  ### PhpSpreadsheet 1.6.0 deprecations aborted the request (before library PR #905)
62
- The vendored PhpSpreadsheet is **1.6.0** (library composer constraint `^1.6`, which supports only PHP 5.6/7.0) and emits `E_DEPRECATED` all over on PHP 8.x. Until library PR #905 (2026-10-22) a single deprecation during the xlsx parse aborted the request and (when Sentry is absent) echoed "Sentry is not installed…" into the JSON. Now deprecations are only logged ([PHP 8.5 compatibility](../../library/features/php-8-5-runtime-compatibility.md)); a warning or notice still aborts. Fix: install a **scoped `set_error_handler`** at the top of the endpoint that swallows non-fatal diagnostics (returns `true`) for the duration of the action; real fatals and thrown exceptions still reach the `catch`. The framework mechanism is documented in [mvc-data-access-patterns](./mvc-data-access-patterns.md).
62
+ The vendored PhpSpreadsheet is **1.6.0** (library composer constraint `^1.6`, which supports only PHP 5.6/7.0) and emitted `E_DEPRECATED` all over on PHP 8.x (all cleared in library TRUE-83159, see the compatibility doc below). Until library PR #905 (2026-10-22) a single deprecation during the xlsx parse aborted the request and (when Sentry is absent) echoed "Sentry is not installed…" into the JSON. Now deprecations are only logged ([PHP 8.5 compatibility](../../library/features/php-8-5-runtime-compatibility.md)); a warning or notice still aborts. Fix: install a **scoped `set_error_handler`** at the top of the endpoint that swallows non-fatal diagnostics (returns `true`) for the duration of the action; real fatals and thrown exceptions still reach the `catch`. The framework mechanism is documented in [mvc-data-access-patterns](./mvc-data-access-patterns.md).
63
63
 
64
64
  **Security:** 1.6 is not safe for untrusted xlsx (XXE scanner bypass, no zip-size limits). New upload features parse in worker2 instead — see [job bridge](./worker2-job-bridge.md).
65
65
 
@@ -73,5 +73,6 @@ The vendored PhpSpreadsheet is **1.6.0** (library composer constraint `^1.6`, wh
73
73
  - [Tools architecture](../architecture.md)
74
74
 
75
75
  ## Change history
76
+ - 2026-10-23 — PhpSpreadsheet PHP 8 deprecations cleared in library TRUE-83159 (bala)
76
77
  - 2026-10-23 — Deprecations no longer abort since library PR #905 (TRUE-83107) (bala)
77
78
  - 2026-10-13 — Noted PhpSpreadsheet 1.6 is unsafe for untrusted xlsx; new uploads parse in worker2 (bala)
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-13
9
+ updated: 2026-10-23
10
10
  owners: [mhammontree, bala, ajean]
11
11
  files:
12
12
  - tools/_/app/worker.php
@@ -87,8 +87,8 @@ empty result — because the wrapper format is worker2's, not a contract.
87
87
  worker2 accepts only keys matching that prefix's regex, hardcodes bucket/region, and deletes the
88
88
  object in `finally`. Tools deletes it too when enqueue fails or the job times out.
89
89
  - **Parse untrusted uploads (xlsx) in worker2, not tools.** The library's vendored PhpSpreadsheet
90
- is **1.6**: XXE scanner bypass via single-quoted UTF-7 encoding, no zip-size limits, and PHP 8
91
- deprecations that the 1.0 handler turns into fatals. worker2 has PhpSpreadsheet 3.10 — use
90
+ is **1.6**: XXE scanner bypass via single-quoted UTF-7 encoding and no zip-size limits (its PHP 8
91
+ deprecations were cleared in library TRUE-83159; the security gaps were not). worker2 has PhpSpreadsheet 3.10 — use
92
92
  `IOFactory::load($path, 0, [IOFactory::READER_XLSX])` there.
93
93
  - **Credentials: no key in tools config since TRUE-82705 (2026-10-12).** `config.production.ini`
94
94
  `[aws]` holds only `region`, so `enqueue()` builds `SqsClient` from the SDK default chain (EB
@@ -107,6 +107,7 @@ empty result — because the wrapper format is worker2's, not a contract.
107
107
  4. Dev-bypass login must use a `Client_True` email (e.g. `@togatech.com`, not `@goagilant.com`).
108
108
 
109
109
  ## Change history
110
+ - 2026-10-23 — PhpSpreadsheet PHP 8 deprecations cleared (library TRUE-83159); xlsx still parses in worker2 for security (bala)
110
111
  - 2026-10-13 — Added the S3 file hand-off pattern, "parse untrusted xlsx in worker2" (library PhpSpreadsheet 1.6), and the keyless `[aws]` credentials (TRUE-82662) (bala)
111
112
  - 2026-10-06 — Added the in-progress check for self-chunking jobs (newest row unfinished OR `run CONTINUES`, 60 min window), from the /clickup/aliases Sync card. (ajean)
112
113
  - 2026-10-05 — Added: isSuccess=1 can hide a failed action, JSON-column parameters compare, DB-level duplicate-enqueue guard (Etilize Refresh page). (bala)
@@ -63,9 +63,9 @@ For logic-only tests with no bootstrap, use the
63
63
  `NO_PROXY=127.0.0.1` (so the mock NetSuite still answers).
64
64
 
65
65
  ### Step 4 — run on PHP 7.2
66
- Use Laragon's PHP 7.2.33 build (matches prod). On PHP 8.1 the old PhpSpreadsheet in `library`
67
- hits an `Iterator` return-type deprecation. Older library builds exited on it; since library PR #905
68
- (2026-10-22) it is only logged ([PHP 8.5 compatibility](../../library/features/php-8-5-runtime-compatibility.md)).
66
+ Use Laragon's PHP 7.2.33 build (old worker tier). worker1.5 prod runs PHP 8.5, so also run with an
67
+ 8.5 binary. The vendored PhpSpreadsheet raises no PHP 8 deprecations since library TRUE-83159; old
68
+ `phpexcel/` does not parse on PHP 8 ([PHP 8.5 compatibility](../../library/features/php-8-5-runtime-compatibility.md)).
69
69
 
70
70
  ## Gotchas
71
71
  - **`App_Framework_Worker::initialize()` cannot run twice in one process** — the second
@@ -85,6 +85,7 @@ hits an `Iterator` return-type deprecation. Older library builds exited on it; s
85
85
  - [App_Email side effects & test mode](../../library/features/email-test-mode-and-write-free-sends.md)
86
86
 
87
87
  ## Change history
88
+ - 2026-10-23 — PhpSpreadsheet clean on PHP 8 (TRUE-83159); worker1.5 prod is 8.5, phpexcel does not parse there (bala)
88
89
  - 2026-10-23 — Deprecations no longer exit since library PR #905 (TRUE-83107) (bala)
89
90
  - 2026-10-16 — Created from the cron 7 local test harness (prod-pointing configs, Sentry, SMTP,
90
91
  PHP 7.2, Windows `:` file names, NetSuite 500/unreachable test). (bala, TRUE-82888)
@@ -43,7 +43,9 @@ Since TRUE-83078 (2026-10-22) every 1.0 cron runs on ONE server: EB env `worker1
43
43
  - A `library` fix reaches worker1.5 only on the NEXT worker1.5 deploy: `ebs/setup_git_libraries.php` clones `library`, `_underscore`, `dbchanges`, `resources`, `togadesk` at branch `_production` during each deploy. Merging library alone changes nothing on the box.
44
44
  5. **Recover after crons run again.**
45
45
  - Backlog crons (MITS transmit, SR→SO, email send, NetSuite syncs) drain by themselves. Watch the backlog fall; do not assume.
46
- - Fixed-time daily/weekly jobs do NOT catch up. Re-run each missed one by hand (`php /var/www/html/crons/<path>` on the box) and tell the people who expect it.
46
+ - Fixed-time daily/weekly jobs do NOT catch up. Re-run each missed one by hand as root (`php /var/www/html/crons/<path>` on the box) and tell the people who expect it. The worker1.5 cron list "Run Now" button has no handler (display only).
47
+ - Two traps: report crons usually have no duplicate-send guard (a second run emails again), and a report built on "last 24 h from `NOW()`" shifts its window when run late (NYC DOE cases in [ServiceNow integration § Gotchas](../../../../clients/nycdoe/features/servicenow-integration.md)).
48
+ - Proof of send = legacy `Logs.Emails` row (subject, `isSuccess = 1`, `to`). In `Common.CronJobExecutions`, `job` holds only the file name (not the path) when the script was run from inside its folder.
47
49
 
48
50
  ## Triage failing crons in `worker-cron.log`
49
51
 
@@ -56,6 +58,7 @@ Known entries (TRUE-83163, 2026-10-23):
56
58
  - `togadesk desk/includes/functions.php:1017` `$ticket['clientid']` offset on false (~1,500 per log): Desk crons do not use App_Error, so noise, not a crash.
57
59
  - `get_new_detectprotectservices.php`: the remote feed returns HTML, not CSV, since ~2025-09-11. Remote issue; the cron now skips those rows.
58
60
  - `addDeviceTickets.php` (ManageEngine): fails loud with curl error / HTTP code / body snippet, 5 s timeout (`MANAGEENGINE_TIMEOUT_SECONDS`). ManageEngine host reachability from worker1.5 is still open.
61
+ - PHP parse errors on `$str{0}` from `library/phpexcel/`: 7 report crons die every run (open, TRUE-83159); list in [PHP 8.5 compatibility § Gotchas](../../library/features/php-8-5-runtime-compatibility.md).
59
62
  - NYC DOE `send_request_item_updates.php` is broken in git (undefined vars); see [ServiceNow integration § Gotchas](../../../../clients/nycdoe/features/servicenow-integration.md).
60
63
 
61
64
  ## Gotchas
@@ -65,8 +68,9 @@ Known entries (TRUE-83163, 2026-10-23):
65
68
 
66
69
  ## Worked example (2026-10-22 16:56 → 2026-10-23 ~10:30 CT, TRUE-83121)
67
70
 
68
- TRUE-83078's installer wrote `SHELL=/bin/bash`. 222 of 234 active crons never checked in. Seen: 16 Compass SOs not sent to MITS (oldest 836 min, MITS returned 200 on every call it got, last call 16:10), all Compass 1.0 workflow crons, NetSuite toga2 syncs, Prudential SR→SO (12 REQs), 141 unsent `Common.EmailsQueued` ticket emails (RUMC, TOW, PC Matic, Apiary, Craftex), and NYC DOE "Send DOE Courier Installation Schedule (7:00 AM)" and "Daily Open and Closed DOE Report (1:00 AM)" missed on 2026-10-23. Fixed by dropping the `SHELL` line (worker1.5 PR #8, `ce0f0b2`). Backlogs drained by ~10:52 CT; missed DOE reports needed a manual re-run.
71
+ TRUE-83078's installer wrote `SHELL=/bin/bash`. 222 of 234 active crons never checked in. Seen: 16 Compass SOs not sent to MITS (oldest 836 min, MITS returned 200 on every call it got, last call 16:10), all Compass 1.0 workflow crons, NetSuite toga2 syncs, Prudential SR→SO (12 REQs), 141 unsent `Common.EmailsQueued` ticket emails (RUMC, TOW, PC Matic, Apiary, Craftex), and NYC DOE "Send DOE Courier Installation Schedule (7:00 AM)" and "Daily Open and Closed DOE Report (1:00 AM)" missed on 2026-10-23. Fixed by dropping the `SHELL` line (worker1.5 PR #8, `ce0f0b2`). Backlogs drained by ~10:52 CT; missed DOE reports were re-run by hand at 13:32 and 13:38 CT (both `Logs.Emails` `isSuccess = 1`).
69
72
 
70
73
  ## Change history
74
+ - 2026-10-23 — Manual re-run checks (no duplicate guard, 24 h window shift, `Logs.Emails` proof, `job` name, "Run Now" display only); phpexcel parse-error entry (TRUE-83159). (bala)
71
75
  - 2026-10-23 — Added log triage steps and known log entries after the PHP 8.5 stabilization (TRUE-83163). (jcardinal)
72
76
  - 2026-10-23 — Created from the worker1.5 cron outage after the single-server move (TRUE-83121, TRUE-83116, TRUE-83138). (bala)
@@ -68,7 +68,8 @@
68
68
  | [Service request links (ServiceRequests_Tickets, ServiceRequests_ServiceRequests bridges)](features/service-request-links.md) | Many-to-many links from a service request to tickets and to other service requests; open before linking SRs, reading `_linkCount`, or touching the old `ServiceR |
69
69
  | [2.0 Service Request status (`_status`) is derived; stages are manual-only; standard schedule / crew / location columns](features/service-request-status-and-stages.md) | How a 2.0 service request's `_status` is computed, what stages are for, the standard SR columns, and the set-stage write; open when an SR shows the wrong status |
70
70
  | [Shipping Carriers & Carrier Account Numbers (records, matching, duplication, deletion)](features/shipping-carriers-and-accounts.md) | Per-client `ShippingCarriers` / `ShippingCarrierAccountNumbers` model, how the syncs match/create carrier rows, and the duplication + blank-carrier + pagination |
71
- | [SLA engine (_Component_Sla) — record-generic (tickets + service requests), two clocks, per-stage effects, client switch + operator recompute](features/sla-engine.md) | How 2.0 SLA due times, pauses and the per-client switch are computed and written for tickets AND service requests (`_Component_Sla`), on all hours or a client's |
71
+ | [SLA engine (_Component_Sla) — record-generic (tickets + service requests), two clocks, per-stage effects, platform defaults, case rollup, client switch + operator recompute](features/sla-engine.md) | How 2.0 SLA due times, pauses and the per-client switch are computed and written for tickets AND service requests (`_Component_Sla`), on all hours or a client's |
72
+ | [Step / task rules (_Component_StepTasks) — record-generic task order lock, done stamp, open scope tasks gate](features/step-tasks.md) | The one place for step/task rules on any record with a Tasks tab (tickets, service requests); open when a step is wrongly locked, a resolve is blocked by open t |
72
73
  | [_String helpers — ASCII-safe HTML entity encoding (and the parseBetween trap)](features/string-html-entity-helpers.md) | `_String` static string utilities: use `encodeToAsciiHtmlEntities` for any dynamic text going into a 2.0 email; open when escaping email text or parsing substri |
73
74
  | [Surface FIELD ↔ RecordField binding (maxLength / validationType / isWritable fold-in)](features/surface-field-recordfield-binding.md) | Resolver capability that folds `Core.RecordFields` input-contract metadata (maxLength/validationType/isWritable) onto a surface FIELD element; open when a 2.5 s |
74
75
  | [DECLINED 2026-10-08 — re-key Client.SurfaceOverrides from numeric Core ids to uuids](features/surface-overrides-uuid-rekey.md) | **Decision 2026-10-08 (jcardinal): keep numeric ids; no uuid re-key.** The plan below is not scheduled; see the Layer boundary in [surface-resolver](surface-res |
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-18
9
+ updated: 2026-10-23
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/Capability.php
@@ -79,6 +79,11 @@ exist only for `aclDatabase = 'CLIENT'` records.
79
79
  - **⚠ Open (security, pre-existing):** the client `Base` role (every user holds it) has full CRUD on
80
80
  `user-roles`, `roles`, `acl-field-permissions`, `acl-custom-field-permissions` — a user can grant
81
81
  themselves more access. Developer decision: fix when Hub is built.
82
+ - **Showing a role's capabilities (any authoring UI): group by the record each capability grants** (record's
83
+ Title Case plural name), never by an inferred parent record. `InherentRecordChildren` is not a single tree — a
84
+ record can have many parents — so there is no "Other" bucket; a capability granting several records shows under
85
+ each (developer decision 2026-10-23; Desk RoleDetail does this).
82
86
 
83
87
  ## Change history
88
+ - 2026-10-23 — Capability display rule: group by granted record, never by inferred parent (TRUE-82871). (jcardinal)
84
89
  - 2026-10-18 — Created: Capabilities layer, dual-read, writable-wins field merge, role delete rules (TRUE-82880). (jcardinal)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-23
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/Customer.php
@@ -17,9 +17,9 @@ files:
17
17
  - _underscore/Model/Client/Customers/File.php
18
18
  - _underscore/Model/Client/CustomerNote.php
19
19
  - dbchanges2/Client/2026-10-17e - TRUE-82903 CustomerTiersIndustries.sql
20
- - dbchanges2/Core/2026-10-17h - TRUE-82903 CustomerTiersIndustries.sql
21
20
  - dbchanges2/Client/2026-10-17b - CustomerLocationsFilesNotes.sql
22
- - dbchanges2/Core/2026-10-17b - CustomerLocationsFilesNotesActivity.sql
21
+ - dbchanges2/Client/2026-10-23i - TRUE-82871 CustomersClientLink.sql
22
+ - dbchanges2/Core/2026-10-23q - TRUE-82871 ClientProfileCore.sql
23
23
  related:
24
24
  - ./record-hard-delete.md
25
25
  - ./record-activity-timeline.md
@@ -45,6 +45,8 @@ Built for TOGa Desk (TRUE-82870, TRUE-82903), usable by any app. All tables live
45
45
  | `Customers.industryId` | nullable FK → Industries | |
46
46
  | `Customers.dateCustomerSince` | `DATE` | |
47
47
  | `Customers._siteCount` | non-stored `FIELD_SQL` COUNT of `Locations_Customers` rows | |
48
+ | `Customers.clientId` | `INT UNSIGNED NULL`, `UNIQUE`, no FK — the Core client this customer IS (client profile link) | RecordFields 3395 |
49
+ | `Customers._health` | read-time `FIELD_SQL`, worst SLA state of open work (see [SLA engine](./sla-engine.md)) | |
48
50
  | `CustomerTiers` | lookup (name, slug, sortOrder, isActive) | Records 387 `customer-tiers` |
49
51
  | `Industries` | lookup, same shape | Records 388 `industries` |
50
52
  | `Locations_Customers` | many-to-many bridge, `UNIQUE (locationId, customerId)` | Records 382 `location-customers` |
@@ -76,6 +78,13 @@ RecordFields 2801-2814 cover 382-384. SQL: `Client/2026-10-17e` + `Core/2026-10-
76
78
  382/383: CRUD. 384: create + read only, only `note` writable.
77
79
  - **Delete:** see [record hard delete](./record-hard-delete.md). **Activity:** root `customers` →
78
80
  surface `customer-activity` (see [record activity timeline](./record-activity-timeline.md)).
81
+ - **Client profile link = cross-cluster link pattern (developer decision, TRUE-82871).** A Client-DB row points at a Core
82
+ row by storing the Core id in a plain `INT UNSIGNED NULL` column — same convention as `Users.clientId` /
83
+ `Users_Clients.clientId`. No FK, never joined (Core is another cluster). `Customers` PRE POST / PUT / PATCH
84
+ interceptors check the id on the Core connection, enforce one customer per Core client per client DB, and allow
85
+ link/unlink only to the caller's home client or one they have a `Users_Clients` row for. In a TableView the id
86
+ shows as a raw value (no join — see [tableview-joins](./tableview-joins.md#gotchas)). The front end merges the
87
+ Core-only `clients` listing with one extra customers call (filter `clientId`) on the HOME client token.
79
88
  - **Listing a customer's locations:** separate parent-scoped TableView `desk-customer-locations` (Locations base,
80
89
  OUTER joins → Locations_Customers → Customers, filtered by `Customers.uuid`). See gotchas.
81
90
 
@@ -93,7 +102,14 @@ RecordFields 2801-2814 cover 382-384. SQL: `Client/2026-10-17e` + `Core/2026-10-
93
102
  - **Copied RecordFields need `name`.** `CaseNotes` RecordFields 2743-2749 have NULL `name` (not fixed), so the
94
103
  copy 2809-2814 came out NULL too; filled by `Core/2026-10-17d`. Set `name` on every new RecordField.
95
104
 
105
+ - **⚠ `Customers.clientId` is only trusted in the HOME client DB.** Any tenant can store any Core id in its own DB.
106
+ Read a client's profile only from the home DB on the home token; never look up "the customer that is client X"
107
+ in another client DB.
108
+ - **`client` body key is rejected (400).** api2 maps `client` → `clientId` AFTER the PRE interceptor runs, so the
109
+ alias would skip the check. PATCH needs its own interceptor row (api2 keys the lookup by method).
110
+
96
111
  ## Change history
112
+ - 2026-10-23 — `Customers.clientId` client profile link (cross-cluster id column, interceptor-validated on Core, home/`Users_Clients` only) and `_health` (TRUE-82871). (jcardinal)
97
113
  - 2026-10-17 — Added CustomerTiers (387), Industries (388), `_Model_Client_CustomerLookup` interceptors, `customerTierId`, `industryId`, `dateCustomerSince`, `_siteCount` (TRUE-82903). (jcardinal)
98
114
  - 2026-10-17 — Filled NULL `name` on RecordFields 2809-2814 (Core/2026-10-17d). (jcardinal)
99
115
  - 2026-10-17 — Created: primaryLocationId, _contactCount, Locations_Customers, Customers_Files, CustomerNotes (TRUE-82870). (jcardinal)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-18
9
+ updated: 2026-10-23
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Core/Record.php
@@ -34,7 +34,9 @@ language. Platform rule: **a field's visible label comes from `Core.RecordFields
34
34
  ## How it works
35
35
 
36
36
  - **Names:** `name` / `pluralName` from Messages via `_Model_Core_Surface::resolveMessageValues()`;
37
- fallback order: Message → frozen `DefaultRecordSettings` → `Core.Records.name`.
37
+ fallback order (single and batch, both names): Message → frozen `DefaultRecordSettings` (`label-singular` /
38
+ `label`) → `Core.Records.name`. `Core.Records.name` is the Title Case PLURAL base name, so it is the right
39
+ last resort for `pluralName` (e.g. `items`: nameMessage "Item", no plural message → "Items").
38
40
  - **Batch (`metaForRoutes`):** one Records query, one Messages lookup, one `DefaultRecordSettings` query for all routes.
39
41
  - **Response path:** record-script nesting adds the payload key — batch is read at
40
42
  `data.records.meta.records.<route>` (see [record scripts](../../api2/features/record-scripts.md)).
@@ -61,5 +63,6 @@ language. Platform rule: **a field's visible label comes from `Core.RecordFields
61
63
  The rest of the meta is the same for every tenant.
62
64
 
63
65
  ## Change history
66
+ - 2026-10-23 — `pluralName` falls back to `Core.Records.name` (was null when no plural Message and no `label` setting) (TRUE-82871). (jcardinal)
64
67
  - 2026-10-18 — Added `icon` / `iconWeight`, name fallback to `Core.Records.name`, and batch `?routes=` (max 400) (TRUE-82880). (jcardinal)
65
68
  - 2026-10-15 — Added `fieldLabels` (gated by record + field READ) and blox `fieldLabelOf`; noted the ungated base meta (TRUE-82836). (jcardinal)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-19
9
+ updated: 2026-10-23
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/ServiceRequest.php
@@ -47,6 +47,7 @@ How a 2.0 service request's `_status` is computed, what stages are for, the stan
47
47
  - **Per-client stage → status links** go in `Client_<Name>/` files (e.g. `Client_Prudential` / `Client_Growrk` `2026-10-19a`), never a `Client/` fan-out.
48
48
  - **Standard columns:** `assignedToContactId` (Lead), `reportedByContactId`, `locationId` (the site — NOT `deliverToAddressId`, which is the shipping address), `isRemote`, `dtScheduledStart` / `dtScheduledEnd` (NOT `dateDelivery`). `ServiceRequestTypes`: `isSiteVisit`, `iconName`, `colorHex`. Bridges `ServiceRequests_Users` (crew), `_Tags`, `_Files` (Core.Records 422-424, RecordFields 3106-3152).
49
49
  - **Set stage** = table-based `PUT /v2/service-requests/{uuid}` with nested `serviceRequestNotes` (the reason note). A stage PUT without a uuid = 400 for signed-in users (CSO).
50
+ - **Steps / tasks:** a resolve/close is refused while a scope task is open (signed-in users only) — see [step / task rules](./step-tasks.md).
50
51
  - **Crew "required"** is a UI rule only — integration-created SRs send no crew.
51
52
  - **Base hooks** (`prePost` / `postPost` / `postPut` / `postPatch`) are registered for every client by `Core/2026-10-19i` (`clientId` / `apiId` NULL). They run the schedule check, the set-stage gate, and the [SLA engine](./sla-engine.md).
52
53
 
@@ -57,4 +58,5 @@ How a 2.0 service request's `_status` is computed, what stages are for, the stan
57
58
  - **⚠ A NULL stage is now the normal case — never INNER JOIN the SR stage.** `User::_openWorkCount` and `Case::_resolvedCount` used INNER JOIN on stage + `isOpen` and dropped stageless SRs. Rule: open = no stage OR stage `isOpen = 1`; open work counts the SR's own crew.
58
59
 
59
60
  ## Change history
61
+ - 2026-10-23 — Open scope tasks block resolve/close for signed-in users ([step-tasks](./step-tasks.md)) (TRUE-82871). (jcardinal)
60
62
  - 2026-10-19 — `_status` derived from fields; stages manual-only; schedule / crew / location / reported-by columns; client overrides call `parent::`; open counts include stageless SRs (TRUE-82915). (jcardinal)
@@ -1,12 +1,12 @@
1
1
  ---
2
- title: SLA engine (_Component_Sla) — record-generic (tickets + service requests), two clocks, per-stage effects, client switch + operator recompute
2
+ title: SLA engine (_Component_Sla) — record-generic (tickets + service requests), two clocks, per-stage effects, platform defaults, case rollup, client switch + operator recompute
3
3
  framework: "2.0"
4
4
  repo: _underscore
5
5
  project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-20
9
+ updated: 2026-10-23
10
10
  owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Component/Sla/Sla.php
@@ -16,9 +16,9 @@ files:
16
16
  - _underscore/Model/Client/BusinessHoliday.php
17
17
  - _underscore/Model/Client/Ticket.php
18
18
  - _underscore/Model/Client/TicketStage.php
19
- - _underscore/Model/Client/TicketNote.php
20
19
  - _underscore/Model/Client/ServiceRequest.php
21
- - _underscore/Model/Client/ServiceRequestNote.php
20
+ - _underscore/Model/Client/Case.php
21
+ - _underscore/Model/Core/SlaDefault.php
22
22
  - _underscore/Model/Core/Client.php
23
23
  related:
24
24
  - ./ticket-status-from-stage.md
@@ -40,6 +40,8 @@ How 2.0 SLA due times, pauses and the per-client switch are computed and written
40
40
  - **Tickets (descriptor 1):** `TicketSlas`, `TicketStages.slaEffect` + `slaFirstResponseEffect`, `TicketNotes`, contact = `contactId`. Moving tickets onto the descriptor changed nothing (275-scenario old-vs-new harness and a live recompute replay: 0 diffs).
41
41
  - **Service requests (descriptor 2):** same 9 stored SLA columns + 11 calc fields on `ServiceRequests`; targets `ServiceRequestSlas`; effect `ServiceRequestStages.slaEffect`. No reply-effect column (`hasReplyEffectColumn = false`) — the reply clock follows `slaEffect`. Contact = `requestedForContactId`. First response = first external note with a signed-in author (`authorUserId`) OR a contact other than the requester.
42
42
  - **Targets:** `(typeColumn, urgencyId)` → minutes (MINUTES ×1, HOURS ×60, DAYS ×1440). Any other unit (e.g. a leftover `BUSINESS_DAYS`) = no target. Business time comes from the clock below, never from a unit.
43
+ - **Platform defaults (`Core.SlaDefaults`, `_Model_Core_SlaDefault`):** one row per `(recordKind TICKET | SERVICE_REQUEST, urgencySlug)`, same target shape as the client rows. A client with NO `TicketSlas` / `ServiceRequestSlas` row for a (type, urgency) inherits the default; a client row — even one with empty targets — always wins. Matched in PHP: client `Urgencies.slug` read on the client connection, defaults read once per request on Core (never joined). This is what lets a UI show Default vs Custom and "Reset" (= delete the client row). Unit/amount rules: `_Trait_ClientSla`.
44
+ - **Case rollup (stored, write-changing values only):** `Cases.dtSlaNextBreach`, `dtSlaNextAtRisk`, `slaRunningCount`, `slaPausedCount`, `slaCompleteCount`. Written ONLY by `recomputeCaseRollup()`: lock the case row first (no-op `UPDATE … SET id = id`, opens the lazy transaction), then ONE raw `UPDATE Cases … JOIN (aggregate over every record type)` — never `_Model::save` (cascade), no read-then-write gap. Several cases → ascending id (lock order). Triggered by every SLA write of a member record, for old AND new case on a `caseId` change, and on child delete (`excludeRecord`); `recomputeClient` rebuilds all at the end (deferred during the bulk run). Breaching / at-risk counts and `_health` (Case + Customer, `ON_TRACK` / `AT_RISK` / `DELAYED`, Case also `ON_HOLD`) are read-time `FIELD_SQL` from `NOW()` via `itemCountSql()` / `itemExistsSql()` — never stored, never settable (they change with time, not writes).
43
45
  - **Clock per target:** `TicketSlas.clock` / `ServiceRequestSlas.clock` ENUM `ALL_HOURS` (default) | `BUSINESS_HOURS`. Locked on the record at create with the targets: `slaClock` + `slaFirstResponseUnit` / `slaResolutionUnit` (the unit is locked because locked minutes cannot tell "24 hours" from "1 day"). The calendar itself is NOT locked.
44
46
  - **Due:** `ALL_HOURS` = `dtCreated + targetMinutes + pausedMinutes`, per clock. `BUSINESS_HOURS` = `computeDueOnClock()`: MINUTES/HOURS add open minutes; DAYS = same open-time point on the Nth open day; paused time counts open minutes only (calendar as of the resume). Budget calc fields of a business record read wall minutes from start to due (`budgetMinutesSql`).
45
47
  - **Business calendar = `_Component_SlaCalendar`** (`_underscore/Component/SlaCalendar/SlaCalendar.php`), the only code that knows hours, holidays and zone. Pure math on epochs (probe from CLI); only `load()` / queue helpers touch the DB.
@@ -79,6 +81,7 @@ How 2.0 SLA due times, pauses and the per-client switch are computed and written
79
81
  - [Re-runnable additive migrations](../../dbchanges2/features/rerunnable-additive-inserts.md)
80
82
 
81
83
  ## Change history
84
+ - 2026-10-23 — Case rollup (5 stored columns, one raw UPDATE, case locked first; health read-time) and platform defaults `Core.SlaDefaults` (client row wins; Default/Custom/Reset) (TRUE-82871). (jcardinal)
82
85
  - 2026-10-20 — Business-hours clock: per-target `clock` ENUM locked on the record, `_Component_SlaCalendar` (hours, holidays, IANA zone, default Mon-Fri 08-17), worker2 `Sla/BusinessHours/Recompute` on calendar change (TRUE-82934). (jcardinal)
83
86
  - 2026-10-19 — Engine made record-generic via `descriptor()`; service requests are descriptor 2 (same columns, `ServiceRequestSlas`, reply clock follows `slaEffect`); tickets unchanged (0 diffs) (TRUE-82915). (jcardinal)
84
87
  - 2026-10-16 — Reply clock gets its own per-stage effect (`slaFirstResponseEffect`, pauses outside new/open/reopened); operator recompute via `{"recomputeSla": true}`; per-client named lock (TRUE-82849). (jcardinal)
@@ -0,0 +1,56 @@
1
+ ---
2
+ title: Step / task rules (_Component_StepTasks) — record-generic task order lock, done stamp, open scope tasks gate
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-23
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Component/StepTasks/StepTasks.php
13
+ - _underscore/Model/Client/TicketStep.php
14
+ - _underscore/Model/Client/TicketStepTask.php
15
+ - _underscore/Model/Client/ServiceRequestStep.php
16
+ - _underscore/Model/Client/ServiceRequestStepTask.php
17
+ - _underscore/Model/Client/ServiceRequest.php
18
+ - dbchanges2/Client/2026-10-23g - TRUE-82871 SrTasks.sql
19
+ - dbchanges2/Core/2026-10-23m - TRUE-82871 SrTasksCore.sql
20
+ related:
21
+ - ./sla-engine.md
22
+ - ./service-request-status-and-stages.md
23
+ - ./ticket-status-from-stage.md
24
+ ---
25
+ The one place for step/task rules on any record with a Tasks tab (tickets, service requests); open when a step is wrongly locked, a resolve is blocked by open tasks, or you add Tasks to another record type.
26
+
27
+ ## Summary
28
+
29
+ `_Component_StepTasks` (static, shared by every 2.0 app) decides the task-order lock, stamps `dtDone`, and counts open
30
+ scope tasks. Called from the task models' `save()` and the record models' stage gates. Record-generic via
31
+ `descriptor(RECORD_TICKET | RECORD_SERVICE_REQUEST)` (same idea as the [SLA engine](./sla-engine.md)): record table,
32
+ steps table + FK, tasks table + FK, order mode. **New record type with Tasks = a new descriptor, never a copy of the rules.**
33
+ Detail: toga25-desk docs.
34
+
35
+ ## How it works
36
+
37
+ - **Tables:** `TicketSteps` / `TicketStepTasks`, `ServiceRequestSteps` / `ServiceRequestStepTasks`. Steps have
38
+ `kind` ENUM `MANUAL` | `SCOPE` (`SCOPE` = copied from a Scope version; `MANUAL` = added by hand).
39
+ - **Task order lock:** a step is LOCKED while an EARLIER step (`sortOrder`, then `id`) is incomplete; a step is
40
+ complete when it has no tasks or all are done. Checked against current stored state, so unchecking a task in the
41
+ current or an earlier complete step stays allowed.
42
+ - Tickets (`ORDER_FLAG`): only while `Tickets.isTaskOrderEnforced` is on, over every step.
43
+ - Service requests (`ORDER_ALWAYS`): always, over `SCOPE` steps only; `MANUAL` steps never lock and are never locked.
44
+ - **Done stamp:** `isDone` change → `dtDone` = DB `NOW()` when done, NULL when undone; no `isDone` change = untouched.
45
+ - **SR resolve gate:** `ServiceRequest` set-stage gate refuses a resolved/closed stage or linked status while
46
+ `countOpenScopeTasks()` > 0 (manual tasks never block).
47
+ - **Reads go to the WRITE connection** (`writerQuery()`) so they see this request's own writes.
48
+
49
+ ## Gotchas
50
+
51
+ - **The SR open-task gate runs for signed-in users only** (same callers as the set-stage rule). API keys and workers
52
+ (integrations) can resolve/close an SR with open scope tasks — by design.
53
+ - Table/column names in SQL come only from `descriptor()` (hardcoded allowlist); never from input.
54
+
55
+ ## Change history
56
+ - 2026-10-23 — Created: shared step/task component; service requests get Steps/Tasks as descriptor 2 (TRUE-82871). (jcardinal)
@@ -6,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-23
10
10
  owners: [bala, apeterson, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
@@ -225,8 +225,10 @@ Core-level reference ids used (identical across all clients and environments): *
225
225
  - **An INNER join to an EMPTY FK empties the whole grid with NO error.** Third failure mode: an inner join on an FK that is unpopulated for the tenant drops every row (meta looks fine, data returns 0). Use LEFT OUTER for an optional FK, or verify FK population before choosing INNER.
226
226
  - **`PurchaseOrderItems` has no `itemId` — it reaches the catalog via `vendorItemId → VendorItems → Items`.** Converting a sales-order-item view (which joins Items directly) to purchase-order-items needs an extra **VendorItems** hop inserted before Items, with the Items join repointed to `VendorItems.itemId`. Don't just recolumn.
227
227
  - **Never add a many-to-many (bridge) OUTER join to a shared view** — every listing on that view gets duplicate rows. Use a separate parent-scoped view.
228
+ - **A FOREIGNKEY column with no `TableViewJoin` is shown as its raw id** (plain-field branch of `meta()`). Use this for a cross-cluster FK that cannot be joined (e.g. `Customers.clientId` → `Core.Clients`). Before 2026-10-23 `meta()` assumed every FK had a join and 500'd with `Undefined array key ""`. The model-config type lookup uses `?? null` because a RecordField can have an ACL grant but no model property.
228
229
 
229
230
  ## Change history
231
+ - 2026-10-23 — FK field with no join takes the plain-field branch (raw id); fixes `Undefined array key ""` on `GET /v2/table-views/meta` (TRUE-82871). (jcardinal)
230
232
  - 2026-10-17 — Many-to-many join in a shared view duplicates rows; use a parent-scoped view (TRUE-82870). (jcardinal)
231
233
 
232
234
  ## Related
@@ -6,7 +6,7 @@ project: API
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-17
9
+ updated: 2026-10-23
10
10
  owners: [apeterson, bala, jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Client/TableView.php
@@ -134,6 +134,7 @@ So the visible failure is a broken SQL query while the actual defect is one miss
134
134
  - **`recordFieldId` is cross-DB** — it indexes `Core.RecordFields`, not anything in the client DB. Resolve the id in `Core`, but insert the `TableViewFields` row in the client DB.
135
135
  - **Don't assume `Core.RecordFields`/`Core.Records` ids are stable across environments** — look them up by `field` name + record.
136
136
  - **Make column migrations idempotent** (`NOT EXISTS` guard) — they run per client DB.
137
+ - **Never hardcode `index` on a new `TableViewFields` row** — use `(SELECT MAX(index) FROM …) + offset` (derived-table-wrapped). `UNIQUE (tableViewId, index)` gives 1062 when another file already added a column to the same view.
137
138
  - **`isVisible=0` still costs a fetch** — the field is selected into the projection; only the render is suppressed.
138
139
  - **`type` drives rendering + filtering, NOT sort order** — a text column typed `NUMBER` still sorts alphabetically. To sort a label column by a rank, set `sortRecordFieldId` instead.
139
140
  - **`NUMBER` on a column with non-numeric values renders those cells BLANK** (`Number("kit 207")` → NaN). Check every tenant's data first.
@@ -152,5 +153,6 @@ So the visible failure is a broken SQL query while the actual defect is one miss
152
153
  - [TableView joins](../../_underscore/features/tableview-joins.md)
153
154
 
154
155
  ## Change history
156
+ - 2026-10-23 — New column rows: `index` = MAX + offset, never a literal (1062 on the unique key) (TRUE-82871). (jcardinal)
155
157
  - 2026-10-17 — `SELECT` type is safe only on a joined FK column; main-table enum → calc label + FilterOptions. (jcardinal)
156
158
  - 2026-09-23 — Recorded that `Core.RecordFields.type` drives display/filter only and never sorting; that `NUMBER` on a text column blanks non-numeric rows (Compass `kit 205-207` bundles) while `BOOLEAN` fixes genuinely blank flag columns; and that the flag columns are NULLable so a `= 0` migration guard matches nothing. (apeterson)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-19
9
+ updated: 2026-10-23
10
10
  owners: [bala, kyalamarthi, apeterson, ajean, jcardinal, rgirish]
11
11
  files:
12
12
  - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
@@ -560,7 +560,12 @@ an INSERT that names only `uuid`/`tableViewId`/`recordFieldId`/`isVisible`/`inde
560
560
  for a migration to change a calculated field's expression; there is none. See
561
561
  [FIELD_SQL calculated fields](../../_underscore/features/calculated-sql-fields.md).
562
562
 
563
+ - **`CONVERT(X'…' USING utf8mb4)` only in `VALUES` / `SET`, never in a comparison.** `WHERE col = CONVERT(X'…' USING utf8mb4)`
564
+ raises **1267 "Illegal mix of collations (utf8mb4_unicode_ci) and (utf8mb4_0900_ai_ci)"** (the converted literal
565
+ gets utf8mb4's default `0900_ai_ci`, the column is `unicode_ci`, and MySQL cannot pick one). Guard/compare by `HEX(col) = '…'` or a plain ASCII key (slug, uuid).
566
+
563
567
  ## Change history
568
+ - 2026-10-23 — `CONVERT(X'…' USING utf8mb4)` in a WHERE comparison = 1267; use it only in VALUES/SET (TRUE-82871). (jcardinal)
564
569
  - 2026-10-19 — `<>` copy-update guard skips a case-only change on a `_ci` column; use `BINARY` (TRUE-82915). (jcardinal)
565
570
  - 2026-10-17 — `TicketTypes.code` has no UNIQUE and may hold duplicates; guard seeds on `code` (TRUE-82889). (jcardinal)
566
571
  - 2026-10-16 — Backfill an api2-identifier column before the deploy (seed file), stragglers + ALTER after (TRUE-82841). (jcardinal)
@@ -6,8 +6,8 @@ project: Database Changes
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-30
10
- owners: [apeterson, rgirish]
9
+ updated: 2026-10-23
10
+ owners: [apeterson, rgirish, jcardinal]
11
11
  files:
12
12
  - Client/2026-08-11b - SalesOrderPurchaseOrdersField.sql
13
13
  - Client/2026-08-13 - UnitsForItemsPO_SalesOrderItemsJoin.sql
@@ -57,8 +57,12 @@ The model SELECTs every declared property, so one tenant DB without the column =
57
57
  - Fan-out folders (`Client/`) run per tenant, so a file can be present in one client schema and genuinely absent in another. Always verify against the specific schema, not "the environment".
58
58
  - **The migration runner can report success while skipping DBs.** 2026-09-30 `isOpen` rollout: `Client_Apiary`, `Client_Craftex` (both tables) and `Client_True` (`PurchaseOrderItems`) were missed; after `_underscore` deployed, prod raised Logs.Issue 895 `Unknown column 'isOpen' in 'field list'` 49 times until fixed with schema-qualified ALTERs. Always run the verify query above.
59
59
  - MySQL 8.0.39 (Dynamic row format): `ADD COLUMN ... AFTER` was instant even on 565k rows — size is not a reason to delay.
60
+ - **`SQL Error 1193 Unknown system variable 'X'` from a valid `UPDATE T` / `SET X = …` file = the editor ran only the `SET …` line** (run current statement / selection), which MySQL reads as a session-variable SET. The file is fine; run the whole script (DBeaver: Alt+X), then verify.
60
61
 
61
62
  ## Related
62
63
 
63
64
  - [dbchanges2 architecture](../architecture.md)
64
65
  - [Surface Layer Schema](../features/surface-layer-schema.md)
66
+
67
+ ## Change history
68
+ - 2026-10-23 — 1193 "Unknown system variable" = partial run of a multi-line UPDATE; run the whole script (TRUE-82871). (jcardinal)
@@ -22,7 +22,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
22
22
 
23
23
  ## 2.0 framework
24
24
 
25
- - **_underscore** (_Underscore) _(framework core)_ — 114 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
25
+ - **_underscore** (_Underscore) _(framework core)_ — 115 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
26
26
  - **worker2** (Worker) — 94 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
27
27
  - **api2** (API) — 27 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
28
28
  - **dbchanges2** (Database Changes) _(framework core)_ — 23 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
@@ -24,6 +24,7 @@ files:
24
24
  - worker/crons/sync/nycdoe/receive_edi_purchase_orders.php
25
25
  - worker/crons/sync/nycdoe/send_edi_open_invoices.php
26
26
  - worker/crons/notifications/nycdoe/
27
+ - worker1.5/crons/notifications/reports/send_daily_open_closed_doe_report.php
27
28
  - worker/schedules/cron.worker.sync.json
28
29
  - worker/schedules/cron.worker.notification.json
29
30
  - library/app/api/nycdoe.php
@@ -90,7 +91,8 @@ The NYCDOE/ServiceNow integration mirrors DOE's ServiceNow tickets (Incidents +
90
91
  | Site-id monitor | `email_notification_siteid.php` | daily 7:01 AM |
91
92
  | EDI 850 POs in → NetSuite SO + 997 ack | `receive_edi_purchase_orders.php` (`senderId == 'NYCDOE'` only) — see [edi-850-receive-and-997-ack.md](edi-850-receive-and-997-ack.md) | 9:30 AM & 3:30 PM |
92
93
  | EDI invoices out (NetSuite saved search 2108) | `send_edi_open_invoices.php` | every 2 h at :13 |
93
- | Installation schedule reports | `notifications/nycdoe/send_installation_schedules_doe.php` (3 business days ahead, weekends skipped, **holidays NOT skipped**), daily + current-week variants, weekly part-orders | 5–8 AM daily/Mondays |
94
+ | Installation schedule reports | `notifications/nycdoe/send_installation_schedules_doe.php` ("NY Minute Schedule", 7:00 AM; 3 business days ahead, weekends skipped, **holidays NOT skipped**), daily + current-week variants, weekly part-orders | 5–8 AM daily/Mondays |
95
+ | Daily open/closed report | `notifications/reports/send_daily_open_closed_doe_report.php` ("closed" tabs = rows updated in the last 24 h from `NOW()`) | daily 1:00 AM |
94
96
 
95
97
  **Unscheduled / dormant:** `download_tickets*.php` (superseded by `import_inc.php` / `import_asn.php`), `send_installation_schedules.php` (base variant). **Worker branch `TRUE-80519` (commit `9e1dc9afa`, takes effect when that PR merges):** the 5 DOA files (`5_DOA_set_item_fulfillment_as_shipped.php`, `DOA_3_B_create_installation_ticket.php`, `DOA_3_create_installation_ticket.php`, `DOA_3_create_installation_ticket_savedsearch.php`, `DOA_delete_query_for_doe.php`) move to `crons/sync/DOA/`; `doe_install_fix.php`, `backfill_technician_identities.php`, `test_assignment_revert_guard.php`, `test_multi_po_receipt_resolution.php` are deleted (none scheduled or called; recover from git history before `9e1dc9afa`). Note `sync/syncro/download_tickets.php` in the schedule is the **Syncro** integration, not NYCDOE. A second `crons/infrastructure/import_asn.php` also calls the asnprocessors but is **not scheduled** — only `legacy_import_asn.php` runs.
96
98
 
@@ -177,6 +179,7 @@ All **live** DOE business logic is 1.0. The only other 2.0 code is the in-progre
177
179
  5. With no staging environment, verify with a standalone arithmetic simulation plus a read of the consumer query; `php -l` every touched file.
178
180
 
179
181
  ## Gotchas
182
+ - **Re-running a missed DOE report by hand** (steps in [Diagnosing worker1.5 crons](../../../1.0/apps/worker1.5/workflows/diagnosing-crons-not-running.md)): safe to repeat, but each run emails DOE again (no duplicate guard). A late open/closed run moves its 24 h window, so closures between 1:00 AM and the late run drop out of the "closed" tabs. Both report crons are clean on PHP 8.5 since library TRUE-83159 (byte-identical xlsx on 7.2 / 8.1 / 8.5).
180
183
  - **OPEN (seen 2026-10-22, not investigated) — `Undefined offset` at `library/app/asnprocessor/apple.php:74`** from `legacy_import_asn.php`; prod `Logs.Issue` #1115, new that day.
181
184
  - **⚠ OPEN BUG (found 2026-10-19, not fixed) — 1.0 Lexmark/CDW dates save as `1969-12-31`.** CDW sends `YYYY/MM/DD`, but `library/app/asnprocessor/lexmark.php` reads dates with `App_Date::convertMDYYYYtoSQLDate`. All 5,244 of 5,244 `App_ASNProcessor_Lexmark` queue rows since 2026-09-01 have `dateShipped` (and other dates) = 1969-12-31. 2.0 Parse reads `YYYY/MM/DD` correctly. Never trust a 1.0 Lexmark/Acer ship date.
182
185
  - **⚠ OUTAGE (fixed TRUE-82735, not deployed 2026-10-14) — the RITM and POD crons never finished for months.** Signs: `Common.CronJobExecutions.dtCheckOut` NULL on every run, zero ServiceNow calls. RITM cron: Feb–Mar 2026 refactors dropped the per-MSO inner loop → `Logs.Issue` 1R *"Undefined variable: row2"* every run since at least 2026-05-07 (last sane version `ce98cd2b5`). POD cron: the file query had **no `ORDER BY`**, so every run hit the same poison file (RITM1387575, already closed → empty-body 200 on `-7` → `Logs.Issue` 1S *"Empty response body from API (HTTP 200)"*), died, never set `dtTransmitted`, and re-uploaded that POD every 5 min; 6,718 files stuck, last sent 2026-09-04. Lesson: an unordered work queue + one poison row + a run-killing throw = a silent total outage; order the queue, isolate per row, and capture. **Still broken on worker1.5 (2026-10-23, TRUE-83163):** its `crons/sync/nycdoe/send_request_item_updates.php` (copied in `58dab19`) has undefined `$row2` / `$locationSysId` / `$sendLocationUpdate` / `$ritmNumber` — a git bug, not PHP 8; left unfixed by choice.
@@ -295,6 +298,7 @@ All **live** DOE business logic is 1.0. The only other 2.0 code is the in-progre
295
298
  - Local deep-dive (full WR260236464 case study with record IDs): `doe_service_now_integration.md` in Mark's WWW workspace root; also the `service-now-integration` skill (`asn-import-flow.md`)
296
299
 
297
300
  ## Change history
301
+ - 2026-10-23 — Daily open/closed report row; manual re-run gotcha (duplicate email, 24 h window shift) (TRUE-83159) (bala)
298
302
  - 2026-10-23 — RITM cron still broken in the worker1.5 copy (undefined vars); not fixed on TRUE-83163 (jcardinal)
299
303
  - 2026-10-22 — Open: new `apple.php:74` Undefined offset (`Logs.Issue` #1115), seen during TRUE-82983 (jcardinal)
300
304
  - 2026-10-22 — TRUE-83064: repaired customer PO `WR270089579` (two ASNs, Acer + Lexmark vendor) via dbchanges#270; open-bug gotcha — Stage 5 takes the manufacturer from the ASN vendor and keys the MSO on the ASN; cross-received serials dead-end silently; 152 mislabeled assets on Lexmark model `23782` remain. No code change. (sking)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.1133",
3
+ "version": "1.0.1135",
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",