toga-ai 1.0.1039 → 1.0.1041
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/features/compass-partial-in-transit-delivered-emails.md +7 -0
- package/knowledge/2.0/apps/worker2/features/all-client-email-queue-monitor.md +10 -3
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/compass-canada/profile.md +6 -0
- package/knowledge/clients/compass-canada/workflows/copying-a-us-user-into-the-canada-tenant.md +7 -2
- package/knowledge/clients/compass-usa/features/approval-decision-flow.md +8 -1
- package/knowledge/clients/compass-usa/features/contact-email-resolution.md +6 -2
- package/knowledge/standalone/apps/togatech/INDEX.md +4 -2
- package/knowledge/standalone/apps/togatech/architecture.md +20 -14
- package/knowledge/standalone/apps/togatech/features/navv2-global-nav.md +13 -17
- package/knowledge/standalone/apps/togatech/features/platform-product-page.md +71 -0
- package/knowledge/standalone/apps/togatech/features/seo-aeo-geo-prerender.md +41 -40
- package/knowledge/standalone/apps/togatech/features/talos-product-page.md +22 -39
- package/knowledge/standalone/apps/togatech/workflows/designer-prototype-port.md +57 -0
- package/knowledge/standalone/apps/togatech/workflows/local-dev-and-headless-verification.md +5 -1
- package/package.json +1 -1
|
@@ -24,6 +24,7 @@ related:
|
|
|
24
24
|
- ../../../clients/compass-usa/features/tracking-number-origin-and-asn-link.md
|
|
25
25
|
- ../../../clients/compass-usa/features/contact-email-resolution.md
|
|
26
26
|
- ../../../2.0/apps/worker2/features/tracking-status-refresh.md
|
|
27
|
+
- ../../../2.0/apps/worker2/features/all-client-email-queue-monitor.md
|
|
27
28
|
- ../../../2.0/apps/worker2/features/oneuptime-worker2-monitoring.md
|
|
28
29
|
- ../../../clients/compass-canada/features/french-order-email-localization.md
|
|
29
30
|
- ../../../clients/compass-usa/features/order-fulfillment-status-per-line.md
|
|
@@ -375,9 +376,15 @@ the tracking number as emailed (it retries next run).
|
|
|
375
376
|
as is (decided 2026-10-15, TRUE-82847).
|
|
376
377
|
- Delivered template = prod `emailTemplateId` 26 in `Logs_Compass.Email`; baseline 2026-09-15..10-15:
|
|
377
378
|
959 SENT, 1 FAILED.
|
|
379
|
+
- **⚠ The delivered cron never checks the send result.** It calls `App_Api_Toga2::send(..., false)`
|
|
380
|
+
(`$throwExceptionOnApiError = false`) and ignores the response, so it marks
|
|
381
|
+
`TrackingNumbers.status = 'DELIVERED'` and inserts notifications even when api2 rejects the email.
|
|
382
|
+
Its "only mark DELIVERED after confirmed send" comment is wrong. "Sent" only means queued to
|
|
383
|
+
`Logs_Compass.Email`; worker2 `Infrastructure/Email/Send` does the real send. Not changed.
|
|
378
384
|
|
|
379
385
|
## Change history
|
|
380
386
|
- 2026-10-15 — Delivered crons (USA + Canada) pick one active `Users` row with email + Contacts fallback; documented the 2-active-users duplicate. (jcardinal)
|
|
387
|
+
- 2026-10-15 — Recorded that the delivered cron ignores the send result (TRUE-82846). (bala)
|
|
381
388
|
- 2026-10-13 — Recorded that the USA delivered cron has no date window, so bulk ASN loads must suppress first. (bala)
|
|
382
389
|
|
|
383
390
|
## Related
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-15
|
|
10
10
|
owners: ["jcardinal", "bala"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Monitor/Operations.php
|
|
@@ -19,6 +19,7 @@ related:
|
|
|
19
19
|
- ./monitoring-framework.md
|
|
20
20
|
- ../../_underscore/features/email-send-pipeline.md
|
|
21
21
|
- ../../_underscore/features/per-client-database-connections.md
|
|
22
|
+
- ../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md
|
|
22
23
|
---
|
|
23
24
|
|
|
24
25
|
Cross-client health check for the 2.0 outbound email queue (`Logs_<Client>.Email`); open when working on email-queue alerting or an all-client DB fan-out.
|
|
@@ -49,7 +50,9 @@ Steps:
|
|
|
49
50
|
|
|
50
51
|
### Alarm rule (explicit product decision)
|
|
51
52
|
|
|
52
|
-
Alarm on **`FAILED > 0` only
|
|
53
|
+
Alarm on **`FAILED > 0` only** (`ALARM_FAILED_COUNT = 0`). `PENDING` is reported as context and **never** alarms — a queue with work in it is normal, and the sender cron runs every minute.
|
|
54
|
+
|
|
55
|
+
**No time window:** the count is every `FAILED` row ever, so **one** failed email holds the alarm `HIGH` until the 90-day `Email` cleanup removes it or someone changes its `status` by hand.
|
|
53
56
|
|
|
54
57
|
### Payload contract — `alarm` and `probe` are SEPARATE tokens
|
|
55
58
|
|
|
@@ -75,6 +78,7 @@ OneUptime can only string-match a request body (no numeric comparison, no JSON k
|
|
|
75
78
|
|
|
76
79
|
## Gotchas
|
|
77
80
|
|
|
81
|
+
- **⚠ Triage: the OneUptime alarm "Compass email queue backlog is too high" IS this monitor** (the UI name was never renamed; there is no threshold of 20/25 and no `worker/monitor/compass/` code). It almost always means `FAILED` rows, not a backlog. First step: `SELECT id, subject, failureReason FROM Email WHERE status = 'FAILED'` on the named client's logs DB, then fix the **producer** that queued the bad row. 2026-10-15 (TRUE-82846): one row, `Logs_Compass.Email` id 146380, `failureReason` "You must provide at least one recipient email address.", 0 pending — caused by the Compass delivered cron, see [Compass in-transit & delivered emails](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md).
|
|
78
82
|
- **⚠ This monitor is blind to an email that was never queued.** It reads `Logs_<Client>.Email`, so it can only see a send that already produced a row. If the producing cron dies, stalls or never selects the work, this monitor reports a **healthy empty queue**. That is exactly what happened during the Compass USA in-transit outage (2026-08-27 → 2026-09-03): the 1.0 sending cron timed out before creating a single email row and this monitor stayed green all week. Pair any email-queue monitor with a **backlog monitor on the producer's input** — first instance `Monitor/Compass/InTransitEmailQueue`, see [OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md).
|
|
79
83
|
- **A client's database name is NOT derivable from `clientIdentifier`.** `Compass_Usa` → `Logs_Compass`. Always resolve `Clients.logDatabaseId` → `Databases.name`. Building `'Logs_' . $clientIdentifier` silently targets a nonexistent database. See [per-client database connections](../../_underscore/features/per-client-database-connections.md).
|
|
80
84
|
- **Do not use `_Database::registerClientDatabases()` for an all-client fan-out.** It INNER JOINs the client and archive databases alongside the log database, so a client missing either row drops out and the method then dereferences a null row and throws. It also opens three connections when only the log one is needed. Run a log-database-only variant with LEFT OUTER JOINs instead.
|
|
@@ -84,7 +88,10 @@ OneUptime can only string-match a request body (no numeric comparison, no JSON k
|
|
|
84
88
|
- **An unconditional `SUM(status = 'X')` cannot use an index.** `SELECT SUM(status='FAILED') FROM Email` full-scans, reading every historical `SENT` row despite `idx_email_status`. Use `WHERE status IN (...) GROUP BY status`. This runs per client on a cron, so the difference compounds.
|
|
85
89
|
- **There is no `_Query::setTimeout()` in the 2.0 framework** (only `_ApiRequest::setTimeout()`). A 2.0 worker therefore has **no per-query timeout**, so a hung database cluster blocks the fan-out until the connection itself times out. Relevant to any loop over all clients.
|
|
86
90
|
- **A passing dev run does not prove cross-cluster correctness.** Non-prod is a single all-in-one database endpoint; production splits Core / `Client_*` / `Logs_*` / `Archive_*` / `Cache` across **separate clusters**.
|
|
87
|
-
- **
|
|
91
|
+
- **Live in prod** — it fired on a real Compass `FAILED` row on 2026-10-15.
|
|
92
|
+
|
|
93
|
+
## Change history
|
|
94
|
+
- 2026-10-15 — Recorded no-time-window alarm behavior and the "Compass email queue backlog" triage path; confirmed live in prod (TRUE-82846). (bala)
|
|
88
95
|
|
|
89
96
|
## Related
|
|
90
97
|
|
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) — 10 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
|
- **talos** (TOGa Talos Chat) — 3 doc(s) → [standalone/apps/talos/INDEX.md](standalone/apps/talos/INDEX.md)
|
|
45
45
|
- **talos-backend** (TOGa Talos) — 32 doc(s) → [standalone/apps/talos-backend/INDEX.md](standalone/apps/talos-backend/INDEX.md)
|
|
@@ -144,6 +144,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
|
|
|
144
144
|
instead of **12 / 8 / 33**. Removing the hack gives them *more* access; whether Canada should get
|
|
145
145
|
it is an **open decision**. See
|
|
146
146
|
[Storefront persona gating](../../2.0/apps/toga2-commerce/features/storefront-persona-gating-and-home-counts.md).
|
|
147
|
+
- **⚠ "Admin Canada" persona 31 grants only 2 categories (found 2026-10-15, TRUE-82848).** Categories
|
|
148
|
+
come from `Personas_Assortments`, not bundles/items. Persona 31 has every bundle + item but only
|
|
149
|
+
assortments 17 + 18, so a user with persona 31 alone (James Fuller, CA user 31583) sees 2 of 16
|
|
150
|
+
categories. Fix `dbchanges2/Client_CompassCanada/2026-10-15a - AdminCanadaPersonaAllAssortments.sql`
|
|
151
|
+
is **not yet run** in any env. See
|
|
152
|
+
[Copying a US user into Canada](workflows/copying-a-us-user-into-the-canada-tenant.md).
|
|
147
153
|
- **⚠ "Blank page at `https://compasscanada.togacommerce.com/`" (reported by Compass Canada IT,
|
|
148
154
|
Sept 2026) is a shared auth-gating bug, not a Canada or caching problem.** A stale **public**
|
|
149
155
|
`accessToken` made the app think the visitor was signed in, so `/` rendered nothing until the
|
package/knowledge/clients/compass-canada/workflows/copying-a-us-user-into-the-canada-tenant.md
CHANGED
|
@@ -6,10 +6,11 @@ project: Database Changes
|
|
|
6
6
|
client: compass-canada
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-15
|
|
10
10
|
owners: [bala]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Client_CompassCanada/2026-09-28a - AddJamesFullerSuperUser.sql
|
|
13
|
+
- dbchanges2/Client_CompassCanada/2026-10-15a - AdminCanadaPersonaAllAssortments.sql
|
|
13
14
|
- worker2/Worker/Client/Compass/PeopleFile.php
|
|
14
15
|
related:
|
|
15
16
|
- ../profile.md
|
|
@@ -17,6 +18,7 @@ related:
|
|
|
17
18
|
- ../../compass-usa/workflows/granting-persona-bundle-access.md
|
|
18
19
|
- ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
|
|
19
20
|
- ../../../2.0/apps/_underscore/features/acl-permission-chain.md
|
|
21
|
+
- ../../../2.0/apps/toga2-commerce/features/storefront-persona-gating-and-home-counts.md
|
|
20
22
|
---
|
|
21
23
|
Giving a Compass **USA** person a login on the Canada TechHub portal: the guarded insert chain, the uuid role/persona mapping, and the worker2 guard that stops the next PEOPLE run switching them off.
|
|
22
24
|
|
|
@@ -62,7 +64,7 @@ Role ids differ between the two tenants. Look up `(SELECT r.id FROM Roles r WHER
|
|
|
62
64
|
|
|
63
65
|
`PeopleFile.php` already keeps `ROLE_UUID_BASE` / `ROLE_UUID_COMPASS_BASE` / `ROLE_UUID_MANAGER` constants for the same reason — reuse those values.
|
|
64
66
|
|
|
65
|
-
**Personas do not map by name either.** The two Canada admin personas: **Admin Canada** id 31, uuid `94eb162c-7adc-bc2e-66fa-c19fed1d1288` (widest
|
|
67
|
+
**Personas do not map by name either.** The two Canada admin personas: **Admin Canada** id 31, uuid `94eb162c-7adc-bc2e-66fa-c19fed1d1288` (widest for bundles + items: 213 items, all active bundles; this is Gina Prince's) and **Managers/VIPs** id 33, uuid `7961068f-8b31-1daf-5f52-0e45a09beb98` (168 items, 21 bundles, 13 categories). Persona **All** is id 1. **Persona 31 was NOT widest for categories** (see Gotchas) until `2026-10-15a` runs. Persona grants resolve through three sources, not just `Users_Personas` — see [Granting persona/bundle access](../../compass-usa/workflows/granting-persona-bundle-access.md).
|
|
66
68
|
|
|
67
69
|
### 4. Add the address to `ALWAYS_ACTIVE_EMAILS_CA`
|
|
68
70
|
In `worker2/Worker/Client/Compass/PeopleFile.php`, **lowercase**, in `ALWAYS_ACTIVE_EMAILS_CA` — **not** `ALWAYS_ACTIVE_EMAILS_BOTH`. The BOTH list's `ensureCrossRegionUsersActive()` also forces `isActive = 1` on the **US** row every run, which would override a real US termination. `isEmailAlwaysActive()` lowercases its input, so a mixed-case constant entry never matches. Both layers of the guard are described in [PEOPLE-File User Lifecycle](../../compass-usa/features/people-file-user-lifecycle.md).
|
|
@@ -71,6 +73,8 @@ In `worker2/Worker/Client/Compass/PeopleFile.php`, **lowercase**, in `ALWAYS_ACT
|
|
|
71
73
|
Run `sql-reviewer` on the SQL file and `php-reviewer` on the PeopleFile change, then give the developer numbered run steps (SQL file + environment order, then the worker2 deploy).
|
|
72
74
|
|
|
73
75
|
## Gotchas
|
|
76
|
+
- **"Admin Canada" (31) alone showed only 2 categories (found 2026-10-15, TRUE-82848).** Storefront categories = `Personas_Assortments` rows for any of the user's personas, not bundles/items. Persona 31 had all bundles + items but only assortments 17 (APC Battery Back-UPs) and 18 (Webcams), so James Fuller (CA user 31583, persona 31 only) saw 2 of 16 categories. Gina Prince / Caleb Moore never noticed because their persona 33 adds 13 categories. Fix: `dbchanges2/Client_CompassCanada/2026-10-15a - AdminCanadaPersonaAllAssortments.sql` links persona 31 (by uuid) to every active assortment and drops a duplicate 18 row; verify expects 16 links. **Not yet run in any env** (beta had the same gap). Until it runs, give a hand-added admin persona 33 too, or check `Personas_Assortments` for the chosen persona.
|
|
77
|
+
- **A NULL `Users.locationId` does not hide catalogue.** Bundle/item ACL expressions (`AclRecordExpressions` 251 items, 253 bundles) still pass through `Users_Personas`.
|
|
74
78
|
- **`Client_CompassCanada.Users` has no `username`, `isSuperUser`, `personaId` or `roleId` column.** Access is entirely `Users_Roles` + `Users_Personas`. The login/personnel username is `c_hrEmpUsername`, stored lowercase.
|
|
75
79
|
- **Leave `Users.password` NULL.** Login is SSO on the email address: only **45 of 28,788** `Client_CompassCanada` users hold a password, and the US rows for these people are NULL too.
|
|
76
80
|
- **`Users.email` uses a case-insensitive collation**, so the stored case does not affect matching — `Client_Compass` often stores these addresses uppercase while Canada stores them lowercase.
|
|
@@ -86,3 +90,4 @@ Run `sql-reviewer` on the SQL file and `php-reviewer` on the PeopleFile change,
|
|
|
86
90
|
|
|
87
91
|
## Change history
|
|
88
92
|
- 2026-09-28 — Created from the James Fuller hand-add; records the uuid role/persona map and the `ALWAYS_ACTIVE_EMAILS_CA`-not-BOTH rule (bala)
|
|
93
|
+
- 2026-10-15 — Persona 31 had only 2 categories; corrected the "widest catalogue" claim, added the assortment fix (TRUE-82848) (bala)
|
|
@@ -18,6 +18,7 @@ files:
|
|
|
18
18
|
- dbchanges2/Client_Compass/2026-09-10b - RegisterApprovalDecisionPrePostInterceptor.sql
|
|
19
19
|
- dbchanges2/Core/2026-10-09a - ManagerAutoApproveSettings.sql
|
|
20
20
|
- dbchanges2/Client_Compass/2026-10-09a - ManagerAutoApproveSettingsSeed.sql
|
|
21
|
+
- dbchanges2/Client_Compass/2026-10-15a - VipAutoApproveOptOut.sql
|
|
21
22
|
related:
|
|
22
23
|
- mr-ma-order-approval-and-status.md
|
|
23
24
|
- ../../../2.0/apps/toga25-supply/features/action-button-rule-engine.md
|
|
@@ -103,7 +104,12 @@ Data (all ids identical in prod, dev-sandbox, client-sandbox):
|
|
|
103
104
|
- `dbchanges2/Client_Compass/2026-10-09a - ManagerAutoApproveSettingsSeed.sql` — `PersonaGlobalSettings` persona **35** (VIP Role) / setting 18 = `'5000'`; `UserGlobalSettings` user 60817 / setting 19 = `'1'`. Literal ids, no Core reference.
|
|
104
105
|
- **Release order: Core SQL → Client_Compass SQL → deploy `_underscore`.** Code first = ALL VIP auto-approval stops until seeded.
|
|
105
106
|
- Persona 35 is a safe stand-in for `c_isVip`: PeopleFile grants it to every VIP and removes it when VIP is dropped (prod: 2269/2269 active VIPs hold it, no non-VIP does) — see [Persona model](persona-model-and-levy-gating.md). Replay of the last 5,184 prod manager decisions: old rule 1,663 auto-approved, new 1,662; the only difference is user 60817.
|
|
106
|
-
- To change a limit: edit the persona's setting-18 row (or seed one on another persona).
|
|
107
|
+
- To change a limit: edit the persona's setting-18 row (or seed one on another persona).
|
|
108
|
+
- **To stop auto-approve for a manager (recurring "remove VIP auto approval" request):** add a setting-19 = `'1'` row; clearing `c_isVip` does nothing. Steps:
|
|
109
|
+
1. Find the user in **prod** `Client_Compass.Users` by **email** (request names often differ; duplicate/inactive accounts exist, pick the active one).
|
|
110
|
+
2. New `dbchanges2/Client_Compass/` file: one guarded `INSERT ... SELECT ... FROM DUAL WHERE NOT EXISTS` per user into `UserGlobalSettings` (`settingId 19`, `value '1'`, random v4 uuid literal), verify SELECT at end.
|
|
111
|
+
3. File is **prod-only**: `Users.id` differs per environment (settings/persona ids do not).
|
|
112
|
+
- Opted out so far (prod, 9 rows): 60817 (`2026-10-09a`); 60814, 60836, 60818, 60835, 60843, 60840, 60839, 43717 (`2026-10-15a - VipAutoApproveOptOut.sql`, TRUE-82852).
|
|
107
113
|
|
|
108
114
|
Evaluated in exactly three places, all calling the shared check:
|
|
109
115
|
- **(a) Order creation** — `_Model_Compass_SalesOrder::postPost`, inside `elseif (!empty($managerUserUuid))` (manager = delegate or requester's `Users.supervisorUserId`).
|
|
@@ -202,6 +208,7 @@ When a step-2 (Manager) decision is reassigned, `_swapManagerEmailAddress()` upd
|
|
|
202
208
|
- **The upstream cause of an unassigned step 2 is a user saved with no `supervisorUserId`.** The admin add-user screen allows saving a user without one, and the PEOPLE-file import may or may not backfill it later. That data hole feeds both the `!$hasManager` wipe above and the never-created step-2 decision at order creation. Not fixed — the guard only stops the order being *stranded*, it does not give it a manager.
|
|
203
209
|
|
|
204
210
|
## Change history
|
|
211
|
+
- 2026-10-15 — Opted 8 more managers out of auto-approve via setting 19; recorded the opt-out procedure (TRUE-82852). (bala)
|
|
205
212
|
- 2026-10-15 — postPut re-picks the step-2 manager only on a real "order for" contact change, detected in prePut via `$api->passThrough` (TRUE-82727, `_underscore` PR #771). (bala)
|
|
206
213
|
- 2026-10-09 — Manager auto-approve moved from `c_isVip` + hardcoded 5000 to persona setting 18 (limit) + user setting 19 (opt-out); postPut now compares subtotal (TRUE-82608). (bala)
|
|
207
214
|
- 2026-10-06 — Recorded that no delegate + no supervisor saves an order with no step-2 decision, and that the commerce cart now blocks it (TRUE-82423). (bala)
|
|
@@ -6,11 +6,12 @@ project: _Underscore
|
|
|
6
6
|
client: compass-usa
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-15
|
|
10
10
|
owners: ["bala"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Compass/PurchaseOrder.php
|
|
13
13
|
- worker/crons/toga2/compass/update_salesorder_status_from_odp.php
|
|
14
|
+
- worker/crons/toga2/compass/send_delivered_email.php
|
|
14
15
|
related:
|
|
15
16
|
- ./mits-po-to-so-item-linking.md
|
|
16
17
|
- ./people-file-user-lifecycle.md
|
|
@@ -81,11 +82,14 @@ The remaining ~42,900 crossed contacts were NOT mass-repaired. Consumers carry t
|
|
|
81
82
|
|
|
82
83
|
## Gotchas
|
|
83
84
|
- **⚠ The pointer is crossed on ~14% of Compass contacts — treat an unguarded read as a bug.** Applies to reports, exports, notification crons, api2 payload interceptors, anything.
|
|
84
|
-
- **A `Users` row is a different lookup and does not have this problem.** `Users.email` is keyed by `Users.contactId` directly. Where both exist, prefer `Users.email` and fall back to the guarded `ContactEmailAddresses` read — that is the `IFNULL(Users.email, ContactEmailAddresses.emailAddress)` precedence used by the in-transit cron. EDI-created MR orders have **no** `Users` row at all, which is why the fallback exists.
|
|
85
|
+
- **A `Users` row is a different lookup and does not have this problem.** `Users.email` is keyed by `Users.contactId` directly. Where both exist, prefer `Users.email` and fall back to the guarded `ContactEmailAddresses` read — that is the `IFNULL(Users.email, ContactEmailAddresses.emailAddress)` precedence used by the in-transit cron. EDI-created MR orders have **no** `Users` row at all, which is why the fallback exists. **But one contact can have several `Users` rows, some with NULL email** — a bare `JOIN Users ON Users.contactId = …` fans out to one row per user; pick one active emailable user with a single-user subquery (`MAX(id)` pattern in the [delivered cron gotcha](../../../1.0/apps/worker/features/compass-partial-in-transit-delivered-emails.md)).
|
|
85
86
|
- **`_Model_Compass_PurchaseOrder`'s contact lookup still has no ownership guard** (as of 2026-09-04). Fixing it there stops *new* crossings; the existing 42,926 remain either way.
|
|
86
87
|
- **Compass Canada shares the model** (`Model/Compass/Canada/PurchaseOrder.php` inherits the parent), so the same lookup shape runs against `Client_CompassCanada`. Not measured there.
|
|
87
88
|
- Duplicate-contact creation and duplicate-**user** creation are separate problems with separate causes — the `Users` duplicates come from the PEOPLE-file reactivation window, see [PEOPLE-file user lifecycle](./people-file-user-lifecycle.md).
|
|
88
89
|
|
|
90
|
+
## Change history
|
|
91
|
+
- 2026-10-15 — Noted duplicate `Users` rows per contact fan out a `Users.contactId` join; pick one emailable user (TRUE-82846). (bala)
|
|
92
|
+
|
|
89
93
|
## Related
|
|
90
94
|
- [MITS PO → SO item linking](./mits-po-to-so-item-linking.md)
|
|
91
95
|
- [PEOPLE-file user lifecycle](./people-file-user-lifecycle.md)
|
|
@@ -5,7 +5,9 @@
|
|
|
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
|
| [NavV2 Global Nav — menus, panel shell, scroll reveal, Talos FAB](features/navv2-global-nav.md) | togatech NavV2 header (designer "phase one"): data-driven menus, one shared panel shell + WebGL canvas, `data-nav-reveal` scroll hide/show, opt-in `data-nav-pin |
|
|
8
|
-
| [
|
|
9
|
-
| [
|
|
8
|
+
| [Platform Product Page (/our-platform) — ported designer prototype](features/platform-product-page.md) | togatech `/our-platform` TOGa Platform product page, ported from designer prototype `our-platform-v5`; open for the page-root-as-`<html>` rule, effect-order rul |
|
|
9
|
+
| [SEO / AEO / GEO — Prerendering, Single-Source Meta & Structured Data](features/seo-aeo-geo-prerender.md) | togatech SEO/AEO/GEO — build-time Puppeteer prerender + hydration, code-split routes (`lazyRoute`), single-source per-page meta + JSON-LD; open for the robots d |
|
|
10
|
+
| [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 its talos.css port edits, section notes, open port work and pla |
|
|
10
11
|
| [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`. |
|
|
12
|
+
| [Porting a Designer Prototype to a togatech Page](workflows/designer-prototype-port.md) | togatech procedure for turning a designer HTML/JSX/CSS prototype into a live page (`/talos`, `/our-platform`): where drops and port tools live, generated prefix |
|
|
11
13
|
| [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,12 +6,13 @@ project: TOGA Technology Website
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: architecture
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-10-15
|
|
10
10
|
owners: [jcardinal, tcox]
|
|
11
11
|
files:
|
|
12
12
|
- togatech/src/main.tsx
|
|
13
13
|
- togatech/src/App.tsx
|
|
14
14
|
- togatech/src/routes.tsx
|
|
15
|
+
- togatech/src/utils/lazyRoute.tsx
|
|
15
16
|
- togatech/src/lib/api.ts
|
|
16
17
|
- togatech/src/lib/contentful.ts
|
|
17
18
|
- togatech/src/themeConfig/ThemeContext.tsx
|
|
@@ -20,16 +21,18 @@ files:
|
|
|
20
21
|
- togatech/src/components/NavV2/navRoutes.ts
|
|
21
22
|
- togatech/src/components/templates/AppLayout/AppLayout.tsx
|
|
22
23
|
- togatech/src/routes.config.json
|
|
23
|
-
related:
|
|
24
|
+
related:
|
|
25
|
+
- features/seo-aeo-geo-prerender.md
|
|
26
|
+
- workflows/designer-prototype-port.md
|
|
24
27
|
---
|
|
25
28
|
|
|
26
29
|
Public TOGA Technology marketing site — standalone React/TS SPA (no PHP framework); open for stack, bootstrap, routing, page layout, integrations, build/deploy.
|
|
27
30
|
|
|
28
31
|
## Summary
|
|
29
32
|
|
|
30
|
-
Public-facing TOGA Technology corporate/marketing website. Standalone single-page React app — uses **neither** PHP framework (1.0 `App_` nor 2.0 `_underscore`), hence the `standalone/` partition. Consumes TOGA backend services (email + Talos AI API) over HTTP, pulls editorial content from Contentful, ships **no server code** — builds to static assets served from `dist/`. Local path in Claude memory (`repo-path-togatech`), not here.
|
|
33
|
+
Public-facing TOGA Technology corporate/marketing website. Standalone single-page React app — uses **neither** PHP framework (1.0 `App_` nor 2.0 `_underscore`), hence the `standalone/` partition. Consumes TOGA backend services (email + Talos AI API) over HTTP, pulls editorial content from Contentful, ships **no server code** — builds to static, prerendered assets served from `dist/`. Local path in Claude memory (`repo-path-togatech`), not here.
|
|
31
34
|
|
|
32
|
-
**Critical rules:** All config comes from Vite `VITE_*` env vars (values in `.env`, never in docs). `togatech/.env` is currently **tracked in git** with live `VITE_*` creds (Contentful token, Talos API key) — treat as committed secrets.
|
|
35
|
+
**Critical rules:** All config comes from Vite `VITE_*` env vars (values in `.env`, never in docs). `togatech/.env` is currently **tracked in git** with live `VITE_*` creds (Contentful token, Talos API key) — treat as committed secrets. Code-split a prerendered route only through `lazyRoute` + `CODE_SPLIT_ROUTES` — plain `React.lazy` + `<Suspense>` or the router's `lazy` blanks the page on hydrate (features/seo-aeo-geo-prerender.md).
|
|
33
36
|
|
|
34
37
|
## Stack
|
|
35
38
|
|
|
@@ -38,13 +41,15 @@ Public-facing TOGA Technology corporate/marketing website. Standalone single-pag
|
|
|
38
41
|
- **React Router 7.9** via `createBrowserRouter` (data-router API).
|
|
39
42
|
- **@agilant/toga-blox** (internal design-system / component + API library) — shared UI and the `apiGet` HTTP helper (`@agilant/toga-blox/dist/api/apiFunctions`).
|
|
40
43
|
- **FontAwesome Pro** icons.
|
|
44
|
+
- **three.js 0.184.0** (+ `@types/three`, pinned) — only via `await import()` inside engine effects; rules in features/platform-product-page.md.
|
|
45
|
+
- **Font:** Plus Jakarta Sans, self-hosted variable woff2 (`src/assets/fonts/`, weights 200–800 + italic, `@font-face` in `src/styles/index.css`) — no Google Fonts link. `html, body` get `-webkit-font-smoothing: antialiased`, `-moz-osx-font-smoothing: grayscale`, `text-rendering: optimizeLegibility` site-wide.
|
|
41
46
|
- **Cypress 15** for e2e.
|
|
42
47
|
|
|
43
48
|
## Application bootstrap
|
|
44
49
|
|
|
45
50
|
`src/main.tsx` → `src/App.tsx` → `src/routes.tsx`.
|
|
46
51
|
|
|
47
|
-
- `main.tsx` mounts `<App/>` inside `HelmetProvider` (react-helmet-async, per-page SEO `<head>`) and `StrictMode`.
|
|
52
|
+
- `main.tsx` mounts `<App/>` inside `HelmetProvider` (react-helmet-async, per-page SEO `<head>`) and `StrictMode`. Prerendered path → `hydrateRoot` (after preloading a code-split route's chunk); else `createRoot`.
|
|
48
53
|
- `App.tsx` wires global providers, in order:
|
|
49
54
|
1. `PersistQueryClientProvider` — TanStack **React Query** client persisted to `localStorage` (persister key `"commerce"`); default query `staleTime` 24h.
|
|
50
55
|
2. `ThemeProvider` (`src/themeConfig/ThemeContext.tsx`) — applies active theme from `themes.json` (color/btn token sets, e.g. `DEFAULT`).
|
|
@@ -61,18 +66,18 @@ Public-facing TOGA Technology corporate/marketing website. Standalone single-pag
|
|
|
61
66
|
| `/` (index) | `pages/Home` |
|
|
62
67
|
| `/about` | `pages/About` |
|
|
63
68
|
| `/what-we-do` | `pages/WhatWeDo` |
|
|
64
|
-
| `/our-platform` | `pages/OurPlatform` |
|
|
69
|
+
| `/our-platform` | `pages/OurPlatform` (code-split — features/platform-product-page.md) |
|
|
65
70
|
| `/contact` | `pages/Contact` |
|
|
66
|
-
| `/talos` | `pages/Talos` (code-split —
|
|
71
|
+
| `/talos` | `pages/Talos` (code-split — features/talos-product-page.md) |
|
|
67
72
|
| `/privacy-policy` | `pages/PrivacyPolicy` |
|
|
68
73
|
| `/terms-and-conditions` | `pages/TermsAndConditions` |
|
|
69
74
|
| every `STUB_ROUTES` path | `pages/Stub` placeholder (`<StubPage title>`) |
|
|
70
75
|
| `*` | `pages/NotFound` |
|
|
71
76
|
|
|
72
77
|
- **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 (
|
|
74
|
-
- Pages are imported eagerly except `/talos` (`
|
|
75
|
-
- **AppLayout → page contract:** `<Outlet context
|
|
78
|
+
- **Prerender list:** `src/routes.config.json` lists the static routes to prerender + sitemap. Add every new live route there (checklist: features/seo-aeo-geo-prerender.md).
|
|
79
|
+
- Pages are imported eagerly except the code-split ones (`/talos`, `/our-platform`): `lazyRoute` (`utils/lazyRoute.tsx`) + `CODE_SPLIT_ROUTES` in `routes.tsx`; `main.tsx` preloads the current one before hydrating.
|
|
80
|
+
- **AppLayout → page contract:** `<Outlet context>` carries `openTalosChat()`, `askTalos(question)`, `closeTalosChat()`, `isTalosChatOpen`; pages read it via `useAppLayoutContext()` (`AppLayout/appLayoutContext.ts`). AppLayout owns the one Talos chat modal — never mount a second. `ROUTES_WITH_OWN_FOOTER` / `ROUTES_WITH_OWN_LAUNCHER` (`/talos`, `/our-platform`) hide the old site footer / NavV2 FAB on pages that render `FooterV2` / `TalosLauncher`; retire them when the redesign goes site-wide.
|
|
76
81
|
- A Cypress-only test route swaps `WhatWeDoPage` for a throwing component to exercise the error boundary (`VITE_CY_TEST` / `window.Cypress`).
|
|
77
82
|
|
|
78
83
|
## Page structure — MVVM per page
|
|
@@ -90,17 +95,18 @@ pages/Home/
|
|
|
90
95
|
|
|
91
96
|
- **viewModel** holds data fetching (React Query hooks), form field defs (`FIELDS`), behavior.
|
|
92
97
|
- **view** is presentational. Keeps page logic testable and out of JSX.
|
|
98
|
+
- Redesigned pages (`Talos`, `OurPlatform`) are ports of designer prototypes: one generated page-scoped CSS file + `view/sections/` + engine hooks — workflows/designer-prototype-port.md.
|
|
93
99
|
|
|
94
100
|
## Shared layers
|
|
95
101
|
|
|
96
|
-
- `src/components/` — reusable UI
|
|
102
|
+
- `src/components/` — reusable UI: NavV2 global nav (incl. `TalosFab`), Header, Footer (old) and `FooterV2` (redesign), `TalosLauncher` (page orb + guide), `SiteFaq`, `Reveal`, `TalosMark`, Hero, Carousel/Swiper wrappers, Modal, TalosChat + TalosChatInput, motion/animation helpers, animated SVGs, etc.
|
|
97
103
|
- `src/components/templates/` — layout primitives: `AppLayout` (router root chrome; Outlet context + `appLayoutContext.ts`), `DefaultContainer`, `MarginContainer`, `GlowingContainer`, `ErrorContainer`.
|
|
98
104
|
- `src/lib/` — data/integration:
|
|
99
105
|
- `api.ts` — `useSendEmail` (contact form) and `postTalosMessage` (AI chat).
|
|
100
106
|
- `contentful.ts` — Contentful delivery client (`createClient`).
|
|
101
107
|
- `talosSystemPrompt.ts` — Talos chat system prompt.
|
|
102
108
|
- `types.ts` — shared API/domain types (`ChatMessage`, `OutputField`, `ChatAPIError`…).
|
|
103
|
-
- `src/utils/` — helpers (`emphasizeText`, `getFontAwesomeIcon`, `animateSections`) and hooks (`useFocusTrap`, `useLockBgScroll`).
|
|
109
|
+
- `src/utils/` — helpers (`emphasizeText`, `getFontAwesomeIcon`, `animateSections`, `lazyRoute`, `isPrerender`, `isPinnedSceneLocked`) and hooks (`useFocusTrap`, `useLockBgScroll`, `useRootStyles`, `useBtnSpot`).
|
|
104
110
|
- `src/themeConfig/` — `ThemeContext.tsx`, `themes.json`, `types.ts`.
|
|
105
111
|
|
|
106
112
|
## External integrations
|
|
@@ -114,9 +120,9 @@ All via Vite `VITE_*` env vars (values in `.env`, never in repo docs):
|
|
|
114
120
|
|
|
115
121
|
## Build, environments, deploy
|
|
116
122
|
|
|
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).
|
|
123
|
+
- `package.json` scripts: `dev`, `togatech` (dev with `--host togatech`), `build` (`tsc && vite build` + Chrome install + Puppeteer prerender — features/seo-aeo-geo-prerender.md), `buildBeta` (`--mode beta`), `buildGamma` (`--mode gamma`), `preview`, `lint` (ESLint — non-functional: no eslint dependency or config; see workflows/local-dev-and-headless-verification.md).
|
|
118
124
|
- Multi-environment: `development`, `beta`, `gamma`, `production` (Vite modes).
|
|
119
|
-
- Vite `base: "/"`, output to `dist/`, `publicDir: public
|
|
125
|
+
- Vite `base: "/"`, output to `dist/`, `publicDir: public` (Amplify serves it through S3/CloudFront). A custom `robotsPlugin` copies `public/robots.prod.txt` to `dist/robots.txt` only when Vite `mode === "production"`, else `robots.dev.txt` — never gate it on `NODE_ENV` (features/seo-aeo-geo-prerender.md).
|
|
120
126
|
- `@` path alias → `./src` (Vite `resolve.alias`).
|
|
121
127
|
- Dev server `allowedHosts: ["togatech"]` (matches the `togatech` host script).
|
|
122
128
|
|
|
@@ -6,38 +6,30 @@ project: TOGA Technology Website
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-15
|
|
10
10
|
owners: ["tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- togatech/src/components/NavV2/view/NavV2.tsx
|
|
13
13
|
- togatech/src/components/NavV2/view/TalosFab.tsx
|
|
14
|
-
- togatech/src/components/NavV2/view/NavDestLink.tsx
|
|
15
14
|
- togatech/src/components/NavV2/view/SilkCanvas.tsx
|
|
16
15
|
- togatech/src/components/NavV2/view/panels/WhatPanel.tsx
|
|
17
16
|
- togatech/src/components/NavV2/view/panels/CompanyPanel.tsx
|
|
18
|
-
- togatech/src/components/NavV2/view/panels/IndustriesPanel.tsx
|
|
19
|
-
- togatech/src/components/NavV2/view/panels/ResourcesPanel.tsx
|
|
20
17
|
- togatech/src/components/NavV2/view/panels/PanelFooter.tsx
|
|
21
18
|
- togatech/src/components/NavV2/navV2.module.css
|
|
22
|
-
- togatech/src/components/NavV2/types.ts
|
|
23
19
|
- togatech/src/components/NavV2/navRoutes.ts
|
|
24
|
-
- togatech/src/components/NavV2/viewModel/useNavV2.ts
|
|
25
20
|
- togatech/src/components/NavV2/viewModel/useNavReveal.ts
|
|
26
|
-
- togatech/src/components/NavV2/viewModel/useCompactViewport.ts
|
|
27
21
|
- togatech/src/components/NavV2/viewModel/useRovingTabs.ts
|
|
28
|
-
- togatech/src/components/NavV2/viewModel/useDockedState.ts
|
|
29
22
|
- togatech/src/components/NavV2/viewModel/FIELDS/NAVFIELDS.json
|
|
30
23
|
- togatech/src/components/MobileNav/MobileNav.tsx
|
|
31
|
-
- togatech/src/
|
|
32
|
-
- togatech/public/assets/nav/logo-talos-ai.svg
|
|
33
|
-
- togatech/public/assets/nav/logo-toga-platform.svg
|
|
34
|
-
- togatech/tailwind.config.js
|
|
24
|
+
- togatech/src/utils/isPinnedSceneLocked.ts
|
|
35
25
|
related:
|
|
36
26
|
- ../architecture.md
|
|
37
27
|
- talos-product-page.md
|
|
28
|
+
- platform-product-page.md
|
|
29
|
+
- ../workflows/designer-prototype-port.md
|
|
38
30
|
---
|
|
39
31
|
|
|
40
|
-
togatech NavV2 header (designer "phase one"): data-driven menus, one shared panel shell + WebGL canvas, `data-nav-reveal` scroll hide/show, opt-in `data-nav-pinned` for sticky scenes, Talos FAB rules, design-anchor rule. Open before touching the nav or adding a full-screen sticky section.
|
|
32
|
+
togatech NavV2 header (designer "phase one"): data-driven menus, one shared panel shell + WebGL canvas, `data-nav-reveal` scroll hide/show, opt-in `data-nav-pinned` for sticky scenes, Talos FAB rules, type-spec line-height rule, design-anchor rule. Open before touching the nav or adding a full-screen sticky section.
|
|
41
33
|
|
|
42
34
|
## Summary
|
|
43
35
|
|
|
@@ -57,7 +49,10 @@ Global header in `src/components/NavV2/` (MVVM: `view/`, `viewModel/`, menu data
|
|
|
57
49
|
- `.panel[hidden]{display:none}` is required — `.panel{display:flex}` beats the `hidden` attribute.
|
|
58
50
|
- `navPanelIn` slide-in plays on open (display none → flex restarts it); switching menu to menu does not replay it.
|
|
59
51
|
|
|
60
|
-
**Type:** matches the
|
|
52
|
+
**Type:** matches the "Mega-menu type spec" PDF (TRUE-82042; QC 186/186 rows, within 0.5px of the design). It supersedes the older `navigation-type-spec.md` — tcox: "go by the new pdf".
|
|
53
|
+
- Nav root base type: weight 400, line-height 1.5, colour `#fff`, the spec's font stack (site font: [architecture.md](../architecture.md) Stack).
|
|
54
|
+
- **An item with no stated line-height inherits 1.5** (the old spec's "normal" values are void). Exception: top-bar trigger buttons keep `line-height: normal` (the design's plain buttons).
|
|
55
|
+
- `navV2.module.css` resets the site-wide word-spacing inside the nav: `.root :where(p,h2,h3,h4,h5,h6,a,li,button){word-spacing:normal}` (same as page ports — see [designer-prototype-port.md](../workflows/designer-prototype-port.md) Gotchas).
|
|
61
56
|
|
|
62
57
|
**Mobile:** `MobileNav` closes on resize only when width > 1280 (`MOBILE_NAV_MAX_WIDTH_PX`), matching `tailwind.config.js` `md: {max:'1280px'}` (AppLayout: NavV2 `md:hidden`, hamburger `hidden md:block`).
|
|
63
58
|
|
|
@@ -68,11 +63,11 @@ Global header in `src/components/NavV2/` (MVVM: `view/`, `viewModel/`, menu data
|
|
|
68
63
|
- Glass look (blur / grain / edge) only in `shown`.
|
|
69
64
|
- `useCompactViewport.ts`: compact padding when viewport height ≤ 1000px.
|
|
70
65
|
|
|
71
|
-
**Pinned scenes (opt-in):** a full-screen sticky scene must carry `data-nav-pinned` so the bar does not pop back during it. Current users: Home `Difference.tsx`, WhatWeDo `CardScroll.tsx`. Add the attribute to any new scroll-pinned section.
|
|
66
|
+
**Pinned scenes (opt-in):** a full-screen sticky scene must carry `data-nav-pinned` so the bar does not pop back during it. `utils/isPinnedSceneLocked.ts` reads it for both the nav and the Talos guide (`TalosLauncher waitForPins` holds bubbles while a scene is locked). Current users: Home `Difference.tsx`, WhatWeDo `CardScroll.tsx`, `/our-platform` `.pfc-stage` and SolutionsStage `.pseq__pin`. Add the attribute to any new scroll-pinned section.
|
|
72
67
|
|
|
73
68
|
**Section anchors:** `src/styles/index.css` has `:where(section[id]:not(#top)) { scroll-margin-top: 104px }` (zero specificity — a page rule overrides it freely).
|
|
74
69
|
|
|
75
|
-
**Talos FAB (`TalosFab.tsx`):** a `<button>` that opens the global chat via `openTalosChat` (same as the bar pill) — not a `Link` to `/talos`. Visible only past one screen of scroll **and** while the bar is not `shown`; unfocusable while hidden. Ask Talos pill + FAB use the design's light-trail ring — no box-shadow glow in any state.
|
|
70
|
+
**Talos FAB (`TalosFab.tsx`):** a `<button>` that opens the global chat via `openTalosChat` (same as the bar pill) — not a `Link` to `/talos`. Visible only past one screen of scroll **and** while the bar is not `shown`; unfocusable while hidden. Ask Talos pill + FAB use the design's light-trail ring — no box-shadow glow in any state. Pages in AppLayout `ROUTES_WITH_OWN_LAUNCHER` (`/talos`, `/our-platform`) hide the FAB and render `components/TalosLauncher` (orb + guide) instead.
|
|
76
71
|
|
|
77
72
|
## Gotchas
|
|
78
73
|
|
|
@@ -82,8 +77,9 @@ Global header in `src/components/NavV2/` (MVVM: `view/`, `viewModel/`, menu data
|
|
|
82
77
|
- **Tailwind screens here are max-width (desktop-first):** `md:` means "≤ 1280px", not "≥ 768px".
|
|
83
78
|
- **Tailwind line-height traps:** `text-xs/sm/base/lg` set their own line-height; `leading-normal` is `1.5`, not CSS `line-height: normal`. When the design says "normal", use `leading-[normal]`.
|
|
84
79
|
- **Set logo size by inline style, not the `height` attribute** — Tailwind preflight `img{height:auto}` overrides it (the TOGa Platform SVG is 98x100, not square).
|
|
85
|
-
- Nav type
|
|
80
|
+
- Nav type changes are checked against the "Mega-menu type spec" PDF (TRUE-82042), not the older `navigation-type-spec.md` (TRUE-82043).
|
|
86
81
|
|
|
87
82
|
## Change history
|
|
83
|
+
- 2026-10-15 — Matched the "Mega-menu type spec" PDF: nav base type 400 / 1.5 / #fff, unstated line-heights inherit 1.5, trigger buttons stay normal; /our-platform pinned scenes; TalosLauncher pages hide the FAB (TRUE-82042 / TRUE-82652, tcox)
|
|
88
84
|
- 2026-10-12 — Type spec match, Platforms logos, one shared panel shell + SilkCanvas (WebGL context leak fix), MobileNav closes only above 1280px (TRUE-82042, tcox)
|
|
89
85
|
- 2026-10-05 — Phase-one redesign: 4 → 2 menus, `flatLinks[]`, Platforms/Company panels rebuilt, scroll reveal + `data-nav-pinned`, compact viewport, FAB visibility rules (tcox)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Platform Product Page (/our-platform) — 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-10-15
|
|
10
|
+
owners: ["tcox"]
|
|
11
|
+
files:
|
|
12
|
+
- togatech/src/pages/OurPlatform/OurPlatformPage.tsx
|
|
13
|
+
- togatech/src/pages/OurPlatform/ourPlatform.css
|
|
14
|
+
- togatech/src/pages/OurPlatform/viewModel/platformRoot.ts
|
|
15
|
+
- togatech/src/pages/OurPlatform/viewModel/pageSignals.ts
|
|
16
|
+
- togatech/src/pages/OurPlatform/viewModel/useLattice.ts
|
|
17
|
+
- togatech/src/pages/OurPlatform/viewModel/usePfCover.ts
|
|
18
|
+
- togatech/src/pages/OurPlatform/viewModel/three
|
|
19
|
+
- togatech/src/pages/OurPlatform/view/sections/ElectricGrid/ElectricGrid.tsx
|
|
20
|
+
- togatech/src/pages/OurPlatform/view/sections/CoreApps
|
|
21
|
+
- togatech/src/pages/OurPlatform/viewModel/FIELDS
|
|
22
|
+
- togatech/public/assets/platform
|
|
23
|
+
- togatech/cypress/e2e/ourPlatformPage/platformPage.cy.ts
|
|
24
|
+
related:
|
|
25
|
+
- ../architecture.md
|
|
26
|
+
- ../workflows/designer-prototype-port.md
|
|
27
|
+
- talos-product-page.md
|
|
28
|
+
- seo-aeo-geo-prerender.md
|
|
29
|
+
- navv2-global-nav.md
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
togatech `/our-platform` TOGa Platform product page, ported from designer prototype `our-platform-v5`; open for the page-root-as-`<html>` rule, effect-order rules, three.js/WebGL engines, video weight, placeholder copy.
|
|
33
|
+
|
|
34
|
+
## Summary
|
|
35
|
+
|
|
36
|
+
`/our-platform` (`src/pages/OurPlatform/`) is a 13-section product page ported from the designer prototype `our-platform-v5` (TRUE-82652). It replaced the old OurPlatform page; the old page, `components/AccordionCard` and the 5 old specs in `cypress/e2e/ourPlatformPage/` were deleted — one smoke spec remains, `platformPage.cy.ts`. Port method: [designer-prototype-port.md](../workflows/designer-prototype-port.md). Layout: `view/sections/<Section>/`, `view/shared/` (`PlatformDot`, `TalosSecondaryBtn`), hooks and engines in `viewModel/`, three.js engines in `viewModel/three/`, copy in `viewModel/FIELDS/*.json`, assets in `public/assets/platform/` (~22 MB). Code-split, prerendered; renders `FooterV2`, `TalosLauncher` and the `platformSchema` JSON-LD.
|
|
37
|
+
|
|
38
|
+
## How it works
|
|
39
|
+
|
|
40
|
+
**Page root = the design's `<html>`** (`viewModel/platformRoot.ts`):
|
|
41
|
+
- Every attribute and custom property the design set on `documentElement` (`data-pfc`, `data-pfc-state`, `data-grid`, `data-eg-tone`, `--tier-content`, `--lattice-mask`, …) is written on `.platformPage`, because `ourPlatform.css` maps `:root` / `html` / `body` onto it. Find the root with `getPlatformRoot()`.
|
|
42
|
+
- Viewport reads (`clientWidth`, `innerHeight`) stay on `document.documentElement`.
|
|
43
|
+
- Constant root vars (`PFC_ROOT_VARS`, `--lt-bright`) render in the root's `style`, so the prerender and the first client render match.
|
|
44
|
+
|
|
45
|
+
**CSS:** `ourPlatform.css` is generated by `design-files/_port-tools/build-platform-css.cjs` (local only). Never hand-edit it in bulk — add an `EDITS` entry or a port addition and regenerate. Skipped design files: `nav-v2.css` (NavV2 is ported), `card-glow.css`, `solutions-b3-v5.css`.
|
|
46
|
+
|
|
47
|
+
**Effect order (load-bearing):**
|
|
48
|
+
- `useLattice` runs in the layout effect of the page's **first child**, `ElectricGrid`, so the grid tier exists before any section measures (React runs child layout effects first, in tree order). The page root's ref is not attached yet then, so it finds the root with `closest()`.
|
|
49
|
+
- `pageSignals.ts` replaces the design's module globals (`heroVidCtl`, `lightApi`). The cover (`usePfCover`, layout effect) runs before the hero video loop and the cover light register (passive effects), so `pageSignals` keeps the last requested hero-sleep / cover-visible state and applies it when each registers.
|
|
50
|
+
|
|
51
|
+
**three.js / WebGL:**
|
|
52
|
+
- `three` 0.184.0 + `@types/three`, both pinned.
|
|
53
|
+
- `three` is imported only by modules in `viewModel/three/`, and those load only via `await import("./three/<engine>")` inside an engine effect — never a static import (it would pull three.js into the page chunk), never under prerender, only near the viewport.
|
|
54
|
+
- Engines dispose and call `renderer.forceContextLoss()` on cleanup. `renderer.debug.checkShaderErrors = false` — otherwise ANGLE/D3D precision warnings flood the console.
|
|
55
|
+
- The design's WebGL custom elements are rebuilt as hooks on empty React hosts.
|
|
56
|
+
|
|
57
|
+
**Layout integration:**
|
|
58
|
+
- Listed in AppLayout `ROUTES_WITH_OWN_FOOTER` and `ROUTES_WITH_OWN_LAUNCHER`. `TalosLauncher` gets `leftHero={platformLeftHero}` (the cover rises over a pinned hero, so the nav docks late) and `waitForPins`.
|
|
59
|
+
- Pinned scenes carry `data-nav-pinned`: `.pfc-stage` (the cover) and SolutionsStage `.pseq__pin`. NavV2 reveal and the Talos guide both read it — see [navv2-global-nav.md](navv2-global-nav.md).
|
|
60
|
+
- The CoreApps app window raises the page root's `z-index` while open, so it covers NavV2 (the root has `isolation: isolate`).
|
|
61
|
+
|
|
62
|
+
**Links:** design `contact.html` → `/contact` (also the placeholder `#book-roi-session`); `talos.html` → `/talos`. "Explore TOGa" (not wired in the design) scrolls to the cover and focuses `#modes`.
|
|
63
|
+
|
|
64
|
+
**Open copy (2026-10-15):** FAQ Q2–Q6 answers not verified; ROI, Outcomes, app-window and Talos guide copy are placeholders.
|
|
65
|
+
|
|
66
|
+
## Gotchas
|
|
67
|
+
|
|
68
|
+
- **Video weight:** in a prod build on fresh Chrome the videos cost ~0.7s LCP and ~200–290 MB of Chrome memory. The design's two-copy crossfade downloads the hero video ~1.5×. Moving videos to a separate S3 bucket changes nothing — Amplify already serves `public/` through S3/CloudFront (same bytes, same memory). Real wins: re-encode `hero-loop-3.mp4` (13.5 MB), poster PNG → webp. Deferred by tcox to a later optimization pass. The site-wide JS bundle is the bigger LCP problem — see [seo-aeo-geo-prerender.md](seo-aeo-geo-prerender.md) Gotchas.
|
|
69
|
+
|
|
70
|
+
## Change history
|
|
71
|
+
- 2026-10-15 — Rebuilt /our-platform from designer prototype our-platform-v5; old page, AccordionCard and 5 old Cypress specs deleted (TRUE-82652, tcox)
|
|
@@ -6,85 +6,86 @@ project: TOGA Technology Website
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
10
|
-
owners: ["ajean", "jcardinal"]
|
|
9
|
+
updated: 2026-10-15
|
|
10
|
+
owners: ["ajean", "jcardinal", "tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- togatech/vite.config.ts
|
|
13
13
|
- togatech/scripts/prerender.mjs
|
|
14
14
|
- togatech/src/routes.config.json
|
|
15
15
|
- togatech/src/main.tsx
|
|
16
|
-
- togatech/src/
|
|
16
|
+
- togatech/src/routes.tsx
|
|
17
|
+
- togatech/src/utils/lazyRoute.tsx
|
|
18
|
+
- togatech/src/utils/isPrerender.ts
|
|
17
19
|
- togatech/src/components/SEO/Seo.tsx
|
|
18
20
|
- togatech/src/components/SEO/JsonLd.tsx
|
|
19
21
|
- togatech/src/components/SEO/schema.ts
|
|
20
|
-
- togatech/src/components/templates/AppLayout.tsx
|
|
21
|
-
- togatech/src/pages/WhatWeDo/WhatWeDoPage.tsx
|
|
22
|
-
- togatech/src/pages/About/AboutPage.tsx
|
|
23
|
-
- togatech/src/pages/Contact/ContactPage.tsx
|
|
24
|
-
- togatech/src/pages/OurPlatform/OurPlatformPage.tsx
|
|
25
22
|
- togatech/public/robots.prod.txt
|
|
26
23
|
- togatech/public/sitemap.xml
|
|
27
24
|
- togatech/public/llms.txt
|
|
28
|
-
related:
|
|
25
|
+
related:
|
|
26
|
+
- ../architecture.md
|
|
27
|
+
- talos-product-page.md
|
|
28
|
+
- platform-product-page.md
|
|
29
|
+
- ../workflows/creating-pull-requests.md
|
|
30
|
+
- ../../../standards/brand-naming.md
|
|
29
31
|
---
|
|
30
32
|
|
|
31
|
-
togatech SEO/AEO/GEO — build-time Puppeteer prerender + single-source per-page meta + JSON-LD; open for the robots
|
|
33
|
+
togatech SEO/AEO/GEO — build-time Puppeteer prerender + hydration, code-split routes (`lazyRoute`), single-source per-page meta + JSON-LD; open for the robots de-index bug, adding a route, and Amplify build requirements.
|
|
32
34
|
|
|
33
35
|
## Summary
|
|
34
36
|
|
|
35
|
-
Makes togatech.com visible and citable to search engines **and** AI answer engines (ChatGPT/Perplexity/Claude search, Google AI Overviews). The site is a CSR-only Vite/React SPA, so non-JS crawlers saw an empty `<div id="root">`. This feature fixes a production de-indexing bug and adds **build-time prerendering** so every static route ships real HTML, plus single-source per-page meta and JSON-LD structured data.
|
|
37
|
+
Makes togatech.com visible and citable to search engines **and** AI answer engines (ChatGPT/Perplexity/Claude search, Google AI Overviews). The site is a CSR-only Vite/React SPA, so non-JS crawlers saw an empty `<div id="root">`. This feature fixes a production de-indexing bug and adds **build-time prerendering** so every static route ships real HTML, plus single-source per-page meta and JSON-LD structured data. Merged into `_production` 2026-06-19 (PRs #29, #30).
|
|
36
38
|
|
|
37
39
|
## How it works
|
|
38
40
|
|
|
39
41
|
**Key files / entry points:**
|
|
40
|
-
- `vite.config.ts` — `robotsPlugin()`
|
|
42
|
+
- `vite.config.ts` — `robotsPlugin()` gates on **Vite `mode`**.
|
|
41
43
|
- `scripts/prerender.mjs` — post-build headless-Chrome (Puppeteer) snapshot; chained into the `build` script.
|
|
42
|
-
- `src/routes.config.json` — single source of static route paths (
|
|
43
|
-
- `src/main.tsx` — `hydrateRoot` when a route was prerendered, else `createRoot
|
|
44
|
+
- `src/routes.config.json` — single source of static route paths (prerender + sitemap).
|
|
45
|
+
- `src/main.tsx` — `hydrateRoot` when a route was prerendered, else `createRoot`; preloads a code-split route's chunk first.
|
|
46
|
+
- `src/utils/lazyRoute.tsx` + `CODE_SPLIT_ROUTES` in `src/routes.tsx` — code-split routes that hydrate.
|
|
44
47
|
- `src/components/SEO/{Seo.tsx,JsonLd.tsx,schema.ts}` — shared head + JSON-LD; per-page `meta` lives in each page's `viewModel/FIELDS/*.json`.
|
|
45
48
|
|
|
46
49
|
**Mechanics:**
|
|
47
50
|
1. **robots:** `robotsPlugin` copies `robots.prod.txt` only when `mode === "production"`; beta/gamma/dev get `robots.dev.txt` (`Disallow: /`). `robots.prod.txt` carries an explicit AI-bot allow-list (GPTBot, ClaudeBot, PerplexityBot, …) + `https://` sitemap.
|
|
48
|
-
2. **prerender:** `npm run build` = `tsc && vite build && npx puppeteer browsers install chrome && node scripts/prerender.mjs
|
|
49
|
-
3. **
|
|
50
|
-
4. **
|
|
51
|
+
2. **prerender:** `npm run build` = `tsc && vite build && npx puppeteer browsers install chrome && node scripts/prerender.mjs`. The script serves `dist/` (reading the original shell once, serving it for all nav routes), snapshots each route in headless Chrome, and writes `dist/<route>/index.html`. Runs in a real browser, so **no SSR/window guards needed**. Code-split routes also wait for their page root (`PAGE_READY_SELECTORS`, e.g. `[data-testid="platform-page"]`) — `#root > *` is already true once the layout mounts, so a slow chunk would snapshot an empty page area.
|
|
52
|
+
3. **code-split hydration (`lazyRoute`):** `main.tsx` looks up the current path in `CODE_SPLIT_ROUTES` (lowercased, trailing slash stripped) and awaits its `preload()` before `hydrateRoot` (8s timeout, then hydrates anyway). The route's `Route` decides once per mount: a loaded module renders directly, with no `<Suspense>` in the tree; otherwise (client-side navigation) `React.lazy` + `<Suspense>`. `preload` and the lazy share one import promise; a failed import makes a fresh lazy so the next visit retries. `onRecoverableError` logs any hydration mismatch as `[hydrate] recoverable error…`.
|
|
53
|
+
4. **meta:** each page renders `<Seo {...FIELDS.meta} />` — title/description/keywords/canonical/OG/Twitter from one place per page; canonical/OG URLs derived from one `SITE_URL`. `<JsonLd>` (Helmet `<script type="application/ld+json">`) is baked into the snapshot.
|
|
54
|
+
5. **structured data:** all JSON-LD lives in **one module — `src/components/SEO/schema.ts`**, derived from `FOOTERFIELDS.json` (socials/email/phone) and each page's FIELDS JSON, so editing footer/content updates schema automatically. Exports and their render sites:
|
|
51
55
|
- `organizationSchema` + `websiteSchema` → **AppLayout** (all pages).
|
|
52
56
|
- `serviceSchemas` (4 `Service` nodes) → **WhatWeDoPage**.
|
|
53
57
|
- `platformSchema` (`SoftwareApplication` for the TOGa Platform) → **OurPlatformPage**.
|
|
54
58
|
- `aboutPageSchema` (`AboutPage`) + `personSchemas` (per leader) → **AboutPage**.
|
|
55
59
|
- `contactPageSchema` (`ContactPage`) + `localBusinessSchemas` (per office, from `CONTACTPAGEFIELDS.map.locations`) → **ContactPage**.
|
|
56
60
|
- **Entity graph linked by `@id`:** Organization is `${SITE_URL}/#organization`, WebSite `/#website`, logo `/#logo`. Service/Person/LocalBusiness reference the org via `provider`/`worksFor`/`parentOrganization` `@id`; AboutPage/ContactPage set `isPartOf` the website and `about` the org. Use `@id` cross-references — do not inline duplicate org nodes.
|
|
57
|
-
- `organizationSchema.logo` is an `ImageObject` (`@id` `/#logo`, `url`, `contentUrl`, `caption "TOGA Technology"`); width/height
|
|
58
|
-
- `personSchemas` de-dupes a leader who appears in both `boardMembers` and `members` in `ABOUTPAGEFIELDS`, keyed on lowercased trimmed name
|
|
61
|
+
- `organizationSchema.logo` is an `ImageObject` (`@id` `/#logo`, `url`, `contentUrl`, `caption "TOGA Technology"`); width/height omitted because the logo is an SVG.
|
|
62
|
+
- `personSchemas` de-dupes a leader who appears in both `boardMembers` and `members` in `ABOUTPAGEFIELDS`, keyed on lowercased trimmed name — naive emission produced duplicate `Person` nodes.
|
|
59
63
|
|
|
60
|
-
**
|
|
64
|
+
**New route checklist:**
|
|
65
|
+
1. Add the path to `src/routes.config.json` (prerender + sitemap list).
|
|
66
|
+
2. A nav destination moves from `STUB_ROUTES` to `LIVE_ROUTES` in `NavV2/navRoutes.ts` (removing the stub line retires the placeholder page).
|
|
67
|
+
3. A heavy page (own big CSS, canvas/WebGL) is code-split: `const x = lazyRoute(() => import(...))`, `Component: x.Route`, add it to `CODE_SPLIT_ROUTES`, give the page root a `data-testid` and add it to `PAGE_READY_SELECTORS` in `prerender.mjs`.
|
|
61
68
|
|
|
62
69
|
## Gotchas
|
|
63
70
|
|
|
71
|
+
- **🔴 Code-split a prerendered route only through `lazyRoute` — never plain `React.lazy` + `<Suspense>`, never react-router's route `lazy`.** React 18.3 hydrates a `<Suspense>` boundary only from the `<!--$-->` comment markers React's server renderer writes; the Puppeteer snapshot is client DOM and has none. Hydration throws, the whole root re-renders on the client, and the page area is blank until the chunk loads (confirmed in `react-dom` source, CTO-reviewed). Router `lazy` without a `HydrateFallback` renders blank too.
|
|
72
|
+
- **Every prerendered page still re-renders on load (pre-existing, open).** `/`, `/about` and the rest have a hydration mismatch starting in the shared layout / NavV2, so React throws away the snapshot and re-renders the whole root. Needs its own ticket. Shows in the console as the `[hydrate]` log.
|
|
64
73
|
- **De-indexing bug (root cause):** `robotsPlugin` required `NODE_ENV && VITE_ENV==="production"`, but build scripts set neither → every build shipped `robots.dev.txt` (`Disallow: /`). **Do NOT gate robots on `NODE_ENV`** — `vite build` sets it to `production` for *every* mode, which would expose beta/gamma. Gate on Vite `mode`.
|
|
65
|
-
- **`.npmrc` has `ignore-scripts=true`** → the `postbuild` lifecycle hook never fires. Prerender is therefore chained directly in the `build` script
|
|
74
|
+
- **`.npmrc` has `ignore-scripts=true`** → the `postbuild` lifecycle hook never fires. Prerender is therefore chained directly in the `build` script.
|
|
66
75
|
- **Prerender tooling:** `vite-react-ssg` supports React Router **6 only** (project is RR 7.9); `react-snap` is stale (2022) and won't launch on Node 24. Hence a custom Puppeteer snapshot.
|
|
67
|
-
- **🔴 Chrome must be installed in the build
|
|
68
|
-
- **🔴 `package-lock.json` must be committed:** Amplify preBuild runs `npm ci`, which refuses to run without a committed lockfile (`npm error code EUSAGE`)
|
|
69
|
-
- **
|
|
70
|
-
- **Prerender fail-fast:** a route that never mounts (`#root > *` times out) aborts the build with `process.exit(1)` — otherwise it would snapshot the empty shell (no SEO/JSON-LD) and still exit 0.
|
|
76
|
+
- **🔴 Chrome must be installed in the build:** Amplify/CodeBuild's `npm ci` does **not** download a browser, so `puppeteer.launch()` fails with `Could not find Chrome (ver. …)` at `/root/.cache/puppeteer`. Fixed by `npx puppeteer browsers install chrome` in the `build` script right before `node scripts/prerender.mjs`. puppeteer 25 needs **Node ≥ 22.12** — set the Amplify image accordingly. (~150 MB download per build unless `/root/.cache/puppeteer` is cached.)
|
|
77
|
+
- **🔴 `package-lock.json` must be committed:** Amplify preBuild runs `npm ci`, which refuses to run without a committed lockfile (`npm error code EUSAGE`).
|
|
78
|
+
- **Prerender fail-fast:** a route that never mounts (`#root > *` or its ready selector times out) aborts the build with `process.exit(1)` — otherwise it would snapshot the empty shell and still exit 0.
|
|
71
79
|
- **🔴 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
|
-
- **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
|
-
- **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. 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
80
|
- **🔴 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;
|
|
78
|
-
- **
|
|
79
|
-
- **
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
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`).
|
|
81
|
+
- **Prerender UA tag:** `prerender.mjs` sets a user agent containing `TOGAPrerender`. `App.tsx` skips analytics on it; engines, timers and measured inline styles must skip too — use `isPrerender()` (`src/utils/isPrerender.ts`).
|
|
82
|
+
- **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).
|
|
83
|
+
- **Contact page merge (2026-06-19):** keep production's reCAPTCHA conditional wrap (`GoogleReCaptchaProvider` mounts only when `VITE_RECAPTCHA_V3_SITE_KEY` is set) **and** `<Seo>`/`<JsonLd>`; the old `<Helmet>` block is superseded by `<Seo>`.
|
|
84
|
+
- **Verified data:** 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 wrong specifics — verify schema/llms.txt against FIELDS before shipping.
|
|
85
|
+
- **Brand name:** "TOGA Technology" only — an `alternateName: "Agilant"` draft was reverted. Rule: [brand-naming](../../../standards/brand-naming.md).
|
|
86
|
+
- **Build size:** the main JS bundle is ~14.8 MB (4.9 MB gzip), site-wide — the biggest LCP problem, bigger than any page's videos. Only `/talos` and `/our-platform` are code-split.
|
|
87
|
+
- **FIELDS JSON is component-coupled — no free-prose body slot:** each page's `*FIELDS*.json` is shaped to specific components, so there is no place to drop a long prose block. An audit's "rewrite to 900 words" needs new components; otherwise only sharpen titles/descriptions/keywords/card copy that already have slots.
|
|
88
88
|
|
|
89
89
|
## Change history
|
|
90
|
+
- 2026-10-15 — Corrected the lazy + Suspense rule (it breaks hydration in React 18.3); added `lazyRoute` + chunk preload, prerender ready selectors, site-wide hydration mismatch finding (TRUE-82652, tcox)
|
|
90
91
|
- 2026-09-25 — Added lazy-route, no-DOM-append, TOGAPrerender UA and new-route rules from the /talos build (tcox)
|
|
@@ -6,60 +6,50 @@ project: TOGA Technology Website
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-15
|
|
10
10
|
owners: ["tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- togatech/src/pages/Talos/TalosPage.tsx
|
|
13
13
|
- togatech/src/pages/Talos/talos.css
|
|
14
14
|
- togatech/src/pages/Talos/viewModel/useTalosSparks.ts
|
|
15
|
+
- togatech/src/pages/Talos/viewModel/talosSparksRoute.ts
|
|
15
16
|
- togatech/src/pages/Talos/viewModel/useGlassPanes.ts
|
|
16
17
|
- togatech/src/pages/Talos/viewModel/useTalosPanelSky.ts
|
|
17
18
|
- togatech/src/pages/Talos/viewModel/useTalosSubnav.ts
|
|
18
19
|
- togatech/src/pages/Talos/viewModel/FIELDS
|
|
19
|
-
- togatech/src/pages/Talos/view/sections/TalosChoosing/TalosChoosing.tsx
|
|
20
|
-
- togatech/src/pages/Talos/view/shared/Reveal.tsx
|
|
21
20
|
- togatech/src/pages/Talos/view/sections/TalosHero
|
|
22
|
-
- togatech/src/pages/Talos/view/sections/
|
|
21
|
+
- togatech/src/pages/Talos/view/sections/TalosFaq/TalosFaq.tsx
|
|
22
|
+
- togatech/src/pages/Talos/view/sections/TalosChoosing/TalosChoosing.tsx
|
|
23
23
|
- togatech/src/pages/Talos/view/sections/TalosGovernance
|
|
24
|
-
- togatech/src/routes.tsx
|
|
25
|
-
- togatech/src/components/templates/AppLayout/AppLayout.tsx
|
|
26
|
-
- togatech/src/components/templates/AppLayout/appLayoutContext.ts
|
|
27
|
-
- togatech/src/components/NavV2/view/TalosFab.tsx
|
|
28
24
|
related:
|
|
29
25
|
- ../architecture.md
|
|
26
|
+
- ../workflows/designer-prototype-port.md
|
|
27
|
+
- platform-product-page.md
|
|
30
28
|
- seo-aeo-geo-prerender.md
|
|
31
29
|
- navv2-global-nav.md
|
|
32
30
|
---
|
|
33
31
|
|
|
34
|
-
togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS prototype; open for
|
|
32
|
+
togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS prototype; open for its talos.css port edits, section notes, open port work and placeholder copy.
|
|
35
33
|
|
|
36
34
|
## Summary
|
|
37
35
|
|
|
38
|
-
`/talos` is a 15-section product page (`src/pages/Talos/`) ported from a designer prototype
|
|
36
|
+
`/talos` is a 15-section product page (`src/pages/Talos/`) ported from a designer prototype. Port method (drops, generated CSS, page root as `<html>`, shared pieces): [designer-prototype-port.md](../workflows/designer-prototype-port.md). Layout: `view/sections/<Section>/` (incl. the `TalosSparks` canvas backdrop), `viewModel/use*.ts` hooks, copy in `viewModel/FIELDS/*.json`, images in `public/assets/talos/`. Renders the shared `FooterV2` and `TalosLauncher` (moved out of this page to `src/components/` in TRUE-82652, no behaviour change). Code-split, prerendered, in the sitemap list.
|
|
39
37
|
|
|
40
38
|
## How it works
|
|
41
39
|
|
|
42
|
-
**CSS
|
|
43
|
-
- 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`.
|
|
44
|
-
- Generated by a postcss prefix+concat script, `build-talos-css.cjs`, that lives **outside the repo** (session scratchpad, not `scripts/`). If it is lost, re-create it before the next design CSS change — never hand-edit a regenerated `talos.css` in bulk.
|
|
45
|
-
- After regenerating, re-apply the port edits: add any design token the CSS uses but our copy lacks (the first port missed `--ease-enter`, so `.reveal` / `.tlh-enter` / `.tpl-rev` had no valid transition); drop `.needs-dev*`, `.tvq*`, and design `.talos-fab{display:none !important}` (it is for the design's orb launcher and would hide our NavV2 FAB until the orb is ported).
|
|
46
|
-
- **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.)
|
|
47
|
-
- 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.
|
|
48
|
-
- `<html>`-level styles (`overflow-x: clip` so `position: sticky` works — never `hidden`; `scroll-behavior: smooth`) are set in `useLayoutEffect` (`useRootStyles` in `TalosPage.tsx`) and **removed** on unmount, not "restored" (a prerendered snapshot already carries them, so the "previous" value is wrong).
|
|
49
|
-
- **Reduced motion:** under `prefers-reduced-motion`, `useRootStyles` skips `scroll-behavior: smooth` and `useTalosSubnav` scrolls with `behavior: "instant"` — `"auto"` inherits the `<html>` smooth setting.
|
|
50
|
-
|
|
51
|
-
**Code-split + prerender:** `routes.tsx` wraps the page in `React.lazy` + `<Suspense fallback={null}>` inside the route `Component`. Rules in [seo-aeo-geo-prerender.md](seo-aeo-geo-prerender.md). Page-specific: `useGlassPanes` `useFitGap` (56px under the lede) skips on the `TOGAPrerender` UA so the snapshot does not bake the crawler-viewport margin.
|
|
40
|
+
**talos.css:** generated from the design CSS (`colors_and_type.css`, `site.css`, `talos-orb.css`, `talos.css`) under `.talosPage`. Its builder `build-talos-css.cjs` was not kept — re-create it in `design-files/_port-tools/` (model: `build-platform-css.cjs`) before the next design CSS change. Edits to carry over (also in the `talos.css` header): add the `--ease-enter` token (without it `.reveal` / `.tlh-enter` / `.tpl-rev` have no valid transition); drop `.needs-dev*` and `.tvq*`.
|
|
52
41
|
|
|
53
|
-
**
|
|
54
|
-
-
|
|
55
|
-
-
|
|
56
|
-
-
|
|
42
|
+
**Page specifics:**
|
|
43
|
+
- `useGlassPanes` `useFitGap` (56px under the lede) skips under prerender, so the snapshot does not bake the crawler-viewport margin.
|
|
44
|
+
- Reduced motion: `useTalosSubnav` scrolls with `behavior: "instant"` — `"auto"` inherits the `<html>` smooth setting.
|
|
45
|
+
- The Hero's second button ("Ask Talos", `btn-talos-boxed is-lg is-hero`) calls `openTalosChat` from the AppLayout context (see [architecture.md](../architecture.md) Routing) — never mount a second chat.
|
|
46
|
+
- `TalosLauncher` gets `liftObstacles` = the closing CTA buttons + reCAPTCHA badge.
|
|
57
47
|
|
|
58
|
-
**Section notes:** Voice sits at the end of `.tpanel2` as a `.tproof-*` stat + quote row
|
|
48
|
+
**Section notes:** Voice sits at the end of `.tpanel2` as a `.tproof-*` stat + quote row. Governance `RAIL_COLUMNS = points.length` (7 points). PanelSky has no `.tp2-tw` twinkle; it fades over 360px to `PAGE_FILL`.
|
|
59
49
|
|
|
60
|
-
**Routing:**
|
|
50
|
+
**Routing:** sub-pages `/talos/one|flow|engage` are still `STUB_ROUTES`.
|
|
61
51
|
|
|
62
|
-
**Copy:** the design's
|
|
52
|
+
**Copy:** the design's `COPY.katie` set (Katie signs off copy). Not production-ready (as of 2026-10-12):
|
|
63
53
|
- `TALOSFAQFIELDS.json` — 6 answers are "[Answer not written. Needs Jeff.]"; "Certification three/four" are placeholders.
|
|
64
54
|
- `TALOSVOICEFIELDS.json` — quote still flagged as placeholder.
|
|
65
55
|
- `TalosChoosing` card CTAs ("Book a demo", "See TOGa Platform") are plain spans with no link.
|
|
@@ -67,23 +57,16 @@ togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS pro
|
|
|
67
57
|
|
|
68
58
|
## Port status (design drop 2, 2026-10)
|
|
69
59
|
|
|
70
|
-
Done:
|
|
60
|
+
Done: CSS regen, copy, Hero ask, Voice move, Governance, PanelSky, orb launcher + guide bubbles (now shared `TalosLauncher`). Left:
|
|
71
61
|
1. Compare rebuild — half-stars, info popover, key.
|
|
72
|
-
2. FAQ two
|
|
73
|
-
3. ProductLine "What can X do?" ask buttons + `useTalosSweep
|
|
62
|
+
2. FAQ redesign: two columns, 9 items, per-item ask. Build it on the shared `SiteFaq` (`TalosFaq` today shares only `useFaqAccordion`). Then **delete the temporary bridge rules** under `#talos-faq:not(:has(.tfq-cols))` in `talos.css`.
|
|
63
|
+
3. ProductLine "What can X do?" ask buttons + `useTalosSweep` (a version exists in `OurPlatform/viewModel/`).
|
|
74
64
|
4. Re-sync `talosSparksRoute.ts` — the sparks line loops wrong around the moved Voice section.
|
|
75
|
-
5. `TalosOrbLauncher` / `talos-orb.js` replacing the FAB on `/talos`, `.tfab-label`, TalosGuide bubbles, chat API `open(q, {expanded})` / `close()`.
|
|
76
65
|
|
|
77
66
|
Design's `HeroWeave` / `HeroDots` / old Hero are dead code — do not port.
|
|
78
67
|
|
|
79
|
-
## Gotchas
|
|
80
|
-
|
|
81
|
-
- Design `body{background:transparent}` collapses onto `.talosPage` and wipes the page colour — drop the body background when porting.
|
|
82
|
-
- `.talosPage` needs `isolation: isolate` so negative/low `z-index` backdrop layers paint above its background (it stands in for `<html>`).
|
|
83
|
-
- 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.
|
|
84
|
-
- Tailwind preflight is global and can differ from the design's browser defaults — check margins/list/button resets when a section looks off.
|
|
85
|
-
|
|
86
68
|
## Change history
|
|
69
|
+
- 2026-10-15 — Footer, launcher, Reveal, TalosMark, FAQ accordion, useRootStyles moved to shared components (no behaviour change); generic port rules moved to designer-prototype-port.md (TRUE-82652, tcox)
|
|
87
70
|
- 2026-10-12 — Reduced-motion: no smooth scroll on `<html>`, instant subnav jumps; listed open placeholder copy/CTAs (TRUE-82042, tcox)
|
|
88
|
-
- 2026-10-05 — Design drop 2, batches 1–2: talos.css regen (+`--ease-enter`, `.talos-mark`), Hero "Ask Talos", Voice → `.tproof-*`, FAQ bridge rules, useFitGap prerender skip (tcox)
|
|
71
|
+
- 2026-10-05 — Design drop 2, batches 1–2: talos.css regen (+`--ease-enter`, `.talos-mark`), Hero "Ask Talos", Voice → `.tproof-*`, FAQ bridge rules, useFitGap prerender skip, orb launcher (tcox)
|
|
89
72
|
- 2026-09-25 — Built /talos from the designer prototype; added Outlet context `openTalosChat`, `ROUTES_WITH_OWN_FOOTER`, TalosFab now opens chat (tcox)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Porting a Designer Prototype to a togatech Page
|
|
3
|
+
framework: "standalone"
|
|
4
|
+
repo: togatech
|
|
5
|
+
project: TOGA Technology Website
|
|
6
|
+
client: shared
|
|
7
|
+
type: workflow
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-10-15
|
|
10
|
+
owners: ["tcox"]
|
|
11
|
+
files:
|
|
12
|
+
- togatech/src/utils/hooks/useRootStyles.ts
|
|
13
|
+
- togatech/src/utils/isPrerender.ts
|
|
14
|
+
- togatech/src/utils/isPinnedSceneLocked.ts
|
|
15
|
+
- togatech/src/components/TalosLauncher/view/TalosLauncher.tsx
|
|
16
|
+
- togatech/src/components/FooterV2/FooterV2.tsx
|
|
17
|
+
- togatech/src/components/SiteFaq/SiteFaq.tsx
|
|
18
|
+
- togatech/src/components/Reveal/Reveal.tsx
|
|
19
|
+
- togatech/src/components/templates/AppLayout/AppLayout.tsx
|
|
20
|
+
related:
|
|
21
|
+
- ../architecture.md
|
|
22
|
+
- ../features/talos-product-page.md
|
|
23
|
+
- ../features/platform-product-page.md
|
|
24
|
+
- ../features/seo-aeo-geo-prerender.md
|
|
25
|
+
- ../features/navv2-global-nav.md
|
|
26
|
+
- local-dev-and-headless-verification.md
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
togatech procedure for turning a designer HTML/JSX/CSS prototype into a live page (`/talos`, `/our-platform`): where drops and port tools live, generated prefixed CSS, page root as `<html>`, design JS → hooks, shared page pieces, headless compare. Open before porting or re-porting a design drop.
|
|
30
|
+
|
|
31
|
+
## Summary
|
|
32
|
+
|
|
33
|
+
The designer ships static HTML + in-browser JSX + CSS prototypes. Each redesigned page is a port of one (`/talos` → [talos-product-page.md](../features/talos-product-page.md), `/our-platform` → [platform-product-page.md](../features/platform-product-page.md)). The method below is shared; page-specific rules live in each page doc.
|
|
34
|
+
|
|
35
|
+
## Steps
|
|
36
|
+
|
|
37
|
+
1. **Get the drop.** Drops live in `togatech/design-files/` — local only, excluded via `.git/info/exclude`, never committed. Current prototype: `design-files/uploads/New TOGA WEBSITE/TOGA Website/`. Port tools live in `design-files/_port-tools/` (also local only): `build-<page>-css.cjs`, `design-server.cjs` (serves the design on `localhost:8765`), `compare.cjs` (design vs port screenshots side by side), `hydration-check.cjs` (serves `dist/`, reports hydration errors per prerendered route), `perf.cjs`, `text-diff.cjs`. If a tool is lost, re-create it before the next design change.
|
|
38
|
+
2. **Generate the page CSS — never hand-port it.** A postcss script concatenates the design CSS files **in the HTML's load order** into one plain file (`talos.css`, `ourPlatform.css`), prefixes every selector with the page root class (`.talosPage`, `.platformPage`), maps `:root` / `html` / `body` onto the root, rewrites asset urls to `/assets/<page>/`, keeps keyframe names. Port fixes go in the script's `EDITS` list (each asserts its exact match count, so a new drop that moves the text fails loudly) or its appended "port additions". Regenerate; never bulk hand-edit the output.
|
|
39
|
+
- Plain CSS, **not a CSS module**: design JS finds elements by raw class names/ids and sets keyframe names inline; hashing breaks that (CTO-reviewed).
|
|
40
|
+
3. **Make the page root stand in for `<html>`.** Whatever the design writes on `document.documentElement` (attributes, custom properties) goes on the page root, since the CSS maps `html` there. Viewport reads stay on `documentElement`. Render constant root vars in JSX `style` (prerender-safe). The root needs `isolation: isolate` so low/negative `z-index` backdrop layers paint above its background.
|
|
41
|
+
- Real `<html>` styles (`overflow-x: clip` — never `hidden`, it breaks `position: sticky`; `scroll-behavior: smooth`) go through `useRootStyles(MODULE_CONST)`: layout effect, skips smooth scroll under `prefers-reduced-motion`, and **removes** the properties on unmount (never "restores" — a prerendered snapshot already carries them).
|
|
42
|
+
4. **Port design scripts as viewModel hooks.** Custom elements and WebGL/canvas widgets become hooks on an empty React host (`dangerouslySetInnerHTML={{__html:""}}`) — never append into React-owned nodes (see [seo-aeo-geo-prerender.md](../features/seo-aeo-geo-prerender.md)). Engines skip under `isPrerender()` and pause off-screen and on a hidden tab. Keep the design's timing backstops (IntersectionObserver + rect polls, `setTimeout` retry chains) — rAF / ResizeObserver can go silent on cold load.
|
|
43
|
+
5. **Reuse the shared page pieces — do not re-port them:** `components/FooterV2`, `components/TalosLauncher` (orb + guide bubbles; props `fields`, `liftObstacles`, optional `leftHero` / `waitForPins`; guide memory is per page, keyed by section ids; greeting optional; orb/guide CSS comes from the page's own generated CSS), `components/SiteFaq` (+ `useFaqAccordion`), `components/Reveal`, `components/TalosMark`, `utils/hooks/useBtnSpot`. Add the route to AppLayout `ROUTES_WITH_OWN_FOOTER` / `ROUTES_WITH_OWN_LAUNCHER`. Give every pinned scene `data-nav-pinned` ([navv2-global-nav.md](../features/navv2-global-nav.md)).
|
|
44
|
+
6. **Wire the route** as a code-split prerendered route — checklist in [seo-aeo-geo-prerender.md](../features/seo-aeo-geo-prerender.md).
|
|
45
|
+
7. **Map design links** (`*.html`) to real routes; an anchor the target page lacks falls back to the route (NavV2 Gotchas).
|
|
46
|
+
8. **Compare against the design headlessly** (`compare.cjs`), then `hydration-check.cjs` on a prod build.
|
|
47
|
+
|
|
48
|
+
## Gotchas
|
|
49
|
+
|
|
50
|
+
- **Screenshot width is 12px off.** Puppeteer hides scrollbars, but the site's `html { scrollbar-gutter: stable }` still reserves 12px, so the port renders 12px narrower than the design. Compare at port width = design width + 12, or launch with `ignoreDefaultArgs: ['--hide-scrollbars']`. Real phones use overlay scrollbars — not a bug there.
|
|
51
|
+
- **Design pages load React/Babel from a CDN** and render in the browser — wait for a rendered element, not only `networkidle`.
|
|
52
|
+
- Design `body { background: transparent }` collapses onto the page root and wipes the page colour — drop it.
|
|
53
|
+
- 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.
|
|
54
|
+
- Tailwind preflight is global and differs from browser defaults — check margins, lists and button resets when a section looks off.
|
|
55
|
+
|
|
56
|
+
## Change history
|
|
57
|
+
- 2026-10-15 — Created from the /talos and /our-platform ports: drop + tool locations, EDITS asserts, page root as `<html>`, shared pieces, 12px screenshot offset (TRUE-82652, tcox)
|
|
@@ -6,7 +6,7 @@ project: TOGA Technology Website
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-10-
|
|
9
|
+
updated: 2026-10-15
|
|
10
10
|
owners: ["tcox"]
|
|
11
11
|
files:
|
|
12
12
|
- togatech/package.json
|
|
@@ -17,6 +17,7 @@ files:
|
|
|
17
17
|
related:
|
|
18
18
|
- ../architecture.md
|
|
19
19
|
- ../features/careers-paylocity-job-openings.md
|
|
20
|
+
- designer-prototype-port.md
|
|
20
21
|
---
|
|
21
22
|
|
|
22
23
|
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. Open before local verification.
|
|
@@ -41,6 +42,8 @@ The gotcha-avoidance path for getting `togatech` up on a dev machine and actuall
|
|
|
41
42
|
- `robotsPlugin` hardcodes `./dist`: a `--outDir <other>` scratch build still writes `dist/robots.txt`, and its `closeBundle` errors if `dist/` does not exist. Harmless for scratch builds.
|
|
42
43
|
- A production `vite build` sends GA4 hits even when served from localhost — headless checks against a prod build log real `localhost/...` page views. Verify against `npm run dev` where you can.
|
|
43
44
|
- Watching the network tab is not proof a fetch is broken: see step 3 (persisted query cache).
|
|
45
|
+
- **Git Bash rewrites route arguments.** A script arg like `/our-platform` becomes a Windows path (`C:/Program Files/Git/our-platform`). Prefix the command with `MSYS_NO_PATHCONV=1`.
|
|
46
|
+
- Comparing screenshots against a design page: see [designer-prototype-port.md](designer-prototype-port.md) Gotchas (12px scrollbar-gutter offset, CDN-rendered design pages).
|
|
44
47
|
|
|
45
48
|
## Related
|
|
46
49
|
|
|
@@ -48,6 +51,7 @@ The gotcha-avoidance path for getting `togatech` up on a dev machine and actuall
|
|
|
48
51
|
- [Careers Section — Live Paylocity Job Openings](../features/careers-paylocity-job-openings.md)
|
|
49
52
|
|
|
50
53
|
## Change history
|
|
54
|
+
- 2026-10-15 — Git Bash route-arg path conversion (`MSYS_NO_PATHCONV=1`); link to design-compare gotchas (TRUE-82652, tcox)
|
|
51
55
|
- 2026-10-12 — Root tsconfig checks nothing (build's `tsc` does not type-check); 13 known errors (tcox)
|
|
52
56
|
- 2026-10-05 — Prod build sends GA hits from localhost; robotsPlugin closeBundle fails without dist/ (tcox)
|
|
53
57
|
- 2026-09-25 — No eslint config either; `tsc -p tsconfig.app.json` path-filter tip; robotsPlugin ignores --outDir (tcox)
|
package/package.json
CHANGED