toga-ai 1.0.384 → 1.0.386
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/api2/INDEX.md +1 -0
- package/knowledge/2.0/apps/api2/features/tableview-apiwhereclause-row-filtering.md +1 -0
- package/knowledge/2.0/apps/api2/features/tableview-field-metadata.md +97 -0
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga25-supply/features/record-modals-and-nested-tables.md +45 -2
- package/knowledge/INDEX.md +1 -1
- package/knowledge/sessions/2026-07-20-bdr-vapi-golive-tcox.md +76 -0
- package/package.json +1 -1
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
| [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
|
|
11
11
|
| [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
|
|
12
12
|
| [TableView row-filtering via apiWhereClause (options.where grammar, end to end)](features/tableview-apiwhereclause-row-filtering.md) | `TableViews.apiWhereClause` (TEXT, nullable) is the sanctioned, code-free way to restrict or exclude rows from a 2.0 table view. | api2/Component/Api/V2/V2.php, _underscore/Model/Client/TableView.php, toga2-supply/src/api/toga.ts |
|
|
13
|
+
| [TableView field/column metadata (TableViewFields, hidden projected columns)](features/tableview-field-metadata.md) | The columns of a 2.0 table view are defined by DB metadata, not code. | _underscore/Model/Client/TableView.php, api2/Component/Api/V2/V2.php, dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql |
|
|
13
14
|
| [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
|
|
14
15
|
| [V2 API error/message codes (EV/EZ troubleshooting map)](features/v2-api-error-codes.md) | The V2 JSON engine (`Component/Api/V2/V2.php`) returns short **message codes** in the response `error` field, grouped by family: `EN-*` authentication, `EZ-*` a | api2/Component/Api/V2/V2.php |
|
|
15
16
|
| [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | |
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: TableView field/column metadata (TableViewFields, hidden projected columns)
|
|
3
|
+
framework: "2.0"
|
|
4
|
+
repo: api2
|
|
5
|
+
project: API
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-07-20
|
|
10
|
+
owners: [apeterson]
|
|
11
|
+
files:
|
|
12
|
+
- _underscore/Model/Client/TableView.php
|
|
13
|
+
- api2/Component/Api/V2/V2.php
|
|
14
|
+
- dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql
|
|
15
|
+
related:
|
|
16
|
+
- tableview-apiwhereclause-row-filtering.md
|
|
17
|
+
- ../../toga25-supply/features/meta-driven-table-data.md
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Summary
|
|
21
|
+
|
|
22
|
+
The columns of a 2.0 table view are defined by DB metadata, not code. Per-client tables
|
|
23
|
+
`Client_*.TableViews` / `TableViewFields` / `TableViewJoins` declare the view, its columns, and its
|
|
24
|
+
joins; the frontend consumes the emitted meta (see
|
|
25
|
+
[meta-driven-table-data](../../toga25-supply/features/meta-driven-table-data.md)). Adding or altering
|
|
26
|
+
a column is a **dbchanges2 migration**, not a code change. A `TableViewFields` row with
|
|
27
|
+
`isVisible=0` is still fetched into the row projection but renders **no column** — the sanctioned way
|
|
28
|
+
to make a field available to frontend logic (e.g. a resolver that needs an fk uuid) without showing
|
|
29
|
+
it.
|
|
30
|
+
|
|
31
|
+
## Data model
|
|
32
|
+
|
|
33
|
+
- **`Client_*.TableViews`** — one row per view (per client DB, e.g. `Client_Nychh`).
|
|
34
|
+
- **`Client_*.TableViewFields`** — one row per **column** of a view. Key columns: `recordFieldId`
|
|
35
|
+
(which field this column shows), `tableViewJoinId` (which join in the view the field is reached
|
|
36
|
+
through), `slug` (the frontend/tanstack column id), `isVisible` (0 = projected-but-hidden).
|
|
37
|
+
- **`Client_*.TableViewJoins`** — the view's joins; also drive `options.join`/`ojoin` on the data
|
|
38
|
+
request (see [tableview-apiwhereclause-row-filtering](tableview-apiwhereclause-row-filtering.md)).
|
|
39
|
+
|
|
40
|
+
### `recordFieldId` is a cross-DB reference into shared `Core.RecordFields`
|
|
41
|
+
|
|
42
|
+
`TableViewFields.recordFieldId` does **not** point within the client DB — it references the
|
|
43
|
+
**shared** `Core.RecordFields` table. In `Core`:
|
|
44
|
+
|
|
45
|
+
- `Core.RecordFields.field` — the field-name column (the actual column/attribute name).
|
|
46
|
+
- `Core.RecordFields.recordId` → `Core.Records` — the owning record/model.
|
|
47
|
+
|
|
48
|
+
Reference ids observed for the `Items` record: **`Core.Records` id 21 = Items**, and
|
|
49
|
+
**`Core.RecordFields` id 106 = `Items.uuid`**. (Look these up per field before writing a migration —
|
|
50
|
+
do not assume ids across environments.)
|
|
51
|
+
|
|
52
|
+
## Hidden (projected-but-not-rendered) columns
|
|
53
|
+
|
|
54
|
+
A `TableViewFields` row with `isVisible=0` is still selected into the row's data projection but
|
|
55
|
+
produces no rendered column (on the frontend, `buildTanstackColumns` filters by `isVisible`). This is
|
|
56
|
+
the mechanism behind fields the UI needs but should not display — the existing hidden
|
|
57
|
+
`inventorytype` / `Items.inventoryType` field on the inventory views is the precedent.
|
|
58
|
+
|
|
59
|
+
Use case that drove this doc: the `items-for-purchase-orders` view's rows are sales-order **lines**,
|
|
60
|
+
so `row.uuid` = `SalesOrderItems.uuid`. Opening the item-record modal needs the **catalog**
|
|
61
|
+
`Items.uuid`. Adding a hidden `Items.uuid` column puts `row.Items.uuid` into the projection so the
|
|
62
|
+
frontend `getModalUuid` resolver can open the catalog item (see
|
|
63
|
+
[record-modals-and-nested-tables](../../toga25-supply/features/record-modals-and-nested-tables.md)).
|
|
64
|
+
|
|
65
|
+
## How to add a hidden projected column (migration recipe)
|
|
66
|
+
|
|
67
|
+
1. Find the `Core.RecordFields.id` for the field (join `Core.Records` on `recordId`, match on
|
|
68
|
+
`field`). e.g. `Items.uuid` → 106.
|
|
69
|
+
2. Resolve `tableViewJoinId` — reuse the join through which the field is reached. For an already
|
|
70
|
+
joined table, resolve it from an existing `TableViewFields` row that joins the same table (the
|
|
71
|
+
hidden `inventorytype` Items-join field was the source in the reference migration).
|
|
72
|
+
3. `INSERT INTO TableViewFields (recordFieldId, tableViewJoinId, slug, isVisible, …)` with
|
|
73
|
+
`isVisible=0` and a distinct `slug` (e.g. `itemUuid`). Make it **idempotent** with a
|
|
74
|
+
`WHERE NOT EXISTS (...)` guard so a re-run is a no-op.
|
|
75
|
+
|
|
76
|
+
Cross-client field/column migrations live in **dbchanges2's `Client/` template folder**, not a
|
|
77
|
+
per-client folder — the reference migration
|
|
78
|
+
`dbchanges2/Client/2026-07-20 - ItemsUuidForPurchaseOrderItemsTableView.sql` adds the hidden
|
|
79
|
+
`Items.uuid` column to the `items-for-purchase-orders` view and was verified against `Client_Nychh`.
|
|
80
|
+
|
|
81
|
+
## Gotchas / known issues
|
|
82
|
+
|
|
83
|
+
- **`recordFieldId` is cross-DB** — it indexes `Core.RecordFields`, not anything in the client DB.
|
|
84
|
+
Resolve the id in `Core`, but insert the `TableViewFields` row in the client DB.
|
|
85
|
+
- **Don't assume `Core.RecordFields`/`Core.Records` ids are stable across environments** — look them
|
|
86
|
+
up by `field` name + record.
|
|
87
|
+
- **Make column migrations idempotent** (`NOT EXISTS` guard) — they run per client DB.
|
|
88
|
+
- **`isVisible=0` still costs a fetch** — the field is selected into the projection; only the render
|
|
89
|
+
is suppressed.
|
|
90
|
+
|
|
91
|
+
## Change history
|
|
92
|
+
|
|
93
|
+
- 2026-07-20 — Documented the backend TableView column-metadata model (per-client
|
|
94
|
+
`TableViews`/`TableViewFields`/`TableViewJoins`, `recordFieldId` → shared `Core.RecordFields`
|
|
95
|
+
cross-DB ref, `isVisible=0` projected-but-hidden columns) and the dbchanges2 recipe for adding a
|
|
96
|
+
hidden projected column, discovered while making the Inventory item-record modal open the catalog
|
|
97
|
+
`Items.uuid`. (apeterson)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
| [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/FIELDS/ |
|
|
9
9
|
| [Column Visibility (URL-driven show/hide columns)](features/column-visibility.md) | A "Columns" header button that opens a modal listing every column from the table meta, lets the user show/hide columns, adjusts the table live, and persists the | toga25-supply/src/components/ColumnVisibilityModal/, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableData.tsx |
|
|
10
10
|
| [Meta-Driven Page & Table Setup](features/meta-driven-table-data.md) | A page in this app is **meta-driven end to end**: the page view model fetches *page meta* (labels, sections, ACL) and *table meta* (the columns/fields + table s | toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx, toga25-supply/src/pages/SalesOrders/hooks/useSalesOrdersTableState.ts, toga25-supply/src/hooks/useTablePageMeta.ts, toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts, toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts, toga-blox-npm/dist/hooks/useAssignTableFieldLabels.d.ts, toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts |
|
|
11
|
-
| [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables
|
|
11
|
+
| [Record Modals & Nested Tables](features/record-modals-and-nested-tables.md) | The repo's family of modal + nested-table patterns layered over toga-blox `TableRecordModal` and `PrimaryTable*Layout`. | toga25-supply/src/layout/ItemRecordModalLayout/, toga25-supply/src/layout/SalesOrderRecordModalLayout/, toga25-supply/src/layout/SalesOrderItemsTableLayout/, toga25-supply/src/layout/ItemFulfillmentModal/, toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx, toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json, toga25-supply/src/hooks/useTableCellInteractions.ts, toga25-supply/src/hooks/useServerTableUrlState.ts |
|
|
12
12
|
| [Surface Frontend (DB-driven UI consumption, src/surface/)](features/surface-frontend.md) | The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from `GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported J | toga25-supply/src/surface/useFetchSurfaceMeta.ts, toga25-supply/src/pages/SalesOrders/helpers/surfaceBundleToTenantFields.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/SalesOrderView.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/layoutComponents/SalesOrderSummaryGrid.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/getDetailSections.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderNotesSection.tsx, toga25-supply/src/pages/SalesOrders/helpers/cleanOrder.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/helpers/index.ts, toga25-supply/src/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, toga25-supply/src/surface/componentRegistry.tsx, toga25-supply/src/surface/SurfaceActionBar.tsx, toga25-supply/src/surface/SurfaceSection.tsx, toga25-supply/src/surface/resolve.ts, toga25-supply/src/surface/types.ts, toga25-supply/src/surface/index.ts, toga25-supply/src/pages/Login/LoginPage.tsx, toga25-supply/src/pages/SalesOrders/SalesOrders.tsx, toga25-supply/src/pages/SalesOrders/view/SurfaceRowActions.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx, toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx, toga25-supply/src/pages/Inventory/Inventory.tsx, toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx, toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/surfaceBundleToItemFields.ts, toga25-supply/src/layout/ItemRecordModalLayout/helpers/index.ts, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/useItemRecordModalViewModel.tsx, toga25-supply/src/layout/ItemRecordModalLayout/ItemRecordModalLayout.tsx, toga25-supply/src/layout/ItemRecordModalLayout/components/ItemRecordView.tsx |
|
|
13
13
|
| [AWS Amplify Multi-Environment Deployment](workflows/amplify-deployment.md) | How `toga25-supply` deploys to **all** of its environments on AWS Amplify from a **single shared `amplify.yml`**. | toga25-supply/amplify.yml, toga25-supply/src/api/api.ts, toga25-supply/src/hooks/useAuthenticationFlow.ts, toga25-supply/vite.config.ts, toga25-supply/package.json |
|
|
14
14
|
| [Cypress Testing Harness (component + e2e)](workflows/cypress-testing.md) | The Cypress test harness for the `toga25-supply` frontend, bootstrapped from scratch (`cypress` was already a dependency but there was no config, no `cypress/` | toga25-supply/cypress.config.ts, toga25-supply/cypress/tsconfig.json, toga25-supply/cypress/support/component.tsx, toga25-supply/cypress/support/component-index.html, toga25-supply/cypress/support/e2e.ts, toga25-supply/cypress/support/commands.ts, toga25-supply/cypress/support/fixtures.ts, toga25-supply/cypress/support/mocks/useApprovalModalViewModel.ts, toga25-supply/cypress/component/SalesOrderApprovalModalsLayout.cy.tsx, toga25-supply/cypress/component/RecordApprovalModalLayout.cy.tsx, toga25-supply/cypress/component/EnterPoNumberModal.cy.tsx, toga25-supply/cypress/e2e/salesOrderApproval.cy.ts |
|
|
@@ -6,18 +6,23 @@ project: TOGa 2.5 Supply
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-
|
|
9
|
+
updated: 2026-07-20
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/layout/ItemRecordModalLayout/
|
|
13
13
|
- toga25-supply/src/layout/SalesOrderRecordModalLayout/
|
|
14
14
|
- toga25-supply/src/layout/SalesOrderItemsTableLayout/
|
|
15
15
|
- toga25-supply/src/layout/ItemFulfillmentModal/
|
|
16
|
-
- toga25-supply/src/layout/GenericNestedTables/
|
|
16
|
+
- toga25-supply/src/layout/GenericNestedTables/GenericNestedTables.tsx
|
|
17
|
+
- toga25-supply/src/layout/GenericNestedTables/GenericTableLayout.tsx
|
|
18
|
+
- toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
|
|
19
|
+
- toga25-supply/src/pages/Inventory/viewModel/FIELDS/DEFAULT/inventoryGroupings.json
|
|
17
20
|
- toga25-supply/src/hooks/useTableCellInteractions.ts
|
|
21
|
+
- toga25-supply/src/hooks/useServerTableUrlState.ts
|
|
18
22
|
related:
|
|
19
23
|
- ../architecture.md
|
|
20
24
|
- meta-driven-table-data.md
|
|
25
|
+
- ../../api2/features/tableview-field-metadata.md
|
|
21
26
|
---
|
|
22
27
|
|
|
23
28
|
## What it is
|
|
@@ -107,6 +112,29 @@ A page-level `pageMeta` (from `useFetchPageMeta`) flows down for field-label ove
|
|
|
107
112
|
view model builds `TableLevel[]` arrays, picks the active one via a URL param, and passes `levels`
|
|
108
113
|
to `GenericNestedTables` with `key={activeGrouping.key}` to force remount and avoid stale state.
|
|
109
114
|
|
|
115
|
+
### Decoupling "drill to child tier" from "open this record" on a swap level
|
|
116
|
+
|
|
117
|
+
By default a `"swap"` level consumes the **whole-row click** (`getRowHref`) to drill to the next
|
|
118
|
+
tier, which leaves no room for a per-row record modal. Two optional `TableLevel` hooks split the two
|
|
119
|
+
interactions so both are reachable on one nested row (Inventory `items-for-purchase-orders` /
|
|
120
|
+
`purchase-orders_items` levels are the reference):
|
|
121
|
+
|
|
122
|
+
- **`buildSwapTriggerColumns(triggerSwap)`** — when present, the level renders a dedicated
|
|
123
|
+
first-column action button (Inventory's "See units": a `chevronsRight` icon wrapped in a toga-blox
|
|
124
|
+
`BaseToolTip`) whose click calls `triggerSwap`; the whole-row swap (`getRowHref`) is **disabled**
|
|
125
|
+
and inline child expansion is **suppressed while nested**, so a plain row click is free to open
|
|
126
|
+
`renderModal`. Net UX: chevron → child tier; row click → record modal. The Inventory hydrator
|
|
127
|
+
(`hydrateLevel`, role `"swap"`) wires `buildSwapTriggerColumns: makeSeeUnitsColumns` and
|
|
128
|
+
`renderModal` from the level's `modalKey`.
|
|
129
|
+
- **`getModalUuid(row)`** — override which uuid the record modal opens for a row. `GenericTableLayout`
|
|
130
|
+
wraps `handleRowClick` to store `getModalUuid(row) ?? row.uuid` as the active/modal uuid. Needed
|
|
131
|
+
when the row's own `row.uuid` is not the uuid the target record endpoint expects — e.g. the
|
|
132
|
+
`items-for-purchase-orders` rows are sales-order **lines** (`row.uuid` = `SalesOrderItems.uuid`),
|
|
133
|
+
but the item-record modal must open the **catalog** item, so the hydrator sets
|
|
134
|
+
`getModalUuid: (row) => row?.Items?.uuid`. That requires the catalog uuid to be present in the row
|
|
135
|
+
projection: a **hidden (`isVisible=0`) `Items.uuid` column** is added to the table view — see
|
|
136
|
+
[tableview-field-metadata](../../api2/features/tableview-field-metadata.md).
|
|
137
|
+
|
|
110
138
|
## Pattern 6 — Create/edit record modal (form-driven)
|
|
111
139
|
|
|
112
140
|
Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLayout/`). Form-driven
|
|
@@ -136,6 +164,15 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
|
|
|
136
164
|
- **Submit payload diverges from form shape** — send relation `.uuid`, not the `{uuid,name}` object;
|
|
137
165
|
confirm field names against the real POST/PUT contract.
|
|
138
166
|
- **Nested tables need their own slug + URL-state isolation** or sibling tables clobber each other.
|
|
167
|
+
- **A swap level's `urlParamKey` must NOT equal (or be a `${slug}_`-prefixed variant of) any level's
|
|
168
|
+
slug.** The record-modal / active-row state is stored in a URL param named **exactly** the table
|
|
169
|
+
slug (`src/hooks/useServerTableUrlState.ts`: `activeRowUuid = searchParams.get(slug)`, and
|
|
170
|
+
`handleRowClick` sets `?<slug>=<uuid>`). If a swap level's `urlParamKey` is the same string as a
|
|
171
|
+
level's fetchSlug/slug, a plain row click writes `?<slug>=<uuid>` which `GenericNestedTables` reads
|
|
172
|
+
as "swap active" and jumps to the child tier instead of opening the modal. Fix: give swap keys a
|
|
173
|
+
`swap-` **prefix** (`items-for-purchase-orders` → `swap-items-for-purchase-orders`). A `${slug}_…`
|
|
174
|
+
**suffix** can't be used because `useServerTableUrlState`'s columnFilters parser treats any
|
|
175
|
+
`${slug}_*` key as a column filter — hence a prefix.
|
|
139
176
|
- **`additionalData` in the query key is mandatory** for WHERE-filtered modal tables (Pattern 4).
|
|
140
177
|
- **Per-consumer cache `scope`** — two consumers fetching the same record with different field
|
|
141
178
|
configs must use a `scope` discriminator in the query key or they overwrite each other's cache
|
|
@@ -154,6 +191,12 @@ Scaffold per the `ItemRecordModalLayout` pattern (`src/layout/ItemRecordModalLay
|
|
|
154
191
|
`/approval-decisions/{uuid}`.
|
|
155
192
|
|
|
156
193
|
## Change history
|
|
194
|
+
- 2026-07-20 — Pattern 5: added `buildSwapTriggerColumns(triggerSwap)` and `getModalUuid(row)` to
|
|
195
|
+
`TableLevel` so a swap level's "drill to child tier" (a dedicated "See units" chevron) and
|
|
196
|
+
"open this record" (plain row click → `renderModal`) are decoupled, and a record modal can open a
|
|
197
|
+
different uuid than `row.uuid` (Inventory line rows open the catalog `Items.uuid`). Fixed a URL-param
|
|
198
|
+
collision — swap `urlParamKey` equal to a level slug hijacked the row-click into a swap; swap keys now
|
|
199
|
+
carry a `swap-` prefix — and documented the rule. (apeterson)
|
|
157
200
|
- 2026-06-30 — Recorded two behaviors confirmed while building approval-modal tests: `poNumber`
|
|
158
201
|
is handled by a separate `EnterPoNumberModal` (excluded from the approval layout), and the
|
|
159
202
|
workflow vs single-decision submit payloads differ (string `"1"`/`"0"` + changed-stages-only +
|
package/knowledge/INDEX.md
CHANGED
|
@@ -19,7 +19,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
19
19
|
|
|
20
20
|
- **_underscore** (_Underscore) _(framework core)_ — 35 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
|
|
21
21
|
- **worker2** (Worker) — 31 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
|
|
22
|
-
- **api2** (API) —
|
|
22
|
+
- **api2** (API) — 13 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
|
|
23
23
|
- **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
|
|
24
24
|
- **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
|
|
25
25
|
- **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: session
|
|
3
|
+
slug: bdr-vapi-golive
|
|
4
|
+
title: BDR VAPI call loop — self-heal fix, dialer diagnosis, go-live prep
|
|
5
|
+
author: tcox
|
|
6
|
+
repos: [bdr, ai-bdr, worker2, api2, dbchanges2]
|
|
7
|
+
framework: "2.0"
|
|
8
|
+
client: shared
|
|
9
|
+
status: active
|
|
10
|
+
created: 2026-07-20
|
|
11
|
+
updated: 2026-07-20
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Session: bdr-vapi-golive
|
|
15
|
+
**Date:** 2026-07-20
|
|
16
|
+
**Project/Repo:** bdr + worker2 + api2 + dbchanges2 (2.0)
|
|
17
|
+
**Task:** Get VAPI phone calls working end-to-end on the new BDR Next.js site (fix the primary-phone stranding bug, unblock the dialer, prove the call loop) and prepare same-day go-live for a real campaign.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What WORKED
|
|
22
|
+
- **BDR primary-phone self-heal shipped** — `src/server/toga.ts` + `test/togaPrimaryPhone.test.ts` (10 regression tests written FIRST, all failed on old code; 114/114 suite green after; tsc + eslint clean). Detects a write response echoing `primaryContactPhoneNumber` null OR digit-mismatched, finds the orphaned phone-row uuid (echo → GET /contacts/{uuid} → POST /contact-phone-numbers), re-links via identifier-only `PUT {primaryContactPhoneNumber:{uuid}}` (single-key identifier objects hit api2's forced-MATCH path). `requestCall` throws on failure (route → 500); upsert absorbs + warns. Commit `4ac579e` on `BDR-Phase-2`, merged into deploy branch `_dev-sandbox`.
|
|
23
|
+
- **api2 root cause located precisely** — `Component/Api/V2/V2.php` UPDATE path lines **4985–4996** inject a reverse `contactId` back-reference into nested child objects (commit `91d803f`), defeating the single-uuid forced-MATCH fast-path (L6952) and diverting `getForeignKeyValue()`'s return before the FK assignment (L5047). CREATE path (L4601-4616/4647) has no injection. Verified by an Explore subagent read; documented in knowledge (`api2 nested-relationship-writes.md`).
|
|
24
|
+
- **Dialer blockage root-caused from worker2 source + prod reads** — campaign 2 ('26.05 Ryan Nitti') blocked SOLELY by `dateEnd=2026-06-09` (thrown by `validateCampaignReady` L784-786); its assistant (CampaignAssistants id 3 'Alex') is valid; the dialer cron (Core.CronJobs id 12 'AI BDR', `* * * * *`) is ACTIVE and has errored every minute since June 9. Contact 248 (tcox) is fully eligible: primary phone linked (row 132), `dtNextContactRequested` set, 0 prior attempts.
|
|
25
|
+
- **Test-campaign migration written, reviewed, pushed** — `Client_True/2026-07-20a - AiBdrTcoxTestCampaign.sql` on `feature/ai-bdr-test-campaign` (`e99a367`, pushed). Fences campaign 2, creates '26.07 - AI BDR - TCOX TEST' (copy of campaign 2 config, NO CampaignCallWindows rows → only requested calls possible), links ONLY contact 248. sql-reviewer verdict SHIP (idempotent guards match real UNIQUE constraints).
|
|
26
|
+
- **Accidental `_main` push recovered cleanly** — revert commit `56055a0` pushed (never force-push); verified via read-only DB queries the executor had NOT applied the migration first; feature branch rebuilt by cherry-pick on reverted `_main`.
|
|
27
|
+
- **Read-only diagnosis tooling** — BDR's api2 client creds (from `BDR/.env.local`) work for REST probes; the "TOGa Database Integration" MCP connector (`toga_query`, prod + sandbox envs, read-only) works for SQL. ClickUp MCP works for ticket lookups.
|
|
28
|
+
- **/capture published** 4 knowledge docs incl. new workflow `2.0/apps/ai-bdr/workflows/safe-call-loop-testing.md`.
|
|
29
|
+
|
|
30
|
+
## What did NOT work — DO NOT RETRY THESE
|
|
31
|
+
- **Treating `Campaigns_Contacts.status = PENDING` as the blocker** — PENDING is a NORMAL dialable state; the eligibility SQL checks status only when `dtNextContactRequested IS NULL` (Vapi.php L237), and the webhook resets links to PENDING for retries.
|
|
32
|
+
- **Assuming the campaign date window is checked in `getActiveCampaignsForDialing`** — it is NOT (that query filters only `isActive=1` + assistant INNER JOIN); the date gate lives in `validateCampaignReady`. Half-wrong first theory cost a probe cycle.
|
|
33
|
+
- **Using the dialer's `dryRun` as a "safe" production preflight** — `createContactAttempt` (sets `dtStarted`, leaves `dtEnded` NULL) runs BEFORE the dryRun early-return (Vapi.php L438-466): a prod dry-run writes real attempt rows (counts toward maxAttempts) and WEDGES contacts as "in progress" until `dtEnded` is set. Unwedge: `UPDATE ContactAttempts SET dtEnded=NOW() WHERE contactId=? AND dtEnded IS NULL`.
|
|
34
|
+
- **Beta end-to-end call test** — impossible as wired: Vapi is one production account whose assistants' serverUrl targets production `webhook.togahub.com`, so a beta-placed call's end-of-call report can never find its `contactAttemptUuid` (it lives in the beta DB). Also `worker2/Config/beta.ini` still points `aws_worker_queue_url` at WorkerProductionQueue. Beta is for SQL/eligibility rehearsal via a LOCAL worker2 run only.
|
|
35
|
+
- **Reviving campaign 2 by extending its dateEnd** — rejected: 125 PENDING links, up to 86 contact-level-dialable real prospects, and the every-minute cron starts dialing within 60s of eligibility.
|
|
36
|
+
- **`isOkayToCall=0` mass-flip plan** — verified it WOULD work (hard-required at Vapi.php L231) but rejected: two mass writes to real prospect rows + restore risk (5 contacts already have `isOkayToCall=0` — genuine DNC — and must never be blanket-restored).
|
|
37
|
+
- **Assuming the BDR site was deployed** — no Amplify app exists; ClickUp `868kdf4tj` "BDR - create amplify and sandbox route" is still 'to do' (ClickUp says Alex Peterson; team says Jeff took it — ClickUp stale).
|
|
38
|
+
|
|
39
|
+
## Not tried yet (candidates for next session)
|
|
40
|
+
- **The actual test call** — blocked on the dbchanges2 PR merge (branch pushed, PR not confirmed opened/merged).
|
|
41
|
+
- **UI test A:** Call Now with a DIFFERENT number → proves the self-heal live (pointer re-links to new number; dialer calls the new number).
|
|
42
|
+
- **UI test B:** one scheduled call ~10 min out → the call-later path and `formatCstDateTime` CST conversion have NEVER been runtime-tested.
|
|
43
|
+
- **Purest bug repro:** phone-less HubSpot test contact through the funnel (needs someone with HubSpot access to create it).
|
|
44
|
+
- **api2 engine fix PR** — gate the V2.php 4985-4996 injection to true reverse/has-many relations; confirm `RecordFields.childPolicy` for `Contacts.primaryContactPhoneNumberId` during verification.
|
|
45
|
+
- **worker2 dry-run fix** — move `createContactAttempt` below the dryRun return.
|
|
46
|
+
- **Fix CampaignAssistants id 1 'Mark'** — corrupt 37-char `assistantIdentifier` (Alex's uuid + extra trailing '4'); would 400 at Vapi if assigned.
|
|
47
|
+
- **Real-campaign migration** — awaiting business params (name, dates, timezone, attempt caps; recommended: web-funnel-only = NO CampaignCallWindows rows). Entry links need `hsCampaignId=<new campaign uuid>` for attribution.
|
|
48
|
+
- **Cleanup migration** deactivating the test campaign — must NOT merge in the same window as the create file (executor runs folder files alphabetically in one pass).
|
|
49
|
+
- **Stranded-contact backfill** — 40 of campaign 2's 132 linked contacts lack a primary phone pointer (only matters if that list is revived).
|
|
50
|
+
|
|
51
|
+
## Current file state
|
|
52
|
+
| File | Status | Notes |
|
|
53
|
+
|------|--------|-------|
|
|
54
|
+
| BDR/src/server/toga.ts | committed `4ac579e`, on `BDR-Phase-2` + `_dev-sandbox` | self-heal (TogaPhoneRow, ensurePrimaryPhone, strict requestCall) |
|
|
55
|
+
| BDR/test/togaPrimaryPhone.test.ts | committed `4ac579e`, same branches | 10 regression tests, fake api2 at fetch boundary |
|
|
56
|
+
| dbchanges2/Client_True/2026-07-20a - AiBdrTcoxTestCampaign.sql | on `feature/ai-bdr-test-campaign` `e99a367`, pushed; PR pending | fences campaign 2, creates test campaign, links contact 248 only |
|
|
57
|
+
| dbchanges2 `_main` | revert `56055a0` pushed | accidental direct push recovered; DB was untouched |
|
|
58
|
+
| knowledge (team repo) | 4 docs published via /capture | web-funnel-app, vapi-webhook-handler, nested-relationship-writes, safe-call-loop-testing |
|
|
59
|
+
|
|
60
|
+
## Decisions made
|
|
61
|
+
- **BDR-side self-heal now, api2 engine fix later** — funnel unblocked without a platform-wide risky change; engine fix deferred to its own reviewed PR. Rejected: waiting on api2.
|
|
62
|
+
- **requestCall strict-throws / upsert absorbs** — a "successful" submit whose call can't happen must not report success; but lead priming must never break page render.
|
|
63
|
+
- **One-contact test campaign over `isOkayToCall` flip** — zero writes to prospect data; test campaign has no call windows so only requested calls can ever fire. Rejected: mass flip (restore risk), reviving campaign 2 (mass dialing).
|
|
64
|
+
- **Migration via dbchanges2 PR, not direct SQL** — review + normal per-env process; test call fires ~60s after the executor applies it (timing note is in the file header + PR body).
|
|
65
|
+
- **Skipped the elevated dbchanges2 architecture-doc edit** about missing `_main` branch protection (developer: repo owner already aware).
|
|
66
|
+
|
|
67
|
+
## Blockers
|
|
68
|
+
- **dbchanges2 PR** from `feature/ai-bdr-test-campaign` not yet opened/merged (tcox) → blocks the call test.
|
|
69
|
+
- **Amplify app doesn't exist** (ClickUp `868kdf4tj`, urgent, Sprint 82; Jeff has it per team word) → blocks URL-live. `_dev-sandbox` already contains the fix; needs SSR hosting + the 5 env vars (values' location: `info/.env.local`).
|
|
70
|
+
- **Real-campaign business params** not yet provided (tcox/team).
|
|
71
|
+
|
|
72
|
+
## Exact next step
|
|
73
|
+
> Open + merge the dbchanges2 PR from `feature/ai-bdr-test-campaign` (`e99a367`). When the executor applies it, answer the call from +1 (216) 616-0067 (contact 248's primary phone rings within ~60s), then verify write-back with read-only queries: `ContactAttempts` for `contactId=248` has `dtEnded` + `c_callOutcome` + `c_vapiCallIdentifier` set, and `Contacts.dtNextContactRequested` is cleared. If the call happens but nothing writes back, suspect the webhook (`VAPI_SERVER_SECRET` signature or `end-of-call-report` type guard) in `worker2/Worker/Vapi.php`.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
_Saved by /session-save on 2026-07-20_
|
package/package.json
CHANGED