toga-ai 1.0.613 → 1.0.615

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.
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-08-18
9
+ updated: 2026-08-19
10
10
  owners: [kyalamarthi, jcardinal]
11
11
  files:
12
12
  - tools/assets/clickup/sprint-dashboard.html
@@ -37,7 +37,8 @@ The panel is **state-adaptive: calm when healthy, loud when broken.**
37
37
  cards that **grow to share the column height** (min-height floor, scroll when many). Each
38
38
  affected card puts its icon centered **above** the text, larger, with a role-colored full
39
39
  border; monitor names **wrap** (they used to truncate on the TV). Non-green states keep the
40
- panel header + padded card layout.
40
+ padded card layout. There is **no panel header** in any state — the `mon-head` block (the
41
+ "Status" title + the "live · updated Xs ago" meta line) was removed.
41
42
  - **Unknown** = a neutral grey "STATUS UNKNOWN — Monitor data unavailable" when there is no
42
43
  usable data — this closes the old "0 monitors parsed looks like all-green" gap.
43
44
 
@@ -121,9 +122,12 @@ buckets correctly.
121
122
  ### Panel presentation — state-adaptive (supersedes the solid status circle)
122
123
 
123
124
  The old design (a solid `MonitorCircle`, a per-status legend with counts + percentages, and a
124
- redundant status pill) was **removed**, and the panel header renamed **"Monitor status" →
125
- "Status"**. The governing decision (developer's "Option 2"): **calm when healthy, loud when
126
- broken.**
125
+ redundant status pill) was **removed**. The panel header was first renamed **"Monitor status" →
126
+ "Status"** and then (2026-08-19) **removed entirely** the `mon-head` block held the "Status"
127
+ title (`mon-title`) plus a "live · updated Xs ago" meta line (`mon-meta`); both are gone, along
128
+ with the now-unused `meta` computation and the `okAt` prop that fed it (`now = useNow(1000)`
129
+ stays — the incident list still uses it for ticking state durations). The governing decision
130
+ (developer's "Option 2"): **calm when healthy, loud when broken.**
127
131
 
128
132
  - **`healthy` now requires `total > 0 && rows.length === 0`.** A green hero can no longer show on
129
133
  an empty/malformed response — that renders **Unknown** instead (see below).
@@ -254,6 +258,11 @@ so a reduced-motion viewer never lands on a fully-invisible glow.
254
258
  `.alert-flash`, this same rule also covers the slow `alert-breathe` breathe.
255
259
 
256
260
  ## Change history
261
+ - 2026-08-19 — Removed the monitor panel header entirely. The `mon-head` block — the "Status"
262
+ title (`mon-title`) and the "live · updated Xs ago" meta line (`mon-meta`) — was dropped from
263
+ `MonitorPanel`, together with the now-unused `meta` computation and the `okAt` prop. `now =
264
+ useNow(1000)` was kept (the incident list still ticks state durations off it). Visual cleanup of
265
+ the unattended wall display's status pane. (jcardinal)
257
266
  - 2026-08-18 — **State-adaptive redesign + server-side proxy + Unknown state + /developer rename.**
258
267
  Replaced the solid `MonitorCircle` / legend / percentages / status pill with a calm full-height
259
268
  green hero (full-bleed flush in the green state) vs. a saturated amber/red band with growing,
@@ -0,0 +1,101 @@
1
+ ---
2
+ type: session
3
+ slug: Department-Revert
4
+ title: NYC DOE ticket department and technician revert loop
5
+ author: sking
6
+ repos: [worker, library, dbchanges]
7
+ framework: "1.0"
8
+ client: nycdoe
9
+ status: active
10
+ created: 2026-08-17
11
+ updated: 2026-08-17
12
+ ---
13
+
14
+ # Session: Department-Revert
15
+ **Date:** 2026-08-17
16
+ **Project/Repo:** worker + library + dbchanges (1.0)
17
+ **Task:** Root-cause why NYC DOE repair order 838745 (ServiceNow INC2234900) kept reverting its assigned technician and department in both TogaDesk and ServiceNow, and prevent it for all tickets.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+
23
+ - **Root cause found via production DB forensics, not code reading.** `repair_order_history` for repair 838745 showed the *same* correction applied four times: `Department: DOE Calls Borough -> DOE Manhattan Borough` plus `Technician unassigned: CJackson64@schools.nyc.gov`. The department can only move *from* Calls Borough repeatedly if something silently reset it *to* Calls Borough in between — and that something wrote no history row. This framing is what cracked it.
24
+ - **Three defects identified in `worker/crons/sync/nycdoe/process_tickets.php`** (inbound cron, `*/15`, registered `worker/schedules/cron.worker.sync.json:606`):
25
+ 1. Department overwritten unconditionally on every inbound update, with no comparison to local state — unlike status, which already had an anti-downgrade rank guard directly below it.
26
+ 2. `$ticketDepartment` never reset between `do/while` iterations, so a ticket with an empty / unmapped / `SD QA` `assignment_group` silently inherited the **previous ticket's** department *and* `contractId`.
27
+ 3. Technicians resolved against `people.name` while ServiceNow sends an **email** (`u_technician`). The lookup could therefore only match a row whose *name* was an email — an orphan the cron itself auto-created. It only ever INSERTed, never removed, and used `fetchOne()` where it needed every technician on the order.
28
+ - **Newest-edit-wins guard verified against live data.** SQL returned `guard_allows_overwrite = 0` for 838745 (local `dtUpdated` epoch 1786558258 vs SNOW `sys_updated_on` 1786541919 — local ~4.5h newer). Post-deploy re-check: `ticketDepartmentId = 60` held, and **0** department history rows since 2026-08-13 (it had been flipping ~daily).
29
+ - **Timezone assumption verified before relying on it.** DB session and global `time_zone` are `US/Central`; framework `_.php` forces `America/Chicago`. Same zone, so the epoch comparison is valid. A mismatch here would have made the guard wrong by hours.
30
+ - **Loop-reset contract validated by reimplementation** — pre-fix logic produces `[60, 60]` across two tickets, post-fix produces `[60, null]`. Confirms the cross-ticket contamination was real, not theoretical.
31
+ - **Closed-status prefix matching validated against every real status.** `WORK_IN_PROGRESS_DIAGNOSTIC_COMPLETE` contains "COMPLETE" but `LIKE 'COMPLETE%'` correctly excludes it. A substring match would have frozen 551 in-progress orders. Now pinned by a test.
32
+ - **Orphan scale measured:** 105 orphan `people` rows (name IS an email, no email, no sys_id); 394 assigned technicians with no `referenceId`; 161 resolvable; 58 missing email. Reference case: people **56271** (`CJackson64@schools.nyc.gov`, 190 orders) vs real Calvin Jackson **9781** (`cjackson@togatech.com`, sys_id `775fafea…d43ed`, 799 orders), with duplicate **10225** sharing that sys_id.
33
+ - **Phase 1 + 2 shipped and merged:** dbchanges#264, library#861, worker#1696. Migration has been RUN — `people.serviceNowEmail` exists (varchar, nullable, `MUL`), `referenceId` now indexed, `email` still `UNI` and untouched, and the migration's own acceptance criterion met exactly: **0 populated of 54,528 rows**.
34
+ - **Orphan spread has stopped.** Newest order carrying an orphan is `2026-08-13 08:45:03`; totals fell 10,111 -> 9,934 orders and 1,024 -> 822 open assignments. Remaining orphan links are leftover state, not active damage.
35
+ - **Schema-guard PRs opened:** library#863 (`App_Database::columnExists()`) and worker#1697 (both crons degrade to `people.email`).
36
+
37
+ ## What did NOT work — DO NOT RETRY THESE
38
+
39
+ - **`php -l` cannot be run on this machine.** `which php` -> `php not found`; no `docker` either. Every PHP file in this work is **syntax-unverified**. Do not claim lint passed. Structural brace/paren balance checks were used as a weak substitute.
40
+ - **Naive brace-balance checking produces false positives.** `library/app/api/nycdoev2.php` reports `curly=1` and `library/app/database.php` reports `paren=1` — both are **pre-existing at HEAD**, artifacts of the comment/string stripper, not real imbalances. Always diff the imbalance against `git show HEAD:<file>` before believing it.
41
+ - **Grouping a classification `CASE` by its alias returns duplicate group labels.** The first orphan-classification query returned `MISSING_REFERENCE_ID` twice with different counts and zero `ORPHAN_EMAIL_AS_NAME`, because the `repair_order_technicians` join fans out per assignment. Fix: dedupe per `people.id` in a derived table first, then aggregate. Correct numbers only came out after that.
42
+ - **`git checkout -b <x> origin/_main` fails: `fatal: couldn't find remote ref _main`.** These repos use **`_production`** as the default branch, not `_main`, despite the team git-workflow rule naming `_main`. `origin/HEAD -> origin/_production`.
43
+ - **Branching from a stale `origin/_production` caused a conflicted stash pop** (`UU` on both cron files). `git show origin/_production:…| grep -c serviceNowEmail` returned `0` even though worker#1696 was merged. **Always `git fetch origin` before cutting a branch off a recently merged base.** Recovery was `git reset --hard origin/_production` + re-apply (stash preserved throughout).
44
+ - **`COALESCE(serviceNowEmail, email)` was the wrong shape** — it makes the two addresses mutually exclusive, so a technician resolvable today could stop resolving. Replaced with a two-pass build that indexes BOTH keys, ServiceNow address winning on collision.
45
+ - **Wrong claim, corrected: "zero technicians have a schools.nyc.gov address."** 14,162 `people` rows DO have one in `email` — they are DOE **requesters**, not technicians. Among *assigned technicians*: 0 in `email`, 41 in `name`. Always state which column.
46
+ - **Wrong claim, corrected: "182 queued is safely below `MAX_TICKETS = 200`."** That guard tests `$res->num_rows > 200` against a `SELECT COUNT(...)` query, which always returns **1 row** — so it never fires at any queue depth. Dead code, not a safety valve.
47
+ - **A code deploy ahead of its migration takes the sync down silently.** worker#1696 merged **11:02 CDT**; last successful `process_tickets` run **11:01:36 CDT**. `Unknown column 'people.serviceNowEmail'` every 15 min for **2h34m**, 182 incidents queued. `import_inc` kept succeeding the whole time, so nothing looked broken from outside. Merging a dbchanges PR is NOT the same as running the migration.
48
+
49
+ ## Not tried yet (candidates for next session)
50
+
51
+ - **Run `worker/crons/sync/nycdoe/backfill_technician_identities.php`** — dry run, review CSV, then `--apply`. Requires the worker host (PHP + ServiceNow credentials). Not runnable from a dev box.
52
+ - **History logging for cron-driven department/technician changes** — highest-leverage remaining mitigation; the absence of it is why this bug survived weeks.
53
+ - **Queue-depth alerting** on `NYCDOETickets.dtProcessed` staleness, and alerting on the new `unmatched u_technician email` / `no ServiceNow sys_id` log lines.
54
+ - **Fix the `MAX_TICKETS` `num_rows` bug** in `process_tickets.php`.
55
+ - **Model-level column guard** — `App_Model` builds SELECT/UPDATE from declared fields, so `App_Model_TogaDesk_People` would still break if `serviceNowEmail` were absent. `columnExists()` only guards raw SQL.
56
+ - **`referenceId` backfill** for the 132 technicians with no sys_id (outbound cannot name them to SNOW).
57
+ - **`togadesk/desk/includes/controllers/actions/central/assignDepartment.php:5-16`** — silently deletes ALL technicians whenever a department is set, with no history row.
58
+ - **`bulkEditOnsiteServiceTickets.php:51-53`** — calls `->save()` where it means `->delete()`, so bulk unassignment silently no-ops.
59
+ - **`togadesk/desk/includes/controllers/data/central/view.php`** — interpolates `$_GET['id']` raw into SQL at L22, L45, L105, L182, L343.
60
+ - **Rotate the ServiceNow OAuth credentials** hardcoded in plaintext at `library/app/api/nycdoev2.php:36-50` (client id / secret / refresh token for both stage and production). Pre-existing, committed, untouched by this work. Treat as compromised per the team security rule.
61
+
62
+ ## Current file state
63
+
64
+ | File | Status | Notes |
65
+ |------|--------|-------|
66
+ | `worker/crons/sync/nycdoe/process_tickets.php` | MERGED (#1696) + PR #1697 | Newest-wins dept guard, per-tick state reset, email-based technician matching, no auto-create. #1697 adds the `columnExists` fallback. |
67
+ | `worker/crons/sync/nycdoe/send_ticket_updates.php` | MERGED (#1696) + PR #1697 | Compares against `serviceNowEmail` w/ `email` fallback; no longer blanks `u_technician` when sys_id missing (logged no-op). |
68
+ | `worker/crons/sync/nycdoe/report_technician_identities.php` | MERGED (#1696), NEVER RUN | Read-only classifier. Manual, unscheduled. |
69
+ | `worker/crons/sync/nycdoe/backfill_technician_identities.php` | MERGED (#1696), NEVER RUN | Dry-run default, `--apply` writes. Needs worker host. |
70
+ | `worker/crons/sync/nycdoe/test_assignment_revert_guard.php` | MERGED (#1696), NEVER RUN | Asserts A–F. Read-only, exits non-zero on failure. |
71
+ | `library/app/model/togadesk/people.php` | MERGED (#861) | Declares `$serviceNowEmail`. |
72
+ | `library/app/api/nycdoev2.php` | MERGED (#861) | Adds `getUserByEmail()`, mirroring the zero-caller `getUserSysIdByName()`. |
73
+ | `library/app/database.php` | PR #863 OPEN | Adds `App_Database::columnExists()`, request-cached. |
74
+ | `dbchanges/TOGaDeskSupport/SK/2026-08-12-add-people-servicenow-email.sql` | MERGED (#264) + **RUN in prod** | Column + 2 indexes. Verified: 0 populated of 54,528. |
75
+
76
+ Untouched and NOT mine, left alone deliberately: `library/app/api/carrier/ups.php` (uncommitted on `hotfix/nycdoe-duplicate-unit-creation`), `worker/crons/sync/nycdoe/test_po168215_receipt_resolution.php` (untracked).
77
+
78
+ ## Decisions made
79
+
80
+ - **Newest-edit-wins** for department/technician conflict. Rejected "ServiceNow authoritative" (would require removing the fields from the TogaDesk UI or techs keep losing edits) and "TogaDesk authoritative" (drifts when DOE legitimately reroutes). Mirrors the existing status anti-downgrade guard. 60s clock-skew margin because the two timestamps come from different clocks.
81
+ - **Dedicated `people.serviceNowEmail` column.** Rejected overwriting `people.email` — it is UNIQUE, gates login/password-reset on `count(...) == 1` (a collision locks the account out), addresses ~10 notification send sites, and the AD sync **rewrites it every run** so the value would not persist. Rejected reusing `referenceId` (already overloaded: SNOW sys_id for DOE, TOGA2 contact UUID in `toga2.php`) and `ldap_user`.
82
+ - **Index BOTH email columns rather than `COALESCE`.** Guarantees no technician who resolves today stops resolving, before any backfill. Pinned by `test_technician_still_resolves_via_people_email_when_column_is_null()`.
83
+ - **Non-unique index** on `serviceNowEmail` — a UNIQUE index would break the AD sync insert path the same way `email` already does.
84
+ - **ServiceNow `sys_id` as the mapping source**, matched locally on `referenceId`. Rejected name/surname heuristics: `CJackson64` fits four real Jacksons (Calvin, Cecilia, Cari, Charles). Backfill refuses to act on ambiguity rather than guessing.
85
+ - **Repoint open orders only** (968 at planning, 822 now). Closed orders (9,143) keep their historical technician — already invoiced and signed off.
86
+ - **Deactivate orphans, never delete** — history rows still reference them.
87
+ - **Outbound no longer blanks `u_technician`** when a technician has no sys_id; that wipe is the outbound half of the same reversion bug. Two-line revert if reviewers disagree.
88
+ - **Guard degrades rather than fails fast** — falling back to `people.email` keeps the sync alive; failing fast would have produced the same 2h34m outage with a nicer message.
89
+
90
+ ## Blockers
91
+
92
+ - **No PHP runtime on the dev machine.** `php -l` has never run against any file in this work, and none of the three new scripts have been executed. Stated explicitly in every PR.
93
+ - **Backfill cannot be run from a dev box** — needs the worker host with ServiceNow credentials.
94
+ - **library#863 must merge before worker#1697** (the worker code calls `App_Database::columnExists()`).
95
+
96
+ ## Exact next step
97
+
98
+ > Merge **library#863** first, then **worker#1697**. Then on the **worker host** run `php crons/sync/nycdoe/backfill_technician_identities.php` (dry run — no `--apply`), read the CSV written to the cache folder as `<date>-nycdoe-technician-backfill-dryrun.csv`, and check the `NO_LOCAL_PERSON` and `AMBIGUOUS_LOCAL_MATCH` buckets — not just `RESOLVED` — before re-running with `--apply`.
99
+
100
+ ---
101
+ _Saved by /session-save on 2026-08-17_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.613",
3
+ "version": "1.0.615",
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",