toga-ai 1.0.899 → 1.0.901
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/INDEX.md +1 -1
- package/knowledge/clients/nycdoe/INDEX.md +1 -1
- package/knowledge/clients/nycdoe/features/hold-status-sync.md +20 -6
- package/knowledge/clients/nycdoe/features/servicenow-integration.md +3 -1
- package/knowledge/clients/nycdoe/profile.md +2 -3
- package/knowledge/standalone/apps/togatech/INDEX.md +1 -0
- package/knowledge/standalone/apps/togatech/architecture.md +15 -7
- package/knowledge/standalone/apps/togatech/features/seo-aeo-geo-prerender.md +9 -1
- package/knowledge/standalone/apps/togatech/features/talos-product-page.md +58 -0
- package/knowledge/standalone/apps/togatech/workflows/local-dev-and-headless-verification.md +7 -3
- package/package.json +1 -1
package/knowledge/INDEX.md
CHANGED
|
@@ -39,7 +39,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
39
39
|
|
|
40
40
|
## standalone framework
|
|
41
41
|
|
|
42
|
-
- **togatech** (TOGA Technology Website) —
|
|
42
|
+
- **togatech** (TOGA Technology Website) — 7 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
|
|
43
43
|
- **websocket** (WebSocket Server) — 2 doc(s) → [standalone/apps/websocket/INDEX.md](standalone/apps/websocket/INDEX.md)
|
|
44
44
|
- **forward** (Forwarder) — 3 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
|
|
45
45
|
- **claude** (Claude Harness) — 8 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
|-----|-----------|---------|
|
|
5
5
|
| [NYCDOE 2.0 ASN SFTP Ingestion + Parsing (Sftp/Import → Sftp/Parse → Case/ServiceRequest)](features/asn-sftp-ingestion-2-0.md) | 2.0 | 2.0 rewrite of DOE's vendor-SFTP ASN intake (TRUE-80519 Phases 1–2): per-manufacturer fetch to `Files`, header-validated parse into one Case per customer PO + o |
|
|
6
6
|
| [NYCDOE EDI 850 Receive & 997 Acknowledgement](features/edi-850-receive-and-997-ack.md) | 1.0 | DOE's EDI 850-in → NetSuite SO + inline 997-out ack; open for the 997 build, its "always accept" stub, or a DOE ack complaint. |
|
|
7
|
-
| [NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)](features/hold-status-sync.md) | 1.0 | Bidirectional DOE ticket hold-status sync (ServiceNow ⇄ TOGaDesk), the four defects that let holds silently revert or never re-send,
|
|
7
|
+
| [NYCDOE Ticket Hold-Status Sync (ServiceNow ⇄ TOGaDesk)](features/hold-status-sync.md) | 1.0 | Bidirectional DOE ticket hold-status sync (ServiceNow ⇄ TOGaDesk), the four defects that let holds silently revert or never re-send, the held→complete close clo |
|
|
8
8
|
| [NYCDOE ServiceNow / ASN Integration](features/servicenow-integration.md) | 1.0 | Full DOE ServiceNow/ASN integration map — ticket mirror, ASN → NetSuite SO/TOGa Desk pipeline, outbound status/POD, EDI; open for any DOE cron/pipeline work or |
|
|
9
9
|
| [New York City Department of Education](profile.md) | 1.0 | NYC DOE client profile (1.0 worker tier live; 2.0 ASN rewrite in progress), ServiceNow + EDI; open for DOE's platform scope, endpoints, API clients, and where i |
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: nycdoe
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-25
|
|
10
10
|
owners: [mhammontree, sking]
|
|
11
11
|
files:
|
|
12
12
|
- worker/crons/sync/nycdoe/send_ticket_updates.php
|
|
@@ -23,7 +23,7 @@ related:
|
|
|
23
23
|
- ../../../1.0/apps/worker/architecture.md
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
Bidirectional DOE ticket hold-status sync (ServiceNow ⇄ TOGaDesk), the four defects that let holds silently revert or never re-send, and the outbound cron's selection/throttle model; open before changing DOE hold logic, the selection query, or "which SNOW field is authoritative."
|
|
26
|
+
Bidirectional DOE ticket hold-status sync (ServiceNow ⇄ TOGaDesk), the four defects that let holds silently revert or never re-send, the held→complete close clobber (TRUE-82332), and the outbound cron's selection/throttle model; open before changing DOE hold/close logic, the selection query, or "which SNOW field is authoritative."
|
|
27
27
|
|
|
28
28
|
## Summary
|
|
29
29
|
|
|
@@ -73,6 +73,17 @@ When a DOE repair order is on hold, `send_ticket_updates.php` pushes **the full
|
|
|
73
73
|
- The "Assigned → In Progress" auto-start block is still guarded so it never pushes `state:2` over a held order (TRUE-79922). Named constants: `SN_STATE_IN_PROGRESS` / `SN_STATE_ON_HOLD` / `SN_HOLD_REASON_DEFAULT` in `send_ticket_updates.php`.
|
|
74
74
|
- The existing **empty-body patch-response check** (~L493) still **throws**, so any remaining rejection surfaces loudly. Keep it.
|
|
75
75
|
|
|
76
|
+
### Close vs. hold-release — held→complete never resolved (TRUE-82332)
|
|
77
|
+
**Bug (silent, 2026-09-18 → fix):** for a COMPLETE order the close block sets `state` 6 (8 for `STATUS_COMPLETE_EXCEPTION`) + `close_code` + `close_notes`. The later hold-release block (`!$orderIsOnHold && $snState == 'On Hold'` → `state` 2) **overwrote** it with 2 (In Progress). The PATCH succeeded, `dtSynced` advanced to the newest note, `close_code` now matched ServiceNow (so the `close_code != remote` check never rebuilt the close), and the order left the queue (COMPLETE is eligible only while notes > `dtSynced`). No error, no alert.
|
|
78
|
+
- **Why it surfaced 2026-09-18:** before TRUE-82129 hold pushes did not reach ServiceNow, so almost nothing was On Hold remotely at completion. After it, every held→complete order hit this.
|
|
79
|
+
- **Evidence:** 45 incidents 2026-09-18..09-25, each one close PATCH with `"state":2`, none with 6/8; non-held closes in the same window sent 6. Example INC2131474: GET `Logs.API` id 173203251 "On Hold" → PATCH id 173203252 (2026-09-22 15:08:32) `close_code` "Repaired" + `state` 2 → next GET "In Progress", no more traffic.
|
|
80
|
+
- **Fix (worker `a04c5452a`, branch TRUE-82332, not deployed as of 2026-09-25):** (a) the release block sets `state` 2 only when `!isset($payload['state'])`; (b) the close block also fires when the ServiceNow state is not in `$serviceNowClosedStatesArray`, even if `close_code` matches — so a re-queued stuck ticket gets its close re-sent.
|
|
81
|
+
- **Rule: a later block in the payload builder must not overwrite a `state` an earlier block already set.** Close outranks release.
|
|
82
|
+
- **No PATCH storm:** the earlier "complete + SN Resolved/Closed/Canceled" check retires the ticket before the close block; `dtSynced` moves only after a successful PATCH.
|
|
83
|
+
- **OPEN:** not confirmed ServiceNow accepts On Hold → Resolved (6) in one step. If rejected: empty-body 200 → `$failedPatches`, `dtSynced` not moved, retried every 6-min run, alert email max 1/hr, until someone steps in.
|
|
84
|
+
|
|
85
|
+
**Recovering tickets already stuck (one-off, run AFTER the worker deploy):** they left the queue, so re-queue them — set `Common.NYCDOETickets.dtSynced` = newest `repair_order_notes.notesdate` − 1 second, pinned in the `WHERE` on `id` + `ticketNumber` + current `dtSynced`. Only the newest note becomes "unsent"; verify it is already in ServiceNow (`referenceId` set / not internal) so no duplicate `work_notes`. **Run it before the deploy and it re-sends `state` 2 and drops them again.** Tickets since closed by hand in ServiceNow are harmless: the "both closed" check retires them with one GET, no PATCH. One-off nudge — not a `dbchanges` file.
|
|
86
|
+
|
|
76
87
|
> **DECISION (TRUE-81044) — do NOT map TogaDesk hold reasons onto ServiceNow hold-reason codes.** `hold_reason` stays pinned to `10` ("Other") and the human-readable reason is carried entirely in `u_other_reason_on_hold` / `work_notes`, sourced from TogaDesk. Rationale: ServiceNow's own states are not reliable, so mapping our six `HOLD_*` constants onto their code set buys nothing and adds a second source of truth. **Do not re-propose a reason-code mapping.**
|
|
77
88
|
|
|
78
89
|
### Outbound selection, watermark & throttle (TRUE-82027 current shape)
|
|
@@ -122,7 +133,7 @@ All three `error_log()` calls were removed (the per-ticket technician sys_id mes
|
|
|
122
133
|
- **The invalid-assignment-group email had never sent, ever** — the block built the email and never called `$email->send()` (every sibling DOE cron does, e.g. `2_send_serials_to_netsuite.php:346`). Added, behind the same throttle on its own key `system|ticket_sync_invalid_group_alert`.
|
|
123
134
|
|
|
124
135
|
### Verifying a TRUE-82027-class change (no sandbox — verified in production)
|
|
125
|
-
There is **no sandbox for the SNOW integration**; verification happens in prod. Deploy order is **dbchanges → library → worker**, and dbchanges `_production` does **not** auto-apply — the migration lands on the next worker EB deploy. After deploy, check legacy `Logs.API`
|
|
136
|
+
There is **no sandbox for the SNOW integration**; verification happens in prod. Deploy order is **dbchanges → library → worker**, and dbchanges `_production` does **not** auto-apply — the migration lands on the next worker EB deploy. After deploy, check legacy `Logs.API` (query shapes in *Reference facts* — `LIKE` scans time out) for:
|
|
126
137
|
1. A held order PATCHing `state:3` with **no new note**.
|
|
127
138
|
2. INC2087375's `u_eta` moving from 2026-09-07 to 2026-09-17.
|
|
128
139
|
3. `SELECT COUNT(*) FROM Common.NYCDOETickets WHERE dtLastChecked IS NOT NULL` climbing.
|
|
@@ -160,7 +171,8 @@ Making DOE holds persist took a change in **three repos**, with a **deploy-order
|
|
|
160
171
|
The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets`) is **intentionally kept** as a failover for SNOW API unreliability — not removed by this work.
|
|
161
172
|
|
|
162
173
|
### Reference facts (DOE sync debugging)
|
|
163
|
-
- **SNOW request/response logs:** logged to the legacy `Logs` schema, **`API` table** (`App_Model_Logs_API`, `db_logs`, `_databaseNameOverride 'Logs'`). `requestPayload` / `responsePayload` are queryable CHAR columns — the primary debugging avenue.
|
|
174
|
+
- **SNOW request/response logs:** logged to the legacy `Logs` schema, **`API` table** (`App_Model_Logs_API`, `db_logs`, `_databaseNameOverride 'Logs'`). `requestPayload` / `responsePayload` are queryable CHAR columns — the primary debugging avenue. Time column is **`dtStamp`** (not `dtCreated`). The custom DOE endpoint base is **`/api/nycd3/`** on prod host **`supportadmin.schools.nyc`** (not `nycdoeprod.service-now.com`).
|
|
175
|
+
- **Query shape that works (TRUE-82332):** `endpoint LIKE '%nycd3%...'` and broad `requestPayload LIKE` scans time out (>300s) on the MCP. Use exact matches: PATCH `endpoint = 'https://supportadmin.schools.nyc/api/nycd3/data/tasks/INCxxxx'`; GET `endpoint = 'https://supportadmin.schools.nyc/api/nycd3/v2/data/tasks?domain=diit%2Calm&filter=sys_class_name%3Dincident%5Enumber%3DINCxxxx'`. For sweeps: `FORCE INDEX (dtStamp)`, windows ≤2 days, plus `sourceJob` / `method` filters. The state:3↔state:2 oscillation in TRUE-79922 was proven from this table (INC2074899, INC2185183 showed `state:3` then `state:2` pushed seconds apart in one run).
|
|
164
176
|
- **`NYCDOETickets` (`App_Model_Common_NYCDOETicket`, `db_common` / legacy `Common`)** stores the inbound SNOW record in `rawData`, a **`FIELDTYPE_BLOBSTORAGE`** field (S3 bucket `asifiles`, key `prod/Common/NYCDOETickets/rawData/{id}`) — **NOT a queryable DB column.**
|
|
165
177
|
- **⚠ `repair_orders.dtUpdated` is skewed — never build a selection query on it.** It is written exactly **240 minutes behind** the clock used by `repair_order_notes.notesdate` and `NYCDOETickets.dtSynced`. **Do NOT compensate with `+240 MINUTE`** — 240 is almost certainly UTC-vs-Eastern and becomes 300 at the DST change (2026-11-01), so a hardcoded offset breaks twice a year. TRUE-82027 sidestepped it by removing `dtUpdated` from the query. Mechanism + the second (720-minute) skew: [Field-Service Dispatch](../../../1.0/apps/togadesk/features/field-service-dispatch.md).
|
|
166
178
|
- **⚠ `Logs.API` history is capped at 30 days from 2026-09-17** (Jeff, sprint review) and existing logs are being purged. This investigation depended on history far older than that (9,914 PATCHes back to 2026-08-20; a hold last pushed 22 days earlier; the 2,392-call storm). **Slow-burn bugs like this can no longer be traced backwards — they must be caught by alerting.** NYCDOE stays 1.0 so its logs stay in legacy `Logs.API`; Compass/Elite/WJE move their API logging to 2.0. Agreed: hold the purge until TRUE-82027 is verified in production.
|
|
@@ -181,14 +193,16 @@ The SNOW round-trip note reconciliation (delete-and-reinsert in `process_tickets
|
|
|
181
193
|
- **Diagnosing which writer dropped a hold:** `repair_order_history` (`repairid, userid, dtStamp, note`) records *user-driven* status changes. A hold reverting with **no** history row between the hold and the revert = a raw-`UPDATE` writer (`updateStatus`/`qqStatus`), **not** the worker inbound cron (`process_tickets.php` contains no reference to `ORDER_ASSIGNED_AWAITING_SCHEDULING`). Note that **since TRUE-80060** a *status-changing* `class.repair.php::addNotes` now **calls** `updateStatus()` (so it too can move the cache via that raw `UPDATE`); a **pure comment** still writes nothing to `repair_orders.status`. This history-gap technique is how TRUE-80060 was pinned to `qqStatus()` rather than the SNOW round-trip.
|
|
182
194
|
- **⚠ OUTAGE (TRUE-82129, 2026-09-18) — `send_ticket_updates.php` died on its FIRST ticket of EVERY run, for a whole day.** `App_Database::fetchOne()` (like `fetchRow()` / `numRows()`) takes its result **by reference** (`&$res`, `library/app/database.php` L336/L352/L362). TRUE-82027 (commit `b63fb0136`) passed `App_Database::query(...)` into it **inline**, which raises the notice *"Only variables should be passed by reference"* — and 1.0's error handler **terminates the run on a notice**. Live from the 2026-09-18 10:18 deploy until fixed. **Fix:** assign the `query()` result to a variable first. Symptom set worth recognizing: zero ServiceNow calls after the deploy, `Common.CronJobExecutions.dtCheckOut` NULL on every run, and exactly **one** `NYCDOETickets.dtLastChecked` stamp per 6-minute run. General 1.0 trap — see the 1.0 back-end standard.
|
|
183
195
|
- **⚠ "The outbound cron re-asserts On Hold every run" (business rule 2) was only true in theory between 2026-09-18 10:18 and the TRUE-82129 fix** — the cron was dying on its first ticket. Check the cron actually completes before trusting that guarantee.
|
|
184
|
-
- **OPEN QUESTION — `dtSynced` is not a reliable outbound-only signal.** The 12 reported incidents (INC2298009, INC2298110, INC2297667, INC2299269-77) had `Common.NYCDOETickets.dtSynced` advance from ~2026-09-17/18 05:00–09:46 to ~08:57–09:00 on 2026-09-18 **while the outbound cron was not running**. Suspected `process_tickets.php` (inbound also writes `dtSynced`) but **not confirmed**. Confirm before reading `dtSynced` as proof of an outbound push.
|
|
185
|
-
- **⚠ SECURITY — hardcoded live ServiceNow credentials in `library/app/api/nycdoev2.php` / `nycdoe.php`.** Found 2026-09-18, not fixed; detail in [ServiceNow / ASN Integration](servicenow-integration.md).
|
|
196
|
+
- **OPEN QUESTION — `dtSynced` is not a reliable outbound-only signal.** The 12 reported incidents (INC2298009, INC2298110, INC2297667, INC2299269-77) had `Common.NYCDOETickets.dtSynced` advance from ~2026-09-17/18 05:00–09:46 to ~08:57–09:00 on 2026-09-18 **while the outbound cron was not running**. Suspected `process_tickets.php` (inbound also writes `dtSynced`) but **not confirmed**. More evidence (2026-09-25): some rows have `dtSynced` LATER than their newest note (INC2244995: `dtSynced` 2026-09-25 06:20 vs newest note 09-21 16:04), which the outbound note watermark alone cannot produce. Confirm before reading `dtSynced` as proof of an outbound push.
|
|
197
|
+
- **⚠ SECURITY — hardcoded live ServiceNow credentials in `library/app/api/nycdoev2.php` / `nycdoe.php`.** Found 2026-09-18, not fixed; a test-repo diagnostic script also embeds the same credential; detail in [ServiceNow / ASN Integration](servicenow-integration.md).
|
|
198
|
+
- **KNOWN REMAINING GAP (parked, TRUE-82332) — `u_technician` re-pushed with no change.** The technician email comparison appears never to match, so passes with nothing new still PATCH `u_technician` alone (INC2131474: two such PATCHes at 16:06:30 / 16:06:31). Wasted calls; not fixed.
|
|
186
199
|
- **KNOWN REMAINING GAP (needs a follow-up ticket):** both `qqStatus()` hold gates live inside the `type IN (ONSITE_REPAIR, ONSITE_SERVICE)` branch. The `DEPOT_REPAIR` branch (`repairorder.php` from ~L799) has **no hold gate at all**, so a `DEPOT_REPAIR` order set to any hold status is still silently recomputed out of hold by `updateStatus()` — same class of bug, untouched by TRUE-80060.
|
|
187
200
|
- **KNOWN REMAINING GAP (needs a follow-up ticket) — inbound note reconciliation is paged too small.** `process_tickets.php`'s inbound comment reconciliation fetches only `page_size=50, page_number=1` of the SNOW journal, then **deletes any local note not in that set** (the remainder-delete ~L1326–1330). An order with **>50** comments/work_notes can have local notes silently deleted. It is also inconsistent with `addNotes`, which fetches `page_size=100`.
|
|
188
201
|
- **KNOWN REMAINING GAP — `buildDoubleFieldArray` collapses unsynced notes.** In `process_tickets`, `buildDoubleFieldArray` keys on `IFNULL(referenceId,0)`, so **all** NULL-`referenceId` notes collapse to key `'0'`, making the remainder-delete handle multiple unsynced notes inconsistently.
|
|
189
202
|
- `qqStatus()` collapses all recognized hold sub-statuses to base `HOLD` in `repair_orders.status`; the specific hold **sub-reason persists only in `repair_order_notes.newstatus`**. Any status badge / report / filter reading `repair_orders.status` directly cannot distinguish the six hold variants after a recompute.
|
|
190
203
|
|
|
191
204
|
## Change history
|
|
205
|
+
- 2026-09-25 — TRUE-82332: hold-release no longer overwrites the close `state`; close re-sent when SN not closed; stuck-ticket re-queue recipe; `Logs.API` query shapes. (mhammontree)
|
|
192
206
|
- 2026-09-18 — TRUE-82129: fixed the by-reference `fetchOne()` notice that killed every run; batch loop + run caps replaced LIMIT 1 + per-ticket sleep; alert emails de-PII'd and escaped; unmapped-group email now grouped per run. (mhammontree)
|
|
193
207
|
- 2026-09-17 — TRUE-82027: outbound push runs once per order per pass; `dtLastChecked` split from `dtSynced`; failure alerting instead of a throw. (mhammontree)
|
|
194
208
|
- 2026-08-20 — TRUE-81044: `state` + `hold_reason` + `u_eta` + `u_other_reason_on_hold` pushed together; stopped writing `u_status_task`. (mhammontree)
|
|
@@ -157,7 +157,7 @@ All **live** DOE business logic is 1.0. The only other 2.0 code is the in-progre
|
|
|
157
157
|
|
|
158
158
|
## Gotchas
|
|
159
159
|
|
|
160
|
-
- **⚠ SECURITY (cso review 2026-09-18 — NOT fixed, needs its own ticket): live production ServiceNow credentials are hardcoded in committed PHP.** `library/app/api/nycdoev2.php` and `library/app/api/nycdoe.php` hold the ServiceNow **`clientSecret` and `refreshToken` as literals** (stage values too). Treat as compromised: move them to config/env and **rotate with DOE**. Separately, `Vision_Log.ErrorLog` rows dump `$_SERVER` including live AWS access key id / secret in plain text — its own ticket, same rotation treatment. **No credential values are recorded here or anywhere in the KB.**
|
|
160
|
+
- **⚠ SECURITY (cso review 2026-09-18 — NOT fixed, needs its own ticket): live production ServiceNow credentials are hardcoded in committed PHP.** `library/app/api/nycdoev2.php` and `library/app/api/nycdoe.php` hold the ServiceNow **`clientSecret` and `refreshToken` as literals** (stage values too). A test-repo diagnostic script (committed 2026-06-18) embeds the same prod credential — one rotation covers both. Treat as compromised: move them to config/env and **rotate with DOE**. Separately, `Vision_Log.ErrorLog` rows dump `$_SERVER` including live AWS access key id / secret in plain text — its own ticket, same rotation treatment. **No credential values are recorded here or anywhere in the KB.**
|
|
161
161
|
|
|
162
162
|
- **⚠ OPEN BUG / NAMED PATTERN — "Lenovo Off-Layout ASN File" (silent column shift + unit collapse).** Investigated 2026-08-11 (read-only; **no code fix exists**). Lenovo sends NYCDOE ASNs in **two different column layouts**, and `library/app/asnprocessor/lenovo.php` picks its column mapping by **column COUNT with no header-name validation** (`:110`, `if ($colCount >= 30`), so the off-layout file is parsed with the wrong mapping and silently mis-read.
|
|
163
163
|
- **The two layouts.** The normal daily feed `DOE-Lenovo-ASN-MM-DD-YYYY.CSV` (~2 MB) has **30** columns and matches the code's mapping. A second file, `DOE-Open-Lenovo-MM-DD-YYYY.csv` (**lowercase** `.csv`, ~45 KB), has **31** columns in a **different order**: `Contact Email` moves from column 29 to column **6**, and a trailing `Full Shipment` column is appended. Everything from `Street Address` onward is therefore read **one column too early**. The "extra comma" repair heuristic at `:119-127` does not catch this (unchanged since March 2026 — commits `3442ddcf`, `6956dfbf`, `864cf073`, so not a regression).
|
|
@@ -240,6 +240,7 @@ All **live** DOE business logic is 1.0. The only other 2.0 code is the in-progre
|
|
|
240
240
|
|
|
241
241
|
- **The queue `dedupeKey` format is load-bearing.** A 2026 incident (WR260236464 duplicate repair order) was caused by dedupeKey format drift (4-part keys with serial vs 3-part without) letting old rows re-import; Stage 2 then created a sibling item row (frozen original not reusable), and Stage 5's per-item-row serial scoping duplicated the unit → duplicate RO. Any change to the key format needs a backfill or transitional matcher. Forensic fingerprint: a Unit with `trackingNumberId IS NULL` was added by Stage 5 from NetSuite, not by the importer.
|
|
242
242
|
- **Two dedupe strategies coexist:** SFTP path = INSERT+catch (race-safe); API path = check-then-insert (unique index backstop).
|
|
243
|
+
- **Held→complete incidents never resolving in ServiceNow** = the TRUE-82332 close clobber; mechanism, fix and re-queue recipe in [Hold-Status Sync](hold-status-sync.md).
|
|
243
244
|
- **Timezone traps:** worker runs `America/Chicago`; `repair_orders.dtEta` is stored **Eastern**; ServiceNow `u_eta` is sent as **UTC** `YYYY-MM-DD HH:MM:SS` (converted via `App_Date::convertTimezone()`, only sent when first 19 chars differ from remote).
|
|
244
245
|
- **EDI PO104 leading-zero suppression** (go-live 2026-06-07): DOE's IBM-based translator sends `.94` instead of `0.94` — exposure is in `App_Edi::translatePurchaseOrder850()`, not the cron.
|
|
245
246
|
- **Installation schedule reports skip weekends but not holidays** — empty output near a holiday is expected, not a bug. Empty also just means no ROs with `dtEta` on the exact target date.
|
|
@@ -261,4 +262,5 @@ All **live** DOE business logic is 1.0. The only other 2.0 code is the in-progre
|
|
|
261
262
|
- 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`)
|
|
262
263
|
|
|
263
264
|
## Change history
|
|
265
|
+
- 2026-09-25 — TRUE-82332: cross-link to the close-clobber fix; test-repo copy of the ServiceNow credential noted (mhammontree)
|
|
264
266
|
- 2026-09-25 — Corrected "no 2.0 tenant" (TRUE-80519 2.0 ASN rewrite now exists); DOE-Open-Lenovo marked retired (mhammontree)
|
|
@@ -37,9 +37,8 @@ NYC DOE — TOGA client whose **live** integration runs in the **1.0 worker tier
|
|
|
37
37
|
## Endpoints
|
|
38
38
|
| System | Endpoint | Notes |
|
|
39
39
|
|---|---|---|
|
|
40
|
-
| ServiceNow prod | `https://
|
|
41
|
-
| ServiceNow
|
|
42
|
-
| ServiceNow disposals | `https://supportadmin.schools.nyc` (stage `nycdoedev3`) | disposal service requests |
|
|
40
|
+
| ServiceNow prod (live) | `https://supportadmin.schools.nyc` (stage `nycdoedev3.service-now.com`) | `App_Api_NYCDOEV2` — all live INC/RITM GET/PATCH under `/api/nycd3/`, incl. disposals; V1 `App_Api_NYCDOE` uses the same prod host (stage `supportadminstg.schools.nyc`) |
|
|
41
|
+
| ServiceNow legacy | `https://nycdoeprod.service-now.com` (stage `nycdoedev2`) | only the dormant `download_tickets*.php` — not live |
|
|
43
42
|
| DOE EDI SFTP | `transfer.schools.nyc` (SSH key `doedimedu.pem`) | EDI 850 in, invoices out |
|
|
44
43
|
| Vendor SFTP feeds | Apple / Lexmark / Lenovo (Lenovo: `ftp.asisystem.com`) | legacy ASN file ingestion |
|
|
45
44
|
|
|
@@ -5,5 +5,6 @@
|
|
|
5
5
|
| [TOGA Technology Website Architecture](architecture.md) | Public TOGA Technology marketing site — standalone React/TS SPA (no PHP framework); open for stack, bootstrap, routing, page layout, integrations, build/deploy. |
|
|
6
6
|
| [Careers Section — Live Paylocity Job Openings (About page + /v2/campaigns/jobs proxy)](features/careers-paylocity-job-openings.md) | togatech About page "Join Our Team" section — live Paylocity jobs via unauthenticated `GET /v2/campaigns/jobs` record-script proxy; open for the fetch→proxy→map |
|
|
7
7
|
| [SEO / AEO / GEO — Prerendering, Single-Source Meta & Structured Data](features/seo-aeo-geo-prerender.md) | togatech SEO/AEO/GEO — build-time Puppeteer prerender + single-source per-page meta + JSON-LD; open for the robots-gating de-index bug and Amplify build require |
|
|
8
|
+
| [Talos Product Page (/talos) — ported designer prototype](features/talos-product-page.md) | togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS prototype; open for the design-port CSS approach, lazy-load + prerender rules, page |
|
|
8
9
|
| [Creating Pull Requests on togatech](workflows/creating-pull-requests.md) | Open a PR on `agilantsolutions/togatech` — `gh` CLI lacks `read:org`, so use a raw GitHub REST call with the keychain token; base is `_production`. |
|
|
9
10
|
| [Running togatech Locally & Verifying a Page Without Cypress](workflows/local-dev-and-headless-verification.md) | Get togatech running on a dev machine and verify a data-driven section without Cypress — `npm install` first, `lint` is dead, Cypress 15 fails on Windows. |
|
|
@@ -6,8 +6,8 @@ project: TOGA Technology Website
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [jcardinal]
|
|
9
|
+
updated: 2026-09-25
|
|
10
|
+
owners: [jcardinal, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- togatech/src/main.tsx
|
|
13
13
|
- togatech/src/App.tsx
|
|
@@ -17,6 +17,9 @@ files:
|
|
|
17
17
|
- togatech/src/themeConfig/ThemeContext.tsx
|
|
18
18
|
- togatech/vite.config.ts
|
|
19
19
|
- togatech/package.json
|
|
20
|
+
- togatech/src/components/NavV2/navRoutes.ts
|
|
21
|
+
- togatech/src/components/templates/AppLayout/AppLayout.tsx
|
|
22
|
+
- togatech/src/routes.config.json
|
|
20
23
|
related: []
|
|
21
24
|
---
|
|
22
25
|
|
|
@@ -51,7 +54,7 @@ Public-facing TOGA Technology corporate/marketing website. Standalone single-pag
|
|
|
51
54
|
|
|
52
55
|
## Routing
|
|
53
56
|
|
|
54
|
-
`src/routes.tsx` = single `createBrowserRouter`. All routes render under `components/templates/AppLayout` (
|
|
57
|
+
`src/routes.tsx` = single `createBrowserRouter`. All routes render under `components/templates/AppLayout` (NavV2 header + footer chrome); the `/` route group has `ErrorPage` as its `ErrorBoundary`.
|
|
55
58
|
|
|
56
59
|
| Path | Page |
|
|
57
60
|
|---|---|
|
|
@@ -60,11 +63,16 @@ Public-facing TOGA Technology corporate/marketing website. Standalone single-pag
|
|
|
60
63
|
| `/what-we-do` | `pages/WhatWeDo` |
|
|
61
64
|
| `/our-platform` | `pages/OurPlatform` |
|
|
62
65
|
| `/contact` | `pages/Contact` |
|
|
66
|
+
| `/talos` | `pages/Talos` (code-split — see features/talos-product-page.md) |
|
|
63
67
|
| `/privacy-policy` | `pages/PrivacyPolicy` |
|
|
64
68
|
| `/terms-and-conditions` | `pages/TermsAndConditions` |
|
|
69
|
+
| every `STUB_ROUTES` path | `pages/Stub` placeholder (`<StubPage title>`) |
|
|
65
70
|
| `*` | `pages/NotFound` |
|
|
66
71
|
|
|
67
|
-
-
|
|
72
|
+
- **Nav destinations are data** in `components/NavV2/navRoutes.ts`: `LIVE_ROUTES` (real pages) and `STUB_ROUTES` (placeholder until built — delete the line to retire a stub). Blog routes are commented out.
|
|
73
|
+
- **Prerender list:** `src/routes.config.json` lists the static routes to prerender + sitemap. Add every new live route there (rules: features/seo-aeo-geo-prerender.md).
|
|
74
|
+
- Pages are imported eagerly except `/talos` (`React.lazy` + `<Suspense>` inside the route Component — never the router's `lazy`, it breaks hydration).
|
|
75
|
+
- **AppLayout → page contract:** `<Outlet context={{ openTalosChat }} />`; pages read it via `useAppLayoutContext()` (`AppLayout/appLayoutContext.ts`) to open the global Talos chat modal. `ROUTES_WITH_OWN_FOOTER` (only `/talos`) hides the old site footer on pages that render the redesigned one.
|
|
68
76
|
- A Cypress-only test route swaps `WhatWeDoPage` for a throwing component to exercise the error boundary (`VITE_CY_TEST` / `window.Cypress`).
|
|
69
77
|
|
|
70
78
|
## Page structure — MVVM per page
|
|
@@ -85,8 +93,8 @@ pages/Home/
|
|
|
85
93
|
|
|
86
94
|
## Shared layers
|
|
87
95
|
|
|
88
|
-
- `src/components/` — reusable UI (Header, Footer, Hero, Carousel/Swiper wrappers, Modal, TalosChat + TalosChatInput, motion/animation helpers, animated SVGs, etc.).
|
|
89
|
-
- `src/components/templates/` — layout primitives: `AppLayout` (router root chrome), `DefaultContainer`, `MarginContainer`, `GlowingContainer`, `ErrorContainer`.
|
|
96
|
+
- `src/components/` — reusable UI (NavV2 global nav incl. `TalosFab`, Header, Footer, Hero, Carousel/Swiper wrappers, Modal, TalosChat + TalosChatInput, motion/animation helpers, animated SVGs, etc.).
|
|
97
|
+
- `src/components/templates/` — layout primitives: `AppLayout` (router root chrome; Outlet context + `appLayoutContext.ts`), `DefaultContainer`, `MarginContainer`, `GlowingContainer`, `ErrorContainer`.
|
|
90
98
|
- `src/lib/` — data/integration:
|
|
91
99
|
- `api.ts` — `useSendEmail` (contact form) and `postTalosMessage` (AI chat).
|
|
92
100
|
- `contentful.ts` — Contentful delivery client (`createClient`).
|
|
@@ -106,7 +114,7 @@ All via Vite `VITE_*` env vars (values in `.env`, never in repo docs):
|
|
|
106
114
|
|
|
107
115
|
## Build, environments, deploy
|
|
108
116
|
|
|
109
|
-
- `package.json` scripts: `dev`, `togatech` (dev with `--host togatech`), `build` (`tsc && vite build`), `buildBeta` (`--mode beta`), `buildGamma` (`--mode gamma`), `preview`, `lint` (ESLint
|
|
117
|
+
- `package.json` scripts: `dev`, `togatech` (dev with `--host togatech`), `build` (`tsc && vite build`), `buildBeta` (`--mode beta`), `buildGamma` (`--mode gamma`), `preview`, `lint` (ESLint — non-functional: no eslint dependency or config; see workflows/local-dev-and-headless-verification.md). `build` also runs the Puppeteer prerender (see features/seo-aeo-geo-prerender.md).
|
|
110
118
|
- Multi-environment: `development`, `beta`, `gamma`, `production` (Vite modes).
|
|
111
119
|
- Vite `base: "/"`, output to `dist/`, `publicDir: public`. A custom `robotsPlugin` copies `public/robots.prod.txt` or `public/robots.dev.txt` to `dist/robots.txt` at `closeBundle`, depending on prod detection (`NODE_ENV` + `VITE_ENV`).
|
|
112
120
|
- `@` path alias → `./src` (Vite `resolve.alias`).
|
|
@@ -71,7 +71,11 @@ Makes togatech.com visible and citable to search engines **and** AI answer engin
|
|
|
71
71
|
- **🔴 Hosting requirement:** CloudFront/S3 must serve each prerendered `dist/<route>/index.html` for its path; a blanket SPA rewrite to root `index.html` serves the empty shell and defeats the prerender.
|
|
72
72
|
- **og:image dedupe:** static OG/Twitter tags were removed from `index.html` because `<Seo>` emits per-page ones (Helmet appends rather than replacing pre-existing static tags → duplicates otherwise).
|
|
73
73
|
- **Verified data corrections:** real positioning is *integrated IT services + the ERA TOGa Platform*; real socials are LinkedIn `/toga-tech` + YouTube + Instagram (no Twitter/GitHub/Crunchbase); contact is `website@togatech.com` / `+1 (212) 736-0111`; **5** offices. Prior planning docs had several wrong specifics — verify schema/llms.txt against FIELDS before shipping.
|
|
74
|
-
- **Build size:** JS bundle is ~14.8 MB (4.9 MB gzip) — a real CWV/LCP risk
|
|
74
|
+
- **Build size:** JS bundle is ~14.8 MB (4.9 MB gzip) — a real CWV/LCP risk. Only `/talos` is code-split so far.
|
|
75
|
+
- **🔴 Code-split a prerendered route with `React.lazy` + `<Suspense>` inside the route `Component` — never react-router's route `lazy`.** Router `lazy` without a `HydrateFallback` renders blank and throws away the prerendered HTML; a Suspense boundary keeps the server HTML until the chunk arrives. Pattern: `TalosRoute` in `routes.tsx`.
|
|
76
|
+
- **🔴 Hooks must never append child nodes into React-owned elements.** The snapshot captures the appended nodes → hydration mismatch. Render a host element with `dangerouslySetInnerHTML={{__html:""}}` and clear it on start and cleanup.
|
|
77
|
+
- **Prerender UA tag:** `prerender.mjs` sets a user agent containing `TOGAPrerender`. `App.tsx` skips analytics on it; canvas/animation engines must skip start on it too (e.g. `useTalosSparks.ts`).
|
|
78
|
+
- **New route checklist:** add it to `src/routes.config.json` (prerender + sitemap list); a nav destination also moves from `STUB_ROUTES` to `LIVE_ROUTES` in `NavV2/navRoutes.ts` (removing the stub line retires the placeholder page).
|
|
75
79
|
- **FIELDS JSON is component-coupled — no free-prose body slot:** each page's `*FIELDS*.json` is shaped to specific components (hero, cardScroll, businessProblems, kpi, carousel), so there is **no place to drop a long prose block**. An audit's "rewrite to 900 words" is not structurally possible without building new components — you can only sharpen titles/descriptions/keywords/card copy that already have slots. (`/our-platform` was left unchanged: it is already content-dense — 7 app modules + 4 stacks + testimonials + KPIs — and the original audit under-counted because that content is JS-rendered.)
|
|
76
80
|
- **`personSchemas` duplicate guard:** a leader can be in both `boardMembers` and `members` in `ABOUTPAGEFIELDS` — `personSchemas` must de-dup by lowercased trimmed name, or it emits duplicate `Person` nodes.
|
|
77
81
|
- **🔴 Brand name is "TOGA Technology" only — never "Agilant":** an earlier draft adding `alternateName: "Agilant"` / "(formerly Agilant)" to the org schema was explicitly reverted. Do not reintroduce "Agilant" in schema, copy, or meta. (Captured as a team brand standard — see Related docs.)
|
|
@@ -79,4 +83,8 @@ Makes togatech.com visible and citable to search engines **and** AI answer engin
|
|
|
79
83
|
## Related
|
|
80
84
|
|
|
81
85
|
- standalone/apps/togatech/architecture.md (update its Build/deploy section once PR #30 merges — robotsPlugin gating + prerender step).
|
|
86
|
+
- standalone/apps/togatech/features/talos-product-page.md — first code-split + canvas page under prerender.
|
|
82
87
|
- standalone/apps/togatech/workflows/creating-pull-requests.md — how PRs get opened on this repo (gh CLI is unauthenticated; uses curl + osxkeychain token; base `_production`).
|
|
88
|
+
|
|
89
|
+
## Change history
|
|
90
|
+
- 2026-09-25 — Added lazy-route, no-DOM-append, TOGAPrerender UA and new-route rules from the /talos build (tcox)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Talos Product Page (/talos) — ported designer prototype
|
|
3
|
+
framework: "standalone"
|
|
4
|
+
repo: togatech
|
|
5
|
+
project: TOGA Technology Website
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-25
|
|
10
|
+
owners: ["tcox"]
|
|
11
|
+
files:
|
|
12
|
+
- togatech/src/pages/Talos/TalosPage.tsx
|
|
13
|
+
- togatech/src/pages/Talos/talos.css
|
|
14
|
+
- togatech/src/pages/Talos/viewModel/useTalosSparks.ts
|
|
15
|
+
- togatech/src/pages/Talos/view/shared/Reveal.tsx
|
|
16
|
+
- togatech/src/routes.tsx
|
|
17
|
+
- togatech/src/components/templates/AppLayout/AppLayout.tsx
|
|
18
|
+
- togatech/src/components/templates/AppLayout/appLayoutContext.ts
|
|
19
|
+
- togatech/src/components/NavV2/view/TalosFab.tsx
|
|
20
|
+
related:
|
|
21
|
+
- ../architecture.md
|
|
22
|
+
- seo-aeo-geo-prerender.md
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS prototype; open for the design-port CSS approach, lazy-load + prerender rules, page-owned footer, and opening the Talos chat from a page.
|
|
26
|
+
|
|
27
|
+
## Summary
|
|
28
|
+
|
|
29
|
+
`/talos` is a 15-section product page (`src/pages/Talos/`) ported from a designer prototype (static HTML + in-browser JSX + CSS; the drop-in at `togatech/_design/talos/` is temporary and never committed). Standard MVVM page layout: `view/sections/<Section>/` (incl. `TalosSparks` canvas backdrop and `TalosFooter`), `view/shared/Reveal.tsx`, `viewModel/use*.ts` hooks, copy in `viewModel/FIELDS/*.json`, images in `public/assets/talos/`. Live route, prerendered, in the sitemap list.
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
**CSS port (the reusable approach for any designer prototype):**
|
|
34
|
+
- All design CSS is concatenated **in original cascade order** into one plain file, `talos.css`, every selector prefixed `.talosPage `. `:root` / `html` / `body` selectors map to `.talosPage`. Generated by a one-off postcss prefix script (not in `scripts/`) — regenerate the same way if the design CSS changes.
|
|
35
|
+
- **Not a CSS module:** the design JS finds elements by raw class names/ids and sets keyframe names inline; CSS-module hashing breaks all of that. (CTO-reviewed choice.)
|
|
36
|
+
- Design timing workarounds are kept on purpose: IntersectionObserver + rect-poll backstops and `setTimeout` retry chains (rAF / ResizeObserver can go silent on cold load). Animation loops pause off-screen and on hidden tab.
|
|
37
|
+
- `<html>`-level styles (`overflow-x: clip` so `position: sticky` works — never `hidden`; `scroll-behavior: smooth`) are set in `useLayoutEffect` and **removed** on unmount, not "restored" (a prerendered snapshot already carries them, so the "previous" value is wrong).
|
|
38
|
+
|
|
39
|
+
**Code-split + prerender:** `routes.tsx` wraps the page in `React.lazy` + `<Suspense fallback={null}>` inside the route `Component`. Rules for this live in [seo-aeo-geo-prerender.md](seo-aeo-geo-prerender.md) (router `lazy` vs `React.lazy`, no DOM appends into React-owned nodes, `TOGAPrerender` UA skip for canvas engines).
|
|
40
|
+
|
|
41
|
+
**Layout hooks this page uses:**
|
|
42
|
+
- **Opening the Talos chat:** AppLayout owns the global Talos chat modal and passes `<Outlet context={{ openTalosChat }} />`. Pages call `useAppLayoutContext().openTalosChat()` (`AppLayout/appLayoutContext.ts`). Use this — do not mount a second chat.
|
|
43
|
+
- **Own footer:** `ROUTES_WITH_OWN_FOOTER` in `AppLayout.tsx` (only `/talos` today, tcox's call) hides the old site footer (with contact form); the page renders `TalosFooter`. The old footer stays on all other pages until the redesigned footer goes site-wide — then retire the list.
|
|
44
|
+
- **NavV2 floating Talos button** (`TalosFab.tsx`) is a `<button>` that opens the chat (same as the bar pill), not a `Link` to `/talos`; unfocusable while hidden. Styled from the design's `.talos-persist.talos-fab` (light-trail ring, icon-only, 44px, rises 20px on entrance).
|
|
45
|
+
|
|
46
|
+
**Routing:** `/talos` is in `LIVE_ROUTES` (`NavV2/navRoutes.ts`) and `src/routes.config.json`. Sub-pages `/talos/one|flow|engage` are still `STUB_ROUTES`.
|
|
47
|
+
|
|
48
|
+
**Copy:** uses the design's "Katie" copy set (Katie signs off copy). Still placeholder: FAQ answers (`[Answer not written. Needs Jeff.]`), the Voice section quote, and the demo stage (no video).
|
|
49
|
+
|
|
50
|
+
## Gotchas
|
|
51
|
+
|
|
52
|
+
- Design `body{background:transparent}` collapses onto `.talosPage` and wipes the page colour — drop the body background when porting.
|
|
53
|
+
- `.talosPage` needs `isolation: isolate` so negative/low `z-index` backdrop layers paint above its background (it stands in for `<html>`).
|
|
54
|
+
- Site-global `src/styles/index.css` sets `word-spacing: 0.08em` on `p`, `h2`–`h6`, `a`, `li`, `button` — changes line breaks vs the design. Reset to `normal` inside the page.
|
|
55
|
+
- Tailwind preflight is global and can differ from the design's browser defaults — check margins/list/button resets when a section looks off.
|
|
56
|
+
|
|
57
|
+
## Change history
|
|
58
|
+
- 2026-09-25 — Built /talos from the designer prototype; added Outlet context `openTalosChat`, `ROUTES_WITH_OWN_FOOTER`, TalosFab now opens chat (tcox)
|
|
@@ -6,7 +6,7 @@ project: TOGA Technology Website
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-25
|
|
10
10
|
owners: ["tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- togatech/package.json
|
|
@@ -28,17 +28,21 @@ The gotcha-avoidance path for getting `togatech` up on a dev machine and actuall
|
|
|
28
28
|
1. **`npm install` before anything else, every time you pull.** A stale `node_modules` shows up as a Vite resolve error, not as a missing dependency: `"react-helmet-async" is imported by … but could not be resolved`. Both `react-helmet-async` and `react-google-recaptcha-v3` are declared in `package.json` (added by the SEO work) but absent from an older install, and the site simply will not load.
|
|
29
29
|
2. **Start the dev server** — `npm run dev`, or `npm run togatech` for the `--host togatech` variant (Vite `allowedHosts: ["togatech"]`).
|
|
30
30
|
3. **Clear localStorage before judging any data fetch.** `App.tsx` wraps the app in `PersistQueryClientProvider` with the persister key **`"commerce"`** and a **24h `staleTime`**, so a previously cached query (e.g. `jobPosts`) renders from localStorage with **no network request at all**. Clear the persisted cache **and** `accessToken` / `refreshToken` — a stale public token from `POST /v2/auth/public` will otherwise be reused too.
|
|
31
|
-
4. **Do not expect `npm run lint` to work.** The script is `eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0`, but **`eslint` is not in `package.json` dependencies or devDependencies at all** — there is no local binary, so a clean install cannot lint.
|
|
31
|
+
4. **Do not expect `npm run lint` to work.** The script is `eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0`, but **`eslint` is not in `package.json` dependencies or devDependencies at all** — there is no local binary, so a clean install cannot lint. There is also **no ESLint config** — ad hoc `npx eslint` fails with "couldn't find an eslint.config.* file". Type-check with `npx tsc --noEmit -p tsconfig.app.json`; it has **many pre-existing unrelated errors**, so filter the output by the paths you touched.
|
|
32
32
|
5. **If Cypress won't run, verify headlessly with the repo's Puppeteer.** `cypress` 15.11 fails its own binary verification on this Windows machine — `bad option: --smoke-test` — and `npx cypress install` does **not** fix it. Workaround: `puppeteer` is already a dependency, so drive **system Chrome** directly by passing `executablePath` to `puppeteer.launch()`, navigate to the dev-server URL, and assert on the rendered DOM (the section testids, e.g. `job-posting-section` / `job-opening-cards`). Same technique the production build already uses in `scripts/prerender.mjs`.
|
|
33
33
|
|
|
34
34
|
## Gotchas
|
|
35
35
|
|
|
36
36
|
- A Vite "imported but could not be resolved" error almost always means **stale install**, not a missing package — check `package.json` before adding a dependency.
|
|
37
|
-
- `npm run lint` is effectively dead until eslint
|
|
37
|
+
- `npm run lint` is effectively dead until eslint and an `eslint.config.*` are added to the repo; do not treat a lint failure-to-start as a code problem.
|
|
38
38
|
- Cypress specs are still the source of truth for e2e coverage — the Puppeteer route is a *local verification* workaround, not a replacement. Don't delete or rewrite specs because Cypress won't launch locally.
|
|
39
|
+
- `robotsPlugin` writes `robots.txt` to `./dist` even when you build with `--outDir <other>` — a scratch build still touches `dist/`.
|
|
39
40
|
- Watching the network tab is not proof a fetch is broken: see step 3 (persisted query cache).
|
|
40
41
|
|
|
41
42
|
## Related
|
|
42
43
|
|
|
43
44
|
- [togatech architecture](../architecture.md)
|
|
44
45
|
- [Careers Section — Live Paylocity Job Openings](../features/careers-paylocity-job-openings.md)
|
|
46
|
+
|
|
47
|
+
## Change history
|
|
48
|
+
- 2026-09-25 — No eslint config either; `tsc -p tsconfig.app.json` path-filter tip; robotsPlugin ignores --outDir (tcox)
|
package/package.json
CHANGED