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.
@@ -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-04
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`.** A warranty-availability endpoint reveals whether a warranty exists
237
- at an arbitrary address; unauthenticated probing must be prevented.
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-06-23
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).
@@ -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) — 23 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
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) — 4 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
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-04
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.571",
3
+ "version": "1.0.573",
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",