toga-ai 1.0.656 → 1.0.658

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.
@@ -3,6 +3,7 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [Database Changes (dbchanges2) Repository Architecture](architecture.md) | `dbchanges2` is the **schema-migration / SQL change-set repository** for the entire 2.0 platform. | Core/, Client/, Client_<Tenant>/, Logs/, Logs_Client/, _modules/ |
6
+ | [Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)](features/rerunnable-additive-inserts.md) | Most `dbchanges2` files are **additive data grants** run by hand against production, often more than once (once per environment, or twice because someone was no | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
6
7
  | [Surface Layer Schema (UI presentation/config tables)](features/surface-layer-schema.md) | The persistent schema for the platform-wide **Surface** UI presentation/configuration layer (see the `_underscore` [surface-resolver](../../_underscore/features | dbchanges2/Core/2026-08-24b - SalesOrderStatusFilterSurfaceSeed.sql, dbchanges2/Client_Compass/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_CompassCanada/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Quad/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Client_Nychh/2026-08-24b - SalesOrderStatusFilterHides.sql, dbchanges2/Core/2026-08-21 - SalesOrderDecisionSurfacesReseed.sql, dbchanges2/Core/2026-08-24 - RestoreApproveDenyRowActionsVisibility.sql, dbchanges2/Client_Quad/2026-08-24 - ProdPortApprovePoNumberEnabledRule.sql, dbchanges2/Client_Quad/2026-07-21b - ApproveDisabledTooltipPoNumber.sql, dbchanges2/Client_Quad/2026-08-21 - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_CompassCanada/2026-08-24 - ProdPortApprovalsGateAndApproveStepTwoRule.sql, dbchanges2/Client_Nychh/2026-08-24 - FixInventoryGroupingsUnitsTopologyOverrideIds.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client_Compass/2026-06-25d - SalesOrderSurfaceClientSeed.sql, _underscore/Model/Client/ThemeToken.php, toga25-supply/src/themeConfig.json, dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql, dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql, dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql, dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Compass/2026-06-30a - SalesOrderDisplaySectionManagerOverrides.sql, dbchanges2/Client_CompassCanada/2026-06-30a - SalesOrderSurfaceManagerOverrides.sql, dbchanges2/Client_Quad/2026-06-30a - SalesOrderSurfaceClientOverrides.sql, dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql, dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql, dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql, dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql, dbchanges2/Core/2026-07-17h - Update - ClearApprovalsFilterButtonConfig.sql, dbchanges2/Client_Compass/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Compass/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Client_Quad/2026-07-17a - SalesOrderApprovalActionsOverride.sql, dbchanges2/Client_Quad/2026-07-17b - SalesOrderApprovalsFilterButtonOverride.sql, dbchanges2/Core/2026-07-20a - Update - HideAdminNotesSectionByDefault.sql, dbchanges2/Core/2026-07-20b - Update - NotesSectionFieldElements.sql, dbchanges2/Client_Compass/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_Compass/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20a - AdminNotesSectionVisibilityOverride.sql, dbchanges2/Client_CompassCanada/2026-07-20b - NotesSectionFieldsOverride.sql, dbchanges2/Client_Quad/2026-07-20a - NotesSectionFieldsOverride.sql, dbchanges2/Core/2026-07-20c - Update - VendorItemsToggleSurfaceSeed.sql, dbchanges2/Client_Compass/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_Compass/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20c - ItemRecordEditButtonEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20d - ItemRecordVendorItemsEnable.sql, dbchanges2/Core/2026-07-20e - RestoreApproveDenyRowActions.sql, dbchanges2/Client_Compass/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Client_CompassCanada/2026-07-20e - RowActionsApprovalWorkflowAdminEnable.sql, dbchanges2/Core/2026-07-17 - README - RUN ORDER.md, dbchanges2/Client_Compass/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Compass/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_CompassCanada/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_CompassCanada/2026-07-21b - SalesOrderApprovalsGateEnable.sql, dbchanges2/Client_Quad/2026-07-21a - SalesOrderApproveEnabledRuleOverride.sql, dbchanges2/Client_Quad/2026-07-21b - SalesOrderApproveDisabledTooltipTranslation.sql, dbchanges2/Core/2026-07-21a - SalesOrderDecisionSummarySurfaceSeed.sql, dbchanges2/Core/2026-07-21b - SalesOrderDecisionActionSurfaceSeed.sql, dbchanges2/Client_Quad/2026-07-21c - SalesOrderDecisionSummaryOverride.sql, dbchanges2/Client_Compass/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Client_CompassCanada/2026-07-21c - SalesOrderDecisionSummaryTotalConcat.sql, dbchanges2/Core/2026-07-23a - PoNumberDetailFieldValueKey.sql, dbchanges2/Core/2026-07-29a - SalesOrderApprovalDetailsSurfaceSeed.sql, dbchanges2/Core/2026-08-05b - ApprovalDetailsShippingMethodConcatCharge.sql, dbchanges2/Core/2026-08-14a - NoteBadgesVocabularySurface.sql, dbchanges2/Client/2026-08-14a - NoteBadgeThemeTokens.sql, dbchanges2/Client/2026-08-14b - NoteBadgeUserColorFix.sql, dbchanges2/Client_Compass/2026-08-14a - NoteBadgeDelegateThemeTokens.sql, dbchanges2/Client_CompassCanada/2026-08-14a - NoteBadgeDelegateThemeTokens.sql |
7
8
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | > **A local browser wizard now automates this.** Steps 2–9 below (create DBs, generate Core/API > inserts, append to `Clients_Db.txt`) — plus the dbchanges2 bla | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
8
9
  | [Auditing a client DB that drifted from its models (partially applied module migration)](workflows/client-schema-drift-audit.md) | A recurring 2.0 failure mode: **one client's database drifts from what the PHP models declare**, usually because a `_modules/<module>/` migration was applied to | dbchanges2/Client_Growrk/2026-05-28.sql, dbchanges2/Client_Growrk/2026-08-10c - GrowrkServiceRequestCustomFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10d - GrowrkServiceRequestTypeAndDispositionSeeds.sql, dbchanges2/_modules/netsuite/2026-07-10a - UnitInventoryFields.sql, dbchanges2/Client_Growrk/2026-08-10 - GrowrkUnitInventoryFieldsCatchUp.sql, dbchanges2/Client_Growrk/2026-08-10b - GrowrkUnitItemDescriptionAcl.sql, dbchanges2/Client_Growrk/_modules.txt |
@@ -0,0 +1,139 @@
1
+ ---
2
+ title: Re-runnable additive INSERTs (uuid4 in SQL, guards, and the DISTINCT trap)
3
+ framework: "2.0"
4
+ repo: dbchanges2
5
+ project: Database Changes
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-08-26
10
+ owners: [bala]
11
+ files:
12
+ - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
13
+ related:
14
+ - ../architecture.md
15
+ - ../../_underscore/features/acl-permission-chain.md
16
+ - ../../_underscore/features/tracking-number-bridges.md
17
+ - ../../../../clients/compass-usa/workflows/granting-persona-bundle-access.md
18
+ ---
19
+
20
+ ## Summary
21
+
22
+ Most `dbchanges2` files are **additive data grants** run by hand against production, often more than
23
+ once (once per environment, or twice because someone was not sure it took). Two mechanics decide
24
+ whether that is safe: how you generate the `uuid` every 2.0 table demands, and how you guard the
25
+ insert. Both have a trap that fails **silently** rather than erroring.
26
+
27
+ Every 2.0 table carries a `uuid` column that is `char(36) NOT NULL` **with no default** — the model
28
+ layer fills it in application code, so a hand-written `INSERT` must supply it itself.
29
+
30
+ ## How it works
31
+
32
+ ### Generating a real UUID4 inline
33
+
34
+ The standard is "UUID4, random — never MySQL `UUID()`" (`UUID()` is v1: MAC address + timestamp, and
35
+ sequential). MySQL 8's `RANDOM_BYTES()` gives a genuinely random, correctly **version-4-shaped** value
36
+ in one expression:
37
+
38
+ ```sql
39
+ LOWER(CONCAT(HEX(RANDOM_BYTES(4)), '-', HEX(RANDOM_BYTES(2)), '-4', RIGHT(HEX(RANDOM_BYTES(2)), 3), '-', SUBSTRING('89ab', FLOOR(1 + RAND() * 4), 1), RIGHT(HEX(RANDOM_BYTES(2)), 3), '-', HEX(RANDOM_BYTES(6))))
40
+ ```
41
+
42
+ The two literals are what make it v4 rather than just random hex: the `'-4'` pins the version nibble,
43
+ and `SUBSTRING('89ab', …)` picks the variant nibble. Verified working on prod MySQL **8.0.39**.
44
+
45
+ **Existing rows are not the pattern to copy.** Plenty of live rows across these tables are random hex
46
+ that is *not* v4-shaped (older migrations concatenated `RANDOM_BYTES` without the version/variant
47
+ nibbles). They are fine and must not be "fixed" — but do not use them as the template for new SQL.
48
+
49
+ ### ⚠ `DISTINCT` cannot dedupe a row that carries a random uuid
50
+
51
+ This is the silent one. In an `INSERT … SELECT`, putting the uuid expression inside a
52
+ `SELECT DISTINCT` **disables the dedupe entirely** — the per-row random uuid makes every candidate
53
+ row unique, so `DISTINCT` has nothing to collapse and every duplicate id passes through.
54
+
55
+ ```sql
56
+ # WRONG — DISTINCT never removes anything; the uuid differs on every row
57
+ INSERT INTO Personas_Items (uuid, personaId, itemId)
58
+ SELECT DISTINCT
59
+ LOWER(CONCAT(HEX(RANDOM_BYTES(4)), …)),
60
+ @personaId,
61
+ bi.itemId
62
+ FROM BundleItems bi
63
+ INNER JOIN Bundles b ON b.id = bi.bundleId
64
+ WHERE b.number = '243';
65
+
66
+ # CORRECT — dedupe the id set in a derived table, generate the uuid in the OUTER select
67
+ INSERT INTO Personas_Items (uuid, personaId, itemId)
68
+ SELECT
69
+ LOWER(CONCAT(HEX(RANDOM_BYTES(4)), …)),
70
+ @personaId,
71
+ src.itemId
72
+ FROM (
73
+ SELECT DISTINCT bi.itemId
74
+ FROM BundleItems bi
75
+ INNER JOIN Bundles b ON b.id = bi.bundleId
76
+ WHERE
77
+ b.number = '243' AND
78
+ bi.isActive = 1 AND
79
+ bi.itemId IS NOT NULL
80
+ ) src;
81
+ ```
82
+
83
+ The same shape is the fix for the self-referencing `INSERT … NOT EXISTS` error 1093 (MySQL will not
84
+ let you read the table you are inserting into) — wrap the read in a derived table.
85
+
86
+ ### Guarding the insert: know whether the table has a unique key
87
+
88
+ Whether a re-run is loud or silent depends entirely on the target table's indexes, and bridge tables
89
+ in 2.0 are **inconsistent** about this — some have a composite unique key, some do not. Check
90
+ before you write, not after.
91
+
92
+ - **Unique key present** — a re-run errors. Loud, recoverable, but it aborts the rest of the file.
93
+ Add a guard if you want a clean no-op.
94
+ - **No unique key** — a re-run **silently duplicates every row**, and nothing tells you. Always guard
95
+ these.
96
+
97
+ Both guard shapes are in use and equivalent:
98
+
99
+ ```sql
100
+ # LEFT JOIN … IS NULL
101
+ FROM Users u
102
+ INNER JOIN Roles r ON r.name = 'SuperUser'
103
+ LEFT JOIN Users_Roles ur ON ur.userId = u.id AND ur.roleId = r.id
104
+ WHERE
105
+ u.email = '…' AND
106
+ ur.id IS NULL;
107
+ ```
108
+
109
+ **A guarded insert that inserts zero rows is a success, not a wasted file** — see
110
+ [Client-DB grant migrations are re-runnable NO-OPs](../../_underscore/features/acl-permission-chain.md).
111
+
112
+ ### `LAST_INSERT_ID()` into a session variable ties the whole file to one session
113
+
114
+ The common shape — `INSERT` a parent, `SET @parentId = LAST_INSERT_ID();`, then insert children
115
+ against `@parentId` — means **every phase must run in the same connection**. Reconnecting between
116
+ phases leaves `@parentId` `NULL` and the child inserts either fail on the FK or write `NULL`. Say so
117
+ in the file header, and make the unguarded parent insert the one phase you skip on a re-run (set
118
+ `@parentId` to the existing row by hand instead).
119
+
120
+ ## Gotchas
121
+
122
+ - **A "re-run safe" file is usually only re-run safe *per phase*.** If Phase 1 creates the parent row
123
+ with no `NOT EXISTS` guard, re-running the *whole file* creates a **second parent** and then hangs
124
+ a full set of children off it. Re-run safety of the child phases does not make the file idempotent.
125
+ - **`number` columns are strings.** `Bundles.number`, `Personas.number` and friends are varchar, not
126
+ int — quote them (`WHERE b.number = '243'`) and `CAST(… AS UNSIGNED)` when you need numeric sort
127
+ order. An unquoted comparison forces a string→number coercion that will not use the index.
128
+ - **`Client_*` files may not reach into `Core`.** Unrelated to uuids but it bites the same kind of
129
+ file: separate production clusters mean a `Core.` reference is unrunnable in prod even though it
130
+ works locally. Hardcode `Core.Records` / `Core.RecordFields` ids as literals — see
131
+ [dbchanges2 architecture](../architecture.md).
132
+
133
+ ## Change history
134
+ - 2026-08-26 — Initial: recorded the inline **UUID4** expression built on MySQL 8 `RANDOM_BYTES()`
135
+ (version/variant nibbles pinned; verified on prod 8.0.39) and the silent
136
+ **`SELECT DISTINCT` + random-uuid** trap that lets duplicate rows through — dedupe the id set in a
137
+ derived table and generate the uuid in the outer select. Added the unique-key/guard rule for bridge
138
+ tables (no unique key = silent duplicates on re-run) and the `LAST_INSERT_ID()` one-session
139
+ constraint. Extracted from the Compass Creative Studio persona grant. (bala)
@@ -21,7 +21,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
21
21
  - **_underscore** (_Underscore) _(framework core)_ — 67 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
22
22
  - **worker2** (Worker) — 57 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
23
23
  - **api2** (API) — 25 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
24
- - **dbchanges2** (Database Changes) _(framework core)_ — 9 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
24
+ - **dbchanges2** (Database Changes) _(framework core)_ — 11 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
25
25
  - **toga2-supply** (TOGa Supply) — 9 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
26
26
  - **saml** (SAML SSO Gateway) — 4 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
27
27
  - **toga2-view** (TOGa View Frontend) — 12 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
@@ -20,6 +20,7 @@
20
20
  | [Stranded Approval Reassignment (repointing approvals off dead duplicate Compass Users rows)](features/stranded-approval-reassignment.md) | 2.0 | A Compass employee who leaves and comes back after more than `CONTACT_UNLINK_GRACE_DAYS` (5) is **inserted as a brand-new `Users` row** by the PEOPLE importer i | _underscore/Model/Compass/ApprovalDecision.php, worker2/Worker/Client/Compass/ApprovalReassignment.php, worker2/Worker/Client/Compass/PeopleFile.php |
21
21
  | [Compass USA](profile.md) | 2.0 | Compass USA is a TOGA client running a multi-tier supply-chain commerce operation. | |
22
22
  | [Compass Cross-Kit Bundle Corruption — Detection & Repair](workflows/cross-kit-bundle-corruption.md) | 2.0 | A frontend regression in `toga2-commerce`'s edit-order bundle submission mis-attributed bundle (kit) line items and **fees/warranties** to the **wrong kit**, pe | src/api/syncSalesOrderItemsFromLocalStorageCartToApi.ts |
23
+ | [Granting a Compass user access to a bundle (persona grant) and the SuperUser role](workflows/granting-persona-bundle-access.md) | 2.0 | "Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a super user". | dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql |
23
24
  | [Compass Office Depot Duplicate PO-Line Cleanup (dual-catalog SKU)](workflows/odp-duplicate-po-line-cleanup.md) | 1.0 | The NetSuite fulfillment-sales-order importer duplicated Office Depot purchase-order lines on Compass USA because it reconciled the fulfillment SO against the e | library/app/api/toga2.php, dbchanges2/Client_Compass/2026-07-22a - CleanupOfficeDepotDuplicatePurchaseOrderItems.sql |
24
25
  | [Recovering a Lost Compass ODP EDI 850 Import (re-drop from Logs.FileLog)](workflows/odp-edi-import-recovery.md) | 1.0 | How to recover a Compass **Office Depot EDI 850** import that failed partway — the case where cron **3a** created the ODP SalesOrder header, the follow-up item | worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json |
25
26
  | [Compass ODP Order Pipeline to NetSuite (numbered worker crons)](workflows/odp-order-pipeline-to-netsuite.md) | 1.0 | The end-to-end **Compass Office Depot (ODP) order → NetSuite** pipeline as it actually runs through the 1.0 `worker` crons under `worker/crons/toga2/compass/`, | worker/crons/toga2/compass/workflow/1_transmit_compass_sales_orders_to_mits.php, worker/crons/toga2/compass/workflow/2_transmit_mits_purchase_orders_to_vendors.php, worker/crons/toga2/compass/edi/1_download_edi_s3_create_po_toga.php, worker/crons/toga2/compass/workflow/3a_import_office_depot_purchase_orders.php, worker/crons/toga2/compass/workflow/4_transmit_office_depot_po_acknowledgements.php, worker/crons/toga2/compass/workflow/5_create_netsuite_sales_orders_from_office_depot_purchase_orders.php, worker/schedules/cron.worker.sync.json, library/app/client/compass.php |
@@ -6,7 +6,7 @@ project: Worker
6
6
  client: compass-usa
7
7
  type: client-feature
8
8
  status: active
9
- updated: 2026-08-13
9
+ updated: 2026-08-26
10
10
  owners: [bala]
11
11
  files:
12
12
  - worker2/Worker/Client/Compass/PeopleFile.php
@@ -15,6 +15,7 @@ related:
15
15
  - ../profile.md
16
16
  - ../workflows/persona-refactor-migration.md
17
17
  - ../workflows/persona-population-env-comparison.md
18
+ - ../workflows/granting-persona-bundle-access.md
18
19
  - ../../compass-canada/profile.md
19
20
  ---
20
21
 
@@ -40,16 +41,25 @@ below.
40
41
 
41
42
  | Persona | id | Meaning |
42
43
  |---|---|---|
43
- | Base (was "All") | **1** | Printers only, after the refactor. Every US user holds it. |
44
+ | Base (was "All") | **1** | Printers only, after the refactor. Every US user holds it. **Still named "All" in prod** — the refactor is not live there (verified 2026-08-26). |
44
45
  | Levy | **24** | Levy-sector catalogue. |
45
- | Non-Levy | **resolved by name** — 38 on dev-sandbox *and* prod | Everything that is not a printer. Granted to non-Levy users only. |
46
+ | Non-Levy | **resolved by name** — **38 on dev-sandbox only; does not exist in prod** | Everything that is not a printer. Granted to non-Levy users only. |
46
47
  | VIP | 35 | VIP add-on. |
47
48
  | CDL (MacBook / Surface) | 30 | `@compassdigital.io` + VIP users. |
48
49
 
49
- **Never hardcode the Non-Levy id.** `Personas.AUTO_INCREMENT` is 38 on both dev-sandbox and prod
50
- (max existing id 37), so the migration's `number = '40'` yields **id 38**, not 40. The original
51
- plan and the (now closed) worker2 PR both hardcoded **40** and would have written to the wrong
52
- persona. `PeopleFile` resolves it at runtime:
50
+ **Never hardcode the Non-Levy id.** On dev-sandbox `Personas.AUTO_INCREMENT` was 38 (max existing id
51
+ 37), so the migration's `number = '40'` yielded **id 38**, not 40. The original plan and the (now
52
+ closed) worker2 PR both hardcoded **40** and would have written to the wrong persona.
53
+
54
+ > **⚠ Correction (verified read-only against prod, 2026-08-26): "id 38" was never a prod fact, and is
55
+ > now definitively wrong there.** Prod has **no Non-Levy persona**, and `Personas.id` **38** is
56
+ > **"MyDining- KDS"** — prod's auto-increment moved past 38 after 2026-08-06. Persona **number 40** is
57
+ > taken by that same row. Whenever the migration does run in prod the persona will land on some higher
58
+ > id, which is precisely what name-resolution protects against. Current prod number map and the
59
+ > knock-on effect on the migration's Phase 1:
60
+ > [Persona Refactor](../workflows/persona-refactor-migration.md).
61
+
62
+ `PeopleFile` resolves it at runtime:
53
63
 
54
64
  ```php
55
65
  const PERSONA_NAME_US_NON_LEVY = 'Non-Levy';
@@ -128,6 +138,14 @@ VIP (35) and CDL (30) handling is orthogonal and applies to both tenants unchang
128
138
  constants from this file into tickets, docs, or chat transcripts.
129
139
 
130
140
  ## Change history
141
+ - 2026-08-26 — **Correction from a read-only prod check.** The "Non-Levy = id **38** on dev-sandbox
142
+ *and* prod" claim is wrong: prod has **no Non-Levy persona** and id 38 is **"MyDining- KDS"**, so
143
+ prod's auto-increment is past 38 and persona 1 is still named **"All"**. Reinforces resolve-by-name.
144
+ Also linked the new [bundle-grant workflow](../workflows/granting-persona-bundle-access.md), which
145
+ covers the *other* half of the persona model this doc does not: what a persona grant physically
146
+ consists of (`Personas_Bundles` + **`Personas_Items`** + `Personas_Assortments`) and that the
147
+ effective persona set is a UNION of the user's own personas, their location's, and their sector's.
148
+ (bala)
131
149
  - 2026-08-13 — Split the `Users` row lifecycle out of this doc into
132
150
  [PEOPLE-file User Lifecycle](./people-file-user-lifecycle.md) (duplicate accounts from the 5-day
133
151
  lookup window; raw-SQL deactivation that fires no model hook). This doc stays scoped to persona /
@@ -18,7 +18,7 @@ project: _Underscore
18
18
  client: compass-usa
19
19
  type: profile
20
20
  status: active
21
- updated: 2026-08-25
21
+ updated: 2026-08-26
22
22
  owners: [jcardinal, bala, tcox, apeterson, dfranks]
23
23
  files: []
24
24
  related:
@@ -27,6 +27,7 @@ related:
27
27
  - features/people-file-user-lifecycle.md
28
28
  - workflows/persona-refactor-migration.md
29
29
  - workflows/persona-population-env-comparison.md
30
+ - workflows/granting-persona-bundle-access.md
30
31
  - features/mits-sales-order-transmission-alerting.md
31
32
  - features/asn-to-item-fulfillment.md
32
33
  - features/cost-centers.md
@@ -132,8 +133,17 @@ separate, related client (see its own profile).
132
133
  users got the full catalogue). Never trust pre-2026-08 Levy persona state as intentional.
133
134
  - [Persona Refactor — 8-phase migration & deploy order](workflows/persona-refactor-migration.md)
134
135
  (TRUE-75705) — splits persona 1 into Base (printers, everyone) + Non-Levy. **Run on dev-sandbox
135
- only; production is NOT migrated.** worker2 must deploy first; Phase 8 (ACL expression 235) ships
136
- with the toga2-commerce hack removal.
136
+ only; production is NOT migrated** (re-verified 2026-08-26: prod persona 1 is still "All", there is
137
+ no Non-Levy persona, and id 38 is now "MyDining- KDS"). worker2 must deploy first; Phase 8 (ACL
138
+ expression 235) ships with the toga2-commerce hack removal.
139
+ - [Granting a user access to a bundle (persona grant) + SuperUser](workflows/granting-persona-bundle-access.md)
140
+ — the recurring "give user X sight of kit N" request. **Five tables, not one**: a bundle grant
141
+ without `Personas_Items` renders a **half-visible kit**, because item-level ACL hides kit lines
142
+ whose item the persona cannot see. Also: a Compass persona needs **no** settings rows (all nine
143
+ persona-settings tables are empty across all 38 prod personas), "bundle 243" is `Bundles.number`
144
+ not `id`, and **`Users` has no `username` column** — it is `c_hrEmpUsername`, stored lowercase.
145
+ Super user = `Roles.id 5` added **additively** via `Users_Roles`, never replacing Base /
146
+ Compass Base.
137
147
  - [Prod ↔ dev-sandbox persona comparison](workflows/persona-population-env-comparison.md) — the
138
148
  full-population diff technique, the **join-on-email** rule (`Users.id` means different people per
139
149
  environment), and the drift bundles (188/191/242) that mimic a catalogue grant.
@@ -0,0 +1,163 @@
1
+ ---
2
+ title: Granting a Compass user access to a bundle (persona grant) and the SuperUser role
3
+ framework: "2.0"
4
+ repo: dbchanges2
5
+ project: Database Changes
6
+ client: compass-usa
7
+ type: workflow
8
+ status: active
9
+ updated: 2026-08-26
10
+ owners: [bala]
11
+ files:
12
+ - dbchanges2/Client_Compass/2026-08-26a - CompassCreativeStudioPersona.sql
13
+ related:
14
+ - ../features/persona-model-and-levy-gating.md
15
+ - ./persona-refactor-migration.md
16
+ - ./persona-population-env-comparison.md
17
+ - ../profile.md
18
+ - ../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md
19
+ - ../../../2.0/apps/_underscore/features/acl-permission-chain.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ "Give user X sight of kit N" is a **recurring** Compass request, usually paired with "and make them a
25
+ super user". It looks like a one-row insert into `Personas_Bundles`. It is not — a bundle grant
26
+ without the matching **item** grant renders a **half-visible kit**, because item-level ACL hides any
27
+ kit line whose item the persona cannot see.
28
+
29
+ A complete grant touches **five tables**. `dbchanges2/Client_Compass/2026-08-26a -
30
+ CompassCreativeStudioPersona.sql` is the working template: a 6-phase additive script that creates the
31
+ persona "Compass Creative Studio" (number 41), grants bundle 243, its 19 items and the 6 assortments
32
+ those items live in, assigns the persona to one user, and adds SuperUser.
33
+
34
+ All facts below were verified read-only against **production `Client_Compass` on 2026-08-26**.
35
+
36
+ ## The five tables
37
+
38
+ | Table | Grants | Composite unique key? | Optional? |
39
+ |---|---|---|---|
40
+ | `Personas` (`id`, `uuid`, `name`, `number`) | the persona itself | — | no |
41
+ | `Personas_Bundles` (`personaId`, `bundleId`) | the kit appears | **yes** — re-run **errors** | no |
42
+ | `Personas_Items` (`personaId`, `itemId`) | the kit's **lines** are visible | **no** — re-run **silently duplicates** | **no — see below** |
43
+ | `Personas_Assortments` (`personaId`, `assortmentId`) | browsing the accessory categories | **no** — re-run **silently duplicates** | no |
44
+ | `Users_Personas` (`userId`, `personaId`) | the user holds the persona | **yes** — re-run **errors** | no |
45
+
46
+ That asymmetry is the thing to remember: the two bridges that most need a guard are exactly the two
47
+ that will not protect themselves.
48
+
49
+ ### `Personas_Items` is not optional
50
+
51
+ Every `BundleItems.itemId` of the bundle must also be granted to the persona. Concretely: of bundle
52
+ 243's **19** active line items, persona 1 ("All", which the user already held) covered only **2** —
53
+ so a bundle-only grant would have shown the kit with 17 of its lines missing.
54
+
55
+ ### Derive the assortment set — never hardcode it
56
+
57
+ An assortment qualifies when it holds one of the bundle's items **and** the persona that owns the
58
+ bundle today already grants it. Joining it out means the new persona inherits exactly the browse
59
+ scope the current owner has, with no list to maintain:
60
+
61
+ ```sql
62
+ SELECT DISTINCT ai.assortmentId
63
+ FROM Bundles b
64
+ INNER JOIN BundleItems bi ON bi.bundleId = b.id AND bi.isActive = 1
65
+ INNER JOIN AssortmentItems ai ON ai.itemId = bi.itemId
66
+ INNER JOIN Personas_Bundles pbSource ON pbSource.bundleId = b.id AND pbSource.personaId <> @personaId
67
+ INNER JOIN Personas_Assortments pa ON pa.assortmentId = ai.assortmentId AND pa.personaId = pbSource.personaId
68
+ WHERE b.number = '243'
69
+ ```
70
+
71
+ The `personaId <> @personaId` exclusion matters: by the time this phase runs, the **new** persona
72
+ also owns the bundle, and without it the join would feed on itself.
73
+
74
+ For bundle 243 this resolves to assortments **2** (Monitors), **4** (Computer Accessories), **6**
75
+ (Apple Accessories), **8** (Keyboard/Keypad & Mouse), **10** (Cables) and **19** (Computers).
76
+ **An inactive assortment in the set is normal** — 19 is `isActive = 0` and is still granted, because
77
+ the grant mirrors the source persona rather than judging the data.
78
+
79
+ ## A new Compass persona needs NO settings rows
80
+
81
+ Verified across **all 38** personas in prod: `PersonaAppSettings`, `PersonaGlobalSettings`,
82
+ `PersonaPageSettings`, `PersonaRecordSettings`, `PersonaSectionSettings`,
83
+ `PersonaRecordFieldSettings`, `PersonaTranslations`, `Personas_Currencies` and
84
+ `Personas_VendorItems` are **all empty — 0 rows**. A Compass persona is purely **catalogue scoping**
85
+ (bundles / items / assortments). Do not go hunting for supporting configuration; there is none.
86
+
87
+ ## Persona numbers in prod (2026-08-26)
88
+
89
+ **38 personas.** Numbers in use: **1-23, 27-40, and 100** ("QA Full Access", id 37). **The next free
90
+ number is 41.** `Personas.number` is a *string* column, so quote it.
91
+
92
+ ## "Bundle 243" is `Bundles.number`, not `Bundles.id`
93
+
94
+ The business always quotes the **number**. Bundle **243** is `Bundles.id` **171** ("Compass Creative
95
+ Studio MacBook"). Resolve it in the query (`WHERE b.number = '243'`) rather than pasting an id, and
96
+ never assume the two match.
97
+
98
+ ## Finding the user
99
+
100
+ **`Client_Compass.Users` has no `username` column** — the natural first query fails with
101
+ `Unknown column 'username'`. The Compass login / personnel username lives in **`c_hrEmpUsername`**,
102
+ and it is stored **lowercase**: people quote it uppercase ("KINGK01") but the row says `kingk01`.
103
+ Look users up by `email` or `c_hrEmpUsername` (lowercased), which is also how the nightly PEOPLE cron
104
+ matches them — see [PEOPLE-File User Lifecycle](../features/people-file-user-lifecycle.md).
105
+
106
+ ## Making someone a super user
107
+
108
+ There is **no `isSuperUser` flag on `Users`** — it is a role row. `Client_Compass.Roles.id` **5**,
109
+ name **`SuperUser`** (113 holders as of 2026-08-26), granted **additively** through
110
+ `Users_Roles (userId, roleId)`, which is unique on the pair.
111
+
112
+ **SuperUser is never a replacement for the base roles.** The established prod pattern for a Compass
113
+ super user is **Base (1) + SuperUser (5) + Compass Base (7)**, optionally **+ Manager (8)**. Role
114
+ permissions resolve as a **union** (see
115
+ [ACL Permission Chain](../../../2.0/apps/_underscore/features/acl-permission-chain.md)), so adding
116
+ role 5 while removing 1 or 7 takes access *away*.
117
+
118
+ ## Verify the way the ACL actually resolves personas
119
+
120
+ A user's **effective** persona set is three UNIONed sources, not just `Users_Personas`:
121
+
122
+ 1. `Users_Personas` — personas held directly.
123
+ 2. `Locations_Personas` for the user's own `locationId`.
124
+ 3. `Locations_Personas` on the user's **sector** — five `parentLocationId` hops up
125
+ (location → complex → district → region → division → sector).
126
+
127
+ **Check all three before adding anything** — the user may already reach the bundle through their
128
+ location, in which case the right answer is "no change needed". After the grant, re-run the same
129
+ resolution and confirm the bundle number appears; the verification block at the end of the template
130
+ file does exactly this. The same three-source resolution is what the full-population comparison
131
+ tooling uses — see
132
+ [Prod vs dev-sandbox persona comparison](./persona-population-env-comparison.md).
133
+
134
+ ## Gotchas
135
+
136
+ - **⚠ Re-running the whole file creates a SECOND persona.** Phase 1's `INSERT INTO Personas` has no
137
+ guard, so a second run inserts another persona with the same name and number, and the child phases
138
+ then hang a full set of rows off it. The child phases are individually re-run safe; the **file is
139
+ not idempotent**. To repeat any phase, set `@personaId` to the existing persona id by hand and skip
140
+ Phase 1.
141
+ - **`@personaId` is a session variable — run every phase in ONE session.** A reconnect between phases
142
+ leaves it `NULL`.
143
+ - **`SELECT DISTINCT` will not dedupe a row carrying a generated uuid.** The per-row random value
144
+ makes every row distinct. Dedupe the id set in a derived table and generate the uuid in the outer
145
+ select — full explanation and the UUID4 expression in
146
+ [Re-runnable additive INSERTs](../../../2.0/apps/dbchanges2/features/rerunnable-additive-inserts.md).
147
+ - **⚠ Do not write this SQL against the persona-refactor target state.** As of 2026-08-26 the refactor
148
+ has **not** shipped to prod: persona 1 is still named **"All"** (not "Base"), there is **no
149
+ "Non-Levy" persona**, and persona **number 40 belongs to id 38 "MyDining- KDS"**. See
150
+ [Persona Refactor](./persona-refactor-migration.md).
151
+ - **Everything here is additive.** The user keeps persona 1 and their existing roles; nothing is
152
+ removed. A "remove access" request is a different, destructive job and needs its own review.
153
+
154
+ ## Change history
155
+ - 2026-08-26 — Initial: documented the five-table bundle grant (and that **`Personas_Items` is
156
+ mandatory** — item-level ACL hides kit lines whose item the persona cannot see; persona 1 covered
157
+ only 2 of bundle 243's 19 items), the unique-key asymmetry that makes `Personas_Items` /
158
+ `Personas_Assortments` duplicate silently on re-run, the **derived** assortment join, and that
159
+ **every persona settings table in prod is empty across all 38 personas** so a new persona needs no
160
+ config. Added the `Bundles.number` vs `Bundles.id` trap, the missing `Users.username` column
161
+ (`c_hrEmpUsername`, lowercase), the SuperUser role recipe (`Roles.id 5`, additive, Base + SuperUser
162
+ + Compass Base), and the three-source effective-persona verification. Template file:
163
+ `2026-08-26a - CompassCreativeStudioPersona.sql` (written, not executed). (bala)
@@ -6,7 +6,7 @@ project: Database Changes
6
6
  client: compass-usa
7
7
  type: workflow
8
8
  status: active
9
- updated: 2026-08-06
9
+ updated: 2026-08-26
10
10
  owners: [bala]
11
11
  files:
12
12
  - dbchanges2/Client_Compass/2026-08-06 - PersonaRefactor.sql
@@ -16,6 +16,7 @@ files:
16
16
  related:
17
17
  - ../features/persona-model-and-levy-gating.md
18
18
  - ./persona-population-env-comparison.md
19
+ - ./granting-persona-bundle-access.md
19
20
  - ../profile.md
20
21
  ---
21
22
 
@@ -29,6 +30,32 @@ Levy-sector users). One SQL file, **8 phases**, plus a companion verification sc
29
30
  > (toga2-commerce #306, dbchanges2 #368, worker2 #43) were closed and the work rewritten against
30
31
  > current code and data — treat any plan or skill text predating 2026-08-06 as stale.
31
32
 
33
+ ### ⚠ Everything below Phase 1 describes a TARGET state. Prod is not in it (re-verified 2026-08-26)
34
+
35
+ The phase table, the persona ids and the post-migration figures in this doc describe what prod will
36
+ look like **after** the migration. Read read-only against production `Client_Compass` on
37
+ **2026-08-26**, prod is still pre-migration:
38
+
39
+ | Claim you might carry over | Production reality, 2026-08-26 |
40
+ |---|---|
41
+ | Persona 1 is named "Base" | Persona 1 is still named **"All"** |
42
+ | A "Non-Levy" persona exists | **It does not exist in prod at all** |
43
+ | Non-Levy will land on `Personas.id` **38** | **Id 38 is already taken** — it is **"MyDining- KDS"** |
44
+ | Phase 1's hardcoded `number = '40'` is free | **Number 40 is in use** (by id 38). Numbers **1-23, 27-40 and 100** are taken across **38** personas; **41 is the next free number** |
45
+
46
+ Two consequences before this is ever run in prod:
47
+
48
+ 1. **Phase 1's hardcoded `number = '40'` must be re-checked** — it would create a second persona
49
+ carrying a number that already belongs to "MyDining- KDS".
50
+ 2. **The "id will be 38" reasoning no longer holds in prod.** `Personas.AUTO_INCREMENT` has moved
51
+ past 38 since 2026-08-06, so the new persona will get some higher id. This is exactly why
52
+ `PeopleFile` resolves Non-Levy **by name** — see
53
+ [Persona Model & Levy-Sector Gating](../features/persona-model-and-levy-gating.md). Never hardcode
54
+ the id, and re-read prod rather than this table before running.
55
+
56
+ **Any other Compass persona work must target prod-as-it-is, not this target state** — see
57
+ [Granting a Compass user access to a bundle](./granting-persona-bundle-access.md).
58
+
32
59
  The persona semantics and the Levy-detection code this depends on live in
33
60
  [Persona Model & Levy-Sector Gating](../features/persona-model-and-levy-gating.md). Read that first;
34
61
  the migration is meaningless without the `isLevySector` fix.
@@ -130,6 +157,12 @@ All checks are written as **invariants** or as comparisons against a captured **
130
157
  | Fixed verification user list (HAQIKAH.AARON, KYLE.AARON) | HAQIKAH.AARON is inactive, KYLE.AARON does not exist — **find test users dynamically** |
131
158
 
132
159
  ## Change history
160
+ - 2026-08-26 — **Correction, not new work.** Re-verified read-only against prod: the refactor is
161
+ **still not live**. Prod persona 1 is named **"All"** (not "Base"), there is **no Non-Levy
162
+ persona**, and **id 38 is now "MyDining- KDS"** — so the "Non-Levy = id 38" reasoning and Phase 1's
163
+ hardcoded `number = '40'` (number 40 belongs to id 38) are both stale for prod. Prod holds 38
164
+ personas using numbers 1-23, 27-40 and 100; next free number is 41. Added the target-state warning
165
+ block so nobody writes prod SQL against the post-migration table. (bala)
133
166
  - 2026-08-06 — Initial: rewritten 8-phase migration (earlier draft's Phase 5+9 collapsed into one
134
167
  Levy-excluding assignment; Phase 6 restricted to active users; Phase 7f deletes the retired persona
135
168
  by name; Phase 8 makes ACL expression 235 purely additive). Documented the B2 verification-order
@@ -0,0 +1,95 @@
1
+ ---
2
+ type: session
3
+ slug: bdr-landing-call-flow
4
+ title: BDR /landing "Call me now" flow built to the mock, real callback route wired but dormant
5
+ author: tcox
6
+ repos: [bdr]
7
+ framework: "2.0"
8
+ client: shared
9
+ status: active
10
+ created: 2026-08-26
11
+ updated: 2026-08-26
12
+ ---
13
+
14
+ # Session: bdr-landing-call-flow
15
+ **Date:** 2026-08-26
16
+ **Project/Repo:** bdr (2.0)
17
+ **Task:** On the BDR cybersecurity landing page (C:\WWW\BDR, route `/landing`): acted on the design lead's UI review by adding the chat drawer's "Call me now" button and its full flow (CallNow screen -> calling screen) ported from the restored `AI BDR - Security/` mockup, fixed the "Book a meeting" sizing, and wired a REAL anonymous-lead callback route (`POST /api/landing-call`) that stays dormant (503 -> simulation) until a cybersecurity Toga campaign exists and `TOGA_SECURITY_CAMPAIGN_UUID` is set. Also: diagnosed the chat fallback (origin gate), verified the Calendly link is dead, resolved the `_dev-sandbox` merge conflict. Nothing committed in the BDR working tree.
18
+
19
+ ---
20
+
21
+ ## What WORKED
22
+ - **Chat fallback root-caused with browser-shaped curl probes.** From origin `http://localhost:3000` the full talos-chat flow works end-to-end (token minted, real Sonnet reply with suggestions/cta/cited_slides). From `https://bdr.dev.sandbox.togatech.com` the session mint returns **403** (origin not allowlisted). CORS is fine (`access-control-allow-origin: *`, `authorization,content-type`); one 502 on a preflight was transient (3/3 retries -> 200). Recorded in `landing-chat-drawer.md`; short message for the Talos backend owner was drafted for the developer to send.
23
+ - **Calendly link verified dead**: `calendly.com/toga-technology/risk-assessment` AND `calendly.com/toga-technology` both 404; TOGA has no Calendly. Real booking system is **Cal.com team `toga-technology`** (worker2 `[calcom] team_slug`); the only public event type is `discovery-call` ("Healthcare AI Discovery Call"); prod `Client_True.Campaigns` both use `c_meetingType = discovery-call`. No security event type exists. Recorded in `security-landing-page.md`.
24
+ - **`_dev-sandbox` merge of TRUE-80965**: single conflicted file `src/content/security.ts`, 4 hunks, all resolved to the TRUE-80965 side with `perl -0pi -e 's/<<<<<<< HEAD\r?\n.*?=======\r?\n(.*?)>>>>>>> TRUE-80965\r?\n/$1/gs'`; resolved file byte-identical to `MERGE_HEAD`; tsc/lint/367 tests green; staged. Developer commits the merge.
25
+ - **"Book a meeting" sizing fixed**: root cause = the mock's chat actions are `<button>`s (browser default ~13.3px button font) while ours is an `<a>` inheriting the panel body size. `.security-shell .chatactions .btn{ font-size:13.5px; padding:0 16px; min-height:44px }`.
26
+ - **Call flow built to the mock**: `ChatPanel` `onCall` prop + "Call me now" button (above Book, mock order); `SecurityCallNow.tsx` (trust column, business email with live domain hint, masked phone, submit gated on valid email AND viable phone); `SecurityCalling.tsx` (dialing -> Connected + mm:ss -> End call -> Call ended with rotating recap + Copy, Call me again / Book a meeting / Back to overview); `securityCallFlow.ts` pure helpers; `SecurityLanding` became a `landing|call|calling` router via `SwapFade` (outMs 240 / inMs 460 / rise 12 / drop 18, mock app-shell timings); copy in `security.ts` `callNow` + `calling`. Evidence: tsc clean, lint clean, vitest 376/376 at that point, `next build` clean, `/landing` still prerendered static.
27
+ - **Real callback path built, gated**: `POST /api/landing-call` {email, phoneNumber} -> validate -> require env `TOGA_SECURITY_CAMPAIGN_UUID` (else **503 before any upstream call**) -> `togaLeadSink.upsertLead` (name from email via `nameFromEmail`) -> `togaCallbackService.requestCall(uuid,{type:"now"},phone)` (same path as /api/call-now: dialer + fast dial) -> status/share tokens. Client `submitLandingCall()` returns `{live, statusHandle}`; any non-success -> simulation. `SecurityCalling` drives the live path through `useCallStatusViewModel` (real phases, real Vapi recap, no "End call" on live, unavailable holds at dialing). `/api/landing-call` on the /api/call-now limiter bucket + matcher. `.env.example` documents the var. Evidence: tsc clean, lint clean, **vitest 385/385**, `next build` clean (`ƒ /api/landing-call`, `/landing` static).
28
+ - **Mockup restored + eslint ignore restored**: `"AI BDR - Security/**"` back in `globalIgnores`; lint clean.
29
+ - **KB captured** (`/capture`): CREATE `landing-call-flow.md`; UPDATE `security-landing-page.md`, `landing-chat-drawer.md`, `web-funnel-app.md`, `live-call-status.md`. Pushed to `_main`, 417 docs. Earlier manual pushes today/yesterday: 595620a (mockup deleted, now corrected), 0fa110e (origin gate), 0924ef5 (Calendly dead).
30
+
31
+ ## What did NOT work — DO NOT RETRY THESE
32
+ - **`perl -0pi -e 's/<<<<<<< HEAD\n.*?=======\n(.*?)>>>>>>> TRUE-80965\n/$1/gs'` (LF-only) matched NOTHING** on `src/content/security.ts` — the file is **CRLF**. Use the `\r?\n` variant above. 12 markers untouched meant nothing was mangled; check `grep -c '^<<<<<<<'` after any automated resolution.
33
+ - **Bash single-quoted heredoc for a long markdown file** (session save) failed: `unexpected EOF while looking for matching `''`. Use the Write tool for multi-line files.
34
+ - **Relative paths after a `cd` in one Bash call**: the shell cwd resets to `c:\WWW` between calls, so `cat src/...` in the next call fails with "No such file". Always use absolute paths (`/c/WWW/BDR/...`).
35
+ - **Using `43fdf2b6-7a5d-4e85-be6b-911f42aa787f` as the Toga dialer campaign** — it is the TALOS CHAT campaign (api.togaiq.com `campaign_registry`), NOT a `Client_True.Campaigns` uuid. The dialer would find nothing. Prod Toga campaigns are only `15d0c02f-501e-e081-2ead-61b78c854223` and `d8f7abe6-8f06-0218-03d1-50db575` (both healthcare).
36
+ - **Reusing the studio `CallNow.tsx` / `Success.tsx` on /landing** — rejected before trying: both read `useCampaign()` (CampaignBundle context) and `useLeadRef()`, neither provided on `/landing`; studio CallNow also has no email field. Purpose-built `SecurityCallNow` / `SecurityCalling` instead.
37
+ - **Pointing `TOGA_SECURITY_CAMPAIGN_UUID` at the healthcare test campaign to exercise the real path** — deliberately NOT done and the developer agreed not to: the healthcare Vapi assistant would pitch the wrong offer to a real phone.
38
+ - **Consuming blox Talos directly** (carried forward): unpublished to npm + React `^18` peers vs BDR React 19.2.4. Do not attempt until blox publishes with React 19 peers.
39
+ - **Glob on `c:\WWW` / BDR root** (carried forward): ripgrep timeout. Use `ls`/`find`/`rg` with explicit paths.
40
+ - **Grep content-mode showing `\*` for CSS `/*`** (carried forward): display artifact, not corruption.
41
+
42
+ ## Not tried yet (candidates for next session)
43
+ - **Commit**: the whole working tree (call flow, real route, eslint restore, sizing fix) is uncommitted on top of the `_dev-sandbox` merge (which is staged, uncommitted). Branch naming needs tcox's call.
44
+ - **Send the Alex (apeterson) note** — drafted (in the conversation and summarized in `landing-chat-drawer.md`), not sent: adapter matches his `TalosAdapter` seam, blockers (unpublished, React 18 peers), design divergence (Call me now / Book a meeting actions, cobalt theme).
45
+ - **Send the backend-owner note** — drafted: allowlist `https://bdr.dev.sandbox.togatech.com` (later the prod domain) for talos-chat; campaign `43fdf2b6…`.
46
+ - **Enable real calls** when a cybersecurity campaign + Vapi assistant exist in Toga (runbook `ai-bdr/docs/client-onboarding-sop.md`): set `TOGA_SECURITY_CAMPAIGN_UUID` in `.env.local` + Amplify console, redeploy that version. First live run must verify: Toga accepts an EMPTY `lastName` for email-only leads (e.g. `jdoe@` -> firstName "Jdoe", lastName ""); deployed env has `TOGA_VAPI_TOKEN` (else the screen holds at dialing with the unavailable note).
47
+ - **Booking link**: create a Cal.com team event type for the security offer (`https://cal.com/team/toga-technology/<slug>`), then set `security.ts` `primaryCtaHref` (one line; drives page CTA, drawer Book, calling-screen Book) and pass the same URL as `cta_url` on the talos campaign ingest so the chat's `book_call` cta resolves (currently `cta: null`). Interim option: `discovery-call` (title mismatch "Healthcare AI").
48
+ - Render the chat reply's `cta` button and `cited_slides` thumbnails (needs `GET /api/ai/campaign/{id}`; presigned URLs expire ~7 days).
49
+ - Visual QA of the call screens at phone widths; `SecurityCalling` at <=840px relies on studio.css `.ended-cta` stacking.
50
+ - Optional visible "assistant unavailable" notice in the drawer when a chat reply is `offline: true` (currently silent).
51
+ - KB hygiene (unrelated): 20+ docs end with stray `</content>`/`</invoke>` lines (`grep -rln "^</invoke>$" knowledge/`); `validate` does not catch it.
52
+
53
+ ## Current file state
54
+ | File | Status | Notes |
55
+ |------|--------|-------|
56
+ | src/components/ChatPanel.tsx | modified | `onCall` prop; "Call me now" `<button>` above "Book a meeting" `<a>` |
57
+ | src/flow/screens/SecurityCallNow.tsx | created | Mock CallNow port: email + phone, domain hint, trust list; submit -> `submitLandingCall` -> `onConnect(detail, result)`; never blocks on backend |
58
+ | src/flow/screens/SecurityCalling.tsx | created | Mock Success (calling) port with live/sim split via `useCallStatusViewModel`; `SecurityCallSummary` renders real Vapi recap or rotating static variant + Copy |
59
+ | src/flow/screens/SecurityLanding.tsx | modified | `screen` state `landing\|call\|calling`, `detail`, `callResult`; SwapFade router; `backToOverview` resets recap rotation + scrollTo(0,0); `agentName` derived from `talos.title` |
60
+ | src/lib/securityCallFlow.ts | created | `fillTokens`, `emailDomain`, `isCallFormComplete`, `formatCallTimer`, `nextSummaryVariantIndex`, `resetSummaryRotation` |
61
+ | src/lib/leadName.ts | created | `nameFromEmail` (local part split on `._-+`, digits stripped, empty lastName allowed) |
62
+ | src/flow/landingCallApi.ts | created | `submitLandingCall(email, phone, fetchImpl?)` -> `{live, statusHandle}`; 20s timeout; logs status only |
63
+ | src/app/api/landing-call/route.ts | created | Anonymous-lead callback route, 503 when `TOGA_SECURITY_CAMPAIGN_UUID` unset/non-uuid; mirrors /api/call-now token minting |
64
+ | src/proxy.ts | modified | `/api/landing-call` on the call limiter branch + matcher |
65
+ | .env.example | modified | `TOGA_SECURITY_CAMPAIGN_UUID` documented (must be the CYBERSECURITY Toga campaign, not the chat campaign id) |
66
+ | src/content/security.ts | modified (+ dev's tabs/"Ask Alex Anything") | `talos.callCta`; new `callNow` + `calling` groups (incl. `calling.live` strings); merge-resolved to TRUE-80965 side |
67
+ | src/app/security.css | modified | chat action sizing fix; `.connect-intro*` / `.connect-trust` / `.agent-caption` / `.field-hint` ported; `.ended-cta a.btn` underline reset |
68
+ | eslint.config.mjs | modified | `"AI BDR - Security/**"` ignore RESTORED (mockup back) |
69
+ | AI BDR - Security/ (folder) | restored by dev | design reference, in repo again |
70
+ | test/securityCallFlow.test.ts | created | 9 tests |
71
+ | test/landingCallRoute.test.ts | created | 9 tests (400s, 503 pre-upstream, 500 without echoing input, nameFromEmail) |
72
+ | next-env.d.ts | modified (generated) | churn from tsc/build; discard if unwanted |
73
+
74
+ All BDR changes uncommitted; the `_dev-sandbox` merge is staged and awaiting `git commit`.
75
+
76
+ ## Decisions made
77
+ - **"Match the mock" = match the UI; wire everything that CAN be wired** (developer clarification). Result: real callback route built but dormant; simulation only as fallback.
78
+ - **Keep the real route in the repo, dormant, until a cybersecurity Toga campaign exists** (developer: "as long as it's wired up and ready for the campaign it's good"). Rejected: removing it (rebuild later); testing against the healthcare campaign.
79
+ - **Gate = env var `TOGA_SECURITY_CAMPAIGN_UUID`, 503 before any upstream call when unset** — enabling is config-only; the client treats any non-success as "play the simulation".
80
+ - **Purpose-built `SecurityCallNow`/`SecurityCalling` instead of reusing studio screens** — studio screens need bundle/lead contexts `/landing` lacks; the mock's CallNow differs (email field, trust column).
81
+ - **Live path never fakes progress**: no "End call" on a real call; unavailable status holds at dialing with a note; real Vapi recap replaces the static one; static recap only in simulation.
82
+ - **Anonymous lead name derived from the email local part** (`nameFromEmail`), lastName may be empty — flagged unverified. Rejected: adding a name field (not in the mock); inventing a placeholder surname (pollutes CRM).
83
+ - **Mock deviations kept**: no AmbientQuestions (bundle-driven); recap Copy without Share sheet; "Schedule a call" -> "Book a meeting" (Calendly) since no schedule screen; footnote Privacy/Terms = real togatech.com links.
84
+ - **Book-a-meeting sizing**: pin font-size/padding on `.chatactions .btn` rather than converting the anchor to a button (it must remain a link to open Calendly).
85
+ - **TRUE-80692 alignment: keep the disposable bridge shell**; send Alex a facts-first note (drafted). Rejected: pausing drawer work until blox publishes.
86
+ - **KB filing stays under `2.0/apps/ai-bdr/`**; mockup deletion history kept honest (superseded, not erased).
87
+
88
+ ## Blockers
89
+ None hard. External dependencies: (1) a cybersecurity campaign + Vapi assistant in Toga (owner outside bdr) before real calls can be enabled; (2) Talos backend owner to allowlist the sandbox/prod origins for talos-chat; (3) a Cal.com event type for the security offer (Cal.com owner) before the booking CTA works; (4) apeterson/design alignment on the shared component; (5) tcox's branch/commit go-ahead.
90
+
91
+ ## Exact next step
92
+ > In `C:\WWW\BDR`, decide the branch, then commit the staged `_dev-sandbox` merge and the working-tree call-flow work (`git add -A` — includes the restored `AI BDR - Security/` mockup and eslint ignore). Then send the two drafted notes (Talos backend owner: allowlist `https://bdr.dev.sandbox.togatech.com`; apeterson: shared-component alignment). Real calls stay off until you have the cybersecurity campaign UUID for `TOGA_SECURITY_CAMPAIGN_UUID`.
93
+
94
+ ---
95
+ _Saved by /session-save on 2026-08-26_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.656",
3
+ "version": "1.0.658",
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",