toga-ai 1.0.954 → 1.0.956

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,21 +6,24 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-06
10
10
  owners: ["ajean"]
11
11
  files:
12
12
  - tools/mvc/clickup/aliases/get.php
13
13
  - tools/mvc/clickup/aliases/post.php
14
14
  - tools/_/app/clickup/aliases.php
15
+ - tools/_/app/clickup/aliases/jobs.php
15
16
  - tools/_/app/nav.php
17
+ - tools/assets/css/style.css
16
18
  related:
17
19
  - ./mvc-data-access-patterns.md
18
20
  - ./errors-curation-console.md
19
21
  - ./persona-gated-navigation.md
22
+ - ./worker2-job-bridge.md
20
23
  - ../../../2.0/apps/worker2/features/netsuite-opportunity-client-labels.md
21
24
  ---
22
25
 
23
- Human side of the NetSuite→ClickUp Stakeholders/End-Customer labels at `/clickup/aliases`: point an unresolved company name at a ClickUp option in `Team.ClickUpLabelAliases`; open when curating that queue or before saving an alias.
26
+ Human side of the NetSuite→ClickUp Stakeholders/End-Customer labels at `/clickup/aliases`: point an unresolved company name at a ClickUp option in `Team.ClickUpLabelAliases`, refresh the option mirror, and run the label backfill from buttons; open when curating that queue, saving an alias, or changing the Sync card.
24
27
 
25
28
  ## Summary
26
29
 
@@ -34,6 +37,7 @@ The human side of the NetSuite→ClickUp **Stakeholders / End Customer** labels.
34
37
  - `tools/mvc/clickup/aliases/get.php` — the page (route `/clickup/aliases`).
35
38
  - `tools/mvc/clickup/aliases/post.php` — the save; redirect-after-POST, no JavaScript.
36
39
  - `tools/_/app/clickup/aliases.php` — `App_Clickup_Aliases`: `options()`, `optionsUpdatedAt()`, `all()`, `setOption()`. Connection alias `db_team` (config section `[database_team]`), tables `ClickUpLabelAliases` + `ClickUpLabelOptions`.
40
+ - `tools/_/app/clickup/aliases/jobs.php` — `App_Clickup_Aliases_Jobs`: the **Sync with ClickUp** card's queueing, in-progress guard and lock (tools PR #17, fix PR #18).
37
41
  - `tools/_/app/nav.php` — listed under **Developers** next to `/errors`, personas `Development Team` / `TOGa Technology`.
38
42
 
39
43
  Field ids are constants on the class: Stakeholders `ec15edfd-5920-4c12-a1dc-94ede4a62438`, End Customer `67bb359f-fa59-40e1-b94a-0076450bb7c0`. **The two fields are separate option-id spaces.**
@@ -48,8 +52,20 @@ Field ids are constants on the class: Stakeholders `ec15edfd-5920-4c12-a1dc-94ed
48
52
  - **Auth + CSRF are explicit:** both files call `App_Auth::requireAuth(['TOGa Technology', 'Development Team'])`, and `post.php` additionally calls `App_Page::validateCrossSiteRequestForgery()` — a non-GET request never reaches `App_FrameworkIndex_Tools::actionHandler()`, so **the framework does not validate CSRF for POSTs**.
49
53
  - Storage holds **plain text**; escaping happens exactly once at display. Escaping on the way in would accumulate entities and break equality against the NetSuite name.
50
54
 
55
+ ### Sync with ClickUp card (two buttons)
56
+ Both queue worker2 actions through `App_Worker::enqueue` — see [worker2-job-bridge](./worker2-job-bridge.md). They replace hand-queuing from SQS.
57
+ - **Refresh options** → `Netsuite/OpportunityStakeholderDigest/Daily` `{dryRun:true}`: republishes `Team.ClickUpLabelOptions`, sends no email.
58
+ - **Run backfill** → `Netsuite/OpportunityStakeholderBackfill/Backfill` `{dryRun, exactOnly:true}`. `exactOnly` is always true from this page. Dry run is the default.
59
+ - **Who can do what (decided by ajean):** refresh + dry run = the page personas (`TOGa Technology`, `Development Team`). **Live backfill = `Development Team` only** — checked on the server (`App_Auth::hasAnyPersona` + const `LIVE_BACKFILL_PERSONAS`); the UI only hides the option. Live also needs a server-checked "I checked the dry-run report" box.
60
+ - **In-progress guard:** a button is blocked while its action is running — rule and why in [worker2-job-bridge](./worker2-job-bridge.md#gotchas). Window = 60 min.
61
+ - **Check + insert run under MySQL `GET_LOCK`**; `RELEASE_LOCK` sits in its own try/catch inside `finally`. Lock name = `'tools-aliases:' . md5($action)` — see the 64-char gotcha below.
62
+ - **Failed SQS send** (enqueue outcome b) → the orphan `WorkerJobs` row is set `isSuccess=0`, so it does not block the button for 60 min.
63
+ - **Errors travel as short codes** (`?jobError=<code>`) mapped to fixed text in `get.php`. Never show free text from the URL — a crafted link could show a fake message (CSO finding).
64
+
51
65
  ## Gotchas
52
- - **⚠ An option you just created in ClickUp is INVISIBLE here until worker2 republishes the mirror.** The dropdowns are as fresh as the last worker2 run. **Fix: run any digest — it refreshes the mirror in ~2 seconds, and `dryRun` works too**, because on the digest `$dryRun` means "do not send the email", *not* "do not write". This was exactly the confusion after adding 19 new options.
66
+ - **⚠ An option you just created in ClickUp is INVISIBLE here until worker2 republishes the mirror.** The dropdowns are as fresh as the last worker2 run. **Fix: press Refresh options** (a digest `dryRun` — on the digest `$dryRun` means "do not send the email", *not* "do not write"; ~2 seconds).
67
+ - **⚠ `GET_LOCK` names max 64 chars — longer names ERROR (MySQL 4163), they do not truncate.** The first lock name `'tools-clickup-aliases:<action>'` was 66/70 chars, so both buttons always failed with "Not queued. The job could not be queued." php-reviewer and cso both missed it; only real MySQL caught it. Hash the variable part. General rule in [mvc-data-access-patterns](./mvc-data-access-patterns.md#gotchas).
68
+ - **The digest can say "nothing to report" while unresolved alias rows still exist** — e.g. names that now match an option exactly. An empty digest is not an empty queue; read the *Needs an alias* list.
53
69
  - **While the mirror is EMPTY, saving is disabled entirely** (the queue still renders). That is a distinct state from a failure, and the notice says so: it fills in on the next worker2 digest or backfill run. `optionsUpdatedAt()` shows the freshness date on the page.
54
70
  - **A name with no plausible option cannot be fixed here.** ClickUp's public API cannot create a `labels` option, so someone has to add it in the ClickUp UI first. Those rows still list, ordered by impact.
55
71
  - **`?saved=0` is a real outcome, not a no-op** — somebody pressed Save without changing a dropdown, or every change was rejected by the allowlist. `get.php` distinguishes "not a save" from "a save that changed nothing" via `isset($_GET['saved'])`.
@@ -60,4 +76,8 @@ Field ids are constants on the class: Stakeholders `ec15edfd-5920-4c12-a1dc-94ed
60
76
  - [Tools MVC — data-access patterns](./mvc-data-access-patterns.md)
61
77
  - [Errors curation console](./errors-curation-console.md)
62
78
  - [Persona-gated navigation](./persona-gated-navigation.md)
79
+ - [Tools → worker2 job bridge](./worker2-job-bridge.md)
63
80
  - [ClickUp Opportunity Client vs End-Customer Labels (worker2)](../../../2.0/apps/worker2/features/netsuite-opportunity-client-labels.md)
81
+
82
+ ## Change history
83
+ - 2026-10-06 — Added the Sync with ClickUp card (Refresh options + Run backfill; live backfill Development Team only), WorkerJobs in-progress guard, and the GET_LOCK 64-char fix (tools PR #17, #18). (ajean)
@@ -6,7 +6,7 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-05
9
+ updated: 2026-10-06
10
10
  owners: [jcardinal, bala, ajean]
11
11
  files:
12
12
  - tools/_/app/nav.php
@@ -96,6 +96,9 @@ Use the project's own stripper before logging or emailing a request value: `App_
96
96
  ### Every `$_GET`/`$_POST` value is HTML-escaped on arrival — JSON posts break
97
97
  `library/app/framework.php` (~line 64) runs every `$_REQUEST`/`$_GET`/`$_POST` value through `App_String::xssProtect()` (htmlspecialchars). JSON posted from page JS arrives as `{&quot;...&quot;}` and `json_decode` returns `NULL`. Undo it **once** with `App_String::xssUnprotect()` before `json_decode`, then validate the decoded values as usual.
98
98
 
99
+ ### `GET_LOCK` names max 64 chars — longer names ERROR, they do not truncate
100
+ MySQL error 4163 *"User-level lock name ... should not exceed 64 characters"*. Any prefix + action/route/uuid can pass 64 without anyone noticing, and code review does not catch it — only a real MySQL run does. **Hash the variable part:** `'tools-aliases:' . md5($action)` (46 chars). Hit twice: [clickup-label-aliases-admin](./clickup-label-aliases-admin.md) (both buttons dead on prod) and api2 [cross-client retrieval](../../../2.0/apps/api2/features/cross-client-data-retrieval.md). Same limit in 2.0.
101
+
99
102
  ### By-reference row helpers
100
103
  `buildArrayOfRows`, `fetchOne`, and `fetchRow` take `$res` **by reference**. Assign the `query()` result to a **variable first**, then pass it — passing the call result inline emits PHP "Only variables should be passed by reference", which **renders to the page** when `display_errors` is on.
101
104
 
@@ -160,4 +163,5 @@ set_error_handler(fn(): bool => true); // swallow non-fatal notices/deprecations
160
163
  - [1.0 MVC page pattern & app skeleton](../../library/features/mvc-page-pattern-and-app-skeleton.md)
161
164
 
162
165
  ## Change history
166
+ - 2026-10-06 — Added the `GET_LOCK` 64-char gotcha (hash the variable part). (ajean)
163
167
  - 2026-10-05 — Added the request-input xssProtect gotcha (JSON posts decode to NULL). (bala)
@@ -6,8 +6,8 @@ project: Tools
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-05
10
- owners: [mhammontree, bala]
9
+ updated: 2026-10-06
10
+ owners: [mhammontree, bala, ajean]
11
11
  files:
12
12
  - tools/_/app/worker.php
13
13
  related:
@@ -16,6 +16,7 @@ related:
16
16
  - ../../../2.0/apps/worker2/features/creating-worker-actions.md
17
17
  - ../../../clients/compass-usa/features/people-file-import-admin-page.md
18
18
  - ./mvc-data-access-patterns.md
19
+ - ./clickup-label-aliases-admin.md
19
20
  - ../../../2.0/apps/worker2/architecture.md
20
21
  - ../../../2.0/apps/worker2/features/stranded-worker-jobs.md
21
22
  ---
@@ -55,6 +56,14 @@ empty result — because the wrapper format is worker2's, not a contract.
55
56
  - **`isSuccess = 1` does not mean the work succeeded.** Actions that catch their own errors (e.g. all Etilize actions) return `{success:false, errors:[...]}` and the job is still stamped `isSuccess = 1`. Decide success from the decoded `output` JSON (`success`, `errors`, `fatal`), not the column.
56
57
  - **`Core.WorkerJobs.parameters` is a MySQL JSON column.** MySQL re-orders keys and adds spaces, so `parameters = '<json string>'` never matches. Compare with `parameters = CAST('<json>' AS JSON)`. Filter on the index `WorkerJobs_scheduling_IDX (isSuccess, dtQueued, dtStarted)` first.
57
58
  - **Guard duplicate enqueues in the DB, not the session.** Tools sessions use the `rediscluster` handler with no session locking, so two concurrent requests both read and write `$_SESSION`. Before enqueuing, reuse a pending identical row (same action + `CAST` parameters, last 2 minutes).
59
+ - **Long or self-chunking job: block while it RUNS, not just a 2-minute duplicate window — two checks.** Read the
60
+ newest `Core.WorkerJobs` row for the action (`ORDER BY id DESC LIMIT 1` — the table has no index on
61
+ `action`/`dtCreated`). Running = the row is **unfinished** OR its `output` contains
62
+ `run CONTINUES`, within a 60 min window. The CONTINUES check is required: worker2 hands off the next
63
+ chunk with SQS-first `_Worker::runTask`, so the next chunk's row **does not exist until pickup** (a
64
+ CTO review wrongly said it does). 60 min = the SQS visibility timeout — a failed first delivery is
65
+ hidden that long, then redelivered. Wrap check + insert in `GET_LOCK`. Reference:
66
+ [clickup-label-aliases-admin](./clickup-label-aliases-admin.md).
58
67
  - **Send only the action's exact parameter names** — the dispatcher spreads them as named args; one extra key fails the job (*Unknown named parameter*). See [creating worker actions](../../../2.0/apps/worker2/features/creating-worker-actions.md).
59
68
  - **`isSuccess IS NULL` means two different things** — running, or abandoned. Same trap as
60
69
  [stranded worker jobs](../../../2.0/apps/worker2/features/stranded-worker-jobs.md).
@@ -81,6 +90,7 @@ empty result — because the wrapper format is worker2's, not a contract.
81
90
  4. Dev-bypass login must use a `Client_True` email (e.g. `@togatech.com`, not `@goagilant.com`).
82
91
 
83
92
  ## Change history
93
+ - 2026-10-06 — Added the in-progress check for self-chunking jobs (newest row unfinished OR `run CONTINUES`, 60 min window), from the /clickup/aliases Sync card. (ajean)
84
94
  - 2026-10-05 — Added: isSuccess=1 can hide a failed action, JSON-column parameters compare, DB-level duplicate-enqueue guard (Etilize Refresh page). (bala)
85
95
  - 2026-10-01 — Added the local tools→worker2 test setup and the prod-only `[worker]` config fact (TRUE-82414). (bala)
86
96
  - 2026-09-23 — Created from TRUE-82250: documented the three `enqueue()` outcomes, the log-line-wrapped `output`, and the CTO decision that tools never speaks SFTP (phpseclib 1.x sha1-only KEX + credential duplication). (mhammontree)
@@ -9,6 +9,7 @@
9
9
  | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | Config that varies by client (or client × role) — field overrides, filter buttons, group-by, column pickers, layout toggles — resolved through `src/fieldsConfig |
10
10
  | [Column State (URL-driven show/hide, order & pinning)](features/column-visibility.md) | URL-driven "Columns" button + modal (`cols` param) for show/hide + order + pinning of table columns, client-configurable then Surface-driven; open for column-vi |
11
11
  | [Force Logout on Deployment (useDeploymentGuard)](features/force-logout-on-deployment.md) | Force-signs-out every user when a deploy bumps a Core parameter, so session + caches reset to the new build; open when debugging surprise logouts or `useDeploym |
12
+ | [Loading & save-error states (table skeletons, shimmers, Refresh busy pill, save-error toaster)](features/loading-and-error-states.md) | Owning doc for how supply shows loading (table skeleton, sub-section shimmer, Refresh busy pill, sales-order modal skeletons) and failed saves (error toaster, n |
12
13
  | [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page is meta-driven end to end — page meta + table meta + table data, each keyed by slug via toga-blox hooks; pagination-vs-infinite is decided solely by meta |
13
14
  | [Persisted React Query cache (localStorage `supply-chain-query-cache`)](features/persisted-query-cache.md) | `localStorage["supply-chain-query-cache"]` is the whole React Query cache dehydrated to disk (survives reloads AND sessions); the owning doc for staleTime rules |
14
15
  | [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's modal + nested-table patterns over toga-blox `TableRecordModal` / `PrimaryTable*Layout` — six load-bearing UI patterns (row-click record modal, neste |
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: Loading & save-error states (table skeletons, shimmers, Refresh busy pill, save-error toaster)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-06
10
+ owners: ["tcox"]
11
+ files:
12
+ - toga25-supply/src/components/TableSkeleton/TableSkeleton.tsx
13
+ - toga25-supply/src/components/TableSkeleton/useTableGeometryMemory.ts
14
+ - toga25-supply/src/components/SubSectionShimmer/SubSectionShimmer.tsx
15
+ - toga25-supply/src/components/Shimmer/Shimmer.tsx
16
+ - toga25-supply/src/hooks/useIsTableRefreshing.ts
17
+ - toga25-supply/src/layout/PrimaryTableServerLayout/PrimaryTableServerLayout.tsx
18
+ - toga25-supply/src/layout/PrimaryTableClientLayout/PrimaryTableClientLayout.tsx
19
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/recordShapeMemory.ts
20
+ - toga25-supply/src/utils/saveErrorToaster.ts
21
+ related:
22
+ - ./record-modals-and-nested-tables.md
23
+ - ./transfer-orders-page.md
24
+ - ./persisted-query-cache.md
25
+ - ../workflows/cypress-testing.md
26
+ ---
27
+
28
+ Owning doc for how supply shows loading (table skeleton, sub-section shimmer, Refresh busy pill, sales-order modal skeletons) and failed saves (error toaster, never a modal banner); open before touching any loading/error UI or reviving the old TRUE-79502/79505 branches.
29
+
30
+ ## Summary
31
+
32
+ - **Cold table load** → `TableSkeleton` (real header chrome + remembered column widths, no column shift).
33
+ - **Background refetch** (save, focus, Refresh on warm data) → rows stay; Refresh button shows a busy pill via `useIsTableRefreshing`.
34
+ - **Record modal** (Sales Orders, Transfer Orders) → cold / warm / sub-section skeletons, shaped by remembered record shape.
35
+ - **Failed save/create/edit** → error toaster with `SAVE_ERROR_TOASTER_MESSAGE`. The red `ModalBanner` "error" variant is **retired** (decided 2026-08-25).
36
+
37
+ ## How it works
38
+
39
+ ### Table skeleton gate (`PrimaryTableServerLayout` / `PrimaryTableClientLayout`)
40
+ - Render order: full-body error (`TableStateBlock`) → skeleton → empty/no-results → table.
41
+ - `showSkeleton = isLoadingTableDataLoading || !displayRecords`. Never gate on `isFetching` — `useTableData` keeps prior rows via `placeholderData`, and gating on `isFetching` flashes the skeleton on every modal save / focus refetch. Page Refresh resets the table queries, so it still re-enters the skeleton.
42
+ - `useTableGeometryMemory(slug, …)` stores header fields + pixel widths in `localStorage["toga-table-chrome:<slug>"]` so the skeleton draws the real header before the meta fetch returns. Live fields are label-less until page meta merges labels — skeleton holds the remembered fields until then.
43
+
44
+ ### Refresh busy pill — `useIsTableRefreshing()`
45
+ True while a `["table-data"]` query **that already has data** refetches. Excludes cold loads (skeleton covers them) and infinite-scroll `fetchMore` (scrolling is not a refresh).
46
+
47
+ ### Sales-order modal skeletons — `recordShapeMemory.ts`
48
+ Per-record shape (`itemCount`, `hasInvoices`, `hasShipments`) in `sessionStorage["toga-so-shape:<uuid>"]`, merge-saved as each query settles. The skeleton only draws frames for sections that will exist — no section pops in then vanishes. `SubSectionShimmer` / `Shimmer` give the shared texture.
49
+
50
+ ### Save-error toaster — `src/utils/saveErrorToaster.ts`
51
+ Item, vendor-item, bundle and bundle-item create/edit hooks call `useToaster().addToaster({ status: "error", ...SAVE_ERROR_TOASTER_MESSAGE })`. Raw error goes to the console, never to the user. New save hooks must use the same constant.
52
+
53
+ ### Visual review — temporary dev gallery
54
+ To eyeball every state at once: restore the old gallery with `git show TRUE-79502:src/pages/DevLoadingStates/DevLoadingStatesPage.tsx`, register it in `src/routes.tsx` **inside `AllowedRoutes`** behind `import.meta.env.DEV` at `/dev/loading-states` (needs login). Newer blox `TablePagination` needs `recordsPerPage` inside `paginationMeta` — the old page must be patched. **Delete it before merge**; it is not shipped.
55
+
56
+ ## Gotchas
57
+ - **⚠ blox `useFetchTablePageMeta` has no `isError`.** A failed meta fetch leaves the table on its skeleton forever. Open follow-up.
58
+ - **⚠ `isPageFetch` path in `PrimaryTableServerLayout` is dead code** (never true in practice). Open follow-up — do not build on it.
59
+ - **⚠ Do not merge `_production` into the stale TRUE-79502 / TRUE-79505 branches** — it drops newer prod features. Cut a fresh branch from `_production` and port pieces by hand (done as TRUE-82385).
60
+ - Old-branch pieces deliberately **not** ported: `ModalBanner` error variant + `DevErrorStates` page (TRUE-79505), `BulkProgressToast` (no consumers), `DevLoadingStatesPage` (demo), `SalesOrderItemsTableLayout` (deleted in `_production` 97d20ff). TRUE-79502-update's `useClientFields` / Prudential CSV-BOM edits are unrelated to loading. TRUE-79505-updated, TRUE-79505-prod and TRUE-79501 are already fully in `_production`. TRUE-79506 never existed.
61
+ - A disabled React Query v5 query reports `pending` forever — see [transfer-orders-page](./transfer-orders-page.md) before deriving a skeleton flag from one.
62
+
63
+ ## Change history
64
+ - 2026-10-06 — Ported loading states (TRUE-79502-update) + save-error toaster onto fresh `_production` as TRUE-82385; Transfer Orders wired; Cypress expectations updated. (tcox)
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-10-05
9
+ updated: 2026-10-06
10
10
  owners: [jcardinal, dfranks, mhammontree, tcox, bala, ajean, rgirish]
11
11
  files:
12
12
  - worker2/Worker/
@@ -372,6 +372,12 @@ Debugging consequence: **no row means either "the caller never queued it" or "th
372
372
  and never consumed."** Those have completely different fixes. Distinguish them with the **SQS queue
373
373
  metrics**, not the database.
374
374
 
375
+ A third case: **SQS returned 200 + MessageId, the tier is healthy, and still no row.** A failed first
376
+ delivery stays hidden for the **3600s visibility timeout**, then is redelivered. Seen 2026-10-05 on a
377
+ hand-sent direct-payload message (resend 15 min later ran). So a successful send is not proof the job
378
+ runs soon — check `WorkerJobs` by `uuid`, and if absent, resend (the `uuid` dedupe stops a double run
379
+ only if you reuse the same `uuid`).
380
+
375
381
  #### Telling "the tier stopped consuming" apart WITHOUT SQS metrics
376
382
 
377
383
  There is a database-side tell, because cron rows keep arriving from the scheduler even while nothing
@@ -533,6 +539,7 @@ Rules: rename **both sides in one PR and one deploy**; grep every repo for the o
533
539
  commit-before-SQS transaction pattern that the worker relies on.
534
540
 
535
541
  ## Change history
542
+ - 2026-10-06 — Absent-row section: a 200 + MessageId send can still produce no row for up to 1h (failed first delivery hidden by the visibility timeout). (ajean)
536
543
  - 2026-10-05 — Rule: `runTask` parameters are ids/uuids only; worker reads with bounded wait; producer catch must `_Error::captureException`. (jcardinal)
537
544
  - 2026-10-05 — Added read-host read-after-write gotcha for worker actions (TRUE-82185) (mhammontree)
538
545
  - 2026-09-17 — Added the **half-written `WorkerJobs` row** tell: a row with `dtStarted` but no
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-16
9
+ updated: 2026-10-06
10
10
  owners: ["ajean"]
11
11
  files:
12
12
  - worker2/Worker/Netsuite/Opportunity.php
@@ -17,6 +17,7 @@ files:
17
17
  - tools/mvc/clickup/aliases/get.php
18
18
  - tools/mvc/clickup/aliases/post.php
19
19
  - tools/_/app/clickup/aliases.php
20
+ - tools/_/app/clickup/aliases/jobs.php
20
21
  related:
21
22
  - ./netsuite-opportunity-sync.md
22
23
  - ./creating-worker-actions.md
@@ -129,7 +130,7 @@ Existing tasks are never reached by the sync (see the claim gotcha below), so a
129
130
 
130
131
  ### Operational baseline (what a real pass looks like)
131
132
 
132
- ~769–771 task-bearing opportunities → **1,542 field slots** (2 per opportunity). Three production passes:
133
+ ~769–837 task-bearing opportunities → 2 field slots per opportunity. Production passes:
133
134
 
134
135
  | Pass | Date | Chunks / wall time | Filled | Notes |
135
136
  |---|---|---|---|---|
@@ -137,6 +138,7 @@ Existing tasks are never reached by the sync (see the claim gotcha below), so a
137
138
  | 2 | 2026-08-31 | 5 chunks, 15m | 39 | after 21 new ClickUp options; 1,143 already set |
138
139
  | 3 | 2026-08-31 | 6 chunks, ~17m | 191 | after 100 more options; unmatched 194 → 2, coverage **89.0%** |
139
140
  | 4 | 2026-09-02 | 5 chunks | 26 | after the End Customer re-import; 0 unmatched, 0 fuzzy, coverage **100%** (1,552 of 1,552 slots at 776 opportunities) |
141
+ | 5 | 2026-10-05 | 6 chunks, ~15m | 45 | first run from the Tools buttons; options refreshed (Stakeholders 365→370, End Customer 363→368), 10 aliases assigned; 837 examined, 1,629 already set, 0 unmatched / 0 fuzzy / 0 failed |
140
142
 
141
143
  Every pass: **0 failures**, and totals always reconcile to `examined × 2`.
142
144
 
@@ -146,7 +148,11 @@ Every pass: **0 failures**, and totals always reconcile to `examined × 2`.
146
148
 
147
149
  ### Operating the queue (things you will need and cannot guess)
148
150
 
149
- **The Tools alias dropdown reads worker2's MIRROR, not live ClickUp.** Options you just created in ClickUp are **invisible** on `/clickup/aliases` until worker2 republishes `Team.ClickUpLabelOptions` (`publishLabelOptions()`), and **saving is disabled entirely** while the mirror is empty. **Any digest run refreshes it in ~2 seconds, including `dryRun`** — on the digest, `$dryRun` means "do not send the email", *not* "do not write". This was the fix when 19 newly-added options could not be seen. Details in [Tools ClickUp label-alias admin](../../../../1.0/apps/tools/features/clickup-label-aliases-admin.md).
151
+ **Run it from the Tools page, not by hand.** `/clickup/aliases` has a *Sync with ClickUp* card: **Refresh options** (digest `dryRun`) and **Run backfill** (dry run default, `exactOnly` always true, live = `Development Team` only). Normal round: refresh options → assign aliases → backfill dry run → live → check dry == live. Details in [Tools ClickUp label-alias admin](../../../../1.0/apps/tools/features/clickup-label-aliases-admin.md).
152
+
153
+ **The Tools alias dropdown reads worker2's MIRROR, not live ClickUp.** New ClickUp options are **invisible** there until worker2 republishes `Team.ClickUpLabelOptions` (`publishLabelOptions()`); saving is disabled while the mirror is empty. Any digest run refreshes it, including `dryRun` — on the digest, `$dryRun` means "do not send the email", *not* "do not write".
154
+
155
+ **If you DO hand-send an SQS message, a 200 + MessageId is not proof it will run soon.** 2026-10-05: a direct-payload `{uuid, action, parameters}` for the live backfill got 200 + MessageId and never made a `WorkerJobs` row; a resend 15 min later worked. Check `WorkerJobs` by `uuid` before assuming it runs — see [creating-worker-actions](./creating-worker-actions.md#an-absent-workerjobs-row-is-ambiguous--dont-read-it-as-never-queued).
150
156
 
151
157
  **Seed the alias queue by hand with `recordUnresolvedAliases()`.** Callable directly as a worker action, which is how to surface names the digest's fuzzy blind spot hides (see the gotcha below — still unfixed):
152
158
 
@@ -172,6 +178,7 @@ A one-shot rem+add corrector was scoped and deliberately **not** built.
172
178
  - **⚠ THE FUZZY BLIND SPOT — fuzzy-above-floor names are invisible to the digest AND refused by the backfill, so those gaps have no work queue and never fill.** The digest's `collectUnmatched()` skips anything with a non-null option id (`if ($outcome['id'] !== null || !in_array($outcome['reason'], ACTIONABLE_REASONS)) continue;`) — and a **fuzzy hit HAS an id**, so a fuzzy name never reaches the PM email and therefore never gets written to `Team.ClickUpLabelAliases`. Meanwhile the backfill at `exactOnly:true` **refuses** fuzzy. Net effect measured in production: the digest reported **3 outstanding issues while 169 field slots were actually empty** — 167 of them fuzzy skips. Coverage plateaus around 89% and nothing surfaces the gap. Consequence for whoever is clearing the queue: **assigning every alias in the digest does not clear the fuzzy set**, because those names were never queued — verified that `Lee County Sheriff's Office`, `Polk County Sheriff's Office` and `Plant City Police` are all absent from `ClickUpLabelAliases`. **Suggested fix (not implemented):** the digest should report fuzzy-above-floor as its own actionable reason.
173
179
  - **Adding ClickUp options creates NEW fuzzy near-misses — the fuzzy count can RISE as gaps are filled.** Adding ~121 options moved names out of *unmatched*, but some landed in *fuzzy, skipped* rather than *filled*: a customer that previously had **no** candidate now has one scoring 85–95%. Observed unmatched **231 → 194 → 2** while fuzzy went **166 → 166 → 167**. So "add more options" is not a complete remedy; past a point the remaining work is **aliases, not options**.
174
180
  - **An alias is Pass 0 — ahead of exact AND fuzzy — so a WRONG alias lands on tasks, and seed-if-empty makes it STICKY.** `resolveLabelOption()` (`Opportunity.php` ~L1298) returns confidence `alias` and deliberately bypasses `exactOnly`, which is correct (a human judgement outranks machine guessing) but means a mis-entered alias **will** be written. And because the backfill only fills **empty** fields, no later run corrects it — someone must clear the field in ClickUp by hand. **Operational check worth repeating before every live pass:** audit `ClickUpLabelAliases` for mappings whose **first word differs** from the source name. Doing that on 338 hand-entered aliases surfaced `Hewlett Packard Enterprise → HP` sitting alongside `Hewlett-Packard Enterprise-Health and Life Sciences Team → HPE` — HP Inc. and HPE have been separate companies since 2015.
181
+ - **A "nothing to report" digest does NOT mean the alias queue is empty.** Unresolved rows can remain in `Team.ClickUpLabelAliases` (e.g. names that now match an option). Check the Tools *Needs an alias* list.
175
182
  - **The run report lists only the first 50 fuzzy entries.** With 167 fuzzy skips, the other 117 are unreviewable *anywhere* — a real reporting gap, and the reason the production fuzzy set had to be counted by query rather than read off the report.
176
183
  - **`splitCustomerRefName()` must handle composite sub-customer numbers.** The original pattern `/^(\d+)\s+(.*)$/` (digits then whitespace) could not parse `"6096:6308 Compass Group"` — the colon broke the match, the number stayed in the name, and normalization produced `"60966308compassgroup"`, which can never match a ClickUp option. **15.4% of `Forecast.Customers` use this composite form** (1,418 of 9,211) and they are precisely the hierarchical customers this feature exists to distinguish. Now `/^(\d+(?::\d+)*)\s+(.*)$/` — still requiring the whitespace so `"3M Company"` is not decapitated.
177
184
  - **⚠ `splitCustomerRefName()` strips a NUMERIC prefix only — a NAME hierarchy passes through whole, so AN ALIAS MUST BE KEYED ON THE FULL PATH STRING.** NetSuite sub-customers also arrive as a name path, `" : "`-separated — `"ODP Veyer (B2B) : Compass Group"` — not just the numeric `"6096:6308 Compass Group"` the splitter was written for. The name path survives intact, normalizes to `"odp veyer b2b compass group"`, and can **never** match an option. Consequence when you queue an alias: the full path is the needle `resolveLabelOption()` receives, so an alias entered as just `"Compass Group"` **never fires**. **17 of the 30 populated end-customer rows are this shape.**
@@ -190,3 +197,6 @@ A one-shot rem+add corrector was scoped and deliberately **not** built.
190
197
  - [NetSuite supporting-record backfill worker](./netsuite-supporting-record-backfill-worker.md).
191
198
  - [Tools ClickUp label-alias admin](../../../../1.0/apps/tools/features/clickup-label-aliases-admin.md) — the human alias queue.
192
199
  - [SuiteQL / REST API reference](../../../../1.0/apps/library/features/netsuite-suiteql-api-reference.md) — the List/Record `BUILTIN.DF` distinction.
200
+
201
+ ## Change history
202
+ - 2026-10-06 — Tools Sync card buttons replace hand-queuing; added pass 5 (45 filled, dry == live); hand-sent SQS 200 is not proof of a run; empty digest ≠ empty alias queue. (ajean)
@@ -33,7 +33,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
33
33
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
34
34
  - **ai-bdr** (AI-BDR) — 13 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
35
35
  - **toga2-commerce** (TOGa Commerce) — 29 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
36
- - **toga25-supply** (TOGa 2.5 Supply) — 22 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
36
+ - **toga25-supply** (TOGa 2.5 Supply) — 23 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
37
37
  - **toga25-iq** (TOGa IQ) — 1 doc(s) → [2.0/apps/toga25-iq/INDEX.md](2.0/apps/toga25-iq/INDEX.md)
38
38
  - **toga25-desk** (TOGa Desk 2.5) — 1 doc(s) → [2.0/apps/toga25-desk/INDEX.md](2.0/apps/toga25-desk/INDEX.md)
39
39
  - **toga25-blox** (TOGa Blox 2.5) — 1 doc(s) → [2.0/apps/toga25-blox/INDEX.md](2.0/apps/toga25-blox/INDEX.md)
@@ -42,7 +42,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
42
42
 
43
43
  ## standalone framework
44
44
 
45
- - **togatech** (TOGA Technology Website) — 7 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
45
+ - **togatech** (TOGA Technology Website) — 8 doc(s) → [standalone/apps/togatech/INDEX.md](standalone/apps/togatech/INDEX.md)
46
46
  - **websocket** (WebSocket Server) — 2 doc(s) → [standalone/apps/websocket/INDEX.md](standalone/apps/websocket/INDEX.md)
47
47
  - **forward** (Forwarder) — 3 doc(s) → [standalone/apps/forward/INDEX.md](standalone/apps/forward/INDEX.md)
48
48
  - **claude** (Claude Harness) — 8 doc(s) → [standalone/apps/claude/INDEX.md](standalone/apps/claude/INDEX.md)
@@ -4,7 +4,8 @@
4
4
  |-----|---------|
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
+ | [NavV2 Global Nav — menus, scroll reveal, Talos FAB](features/navv2-global-nav.md) | togatech NavV2 header (designer "phase one"): data-driven menus, `data-nav-reveal` scroll hide/show, opt-in `data-nav-pinned` for sticky scenes, Talos FAB rules |
7
8
  | [SEO / AEO / GEO — Prerendering, Single-Source Meta & Structured Data](features/seo-aeo-geo-prerender.md) | togatech SEO/AEO/GEO — build-time Puppeteer prerender + single-source per-page meta + JSON-LD; open for the robots-gating de-index bug and Amplify build require |
8
- | [Talos Product Page (/talos) — ported designer prototype](features/talos-product-page.md) | togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS prototype; open for the design-port CSS approach, lazy-load + prerender rules, page |
9
+ | [Talos Product Page (/talos) — ported designer prototype](features/talos-product-page.md) | togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS prototype; open for the design-port CSS approach, re-porting a new design drop, laz |
9
10
  | [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`. |
10
11
  | [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. |
@@ -0,0 +1,68 @@
1
+ ---
2
+ title: NavV2 Global Nav — menus, scroll reveal, Talos FAB
3
+ framework: "standalone"
4
+ repo: togatech
5
+ project: TOGA Technology Website
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-10-05
10
+ owners: ["tcox"]
11
+ files:
12
+ - togatech/src/components/NavV2/view/NavV2.tsx
13
+ - togatech/src/components/NavV2/view/TalosFab.tsx
14
+ - togatech/src/components/NavV2/view/NavDestLink.tsx
15
+ - togatech/src/components/NavV2/view/panels/WhatPanel.tsx
16
+ - togatech/src/components/NavV2/view/panels/CompanyPanel.tsx
17
+ - togatech/src/components/NavV2/view/panels/PanelFooter.tsx
18
+ - togatech/src/components/NavV2/navV2.module.css
19
+ - togatech/src/components/NavV2/types.ts
20
+ - togatech/src/components/NavV2/navRoutes.ts
21
+ - togatech/src/components/NavV2/viewModel/useNavV2.ts
22
+ - togatech/src/components/NavV2/viewModel/useNavReveal.ts
23
+ - togatech/src/components/NavV2/viewModel/useCompactViewport.ts
24
+ - togatech/src/components/NavV2/viewModel/useRovingTabs.ts
25
+ - togatech/src/components/NavV2/viewModel/useDockedState.ts
26
+ - togatech/src/components/NavV2/viewModel/FIELDS/NAVFIELDS.json
27
+ - togatech/src/components/MobileNav/MobileNav.tsx
28
+ - togatech/src/styles/index.css
29
+ related:
30
+ - ../architecture.md
31
+ - talos-product-page.md
32
+ ---
33
+
34
+ togatech NavV2 header (designer "phase one"): data-driven menus, `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.
35
+
36
+ ## Summary
37
+
38
+ Global header in `src/components/NavV2/` (MVVM: `view/`, `viewModel/`, menu data in `viewModel/FIELDS/NAVFIELDS.json`). Mobile is `components/MobileNav/MobileNav.tsx`. Destinations come from `navRoutes.ts` (`LIVE_ROUTES` / `STUB_ROUTES`) — see [architecture.md](../architecture.md) Routing.
39
+
40
+ ## How it works
41
+
42
+ **Menus (data-driven):**
43
+ - Two menus shown: What we do, Company. Industries / Resources are hidden in `NAVFIELDS.json`; their panel code is kept for a later phase — re-enable via data, do not rebuild.
44
+ - Flat links are `flatLinks[]` (was single `flatLink`): The TOGA Blog → `/blog` stub, Contact.
45
+ - What we do has 2 tabs (Business Units, Platforms). Tab hover switches the tab but never moves keyboard focus (`useRovingTabs`).
46
+ - Company panel and What-we-do share `panels/PanelFooter.tsx`.
47
+
48
+ **Scroll reveal (`useNavReveal.ts`):** sets `data-nav-reveal = top | hidden | shown` on the **nav root**, not `<html>`.
49
+ - `top` within 80px of page top. Hides after 8px scroll down; shows after 8px scroll up.
50
+ - Never hides while a menu is open, the pointer is over the bar, or keyboard focus is inside (`focusin` forces `shown`; a mouse click does not pin it).
51
+ - Initial render is always `top` (prerender gives the same output every time); the real state is computed on mount.
52
+ - Glass look (blur / grain / edge) only in `shown`.
53
+ - `useCompactViewport.ts`: compact padding when viewport height ≤ 1000px.
54
+
55
+ **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.
56
+
57
+ **Section anchors:** `src/styles/index.css` has `:where(section[id]:not(#top)) { scroll-margin-top: 104px }` (zero specificity — a page rule overrides it freely).
58
+
59
+ **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.
60
+
61
+ ## Gotchas
62
+
63
+ - **Design anchors usually do not exist on our pages.** Use a design link anchor (`#apps`, `#solutions`, `#culture`, `#mission`, `#team`, `#why-us`, …) only if that id exists on the target page; otherwise keep the `navRoutes.ts` route / `/company/*` stub. None of the phase-one anchors exist today.
64
+ - Between ~88px scroll and one screen, neither bar nor FAB shows. Matches the design; open question with tcox — do not "fix" without asking.
65
+ - `/ai-solutions` stub is no longer linked from the nav.
66
+
67
+ ## Change history
68
+ - 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)
@@ -6,13 +6,18 @@ project: TOGA Technology Website
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-10-05
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/useGlassPanes.ts
16
+ - togatech/src/pages/Talos/viewModel/useTalosPanelSky.ts
15
17
  - togatech/src/pages/Talos/view/shared/Reveal.tsx
18
+ - togatech/src/pages/Talos/view/sections/TalosHero
19
+ - togatech/src/pages/Talos/view/sections/TalosVoice
20
+ - togatech/src/pages/Talos/view/sections/TalosGovernance
16
21
  - togatech/src/routes.tsx
17
22
  - togatech/src/components/templates/AppLayout/AppLayout.tsx
18
23
  - togatech/src/components/templates/AppLayout/appLayoutContext.ts
@@ -20,32 +25,48 @@ files:
20
25
  related:
21
26
  - ../architecture.md
22
27
  - seo-aeo-geo-prerender.md
28
+ - navv2-global-nav.md
23
29
  ---
24
30
 
25
- togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS prototype; open for the design-port CSS approach, lazy-load + prerender rules, page-owned footer, and opening the Talos chat from a page.
31
+ togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS prototype; open for the design-port CSS approach, re-porting a new design drop, lazy-load + prerender rules, page-owned footer, opening the Talos chat from a page.
26
32
 
27
33
  ## Summary
28
34
 
29
- `/talos` is a 15-section product page (`src/pages/Talos/`) ported from a designer prototype (static HTML + in-browser JSX + CSS; the drop-in at `togatech/_design/talos/` is temporary and never committed). Standard MVVM page layout: `view/sections/<Section>/` (incl. `TalosSparks` canvas backdrop and `TalosFooter`), `view/shared/Reveal.tsx`, `viewModel/use*.ts` hooks, copy in `viewModel/FIELDS/*.json`, images in `public/assets/talos/`. Live route, prerendered, in the sitemap list.
35
+ `/talos` is a 15-section product page (`src/pages/Talos/`) ported from a designer prototype (static HTML + in-browser JSX + CSS). Design drops are temporary and never committed — latest at `togatech/project_html/uploads/New TOGA WEBSITE/TOGA Website/` (untracked; first drop was `_design/talos/`). Standard MVVM page layout: `view/sections/<Section>/` (incl. `TalosSparks` canvas backdrop and `TalosFooter`), `view/shared/Reveal.tsx`, `viewModel/use*.ts` hooks, copy in `viewModel/FIELDS/*.json`, images in `public/assets/talos/`. Live route, prerendered, in the sitemap list.
30
36
 
31
37
  ## How it works
32
38
 
33
39
  **CSS port (the reusable approach for any designer prototype):**
34
- - All design CSS is concatenated **in original cascade order** into one plain file, `talos.css`, every selector prefixed `.talosPage `. `:root` / `html` / `body` selectors map to `.talosPage`. Generated by a one-off postcss prefix script (not in `scripts/`) — regenerate the same way if the design CSS changes.
40
+ - 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`.
41
+ - 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.
42
+ - 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).
35
43
  - **Not a CSS module:** the design JS finds elements by raw class names/ids and sets keyframe names inline; CSS-module hashing breaks all of that. (CTO-reviewed choice.)
36
44
  - Design timing workarounds are kept on purpose: IntersectionObserver + rect-poll backstops and `setTimeout` retry chains (rAF / ResizeObserver can go silent on cold load). Animation loops pause off-screen and on hidden tab.
37
45
  - `<html>`-level styles (`overflow-x: clip` so `position: sticky` works — never `hidden`; `scroll-behavior: smooth`) are set in `useLayoutEffect` and **removed** on unmount, not "restored" (a prerendered snapshot already carries them, so the "previous" value is wrong).
38
46
 
39
- **Code-split + prerender:** `routes.tsx` wraps the page in `React.lazy` + `<Suspense fallback={null}>` inside the route `Component`. Rules for this live in [seo-aeo-geo-prerender.md](seo-aeo-geo-prerender.md) (router `lazy` vs `React.lazy`, no DOM appends into React-owned nodes, `TOGAPrerender` UA skip for canvas engines).
47
+ **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
48
 
41
49
  **Layout hooks this page uses:**
42
- - **Opening the Talos chat:** AppLayout owns the global Talos chat modal and passes `<Outlet context={{ openTalosChat }} />`. Pages call `useAppLayoutContext().openTalosChat()` (`AppLayout/appLayoutContext.ts`). Use this — do not mount a second chat.
43
- - **Own footer:** `ROUTES_WITH_OWN_FOOTER` in `AppLayout.tsx` (only `/talos` today, tcox's call) hides the old site footer (with contact form); the page renders `TalosFooter`. The old footer stays on all other pages until the redesigned footer goes site-wide — then retire the list.
44
- - **NavV2 floating Talos button** (`TalosFab.tsx`) is a `<button>` that opens the chat (same as the bar pill), not a `Link` to `/talos`; unfocusable while hidden. Styled from the design's `.talos-persist.talos-fab` (light-trail ring, icon-only, 44px, rises 20px on entrance).
50
+ - **Opening the Talos chat:** AppLayout owns the global Talos chat modal and passes `<Outlet context={{ openTalosChat }} />`. Pages call `useAppLayoutContext().openTalosChat()` (`AppLayout/appLayoutContext.ts`). Use this — do not mount a second chat. The Hero's second button ("Ask Talos", `btn-talos-boxed is-lg is-hero`) uses it; there is no expanded chat mode yet.
51
+ - **Own footer:** `ROUTES_WITH_OWN_FOOTER` in `AppLayout.tsx` (only `/talos` today, tcox's call) hides the old site footer; the page renders `TalosFooter`. Retire the list when the redesigned footer goes site-wide.
52
+ - **Floating Talos button:** NavV2's `TalosFab` — see [navv2-global-nav.md](navv2-global-nav.md).
53
+
54
+ **Section notes:** Voice sits at the end of `.tpanel2` as a `.tproof-*` stat + quote row (moved out of `.tpanel`). Governance `RAIL_COLUMNS = points.length` (7 points). PanelSky has no `.tp2-tw` twinkle; it fades over 360px to `PAGE_FILL`.
45
55
 
46
56
  **Routing:** `/talos` is in `LIVE_ROUTES` (`NavV2/navRoutes.ts`) and `src/routes.config.json`. Sub-pages `/talos/one|flow|engage` are still `STUB_ROUTES`.
47
57
 
48
- **Copy:** uses the design's "Katie" copy set (Katie signs off copy). Still placeholder: FAQ answers (`[Answer not written. Needs Jeff.]`), the Voice section quote, and the demo stage (no video).
58
+ **Copy:** the design's default `COPY.katie` set (Katie signs off copy). Still placeholder: FAQ answers and the demo stage (no video).
59
+
60
+ ## Port status (design drop 2, 2026-10)
61
+
62
+ Done: batches 1–2 (CSS regen, copy, Hero ask, Voice move, Governance, PanelSky). Left:
63
+ 1. Compare rebuild — half-stars, info popover, key.
64
+ 2. FAQ two-column, 9 items, per-item ask. Then **delete the temporary bridge rules** under `#talos-faq:not(:has(.tfq-cols))` in `talos.css`.
65
+ 3. ProductLine "What can X do?" ask buttons + `useTalosSweep`.
66
+ 4. Re-sync `talosSparksRoute.ts` — the sparks line loops wrong around the moved Voice section.
67
+ 5. `TalosOrbLauncher` / `talos-orb.js` replacing the FAB on `/talos`, `.tfab-label`, TalosGuide bubbles, chat API `open(q, {expanded})` / `close()`.
68
+
69
+ Design's `HeroWeave` / `HeroDots` / old Hero are dead code — do not port.
49
70
 
50
71
  ## Gotchas
51
72
 
@@ -55,4 +76,5 @@ togatech `/talos` Talos AI product page, ported from a designer HTML/JSX/CSS pro
55
76
  - Tailwind preflight is global and can differ from the design's browser defaults — check margins/list/button resets when a section looks off.
56
77
 
57
78
  ## Change history
79
+ - 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)
58
80
  - 2026-09-25 — Built /talos from the designer prototype; added Outlet context `openTalosChat`, `ROUTES_WITH_OWN_FOOTER`, TalosFab now opens chat (tcox)
@@ -6,7 +6,7 @@ project: TOGA Technology Website
6
6
  client: shared
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-09-25
9
+ updated: 2026-10-05
10
10
  owners: ["tcox"]
11
11
  files:
12
12
  - togatech/package.json
@@ -36,7 +36,8 @@ The gotcha-avoidance path for getting `togatech` up on a dev machine and actuall
36
36
  - A Vite "imported but could not be resolved" error almost always means **stale install**, not a missing package — check `package.json` before adding a dependency.
37
37
  - `npm run lint` is effectively dead until eslint and an `eslint.config.*` are added to the repo; do not treat a lint failure-to-start as a code problem.
38
38
  - Cypress specs are still the source of truth for e2e coverage — the Puppeteer route is a *local verification* workaround, not a replacement. Don't delete or rewrite specs because Cypress won't launch locally.
39
- - `robotsPlugin` writes `robots.txt` to `./dist` even when you build with `--outDir <other>` — a scratch build still touches `dist/`.
39
+ - `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.
40
+ - 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.
40
41
  - Watching the network tab is not proof a fetch is broken: see step 3 (persisted query cache).
41
42
 
42
43
  ## Related
@@ -45,4 +46,5 @@ The gotcha-avoidance path for getting `togatech` up on a dev machine and actuall
45
46
  - [Careers Section — Live Paylocity Job Openings](../features/careers-paylocity-job-openings.md)
46
47
 
47
48
  ## Change history
49
+ - 2026-10-05 — Prod build sends GA hits from localhost; robotsPlugin closeBundle fails without dist/ (tcox)
48
50
  - 2026-09-25 — No eslint config either; `tsc -p tsconfig.app.json` path-filter tip; robotsPlugin ignores --outDir (tcox)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.954",
3
+ "version": "1.0.956",
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",