toga-ai 1.0.571 → 1.0.573
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/1.0/apps/worker/INDEX.md +1 -0
- package/knowledge/1.0/apps/worker/features/staples-cxml-order-import.md +96 -0
- package/knowledge/2.0/apps/api2/features/record-scripts.md +26 -4
- package/knowledge/2.0/apps/toga-blox/features/api-client.md +16 -2
- package/knowledge/INDEX.md +3 -2
- package/knowledge/clients/staples/INDEX.md +5 -0
- package/knowledge/clients/staples/profile.md +41 -0
- package/knowledge/clients/true/profile.md +11 -2
- package/knowledge/standalone/apps/togatech/INDEX.md +2 -0
- package/knowledge/standalone/apps/togatech/features/careers-paylocity-job-openings.md +157 -0
- package/knowledge/standalone/apps/togatech/workflows/local-dev-and-headless-verification.md +75 -0
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| [OneUptime Server monitor + disk/memory hygiene on the 1.0 worker EB host](features/oneuptime-server-monitor-host-hygiene.md) | The 1.0 `agilant-worker` EB environment runs on the **legacy Amazon Linux 1 PHP 7.2 platform** (Apache httpd/prefork, s3fs mounts, cron) and repeatedly went dow | worker/.ebextensions/040_disk_memory_hygiene.config, worker/.ebextensions/045_oneuptime_agent.config, worker/ebs/cron.worker.php, worker/ebs/mount-s3fs-folders.php, worker/ebs/apache_settings.php, worker/ebs/setup_phpini.php |
|
|
12
12
|
| [OneUptime external uptime monitoring for 1.0 workers](features/oneuptime-worker-uptime-monitoring.md) | Every 1.0 worker box self-reports its liveness to an external OneUptime monitor once per minute by curl-POSTing to a per-worker "Incoming Request" heartbeat URL | library/app/worker.php, worker/crons/worker/worker_heartbeat.php |
|
|
13
13
|
| [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
|
|
14
|
+
| [Staples cXML Order Import (SFTP → NetSuite)](features/staples-cxml-order-import.md) | `sync_staples_cxml.php` is an **hourly** cron (runs at **:45**) that imports Staples cXML purchase orders from SFTP into NetSuite as Sales Orders, then writes a | worker/crons/sync/staples/sync_staples_cxml.php |
|
|
14
15
|
| [Diagnosing frozen 1.0 worker cron check-ins (Sentry "missed" flood)](workflows/diagnosing-frozen-cron-checkins.md) | When 1.0 worker cron timestamps freeze and Sentry project `worker1` fills with **`missed`** check-ins, the intuitive diagnosis — a wedged `App_Framework::isProc | worker/.ebextensions/cron.config, library/app/worker.php |
|
|
15
16
|
| [isFulfillable Multi-Client Backfill (all togasupply clients)](workflows/isfulfillable-multi-client-backfill.md) | One-time backfill that catches up `Items.isFulfillable` on **existing** items across **all 17 togasupply clients** (AIG, Broward Sheriff, Canon, Endeavor Health | worker/crons/toga2/netsuite/backfill_isfulfillable_all_clients.php, library/app/api/toga2.php |
|
|
16
17
|
| [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_elite.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql, dbchanges2/_modules/netsuite/2026-08-05 - CLEAN NETSUITE CLINET.SQL |
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Staples cXML Order Import (SFTP → NetSuite)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: worker
|
|
5
|
+
project: Worker
|
|
6
|
+
client: staples
|
|
7
|
+
type: client-feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-13
|
|
10
|
+
owners: [jcardinal]
|
|
11
|
+
files:
|
|
12
|
+
- worker/crons/sync/staples/sync_staples_cxml.php
|
|
13
|
+
related:
|
|
14
|
+
- clients/staples/profile.md
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Summary
|
|
18
|
+
|
|
19
|
+
`sync_staples_cxml.php` is an **hourly** cron (runs at **:45**) that imports Staples cXML
|
|
20
|
+
purchase orders from SFTP into NetSuite as Sales Orders, then writes an 855 acknowledgement
|
|
21
|
+
back. Staples is NetSuite parent entity/customer **internalId 30057**.
|
|
22
|
+
|
|
23
|
+
Flow per run: log into SFTP `sftp.goagilant.com` as `staplessftp`, list `/in`, and for each
|
|
24
|
+
cXML order file:
|
|
25
|
+
|
|
26
|
+
1. Replace a known part-number escaping edge case in the raw file.
|
|
27
|
+
2. Parse via `App_Xml`.
|
|
28
|
+
3. Log the raw file to **FileLog** (schema `Logs`, table `FileLog`, job
|
|
29
|
+
`STAPLES_IMPORT_ORDER`). `App_Model_Logs_FileLog` sets
|
|
30
|
+
`_databaseNameOverride = 'Logs'`, so this writes to the legacy/V1 prod schema `Logs`.
|
|
31
|
+
The full inbound cXML is stored in `FileLog.fileData`.
|
|
32
|
+
4. Dedupe against existing NetSuite sales orders by customer PO
|
|
33
|
+
(`App_NetSuite::getSalesOrdersByCustomerPo`).
|
|
34
|
+
5. Build and add a NetSuite `SalesOrder` under parent customer internalId 30057.
|
|
35
|
+
6. On successful add: write an **855 acknowledgement** cXML to SFTP `/out`, archive the
|
|
36
|
+
source file to `/archive/archive_in`, and delete it from `/in`.
|
|
37
|
+
|
|
38
|
+
### Line-item Location resolution
|
|
39
|
+
|
|
40
|
+
Each line-item's **Location** is resolved by matching the order's Staples `CustomerID`
|
|
41
|
+
(from `<Extrinsic name="CustomerID">`) against NetSuite **saved search 8412**:
|
|
42
|
+
|
|
43
|
+
- match key: `custentity_external_customer_id`
|
|
44
|
+
- location value: `custentity_customer_location`
|
|
45
|
+
|
|
46
|
+
If no match is found, `$internalClientLocationId` stays **NULL**, the line-item Location is
|
|
47
|
+
empty, and **NetSuite rejects the whole add** with `USER_ERROR` "Please enter value(s) for:
|
|
48
|
+
Location".
|
|
49
|
+
|
|
50
|
+
## Onboarding rule — new Staples buying account
|
|
51
|
+
|
|
52
|
+
A Staples buying account is a **NetSuite child customer** of parent 30057. Before that
|
|
53
|
+
account's first order arrives it must exist in NetSuite with:
|
|
54
|
+
|
|
55
|
+
- **External Customer ID** = the account's Staples `CustomerID`, and
|
|
56
|
+
- a valid **Customer Location**
|
|
57
|
+
|
|
58
|
+
so it appears in saved search 8412. If it is missing (or has no Customer Location), every
|
|
59
|
+
order for that account fails the NetSuite add and the file sits on SFTP `/in` retrying
|
|
60
|
+
hourly forever — **with no error surfaced** (see gotchas). Coordinate with the NetSuite
|
|
61
|
+
business admins to create the customer before go-live for any new account.
|
|
62
|
+
|
|
63
|
+
## Gotchas / known issues
|
|
64
|
+
|
|
65
|
+
**Silent-swallow of failed NetSuite adds (durable warning).** The
|
|
66
|
+
`if ($response->writeResponse->status->isSuccess)` block (~line 888) has **no `else`**. A
|
|
67
|
+
failed add logs nothing, emails no one, and leaves the file on `/in`. A file only leaves
|
|
68
|
+
`/in` on (a) add success — ack + archive + delete — or (b) the "already processed" dedupe
|
|
69
|
+
branch — delete. A failed add never removes it, so a pure data gap (e.g. an un-set-up
|
|
70
|
+
customer) is invisible and retries indefinitely. This is why a real backlog went unnoticed
|
|
71
|
+
for ~6 days.
|
|
72
|
+
|
|
73
|
+
**`getSavedSearch(8412)` is called once per order inside the file loop** — expensive
|
|
74
|
+
(~20s/file). A future refactor should fetch/cache it once per run.
|
|
75
|
+
|
|
76
|
+
**Recommended hardening (NOT yet implemented):**
|
|
77
|
+
- Add the missing `else` to email + log add failures (mirror the existing "missing items"
|
|
78
|
+
email) and leave the file for retry.
|
|
79
|
+
- Pre-guard: if `$internalClientLocationId` is NULL, skip the doomed add and alert
|
|
80
|
+
"Staples customer `<CustomerID>` not set up in NetSuite", naming the exact id.
|
|
81
|
+
|
|
82
|
+
## Diagnostic technique — early PHP-warning exit vs. silent add-failure
|
|
83
|
+
|
|
84
|
+
In Worker 1.0 a PHP warning/notice **hard-exits** the cron (`App_Error` → `exit`), so
|
|
85
|
+
nothing after the failure point runs. This cron writes its **FileLog** row (job
|
|
86
|
+
`STAPLES_IMPORT_ORDER`) **before** any NetSuite call. So counting fresh FileLog rows for a
|
|
87
|
+
run distinguishes the two failure modes:
|
|
88
|
+
|
|
89
|
+
- **few** rows → it died early (a PHP warning aborted the run partway), vs.
|
|
90
|
+
- **one row per file** → it processed every file but the NetSuite adds failed.
|
|
91
|
+
|
|
92
|
+
Because the full inbound cXML lives in `FileLog.fileData`, ship-to identity and `CustomerID`
|
|
93
|
+
can be recovered directly from the legacy `Logs` DB without touching SFTP.
|
|
94
|
+
|
|
95
|
+
## Change history
|
|
96
|
+
- 2026-08-13 — Documented feature (first KB entry). Diagnosed a ~6-day, ~30-order backlog stuck on SFTP `/in`: NetSuite rejected every add with `USER_ERROR` "Location" because new child customer INTEGRA PARTNERS INC. (Staples CustomerID 03716185, under parent 30057) was not set up in NetSuite → no saved-search-8412 match → NULL line-item Location. 28 of 30 stuck files were this account; 2 were CustomerID 05611619 (existed but missing Customer Location). Fixed by business admins creating the customer with External Customer ID + Customer Location; re-running the cron drained the backlog. Confirmed working. (jcardinal)
|
|
@@ -6,8 +6,8 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
10
|
-
owners: ["kyalamarthi", "mhammontree"]
|
|
9
|
+
updated: 2026-08-13
|
|
10
|
+
owners: ["kyalamarthi", "mhammontree", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
13
13
|
- _underscore/Model/Team/Sprint.php
|
|
@@ -19,6 +19,7 @@ related:
|
|
|
19
19
|
- ./tableview-apiwhereclause-row-filtering.md
|
|
20
20
|
- ../architecture.md
|
|
21
21
|
- ../../_underscore/features/per-client-database-connections.md
|
|
22
|
+
- ../../../../standalone/apps/togatech/features/careers-paylocity-job-openings.md
|
|
22
23
|
- ../../worker2/features/team-sprint-management.md
|
|
23
24
|
---
|
|
24
25
|
|
|
@@ -233,8 +234,14 @@ CLIENT database as a `CustomRecordScript` — NOT in `Core.RecordScripts`.** Fir
|
|
|
233
234
|
(Addresses 13, Sales orders 14, Entitlements 191, Subscriptions 253). This is safe;
|
|
234
235
|
`Core.RecordScripts` ids are **not** (they have drifted per environment).
|
|
235
236
|
- **Grant `Base` only, resolved by NAME** — role ids differ per client.
|
|
236
|
-
- **Do NOT grant `Public
|
|
237
|
-
at an arbitrary address
|
|
237
|
+
- **Do NOT grant `Public`** *unless the payload is genuinely public information.* A
|
|
238
|
+
warranty-availability endpoint reveals whether a warranty exists at an arbitrary address, so
|
|
239
|
+
unauthenticated probing must be prevented. The legitimate opposite case is
|
|
240
|
+
`GET /v2/campaigns/jobs` (`_Model_True_Campaign::jobs`), which proxies TOGA's public Paylocity
|
|
241
|
+
recruiting feed for the marketing site and **is** granted to True role 4 (Public) — see
|
|
242
|
+
[Careers — Paylocity job openings](../../../../standalone/apps/togatech/features/careers-paylocity-job-openings.md).
|
|
243
|
+
The test is "would an anonymous caller learn anything they shouldn't?", not "is Public ever
|
|
244
|
+
allowed?".
|
|
238
245
|
- Verified by applying the file **twice** to two environments: exactly one script row and one
|
|
239
246
|
grant row each.
|
|
240
247
|
|
|
@@ -302,6 +309,16 @@ an `AclRecordScripts` grant in each client DB.
|
|
|
302
309
|
prior value and restore it, or you disable caching for the rest of the request.
|
|
303
310
|
- **`aclDatabase` has no `TEAM`.** A `Team`-schema record registers as `'CORE'`; picking
|
|
304
311
|
`'CLIENT'` sends the engine looking in the wrong cluster.
|
|
312
|
+
- **⚠ Never hardcode a `Core.RecordScripts` id in a grant — it WILL be renumbered on apply.**
|
|
313
|
+
Concrete case (verified on production 2026-08-13): `Core/2026-07-15a -
|
|
314
|
+
CampaignPaylocityJobsRecordScript.sql` wrote the `campaigns`/`jobs` script as **id 25** ("next
|
|
315
|
+
free; prod max was 24"), and its sibling `Client_True/2026-07-16a -
|
|
316
|
+
CampaignPaylocityJobsScriptAcl.sql` granted `recordScriptId = 25` — but the row landed as **id
|
|
317
|
+
30** in production, so the grants pointed at nothing and a later file
|
|
318
|
+
(`Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql`) had to re-grant against 30. A grant that
|
|
319
|
+
references a stale id inserts a row happily and fails **only** at request time as a missing-grant
|
|
320
|
+
error. Resolve the id by `recordId` + `method` + `route` where the databases allow it, and always
|
|
321
|
+
verify the actual id in the target environment.
|
|
305
322
|
- **Registration is per-environment and split.** A missing `Core.RecordScripts` row → the route
|
|
306
323
|
isn't a script (falls through to normal CRUD / 404-shaped behavior); a missing
|
|
307
324
|
`AclRecordScripts` grant → the caller's roles can't run it. Both rows must exist in every
|
|
@@ -315,6 +332,11 @@ an `AclRecordScripts` grant in each client DB.
|
|
|
315
332
|
|
|
316
333
|
## Change history
|
|
317
334
|
|
|
335
|
+
- 2026-08-13 — TRUE-80091/TRUE-80094 (`GET /v2/campaigns/jobs`): added the concrete evidence that a
|
|
336
|
+
**hardcoded `Core.RecordScripts` id in an ACL grant gets renumbered on apply** (written as 25,
|
|
337
|
+
landed as 30 in production, so the first grant file pointed at nothing and a second file had to
|
|
338
|
+
re-grant), and softened "never grant Public" to "not unless the payload is genuinely public" —
|
|
339
|
+
the jobs feed proxy is a legitimate Public grant. (tcox)
|
|
318
340
|
- 2026-08-06 — TRUE-80282 testing: sharpened the envelope contract from ambiguous ("nested under the
|
|
319
341
|
route segment") to the explicit **two-level `data.<recordRoute>.<scriptRoute>`**, with a
|
|
320
342
|
route-vs-`phpMethod` table and the working precedent
|
|
@@ -6,8 +6,8 @@ project: TOGa Blox
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: [apeterson]
|
|
9
|
+
updated: 2026-08-13
|
|
10
|
+
owners: [apeterson, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- toga-blox/src/api/index.ts
|
|
13
13
|
- toga-blox/src/api/axiosInstance.ts
|
|
@@ -95,6 +95,20 @@ Filterable`, `hyperlinkField`, `imageUrlField`, `sticky`, `precision`, …), `Da
|
|
|
95
95
|
- Only **401** auto-retries; other network errors need React Query `retry`.
|
|
96
96
|
- 401 refresh needs a `refreshToken` in localStorage, else it logs out (hard redirect to `/`).
|
|
97
97
|
- `transactionId` is injected automatically (request tracing); `depth=-1` returns hydrated nesting.
|
|
98
|
+
- **⚠ `fetchPublicToken()` (`POST /auth/public`) is ORIGIN-GATED** — api2 resolves the browser's
|
|
99
|
+
`Origin` header against `Core.Domains` to establish `appId`/`clientId` before minting, and
|
|
100
|
+
rejects an unregistered origin with **EN-6 "domain not permitted"**. Every **host variant needs
|
|
101
|
+
its own `Core.Domains` row**: verified 2026-08-13 that `https://togatech.com` and
|
|
102
|
+
`http://localhost` (any port — the port is stripped) are allowed while
|
|
103
|
+
**`https://www.togatech.com` is rejected**. Symptom on a public marketing page is *silent* —
|
|
104
|
+
no token, so every anonymous fetch fails and the section renders its empty state. Fix is a
|
|
105
|
+
`Core.Domains` row, not frontend code. See
|
|
106
|
+
[encrypted user-uuid auth handoff](../../api2/features/encrypted-user-uuid-auth-handoff.md) for
|
|
107
|
+
the same gating on the SSO handoff route.
|
|
98
108
|
|
|
99
109
|
## Change history
|
|
110
|
+
- 2026-08-13 — TRUE-80091: recorded that `fetchPublicToken()`/`POST /auth/public` is **origin-gated
|
|
111
|
+
against `Core.Domains` (EN-6 on an unregistered origin)** and that each host variant needs its own
|
|
112
|
+
row — `www.togatech.com` is rejected while `togatech.com` and `localhost` are allowed, which
|
|
113
|
+
silently blanks anonymous data on a public site. (tcox)
|
|
100
114
|
- 2026-06-23 — Documented the API client: axios init contract, auth/refresh interceptors, query-option serialization, generic CRUD, table-data fetchers, and the 2.0 response envelope (apeterson).
|
package/knowledge/INDEX.md
CHANGED
|
@@ -5,7 +5,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
5
5
|
## 1.0 framework
|
|
6
6
|
|
|
7
7
|
- **library** (Library) _(framework core)_ — 18 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
|
|
8
|
-
- **worker** (Worker) —
|
|
8
|
+
- **worker** (Worker) — 24 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
|
|
9
9
|
- **dbchanges** (Database Changes) _(framework core)_ — 1 doc(s) → [1.0/apps/dbchanges/INDEX.md](1.0/apps/dbchanges/INDEX.md)
|
|
10
10
|
- **worker1.5** (Worker 1.5) — 0 doc(s) → [1.0/apps/worker1.5/INDEX.md](1.0/apps/worker1.5/INDEX.md)
|
|
11
11
|
- **togadesk** (TOGa Desk) — 12 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
|
|
@@ -36,7 +36,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
36
36
|
|
|
37
37
|
## standalone framework
|
|
38
38
|
|
|
39
|
-
- **togatech** (TOGA Technology Website) —
|
|
39
|
+
- **togatech** (TOGA Technology Website) — 6 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
|
|
40
40
|
- **websocket** (WebSocket Server) — 2 doc(s) → [standalone/apps/websocket/INDEX.md](standalone/apps/websocket/INDEX.md)
|
|
41
41
|
- **forward** (Forwarder) — 3 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
|
|
42
42
|
- **claude** (Claude Harness) — 7 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
|
|
@@ -57,6 +57,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
57
57
|
- **Quad Graphics** (`quad`) → [clients/quad/INDEX.md](clients/quad/INDEX.md)
|
|
58
58
|
- **Rate** (`rate`) → [clients/rate/INDEX.md](clients/rate/INDEX.md)
|
|
59
59
|
- **Richmond University Medical Center** (`rumcsi`) → [clients/rumcsi/INDEX.md](clients/rumcsi/INDEX.md)
|
|
60
|
+
- **Staples** (`staples`) → [clients/staples/INDEX.md](clients/staples/INDEX.md)
|
|
60
61
|
- **Tow Foundation** (`tow-foundation`) → [clients/tow-foundation/INDEX.md](clients/tow-foundation/INDEX.md)
|
|
61
62
|
- **TOGA Technology** (`true`) → [clients/true/INDEX.md](clients/true/INDEX.md)
|
|
62
63
|
- **Walmart Client Profile** (`walmart`) → [clients/walmart/INDEX.md](clients/walmart/INDEX.md)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Client: Staples `staples`
|
|
2
|
+
|
|
3
|
+
| Doc | Framework | Summary | Files |
|
|
4
|
+
|-----|-----------|---------|-------|
|
|
5
|
+
| [Staples](profile.md) | 1.0 | Staples is a **headless** integration client — no UI, Worker 1.0 only, depending on the `library` (1.0 `App_`) core. | worker/crons/sync/staples/sync_staples_cxml.php, worker/crons/sync/staples/staples_asn_netsuite.php |
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Staples
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
apps:
|
|
5
|
+
- worker
|
|
6
|
+
project: Worker
|
|
7
|
+
client: staples
|
|
8
|
+
type: profile
|
|
9
|
+
status: active
|
|
10
|
+
updated: 2026-08-13
|
|
11
|
+
owners: [jcardinal]
|
|
12
|
+
files:
|
|
13
|
+
- worker/crons/sync/staples/sync_staples_cxml.php
|
|
14
|
+
- worker/crons/sync/staples/staples_asn_netsuite.php
|
|
15
|
+
related:
|
|
16
|
+
- ../../1.0/apps/worker/features/staples-cxml-order-import.md
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Summary
|
|
20
|
+
|
|
21
|
+
Staples is a **headless** integration client — no UI, Worker 1.0 only, depending on the
|
|
22
|
+
`library` (1.0 `App_`) core. Its entire footprint is two crons under
|
|
23
|
+
`worker/crons/sync/staples/`:
|
|
24
|
+
|
|
25
|
+
1. **`sync_staples_cxml.php`** — hourly inbound: pulls cXML purchase orders off SFTP,
|
|
26
|
+
creates NetSuite Sales Orders, and writes 855 acknowledgements back. This is the
|
|
27
|
+
documented feature — see
|
|
28
|
+
**`1.0/apps/worker/features/staples-cxml-order-import.md`**.
|
|
29
|
+
2. **`staples_asn_netsuite.php`** — outbound ASN (Advance Ship Notice) from NetSuite back
|
|
30
|
+
to Staples. (Not yet documented in detail.)
|
|
31
|
+
|
|
32
|
+
**NetSuite identity:** Staples is the NetSuite parent entity/customer **internalId 30057**.
|
|
33
|
+
Individual Staples buying accounts are NetSuite **child customers** matched by their Staples
|
|
34
|
+
`CustomerID` against **saved search 8412** (match key `custentity_external_customer_id`;
|
|
35
|
+
location from `custentity_customer_location`).
|
|
36
|
+
|
|
37
|
+
**Onboarding rule (load-bearing):** any new Staples buying account must be set up in
|
|
38
|
+
NetSuite — as a child customer of 30057, with its **External Customer ID** = the Staples
|
|
39
|
+
`CustomerID` **and** a valid **Customer Location** — *before* its first order arrives.
|
|
40
|
+
Miss either and inbound orders for that account silently jam on SFTP with no error (see the
|
|
41
|
+
feature doc's gotchas).
|
|
@@ -7,12 +7,13 @@ apps:
|
|
|
7
7
|
- worker2
|
|
8
8
|
- dbchanges2
|
|
9
9
|
- api2
|
|
10
|
+
- togatech
|
|
10
11
|
project: _Underscore
|
|
11
12
|
client: true
|
|
12
13
|
type: profile
|
|
13
14
|
status: active
|
|
14
|
-
updated: 2026-08-
|
|
15
|
-
owners: [jcardinal, kyalamarthi]
|
|
15
|
+
updated: 2026-08-13
|
|
16
|
+
owners: [jcardinal, kyalamarthi, tcox]
|
|
16
17
|
files: []
|
|
17
18
|
related:
|
|
18
19
|
- clients/true/features/users-personas-data-model.md
|
|
@@ -34,6 +35,14 @@ Scripts on `_Model_Team_Sprint` (over the `Team` schema), with the `AclRecordScr
|
|
|
34
35
|
scoped to True roles 1/3/4 — hence `api2` is in this client's `apps`. See
|
|
35
36
|
[Sprint Dashboard API](../../2.0/apps/api2/features/sprint-dashboard-api.md).
|
|
36
37
|
|
|
38
|
+
The public **TOGA Technology website** (repo `togatech`, standalone React SPA, app id **41**
|
|
39
|
+
"TOGa Technologies Website") is True's own marketing site and consumes True-scoped api2 endpoints
|
|
40
|
+
**anonymously**: the toga-blox client mints a public token from `POST /v2/auth/public` (gated on
|
|
41
|
+
`Origin` against `Core.Domains` — `togatech.com` is registered, `www.togatech.com` is **not**) and
|
|
42
|
+
calls `GET /v2/campaigns/jobs`, a Record Script on `_Model_True_Campaign` granted to True roles 1
|
|
43
|
+
(Base) and 4 (Public). See
|
|
44
|
+
[Careers — Paylocity job openings](../../standalone/apps/togatech/features/careers-paylocity-job-openings.md).
|
|
45
|
+
|
|
37
46
|
The **Tools** app (1.0; repo `tools`) authenticates True users via **SSO** through
|
|
38
47
|
`saml.togahub.com` and reads the `Client_True` DB **read-only** to gate its internal tooling by
|
|
39
48
|
staff persona (see `1.0/apps/tools/`). Its gateway return domain is registered in `Core.Domains`
|
|
@@ -3,5 +3,7 @@
|
|
|
3
3
|
| Doc | Summary | Files |
|
|
4
4
|
|-----|---------|-------|
|
|
5
5
|
| [TOGA Technology Website Architecture](architecture.md) | The public-facing TOGA Technology corporate/marketing website. | togatech/src/main.tsx, togatech/src/App.tsx, togatech/src/routes.tsx, togatech/src/lib/api.ts, togatech/src/lib/contentful.ts, togatech/src/themeConfig/ThemeContext.tsx, togatech/vite.config.ts, togatech/package.json |
|
|
6
|
+
| [Careers Section — Live Paylocity Job Openings (About page + /v2/campaigns/jobs proxy)](features/careers-paylocity-job-openings.md) | The togatech **About** page renders a "Join Our Team" careers section listing **live job openings pulled from TOGA's Paylocity recruiting feed**. | togatech/src/pages/About/AboutPage.tsx, togatech/src/pages/About/types.ts, togatech/src/pages/About/view/components/JobOpening/JobOpening.tsx, togatech/src/pages/About/viewModel/useAboutViewModel.ts, togatech/src/pages/About/viewModel/FIELDS/ABOUTPAGEFIELDS.json, togatech/cypress/e2e/aboutPage/jobPosting.cy.ts, _underscore/Model/True/Campaign.php, dbchanges2/Core/2026-07-15a - CampaignPaylocityJobsRecordScript.sql, dbchanges2/Client_True/2026-07-16a - CampaignPaylocityJobsScriptAcl.sql, dbchanges2/Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql |
|
|
6
7
|
| [SEO / AEO / GEO — Prerendering, Single-Source Meta & Structured Data](features/seo-aeo-geo-prerender.md) | Makes togatech.com visible and citable to search engines **and** AI answer engines (ChatGPT/Perplexity/Claude search, Google AI Overviews). | togatech/vite.config.ts, togatech/scripts/prerender.mjs, togatech/src/routes.config.json, togatech/src/main.tsx, togatech/src/App.tsx, togatech/src/components/SEO/Seo.tsx, togatech/src/components/SEO/JsonLd.tsx, togatech/src/components/SEO/schema.ts, togatech/src/components/templates/AppLayout.tsx, togatech/src/pages/WhatWeDo/WhatWeDoPage.tsx, togatech/src/pages/WhatWeDo/viewModel/FIELDS/WHATWEDOPAGEFIELDS.json, togatech/src/pages/About/AboutPage.tsx, togatech/src/pages/Contact/ContactPage.tsx, togatech/src/pages/OurPlatform/OurPlatformPage.tsx, togatech/public/robots.prod.txt, togatech/public/sitemap.xml, togatech/public/llms.txt |
|
|
7
8
|
| [Creating Pull Requests on togatech](workflows/creating-pull-requests.md) | How to open a PR against `agilantsolutions/togatech`. | togatech/.git/config |
|
|
9
|
+
| [Running togatech Locally & Verifying a Page Without Cypress](workflows/local-dev-and-headless-verification.md) | The gotcha-avoidance path for getting `togatech` up on a dev machine and actually *seeing* a data-driven section render. | togatech/package.json, togatech/cypress.config.ts, togatech/src/App.tsx |
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Careers Section — Live Paylocity Job Openings (About page + /v2/campaigns/jobs proxy)
|
|
3
|
+
framework: "standalone"
|
|
4
|
+
repo: togatech
|
|
5
|
+
project: TOGA Technology Website
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-13
|
|
10
|
+
owners: ["tcox"]
|
|
11
|
+
files:
|
|
12
|
+
- togatech/src/pages/About/AboutPage.tsx
|
|
13
|
+
- togatech/src/pages/About/types.ts
|
|
14
|
+
- togatech/src/pages/About/view/components/JobOpening/JobOpening.tsx
|
|
15
|
+
- togatech/src/pages/About/viewModel/useAboutViewModel.ts
|
|
16
|
+
- togatech/src/pages/About/viewModel/FIELDS/ABOUTPAGEFIELDS.json
|
|
17
|
+
- togatech/cypress/e2e/aboutPage/jobPosting.cy.ts
|
|
18
|
+
- _underscore/Model/True/Campaign.php
|
|
19
|
+
- dbchanges2/Core/2026-07-15a - CampaignPaylocityJobsRecordScript.sql
|
|
20
|
+
- dbchanges2/Client_True/2026-07-16a - CampaignPaylocityJobsScriptAcl.sql
|
|
21
|
+
- dbchanges2/Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql
|
|
22
|
+
related:
|
|
23
|
+
- ../architecture.md
|
|
24
|
+
- ../../../../2.0/apps/api2/features/record-scripts.md
|
|
25
|
+
- ../../../../2.0/apps/toga-blox/features/api-client.md
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
# Careers Section — Live Paylocity Job Openings
|
|
29
|
+
|
|
30
|
+
## Summary
|
|
31
|
+
|
|
32
|
+
The togatech **About** page renders a "Join Our Team" careers section listing **live job
|
|
33
|
+
openings pulled from TOGA's Paylocity recruiting feed**. The browser never touches Paylocity:
|
|
34
|
+
the site calls **`GET /v2/campaigns/jobs`** on api2, which dispatches a **Record Script** to
|
|
35
|
+
`_Model_True_Campaign::jobs()` in `_underscore`, which server-side proxies the public Paylocity
|
|
36
|
+
feed. The endpoint is reachable **unauthenticated** (public role) via the anonymous token that
|
|
37
|
+
toga-blox mints from `POST /v2/auth/public`.
|
|
38
|
+
|
|
39
|
+
Backend (TRUE-80094: model method + api2 registration + ACL migrations) is **merged and live in
|
|
40
|
+
production** — verified 2026-08-13 returning 7 real postings. The frontend move (TRUE-80091) puts
|
|
41
|
+
the section on **About**; it previously lived on the **Contact** page against static/Contentful
|
|
42
|
+
content, and that Contact-page jobs code, types, fields, and Cypress spec were removed.
|
|
43
|
+
|
|
44
|
+
## Key files / entry points
|
|
45
|
+
|
|
46
|
+
**Frontend (togatech):**
|
|
47
|
+
- `src/pages/About/viewModel/useAboutViewModel.ts` — React Query fetch (`queryKey: ['jobPosts']`,
|
|
48
|
+
24h `staleTime`), envelope unwrap, and `mapJobs()` (Paylocity shape → view shape).
|
|
49
|
+
- `src/pages/About/view/components/JobOpening/JobOpening.tsx` — presentational section (moved
|
|
50
|
+
from `pages/Contact/`); testids `job-posting-section` / `job-opening-cards`.
|
|
51
|
+
- `src/pages/About/AboutPage.tsx` — renders `<JobOpening>` between **Culture** and the
|
|
52
|
+
**Carousel**.
|
|
53
|
+
- `src/pages/About/viewModel/FIELDS/ABOUTPAGEFIELDS.json` — new **`careers`** block (heading,
|
|
54
|
+
subheading, "View All Openings" LinkedIn button, image + alt text).
|
|
55
|
+
- `src/pages/About/types.ts` — `JobOpening`, `JobPostFields`.
|
|
56
|
+
- `cypress/e2e/aboutPage/jobPosting.cy.ts` — moved from `cypress/e2e/contactPage/`, now visits
|
|
57
|
+
`/about`.
|
|
58
|
+
|
|
59
|
+
**Backend:**
|
|
60
|
+
- `_underscore/Model/True/Campaign.php` — `public static function jobs(&$api): object` plus the
|
|
61
|
+
`PAYLOCITY_JOBS_FEED_URL` const (public per-company recruiting feed URL; no credential) and
|
|
62
|
+
`HTTP_STATUS_SUCCESS_MIN/MAX`.
|
|
63
|
+
- api2 dispatch: `Core.RecordScripts` row for record `campaigns` (**recordId 296**), `method GET`,
|
|
64
|
+
`route jobs`, `phpMethod jobs`.
|
|
65
|
+
|
|
66
|
+
## How it works
|
|
67
|
+
|
|
68
|
+
1. **Fetch.** `useAboutViewModel` calls toga-blox `apiGet('campaigns/jobs')` against base
|
|
69
|
+
`VITE_API` (`https://api.togahub.com/v2` in production).
|
|
70
|
+
2. **Auth.** The toga-blox axios interceptor finds no `accessToken` in localStorage and mints an
|
|
71
|
+
anonymous one with `POST /v2/auth/public` (no body), then sends it as `Authorization: Bearer`.
|
|
72
|
+
3. **Dispatch.** api2's `/v2` engine matches `campaigns` (record 296) + trailing segment `jobs`
|
|
73
|
+
to the `Core.RecordScripts` row and invokes `_Model_True_Campaign::jobs(&$api)`. Authorization
|
|
74
|
+
comes from the **script grant**, not record CRUD ACL — see
|
|
75
|
+
[Record Scripts](../../../../2.0/apps/api2/features/record-scripts.md).
|
|
76
|
+
4. **Server-side proxy.** `jobs()` issues a `GET` through `_ApiRequest` (`ENCODE__JSON`) to
|
|
77
|
+
`PAYLOCITY_JOBS_FEED_URL` with `setThrowExceptionsOnFailure(false)`, and returns
|
|
78
|
+
`{success: true, jobs: <decoded feed>}`. On a thrown exception, a non-2xx `responseCode`, or a
|
|
79
|
+
null body it `error_log`s and returns `{success: false, error: '<message>'}` — **the HTTP call
|
|
80
|
+
still succeeds with envelope `isSuccess: true`**, so `data…success` is the inner proxy status
|
|
81
|
+
and is distinct from the platform envelope flag. **Why proxy:** keeps the feed's CORS/origin
|
|
82
|
+
concerns server-side and gives the frontend one consistent API shape.
|
|
83
|
+
5. **Unwrap + map.** The view model reads `envelope.data.campaigns.jobs.jobs.jobs ?? []` (see the
|
|
84
|
+
gotcha below), then `mapJobs()` projects each entry to
|
|
85
|
+
`{id, title, location, type, compensation, postLink}`:
|
|
86
|
+
`jobId → id`, `jobLocation.locationDisplayName → location`,
|
|
87
|
+
`jobTypesArray[0] ?? jobTypes → type`, `salaryDescription → compensation`,
|
|
88
|
+
`applyUrl ?? displayUrl → postLink`.
|
|
89
|
+
6. **Render.** Each card shows title, a target-blank **Apply** link, and a dot-separated
|
|
90
|
+
location / type / compensation row. Empty array → **"No job openings available at this
|
|
91
|
+
time."** (the section itself always renders; copy and image come from the `careers` FIELDS
|
|
92
|
+
block, so a feed outage degrades to static content).
|
|
93
|
+
|
|
94
|
+
## Data model
|
|
95
|
+
|
|
96
|
+
No local schema. The Paylocity feed entries carry `jobId`, `title`, `salaryDescription`,
|
|
97
|
+
`applyUrl`, `displayUrl`, `jobTypes`, `jobTypesArray[]`, `jobLocation.locationDisplayName`.
|
|
98
|
+
|
|
99
|
+
Registration/ACL metadata (all merged to dbchanges2 `_main` and applied to production):
|
|
100
|
+
|
|
101
|
+
| Migration | What it does |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `Core/2026-07-15a - CampaignPaylocityJobsRecordScript.sql` | `Core.RecordScripts` row: recordId **296** (`campaigns`), `GET`, route `jobs`, phpMethod `jobs`. Written with **id 25** (next free at the time). |
|
|
104
|
+
| `Client_True/2026-07-16a - CampaignPaylocityJobsScriptAcl.sql` | `AclRecordScripts` grants to True roles **1 (Base)** and **4 (Public)** — `campaigns` is a CLIENT-`aclDatabase` record, so the caller's *client* roles are checked. |
|
|
105
|
+
| `Client_True/2026-08-05 - CampaignsJobsPublicAcl.sql` | Adds the record-level READ grant for role 4 per app (`AclRecordPermissions`, appId **41** "TOGa Technologies Website" and **39** Talos) **plus** an `AclRecordScripts` invoke grant referencing script id **30**. |
|
|
106
|
+
|
|
107
|
+
## Client variations
|
|
108
|
+
|
|
109
|
+
None. The feed is TOGA's own (client **True** / `Client_True`); no other tenant consumes it.
|
|
110
|
+
|
|
111
|
+
## Gotchas / known issues
|
|
112
|
+
|
|
113
|
+
- **⚠ The array is nested FOUR levels: `data.campaigns.jobs.jobs.jobs`.** Two levels come from
|
|
114
|
+
the record-script envelope contract (`data.<recordRoute>.<scriptRoute>` = `data.campaigns.jobs`),
|
|
115
|
+
the third from the method's own `{success, jobs}` wrapper, and the fourth because the Paylocity
|
|
116
|
+
feed object *itself* has a `jobs` key. Reading one level short yields `undefined` → an
|
|
117
|
+
empty-looking careers section with no error.
|
|
118
|
+
- **⚠ `POST /v2/auth/public` is DOMAIN-GATED on the `Origin` header** against `Core.Domains`.
|
|
119
|
+
Verified 2026-08-13: `https://togatech.com` ✅ and `http://localhost` (any port — the port is
|
|
120
|
+
stripped) ✅, but **`https://www.togatech.com` is REJECTED** with **EN-6 "domain not
|
|
121
|
+
permitted"**. If the site is ever served from the `www` host, no public token is minted and the
|
|
122
|
+
careers section silently renders "No job openings available at this time." Fix is a
|
|
123
|
+
`Core.Domains` row for the `www` origin, not a frontend change.
|
|
124
|
+
- **The inner `success: false` is invisible to the envelope.** A Paylocity outage returns
|
|
125
|
+
HTTP 200 / `isSuccess: true` with `data.campaigns.jobs.success === false`; the frontend
|
|
126
|
+
currently ignores that flag and falls through to the empty state. Check `error_log`
|
|
127
|
+
(`Paylocity jobs feed request failed` / `returned HTTP <code>`) when jobs vanish in production.
|
|
128
|
+
- **⚠ React Query is persisted to localStorage** (persister key `"commerce"`, 24h `staleTime`),
|
|
129
|
+
so after one successful load you will see **no network request** for `jobPosts` for a day.
|
|
130
|
+
When debugging, clear the persisted cache **and** `accessToken`/`refreshToken` from
|
|
131
|
+
localStorage — otherwise you are also testing a stale token.
|
|
132
|
+
- **Script id drift.** The Core migration hardcodes RecordScript **id 25** while the later
|
|
133
|
+
Client_True ACL file references **id 30** — the row was renumbered when applied to production.
|
|
134
|
+
Trust the environment, not the file, when checking grants.
|
|
135
|
+
- **Not shipped yet.** The frontend lives on branch **`TRUE-80091`** (committed, no PR at time of
|
|
136
|
+
writing); base branch for togatech PRs is **`_production`**, not `_main` (see
|
|
137
|
+
[Creating Pull Requests on togatech](../workflows/creating-pull-requests.md)).
|
|
138
|
+
|
|
139
|
+
## Change history
|
|
140
|
+
|
|
141
|
+
- 2026-08-13 — TRUE-80091: careers section moved to the **About** page (between Culture and
|
|
142
|
+
Carousel) and switched to the live Paylocity feed via `GET /v2/campaigns/jobs`; the old
|
|
143
|
+
Contact-page jobs code/types/fields and its Cypress spec were removed. Documented the
|
|
144
|
+
end-to-end path (TRUE-80094 backend, already live in production): api2 record-script dispatch →
|
|
145
|
+
`_Model_True_Campaign::jobs()` server-side proxy → four-level unwrap
|
|
146
|
+
`data.campaigns.jobs.jobs.jobs`, the public-role ACL/app grants, and the
|
|
147
|
+
**`www.togatech.com` `/auth/public` EN-6 rejection** that would blank the section. (tcox)
|
|
148
|
+
|
|
149
|
+
## Related docs
|
|
150
|
+
|
|
151
|
+
- [Record Scripts](../../../../2.0/apps/api2/features/record-scripts.md) — the authoring/dispatch
|
|
152
|
+
contract and the `data.<recordRoute>.<scriptRoute>` envelope rule.
|
|
153
|
+
- [toga-blox API client](../../../../2.0/apps/toga-blox/features/api-client.md) — the axios
|
|
154
|
+
interceptor that mints and attaches the public token.
|
|
155
|
+
- [togatech architecture](../architecture.md) — the SPA this section lives in.
|
|
156
|
+
- [TOGA Technology client profile](../../../../clients/true/profile.md) — the tenant that owns the
|
|
157
|
+
endpoint.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Running togatech Locally & Verifying a Page Without Cypress
|
|
3
|
+
framework: "standalone"
|
|
4
|
+
repo: togatech
|
|
5
|
+
project: TOGA Technology Website
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-08-13
|
|
10
|
+
owners: ["tcox"]
|
|
11
|
+
files:
|
|
12
|
+
- togatech/package.json
|
|
13
|
+
- togatech/cypress.config.ts
|
|
14
|
+
- togatech/src/App.tsx
|
|
15
|
+
related:
|
|
16
|
+
- ../architecture.md
|
|
17
|
+
- ../features/careers-paylocity-job-openings.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
# Running togatech Locally & Verifying a Page Without Cypress
|
|
21
|
+
|
|
22
|
+
## Summary
|
|
23
|
+
|
|
24
|
+
The gotcha-avoidance path for getting `togatech` up on a dev machine and actually *seeing* a
|
|
25
|
+
data-driven section render. Three things bite in order: **stale `node_modules`** (the dev server
|
|
26
|
+
fails outright), **`lint` cannot run from a clean install**, and **Cypress 15's binary fails its
|
|
27
|
+
smoke test on Windows** — plus a persisted React Query cache that makes it look like nothing is
|
|
28
|
+
fetching. Verified on Windows 11, 2026-08-13.
|
|
29
|
+
|
|
30
|
+
## Steps
|
|
31
|
+
|
|
32
|
+
1. **`npm install` before anything else, every time you pull.** A stale `node_modules` shows up as
|
|
33
|
+
a Vite resolve error, not as a missing dependency:
|
|
34
|
+
`"react-helmet-async" is imported by … but could not be resolved`. Both
|
|
35
|
+
`react-helmet-async` and `react-google-recaptcha-v3` are declared in `package.json` (added by
|
|
36
|
+
the SEO work) but absent from an older install, and the site simply will not load.
|
|
37
|
+
2. **Start the dev server** — `npm run dev`, or `npm run togatech` for the `--host togatech` variant
|
|
38
|
+
(Vite `allowedHosts: ["togatech"]`).
|
|
39
|
+
3. **Clear localStorage before judging any data fetch.** `App.tsx` wraps the app in
|
|
40
|
+
`PersistQueryClientProvider` with the persister key **`"commerce"`** and a **24h `staleTime`**,
|
|
41
|
+
so a previously cached query (e.g. `jobPosts`) renders from localStorage with **no network
|
|
42
|
+
request at all**. Clear the persisted cache **and** `accessToken` / `refreshToken` — a stale
|
|
43
|
+
public token from `POST /v2/auth/public` will otherwise be reused too.
|
|
44
|
+
4. **Do not expect `npm run lint` to work.** The script is
|
|
45
|
+
`eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0`, but **`eslint` is
|
|
46
|
+
not in `package.json` dependencies or devDependencies at all** — there is no local binary, so a
|
|
47
|
+
clean install cannot lint. Rely on `tsc` (`npm run build` = `tsc && vite build && …`) for type
|
|
48
|
+
safety and review manually, or install eslint ad hoc.
|
|
49
|
+
5. **If Cypress won't run, verify headlessly with the repo's Puppeteer.** `cypress` 15.11 fails its
|
|
50
|
+
own binary verification on this Windows machine — `bad option: --smoke-test` — and
|
|
51
|
+
`npx cypress install` does **not** fix it. Workaround: `puppeteer` is already a dependency, so
|
|
52
|
+
drive **system Chrome** directly by passing `executablePath` to `puppeteer.launch()`, navigate to
|
|
53
|
+
the dev-server URL, and assert on the rendered DOM (the section testids, e.g.
|
|
54
|
+
`job-posting-section` / `job-opening-cards`). Same technique the production build already uses in
|
|
55
|
+
`scripts/prerender.mjs`.
|
|
56
|
+
|
|
57
|
+
## Gotchas / known issues
|
|
58
|
+
|
|
59
|
+
- A Vite "imported but could not be resolved" error almost always means **stale install**, not a
|
|
60
|
+
missing package — check `package.json` before adding a dependency.
|
|
61
|
+
- `npm run lint` is effectively dead until eslint is added to the repo's dev dependencies; do not
|
|
62
|
+
treat a lint failure-to-start as a code problem.
|
|
63
|
+
- Cypress specs are still the source of truth for e2e coverage — the Puppeteer route is a *local
|
|
64
|
+
verification* workaround, not a replacement. Don't delete or rewrite specs because Cypress won't
|
|
65
|
+
launch locally.
|
|
66
|
+
- Watching the network tab is not proof a fetch is broken: see step 3 (persisted query cache).
|
|
67
|
+
|
|
68
|
+
## Change history
|
|
69
|
+
|
|
70
|
+
- 2026-08-13 — TRUE-80091: first recorded, from bringing the site up to verify the About-page
|
|
71
|
+
careers section — stale `node_modules` blocking the dev server (`react-helmet-async`,
|
|
72
|
+
`react-google-recaptcha-v3`), `eslint` missing from `package.json` so `npm run lint` can't run,
|
|
73
|
+
Cypress 15 `--smoke-test` binary failure on Windows with the Puppeteer + system-Chrome
|
|
74
|
+
workaround, and the localStorage-persisted React Query cache (key `"commerce"`, 24h staleTime)
|
|
75
|
+
masking fetches. (tcox)
|
package/package.json
CHANGED