toga-ai 1.0.228 → 1.0.230

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.
@@ -4,6 +4,8 @@
4
4
  |-----|---------|-------|
5
5
  | [Tools (1.0 Internal-Tools App) Architecture](architecture.md) | **Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind simple interfaces, gated by Client_True staff persona. | tools/index.php, tools/_/app/framework.php, tools/_/app/frameworkindex.php, tools/assets/img/favicon/favicon.ico, tools/assets/img/favicon/favicon-32x32.png, tools/assets/img/favicon/favicon-16x16.png, tools/assets/img/favicon/apple-touch-icon.png, tools/_/app/auth.php, tools/_/app/nav.php, tools/common/header.php, tools/common/footer.php, tools/mvc/get.php, tools/mvc/_TEMPLATE/get.php, tools/docs/ADDING_A_TOOL.md |
6
6
  | [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | The first two tools shipped in the Tools app, both under the **Developers** folder and gated to personas **Development Team** / **TOGa Technology**. | tools/mvc/developers/uuid/get.php, tools/mvc/developers/password/get.php |
7
+ | [Tools MVC — Routing, CSRF & App_Database Access Patterns](features/mvc-data-access-patterns.md) | The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to the Tools app — URL routing, CSRF, and DB access through `App_Databa | tools/_/app/nav.php, tools/mvc/get.php |
7
8
  | [Tools Persona-Gated Navigation (App_Nav)](features/persona-gated-navigation.md) | `App_Nav` is the Tools app's two-level, **persona-gated** navigation. | tools/_/app/nav.php, tools/mvc/get.php |
8
9
  | [Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)](features/saml-sso-auth.md) | `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=` handoff (see the 2.0 SAML downstream integration contract), establishe | tools/_/app/auth.php, tools/mvc/sso/initiate/get.php, tools/mvc/sso/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php, tools/mvc/get.php, tools/config.production.ini |
10
+ | [Talos Pricing UI (Onboarding, Dashboard, Benchmarks, Cost Factors + Estimator)](features/talos-pricing-ui.md) | The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder with four pages plus a client-side estimate engine. | tools/_/app/nav.php, tools/_/app/talos/estimator.php, tools/mvc/talos/onboarding/get.php, tools/mvc/talos/onboarding/post.php, tools/mvc/talos/pricing/get.php, tools/mvc/talos/benchmarks/get.php, tools/mvc/talos/factors/get.php, tools/mvc/talos/factors/post.php, tools/assets/css/style.css |
9
11
  | [Deploying Tools to Elastic Beanstalk (PHP 8.5 / Amazon Linux 2023)](workflows/deploy-to-elastic-beanstalk-al2023.md) | How the **Tools** 1.0 app boots on Elastic Beanstalk running `PHP 8.5 on 64bit Amazon Linux 2023/4.13.1 (aarch64)`. | tools/.ebextensions/004_http_to_https.config, tools/.ebextensions/006_mount-s3fs.config, tools/.ebextensions/007_setup_export_cache_folders.config, tools/.ebextensions/008_setup_ldap.config, tools/.ebextensions/009_setup_phpini.config, tools/.ebextensions/020_setup_git_libraries.config, tools/.ebextensions/050_register_instance_to_shared_application_load_balancer.config, tools/ebs/git.json |
@@ -0,0 +1,77 @@
1
+ ---
2
+ title: Tools MVC — Routing, CSRF & App_Database Access Patterns
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-29
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/_/app/nav.php
13
+ - tools/mvc/get.php
14
+ related:
15
+ - ./persona-gated-navigation.md
16
+ - ../architecture.md
17
+ - ../../library/features/mvc-page-pattern-and-app-skeleton.md
18
+ ---
19
+
20
+ ## Summary
21
+
22
+ The load-bearing 1.0 (`App_`) framework conventions a developer needs when adding a page to
23
+ the Tools app — URL routing, CSRF, and DB access through `App_Database` — with the gotchas
24
+ that bite if you assume the legacy 1.0 behavior. Discovered while building the Talos Pricing
25
+ UI; these apply to any tools-app page.
26
+
27
+ ## Routing
28
+ - `App_MVC::parseRoute` maps a URL to `mvc/<segments>/<method>.php`: **GET → `get.php`,
29
+ POST → `post.php`**.
30
+ - **Writes use `mvc/<route>/post.php`** — NOT the legacy
31
+ `App_FrameworkIndex::actionHandler` `targetAction → app/<x>.php` path. A form posts to the
32
+ same route as its GET page; the `post.php` handler does the write and redirects.
33
+
34
+ ## Nav persona narrowing (gotcha)
35
+ `App_Nav::personasForRoute()` returns the **union** of folder + action personas. A page that
36
+ must be **narrower** than its folder (e.g. a technical-only editor inside a broader folder)
37
+ must `requireAuth()` the narrow persona set **explicitly** — do not pass
38
+ `personasForRoute()`, or the page inherits the wider folder grant. (See
39
+ `persona-gated-navigation` for the nav model.)
40
+
41
+ ## CSRF
42
+ `App_Page::validateCrossSiteRequestForgery()` is **Origin/Referer-based**: for any non-GET
43
+ request the request host must equal `HTTP_HOST`. **No hidden token field is needed** — do not
44
+ add one expecting the framework to validate it.
45
+
46
+ ## App_Database
47
+ - Query: `App_Database::query($sql, $alias)`.
48
+ - Row helpers: `buildArrayOfRows(&$res, $includeNumericIndexes)`, `fetchOne(&$res)`,
49
+ `fetchRow(&$res, true)`.
50
+ - Escaper is `App_Database::sqlEscape()` — **not** `::escape`. (Prefer parameterized queries
51
+ per the security rules; `sqlEscape()` is the framework escaper when needed.)
52
+ - Transactions: `beginTransaction` / `commitTransaction` / `rollbackTransaction`.
53
+ - **Connections** come from `config.*.ini` `[database_<x>]` → alias `db_<x>`. A page can read a
54
+ 2.0 DB read-only through its own connection (e.g. `db_true → Client_True`); adding
55
+ `db_team → Team` is the same pattern.
56
+
57
+ ### Gotcha — by-reference row helpers
58
+ `buildArrayOfRows`, `fetchOne`, and `fetchRow` take `$res` **by reference**. Assign the
59
+ `query()` result to a **variable first**, then pass it — passing the call result inline emits
60
+ PHP "Only variables should be passed by reference", which **renders to the page** when
61
+ `display_errors` is on.
62
+
63
+ ```php
64
+ // WRONG — warning leaks to the page
65
+ $rows = App_Database::buildArrayOfRows(App_Database::query($sql, 'db_team'), false);
66
+
67
+ // CORRECT
68
+ $res = App_Database::query($sql, 'db_team');
69
+ $rows = App_Database::buildArrayOfRows($res, false);
70
+ ```
71
+
72
+ ## Autoloader
73
+ `App_Foo_Bar` → `app/foo/bar.php`, **all lowercase** (per the library CLAUDE.md). E.g.
74
+ `App_Talos_Estimator` → `_/app/talos/estimator.php`.
75
+
76
+ ## Change history
77
+ - 2026-06-29 — Documented tools MVC routing (GET→get.php / POST→post.php; writes via mvc/<route>/post.php, not the legacy actionHandler path), Origin/Referer CSRF (no token field), `App_Database` query/row/txn API + `sqlEscape()` name, the by-reference row-helper warning gotcha, the persona-narrowing gotcha, and the lowercase autoloader mapping. Discovered building the Talos Pricing UI. (jcardinal)
@@ -0,0 +1,94 @@
1
+ ---
2
+ title: Talos Pricing UI (Onboarding, Dashboard, Benchmarks, Cost Factors + Estimator)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-29
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/_/app/nav.php
13
+ - tools/_/app/talos/estimator.php
14
+ - tools/mvc/talos/onboarding/get.php
15
+ - tools/mvc/talos/onboarding/post.php
16
+ - tools/mvc/talos/pricing/get.php
17
+ - tools/mvc/talos/benchmarks/get.php
18
+ - tools/mvc/talos/factors/get.php
19
+ - tools/mvc/talos/factors/post.php
20
+ - tools/assets/css/style.css
21
+ related:
22
+ - ./persona-gated-navigation.md
23
+ - ./mvc-data-access-patterns.md
24
+ - ../architecture.md
25
+ - ../../../2.0/apps/talos/architecture.md
26
+ - ../../../2.0/apps/talos/features/pricing-cogs-model.md
27
+ - ../../../2.0/apps/worker2/features/talos-pricing-automation.md
28
+ ---
29
+
30
+ ## Summary
31
+
32
+ The 1.0 (tools app) face of the **Talos Pricing Platform** — a "Talos Pricing" nav folder
33
+ with four pages plus a client-side estimate engine. Sales onboard a client and sign a
34
+ contract here; everything else is read-only or technical-only. All four pages read the
35
+ **Team DB** through a new `db_team` connection (`[database_team]` in `config.*.ini`; creds
36
+ added by the developer separately).
37
+
38
+ ## How it works
39
+
40
+ ### Nav
41
+ `App_Nav::definition()` gains a **"Talos Pricing"** folder with 4 actions: Onboarding,
42
+ Pricing Dashboard, Usage Benchmarks, Cost Factors. Persona-gated like every other folder
43
+ (see `persona-gated-navigation`). **Cost Factors is technical-only** — because
44
+ `personasForRoute()` returns the *union* of folder+action personas, the Factors page must
45
+ **narrow** explicitly with its own `requireAuth([technical])` rather than trusting
46
+ `personasForRoute()`.
47
+
48
+ ### Estimator (`App_Talos_Estimator`, `_/app/talos/estimator.php`)
49
+ Reads `TalosCostFactors` (with built-in fallback defaults if a key is missing) and produces
50
+ a live cost/user + monthly estimate and recommended per-band per-user fees. **Sales never
51
+ enters conversations-per-user** — that is the input they cannot know; it is derived from the
52
+ cost factors / usage data we already have. The same math drives the live JS estimate on the
53
+ Onboarding page.
54
+
55
+ ### Onboarding (`onboarding/get.php` + `post.php`)
56
+ Sales enters name / slug / offering / # users / features / # apps / min-max margin band /
57
+ org fee / consecutive-months smoothing window. Live JS shows the estimate and recommended
58
+ per-band fees; fees are editable. **"Sign contract" locks the band schedule** and writes
59
+ `TalosClients` + `TalosPricingBands` in a single transaction. Write goes through
60
+ `mvc/talos/onboarding/post.php` (the App_MVC `post.php` convention — see
61
+ `mvc-data-access-patterns`).
62
+
63
+ ### Pricing model surfaced in the UI
64
+ A flat monthly **org fee** (the adjustable margin lever) PLUS a **per-user fee that steps by
65
+ user-count band**. The band rate schedule is locked at signing; when user count crosses a
66
+ band the per-user fee auto-steps to the pre-agreed rate, then the org fee is adjusted to
67
+ restore margin. The per-user fee never changes outside the agreed schedule. (Full
68
+ methodology + COGS economics in the talos `pricing-cogs-model` doc.)
69
+
70
+ ### Service offerings + cost multipliers
71
+ Three offerings with cost multipliers stored in `TalosCostFactors`: **CHAT = 1.0** (base),
72
+ **VOICE_TO_VOICE = 0.25**, **NATURAL_VOICE = 0.50**.
73
+
74
+ ### Read-only pages
75
+ - **Pricing Dashboard** (`pricing/get.php`) — read-only per-client margin/recommendation view.
76
+ - **Usage Benchmarks** (`benchmarks/get.php`) — read-only Langfuse-derived benchmarks.
77
+ - **Cost Factors** (`factors/get.php` + `post.php`) — technical-only editor for the
78
+ `TalosCostFactors` key/value constants (the "technical tab").
79
+
80
+ ## Gotchas
81
+ - **`db_team` is read-only on the dashboards** but read/write on onboarding + factors. Add
82
+ the `[database_team]` config section (alias `db_team`) — same pattern as `db_true → Client_True`.
83
+ - **Voice multiplier wording is ambiguous** — "1/4 / 1/2 the cost of chat" is flagged; full
84
+ voice modeling is deferred. Treat 0.25 / 0.50 as placeholders.
85
+ - **Narrowing personas:** do not use `personasForRoute()` for a page stricter than its folder
86
+ (Cost Factors). See `mvc-data-access-patterns`.
87
+
88
+ ## Security
89
+ - `config.*.ini` already carries committed plaintext secrets (team-accepted, see architecture
90
+ Known issues), including a reused plaintext DB password across `config.production.ini`
91
+ sections — flagged for rotation. Location only; no values recorded. Do not add more secrets.
92
+
93
+ ## Change history
94
+ - 2026-06-29 — Built the Talos Pricing UI: nav folder + 4 pages (Onboarding w/ live JS estimate and contract-signing that locks the band schedule into `TalosClients`+`TalosPricingBands` in one txn; read-only Pricing Dashboard + Usage Benchmarks; technical-only Cost Factors editor) and `App_Talos_Estimator` (reads `TalosCostFactors`, derives conversations/user so sales need not enter it). Reads Team DB via new `db_team` connection. Offerings CHAT/VOICE_TO_VOICE/NATURAL_VOICE = 1.0/0.25/0.50 (voice ambiguous, deferred). (jcardinal)
@@ -12,6 +12,6 @@
12
12
  | [NetSuite REST Client (_Component_Api_Netsuite) — record writes & SuiteQL](features/netsuite-rest-client.md) | `_Component_Api_Netsuite` is the **2.0 `_underscore` NetSuite REST client** — the shared primitive every worker2/api2 NetSuite caller uses for record GETs, Suit | _underscore/Component/Api/Netsuite/Netsuite.php |
13
13
  | [Per-Client Database Connections & the Local Logs Trap](features/per-client-database-connections.md) | When `_underscore` serves a request for a client it opens **three distinct per-client database connections**, not one. | _underscore/Database.php, _underscore/ApiRequest.php, _underscore/Model/Client/Logs/Api.php |
14
14
  | [Recursive Item Fulfillments (upstream mirroring)](features/recursive-item-fulfillments.md) | In a multi-tier supply chain a sales order (SO) spawns a purchase order (PO) that becomes another SO downstream, and so on. | _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Client/ItemFulfillmentItem.php, _underscore/Model/Client/ItemFulfillmentItemUnit.php, _underscore/Model/Client/ItemFulfillmentPackage.php, _underscore/Model/Compass/AdvanceShippingNotice.php, dbchanges2/Core/2026-02-13 - 75601 - RecursiveItemFulfillmentCreation.sql, dbchanges2/Core/2026-06-04 - RecursiveItemFulfillmentPut.sql |
15
- | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
15
+ | [Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)](features/surface-resolver.md) | The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a cached resolver, `_Model_Core_Surface::resolve(&$api, string | _underscore/Model/Core/Surface.php, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql, dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql, _underscore/Model/Core/SurfaceElement.php, _underscore/Model/Core/Action.php, _underscore/Model/Core/Vocabulary.php, _underscore/Model/Core/VocabularyTerm.php, _underscore/Model/Core/Message.php, _underscore/Model/Client/SurfaceOverride.php, _underscore/Model/Client/MessageTranslation.php, _underscore/Model/Client/ThemeToken.php, _underscore/Model/Core/Page.php |
16
16
  | [Tracking-Number Bridge Migration (ASN / Item Fulfillment / Item Receipt)](features/tracking-number-bridges.md) | Shipment tracking numbers used to live as **scalar FK columns** (`trackingNumberId`, `returnTrackingNumberId`) directly on the lowest-level "unit"/"item" tables | api2/Component/Api/V2/V2.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnit.php, _underscore/Model/Client/AdvanceShippingNoticeItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillmentItemUnits/TrackingNumber.php, _underscore/Model/Client/ItemFulfillment.php, _underscore/Model/Prudential/AdvanceShippingNotice.php, _underscore/Model/Compass/AdvanceShippingNotice.php, _underscore/Trait/Netsuite/ItemFulfillment.php, api2/Component/Api/Cxml/Cxml.php, dbchanges2/Client/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Core/2026-06-10 - TrackingNumberBridges.sql, dbchanges2/Client_Prudential/2026-06-15 - ItemFulfillmentTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Quad/2026-06-18a - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19b - ItemFulfillmentItemReceiptTrackingNumberAclLogicGroups.sql, dbchanges2/Client_Nychh/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql, dbchanges2/Client_Quad/2026-06-19a - FixItemFulfillmentTableViewTrackingAndRoot.sql |
17
17
  | [Units for Items for Purchase Orders — Data Structure](features/units-for-items-for-purchase-orders.md) | Describes how unit (serialized inventory) data is linked to sales-order and purchase-order line items behind the `units-for-items-for-purchase-orders` TableView | |
@@ -11,6 +11,8 @@ owners: [jcardinal]
11
11
  files:
12
12
  - _underscore/Model/Core/Surface.php
13
13
  - dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
14
+ - dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
15
+ - dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
14
16
  - _underscore/Model/Core/SurfaceElement.php
15
17
  - _underscore/Model/Core/Action.php
16
18
  - _underscore/Model/Core/Vocabulary.php
@@ -88,6 +90,20 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
88
90
 
89
91
  ## Gotchas
90
92
 
93
+ - **A Core RecordScript needs `Client.AclRecordScripts` rows per role to be invokable — record-level
94
+ `AclRecordPermissions` is necessary but NOT sufficient.** V2 authorizes a scripted-API call **not**
95
+ via `AclRecordPermissions` but via the Client-DB `AclRecordScripts` table
96
+ (`V2.php getRecordScriptPhpMethod: SELECT ... FROM AclRecordScripts WHERE recordScriptId=? AND
97
+ roleId IN(...)`). The original Surface ACL seed built the record-level `AclRecordPermissions` chain
98
+ (Public(1) READ on `surfaces`) but MISSED the per-role dispatch grants, so every meta call 403'd
99
+ (EZ-1) even though record-level READ was granted. Fix: grant the surfaces `meta` script to roles
100
+ Public(1)/SuperUser(3)/Base(4) and the `debug` (metaDebug) script to SuperUser(3) — mirrors the
101
+ existing TableView/Page `meta` scripts. The grant migration is **id-agnostic** (resolves
102
+ `recordScriptId` by route join, so it works on the renumbered 333 block or the originally-provisioned
103
+ 2300 block) and **NOT EXISTS-guarded** (re-runnable). ⚠ Provisioning risk: a `BLANK_CLIENT_DATABASE`
104
+ snapshot may not carry these `AclRecordScripts` rows, so a newly-cut client could repeat the 403
105
+ unless the dated migrations replay on provision — confirm BLANK-snapshot vs migration-replay before
106
+ the next client is cut.
91
107
  - **`_Model_Core_Page::meta()` (Model/Core/Page.php, ~1300 lines) is the engine being replaced** and
92
108
  is dangerous to extend: it interpolates `LIKE '$slug'` (injection-prone), **runs INSERTs during a
93
109
  read**, is N+1, hand-duplicates the 5-layer cascade 6× via UNION, and is uncached. The empirical
@@ -104,7 +120,11 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
104
120
  framework base `_Model` defines a NON-static `debug()` (Model.php:167) and the scripted-API
105
121
  dispatch calls the method statically. Renamed to `metaDebug` and remapped the RecordScript
106
122
  (`route 'debug' → phpMethod 'metaDebug'`). Vet any new scripted-API method name against the
107
- `_Model` base (e.g. `debug`, and check others) before mapping it.
123
+ `_Model` base (e.g. `debug`, and check others) before mapping it. **Already-provisioned envs are a
124
+ latent fatal:** the rename landed in code + the renumbered seed, but envs provisioned before it
125
+ (dev.sandbox) keep the old `phpMethod='debug'` and would fatal if `GET /v2/surfaces/debug` were hit.
126
+ Ship an id-agnostic idempotent `UPDATE` (`SurfaceDebugPhpMethodFix.sql`) to repoint the existing
127
+ RecordScript on those envs.
108
128
  - **The endpoint is `GET /v2/surfaces/meta?slug=<slug>`, not `/v2/surfaces/<slug>/meta`.** TOGA
109
129
  RecordScripts read their args from the **query string**; the path form makes the engine parse the
110
130
  slug as a record uuid and return 404 EV-6. Pattern for any RecordScript:
@@ -118,6 +138,12 @@ resolver, not "later"). It is **not** named `debug()` — see gotchas.
118
138
  admin-CRUD sibling records stay Super-User-only (the resolver reads their tables server-side).
119
139
 
120
140
  ## Change history
141
+ - 2026-06-29 — Deploy fixes (round 2): grant the surfaces `meta`/`debug` RecordScripts in
142
+ `Client.AclRecordScripts` per role (Public/SuperUser/Base) — record-level `AclRecordPermissions`
143
+ alone left every meta call 403 EZ-1 because V2 dispatch authorizes via `AclRecordScripts`
144
+ (`SurfaceRecordScriptAcl.sql`, id-agnostic + NOT EXISTS-guarded). Shipped the id-agnostic idempotent
145
+ `SurfaceDebugPhpMethodFix.sql` to repoint the still-`debug` phpMethod on pre-rename envs (latent
146
+ fatal). Flagged BLANK-provision may not carry these dispatch grants. (jcardinal)
121
147
  - 2026-06-29 — Deploy fixes: renamed the scripted inspector `debug()`→`metaDebug()` (name collided
122
148
  with non-static `_Model::debug()`, fatal); corrected the endpoint to query-string form
123
149
  `GET /v2/surfaces/meta?slug=` (path form 404'd EV-6); granted CORE Public(1) READ on the `surfaces`
@@ -50,8 +50,16 @@ traffic), behavior is **byte-for-byte unchanged** — pure data, zero extra work
50
50
  them. Grant Public(1) `allowRead` on `surfaces` only (the admin-CRUD sibling records stay
51
51
  Super-User-only — the resolver reads their tables server-side). See
52
52
  [surface-resolver](../../_underscore/features/surface-resolver.md) gotchas for the full chain.
53
+ - **Record-level READ is necessary but NOT sufficient — dispatch is authorized via
54
+ `Client.AclRecordScripts`.** V2 (`getRecordScriptPhpMethod`) checks `AclRecordScripts` by
55
+ `recordScriptId` + `roleId`, not `AclRecordPermissions`. Even with Public(1) READ on `surfaces`,
56
+ meta 403'd until the `meta`/`debug` scripts were granted per role in `AclRecordScripts`. See
57
+ [surface-resolver](../../_underscore/features/surface-resolver.md) gotchas.
53
58
 
54
59
  ## Change history
60
+ - 2026-06-29 — Documented the second ACL layer: scripted-API dispatch is authorized via
61
+ `Client.AclRecordScripts` (per `recordScriptId`+`roleId`), not record `AclRecordPermissions` —
62
+ record READ alone still 403'd meta until the dispatch grants were seeded. (jcardinal)
55
63
  - 2026-06-29 — Documented the meta-endpoint ACL: normal app users (CORE Public role 1) need
56
64
  `allowRead` on the `surfaces` record or meta returns 403 EZ-1. (jcardinal)
57
65
  - 2026-06-25 — Added the `surface=<slug>` opt-in option to the V2 engine; attaches per-record action
@@ -3,5 +3,5 @@
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
- | [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-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-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 |
6
+ | [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-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/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 |
7
7
  | [2.0 New-Client Onboarding (manual process)](workflows/client-onboarding.md) | How to manually stand up a new 2.0 client (tenant). | Client/, Client_<Tenant>/, Core/, Logs_Client/ |
@@ -14,6 +14,10 @@ files:
14
14
  - dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql
15
15
  - dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql
16
16
  - dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
17
+ - dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
18
+ - dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
19
+ - dbchanges2/Core/2026-06-29d - VendorItemsSurfaceSeed.sql
20
+ - dbchanges2/Core/2026-06-29e - InventorySurfaceSeed.sql
17
21
  - dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
18
22
  - dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql
19
23
  - dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql
@@ -66,6 +70,24 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
66
70
  - `Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql` — Core seed + the `RecordScripts` resolve row.
67
71
  - `Core/2026-06-29a - ItemsSurfaceSeed.sql` — the Items LIST screen seed: surfaces `items-list` (TABLE → existing `items` TableView id 9, recordId 21) + `items-list-actions` (BUTTON_BAR: refresh/columns/newItem) + Actions + ~11 Messages. Single DEFAULT bundle, no Client overrides.
68
72
  - `Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql` — grants CORE role Public(1) `allowRead` on the `surfaces` record (so app users stop getting 403 EZ-1 on meta). **Id-agnostic**: matches the record by `route='surfaces'`, so it works whether the record id is 333 or the legacy 2300.
73
+ - `Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql` — id-agnostic idempotent `UPDATE` repointing the
74
+ surfaces `debug` RecordScript `phpMethod` from `debug`→`metaDebug` on already-provisioned envs (the
75
+ rename only landed in the renumbered seed; pre-rename envs would fatal on `GET /surfaces/debug`).
76
+ - `Core/2026-06-29d - VendorItemsSurfaceSeed.sql` — the VendorItems LIST screen seed: `vendor-items-list`
77
+ (TABLE → existing `vendor-items` TableView id 10, recordId 19) + `vendor-items-list-actions`
78
+ (BUTTON_BAR: Refresh / Columns[hidden+disabled] / New Vendor Item) + 2 Actions + 11 Messages.
79
+ Single DEFAULT bundle, mirrors Items. (appId supply=1.)
80
+ - `Core/2026-06-29e - InventorySurfaceSeed.sql` — the Inventory LIST screen, **presentation-only**:
81
+ `inventory-list` (TABLE, **recordId NULL + tableViewId NULL** — the page composes
82
+ purchase-orders/items/units via per-level fetchSlugs and has no single backing Record; carries
83
+ `groupByLabels` + `entityTags` in surface `config`) + `inventory-list-actions` (BUTTON_BAR:
84
+ Group by / Refresh). Actions anchored to `items(21)` because `Actions.recordId` is NOT NULL.
85
+ - `Client/2026-06-29c - SurfaceRecordScriptAcl.sql` — grants the surfaces `meta`/`debug` RecordScripts
86
+ in **`Client.AclRecordScripts`** per role (meta → Public(1)/SuperUser(3)/Base(4); debug →
87
+ SuperUser(3)). V2 authorizes scripted-API dispatch via `AclRecordScripts`, NOT record
88
+ `AclRecordPermissions`, so record READ alone left meta 403 EZ-1. Id-agnostic (resolves
89
+ `recordScriptId` by route join) + NOT EXISTS-guarded (re-runnable). See
90
+ [surface-resolver](../../_underscore/features/surface-resolver.md).
69
91
  - `Client/2026-06-25a - SurfaceClientTables.sql` — the 3 Client tables + `INSERT IGNORE Languages('en','English')`.
70
92
  - `Client/2026-06-25b - SurfaceClientSeed.sql` — `ThemeTokens` + `MessageTranslations(en)`.
71
93
  - `Client/2026-06-25c - SurfaceClientAcl.sql` — full ACL chain for the 3 CLIENT-aclDatabase records (recordId refs updated to the 333-block renumber).
@@ -80,6 +102,12 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
80
102
  or every newly provisioned client is born broken. This was appended and provisioning-tested.
81
103
  Treat it as a CI gate (provision a throwaway client; assert the surface meta endpoint returns the
82
104
  seed).
105
+ - **⚠ Unconfirmed: BLANK-provision may not carry the `AclRecordScripts` dispatch grants.** The
106
+ surface meta endpoint is authorized by `Client.AclRecordScripts` rows (see
107
+ [surface-resolver](../../_underscore/features/surface-resolver.md)), seeded by the dated
108
+ `SurfaceRecordScriptAcl.sql`. If a new client is cut from a BLANK snapshot rather than by replaying
109
+ the dated migrations, it could be born with no dispatch grants and repeat the 403 EZ-1. Confirm how
110
+ BLANK-snapshot vs migration-replay provisioning works before the next client is cut.
83
111
  - **`SurfaceOverrides.value` is a stringly-typed escape hatch** — a long `CONFIG`/label override
84
112
  would silently truncate at varchar(255); that is why `c_longValue mediumtext` exists, and the
85
113
  resolver MUST cast strictly per `attribute` (bool/int/string/json) with a logged fallback.
@@ -95,6 +123,11 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
95
123
  full 4-step ACL chain + field permissions in **each** client DB; resolve `roleId` by subselect.
96
124
 
97
125
  ## Change history
126
+ - 2026-06-29 — Added VendorItems + Inventory LIST seeds (`VendorItemsSurfaceSeed.sql` recordId 19 /
127
+ TableView 10; `InventorySurfaceSeed.sql` presentation-only, recordId+tableViewId NULL, Actions
128
+ anchored to items(21)). Added `SurfaceRecordScriptAcl.sql` (per-role `AclRecordScripts` dispatch
129
+ grants — record READ alone 403'd) and `SurfaceDebugPhpMethodFix.sql` (repoint `debug`→`metaDebug`
130
+ on pre-rename envs). Flagged BLANK-provision may miss the `AclRecordScripts` grants. (jcardinal)
98
131
  - 2026-06-29 — Renumbered the reserved seed id blocks (Records 2300-2308→**333–341**, RecordFields
99
132
  2400+→**2246–2433**) in the seed files only — known file-vs-DB drift in provisioned envs. Added
100
133
  the Items LIST seed (`ItemsSurfaceSeed.sql`) and the id-agnostic CORE Public(1) READ ACL grant on
@@ -6,8 +6,8 @@ project: TOGa IQ
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-16
10
- owners: [akhokhani]
9
+ updated: 2026-06-29
10
+ owners: [akhokhani, jcardinal]
11
11
  files:
12
12
  - talos/libs/aegra-api/src/aegra_api/main.py
13
13
  - talos/libs/aegra-api/src/aegra_api/settings.py
@@ -28,6 +28,9 @@ related:
28
28
  - features/mcp-servers.md
29
29
  - features/observability.md
30
30
  - features/deployment.md
31
+ - features/pricing-cogs-model.md
32
+ - ../worker2/features/talos-pricing-automation.md
33
+ - ../../1.0/apps/tools/features/talos-pricing-ui.md
31
34
  ---
32
35
 
33
36
  ## Summary
@@ -152,6 +155,31 @@ lives (env var name, config key) but never the value itself.
152
155
  user-managed** (external). Health check hits `/live`. See
153
156
  `features/deployment.md`.
154
157
 
158
+ ## Talos Pricing Platform (cross-framework)
159
+
160
+ A database-backed pricing/margin system replacing the Excel calculator. Three tiers:
161
+
162
+ - **System of record — Team DB (9 `Talos*` tables, dbchanges2):** TalosClients (contract
163
+ header), TalosPricingBands (locked per-user band schedule), TalosCostFactors (editable
164
+ key/value cost constants — the "technical tab"), TalosUsageMonthly + TalosUsageFeatureMonthly
165
+ (Langfuse import targets), TalosAwsActualsMonthly (Cost Explorer), TalosClientUserCounts
166
+ (headcount for band-breach), TalosCalibrationMonthly + TalosFeeRecommendations (cron-derived).
167
+ Keyed by `clientIdentifier` (= Langfuse `org:<slug>` tag and AWS cost tag) + `periodMonth`
168
+ (1st-of-month DATE), UNIQUE on those for idempotent upsert. Tenant link to `Core.Clients` is a
169
+ plain **indexed column, not a hard FK** (Team DB may be a separate instance). Conventions:
170
+ id+uuid, dtCreated/dtUpdated, InnoDB, utf8mb4_0900_ai_ci.
171
+ - **Sales/admin UI — tools (1.0):** onboarding (locks band schedule), dashboard, benchmarks,
172
+ technical-only Cost Factors editor + `App_Talos_Estimator`. Reads Team DB via `db_team`.
173
+ - **Automation — worker2 (2.0):** monthly crons import AWS actuals, calibrate Langfuse→AWS,
174
+ recompute margins/recommendations, send the leadership report. Uses `_underscore::DB_TEAM`.
175
+
176
+ **Data flow:** Langfuse usage (structure) + AWS Cost Explorer (dollars) → calibrate per client →
177
+ margin vs. locked band → org-fee recommendation after N consecutive out-of-band months.
178
+
179
+ **Critical rules:** Team-DB fact tables are upsert-keyed on `(clientIdentifier, periodMonth
180
+ [, toolCategory])` — never INSERT-only. The Core tenant link is an indexed column, not an FK.
181
+ Always use Langfuse's cache-corrected cost (it undercounts cache badly).
182
+
155
183
  ## Key decisions
156
184
 
157
185
  - **Agent Protocol over a custom shape** — clients (TOGa Hub, TOGa View, third-party)
@@ -13,20 +13,45 @@ related:
13
13
  - ../architecture.md
14
14
  - aegra-api.md
15
15
  - observability.md
16
+ - ../../worker2/features/talos-pricing-automation.md
17
+ - ../../../1.0/apps/tools/features/talos-pricing-ui.md
16
18
  ---
17
19
 
18
20
  ## Summary
19
21
 
20
22
  The cost basis and pricing methodology for selling Talos (TOGa IQ) chat and
21
- voice deployments. The deliverable is a **per-client Excel workbook — TALOS
22
- Pricing Calculator v9** (replaces v8) — that lives on the team's shared AI drive
23
- (under `P:\Work\AI\`), **not** in any repo. This doc captures the durable
24
- knowledge: the cost drivers, measured production rates, and the pricing
25
- decisions, so anyone can re-price Talos without re-deriving the model.
26
-
27
- There is no Talos source code behind this — it is a sales/finance model. Treat
28
- the numbers below as the **April 2026 baseline**; re-measure before relying on
29
- them in a later period.
23
+ voice deployments. This doc captures the durable knowledge: the cost drivers,
24
+ measured production rates, and the pricing decisions, so anyone can re-price
25
+ Talos without re-deriving the model.
26
+
27
+ The COGS token economics below remain valid. **The delivery mechanism has
28
+ pivoted** (2026-06-29) from the per-client Excel workbook (TALOS Pricing
29
+ Calculator v9) to a **database-backed platform**: a Team-DB system of record +
30
+ an onboarding/dashboard UI in the **tools** app (1.0) + **worker2** cron
31
+ automation that calibrates Langfuse usage to AWS actuals each month. Why: Excel
32
+ can't scale and forced sales to guess inputs (esp. avg conversations/user) they
33
+ don't know but we already have in Langfuse. See
34
+ [talos-pricing-ui](../../../1.0/apps/tools/features/talos-pricing-ui.md) and
35
+ [talos-pricing-automation](../../worker2/features/talos-pricing-automation.md);
36
+ a platform architecture doc captures the cross-framework topology.
37
+
38
+ Treat the numbers below as the **April 2026 baseline**; re-measure before relying
39
+ on them in a later period.
40
+
41
+ ## Cost methodology — calibrate Langfuse to AWS (decision, 2026-06-29)
42
+
43
+ Langfuse provides the **structure** (granular per-feature / per-user breakdown);
44
+ AWS provides the **absolute dollars**. Each month, per client:
45
+
46
+ ```
47
+ calibration_factor = AWS_actual / Langfuse_costCorrected
48
+ ```
49
+
50
+ Apply the factor to Langfuse's granular figures so structure comes from Langfuse,
51
+ dollars from AWS. New clients (no AWS history) use a **global blended factor**
52
+ (`SUM(aws) / SUM(langfuse)` across clients) until they have their own rolling
53
+ 3-month factor. **Langfuse badly undercounts cache cost** (sample: $201 recorded
54
+ vs $1,088 corrected) — always use the **cache-corrected** Langfuse figure.
30
55
 
31
56
  ## Chat COGS model
32
57
 
@@ -111,31 +136,41 @@ approach. Per-minute drivers:
111
136
 
112
137
  Adopted to **reduce client-facing price fluctuation while protecting margin**:
113
138
 
114
- 1. **Two-part price.** Client pays a **flat monthly org fee** PLUS a **fixed
115
- monthly per-user fee**. The per-user fee is a stable rate-card value and does
116
- **not** change.
117
- 2. **Margin band.** Sales sets a min and max gross-margin band. The recommended
118
- flat fee is solved so projected margin lands at the **midpoint** of the band:
119
-
120
- ```
121
- flat = COGS / (1 - mid_margin) - per_user_fee * users (floored at 0)
122
- ```
123
- 3. **Smoothing rule.** Actual COGS is tracked monthly per client. The flat fee
124
- is changed **only after** actual margin sits outside the band for a
125
- configurable number of **consecutive** months (**default 3**). The per-user
126
- fee is **never** adjusted.
127
-
128
- ### Workbook structure (TALOS Pricing Calculator v9)
129
-
130
- | Tab | Purpose |
131
- |---|---|
132
- | Sales | Rep-facing inputs + recommended flat fee + min/mid/max margin-band options |
133
- | Voice | Per-minute band pricing |
134
- | Technical | Single source of truth for all raw rates — sales never edits |
135
- | Actuals | Monthly actual-AWS-COGS entry, margin-vs-band status, consecutive-breach flags, suggested mid-band flat fee |
136
- | Engine | Hidden calculation engine |
137
-
138
- The workbook is a **per-client** file on the shared AI drive (`P:\Work\AI\`).
139
+ 1. **Two-part price.** Client pays a **flat monthly org fee** (the adjustable
140
+ margin lever) PLUS a **per-user fee that steps by user-count band**.
141
+ 2. **Locked band schedule.** The per-user **band rate schedule** is fixed at
142
+ **contract signing**. When user count crosses a band, the per-user fee
143
+ auto-steps to the **pre-agreed band rate**; then the **org fee** is adjusted
144
+ to restore margin. The per-user fee **never changes outside the agreed band
145
+ schedule**.
146
+ 3. **Smoothing rule.** Actual margin is tracked monthly per client. The org fee
147
+ change is **only recommended after** margin sits outside the min/max band for
148
+ a configurable number of **consecutive** months (`consecutiveMonths`,
149
+ per-client; default 3).
150
+
151
+ > **Pivot note (2026-06-29):** the earlier model used a *fixed* per-user fee +
152
+ > flat fee solved to the band midpoint. The current model makes the per-user fee
153
+ > a **locked band schedule** (steps with headcount) and uses the **org fee** as
154
+ > the margin lever. The band schedule is stored in `TalosPricingBands`, locked at
155
+ > signing in the tools onboarding UI.
156
+
157
+ ### Delivery (current — database platform)
158
+
159
+ The model now lives across three repos (schema home + topology in the platform
160
+ architecture doc):
161
+
162
+ - **Team DB (9 `Talos*` tables)** — `TalosClients`, `TalosPricingBands`,
163
+ `TalosCostFactors`, `TalosUsageMonthly` + `TalosUsageFeatureMonthly`,
164
+ `TalosAwsActualsMonthly`, `TalosClientUserCounts`, `TalosCalibrationMonthly`,
165
+ `TalosFeeRecommendations`. (`dbchanges2/Team/...TalosPricingTables.sql`)
166
+ - **tools app (1.0)** — onboarding (locks the band schedule), pricing dashboard,
167
+ usage benchmarks, technical-only Cost Factors editor + `App_Talos_Estimator`.
168
+ - **worker2 (2.0)** — monthly crons: import AWS actuals, recompute calibration +
169
+ margins/recommendations, send leadership report.
170
+
171
+ The previous **TALOS Pricing Calculator v9** Excel workbook (Sales / Voice /
172
+ Technical / Actuals / Engine tabs, per-client file under the shared AI drive) is
173
+ **superseded** by this platform but documents the same economics.
139
174
 
140
175
  ## Gotchas / known issues
141
176
 
@@ -150,4 +185,5 @@ The workbook is a **per-client** file on the shared AI drive (`P:\Work\AI\`).
150
185
  under the team shared AI drive; the path can move.
151
186
 
152
187
  ## Change history
188
+ - 2026-06-29 — Pricing **delivery pivoted** from the Excel calculator to a DB platform (Team DB 9 `Talos*` tables + tools 1.0 UI + worker2 crons). Cost methodology recorded as **calibrate Langfuse→AWS** (per-client factor = AWS/Langfuse_corrected; global-blended fallback for new clients; always use cache-corrected Langfuse). Methodology refined: per-user fee is now a **locked band schedule** (steps with headcount) with the **org fee** as the margin lever (was fixed per-user + midpoint-solved flat fee). COGS token economics unchanged. (jcardinal)
153
189
  - 2026-06-29 — Initial pricing/COGS model doc: chat token + feature drivers, voice per-minute drivers, two-part flat+per-user methodology with mid-band solve and consecutive-month smoothing. Captured from TALOS Pricing Calculator v9 (replaces v8). v9 verified to reproduce v8 exactly ($0.811182/conversation, $2,071.18/mo for 50 users × 20 conversations, Document Search only). History ×7 vs ×15-exchanges quirk preserved for parity and flagged for technical review. (jcardinal)
@@ -8,4 +8,4 @@
8
8
  | [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 |
9
9
  | [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 |
10
10
  | [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/, toga25-supply/src/hooks/useTableCellInteractions.ts |
11
- | [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/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, 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/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/fieldsConfig/index.ts |
11
+ | [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/surface/evaluateSurfaceRule.ts, toga25-supply/src/surface/actionRegistry.ts, 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/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 |
@@ -22,6 +22,11 @@ files:
22
22
  - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx
23
23
  - toga25-supply/src/pages/Items/ItemsPage.tsx
24
24
  - toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx
25
+ - toga25-supply/src/pages/VendorItems/VendorItemsPage.tsx
26
+ - toga25-supply/src/pages/VendorItems/viewModel/useVendorItemsPageViewModel.tsx
27
+ - toga25-supply/src/pages/Inventory/Inventory.tsx
28
+ - toga25-supply/src/pages/Inventory/viewModel/useInventoryPageViewModel.tsx
29
+ - toga25-supply/src/pages/Inventory/viewModel/FIELDS/index.ts
25
30
  - toga25-supply/src/fieldsConfig/index.ts
26
31
  related:
27
32
  - ../../_underscore/features/surface-resolver.md
@@ -41,7 +46,11 @@ feature flag — see below). Backend: [surface-resolver](../../_underscore/featu
41
46
  - **`useFetchSurfaceMeta`** — the single fetch-once/React-Query-cached choke point for a surface's
42
47
  meta. Loading a different *record* into the same surface reuses cached meta. Builds the URL as
43
48
  `/surfaces/meta?slug=...` (query string), **not** `/surfaces/<slug>/meta` (the path form makes the
44
- engine parse the slug as a record uuid → 404 EV-6).
49
+ engine parse the slug as a record uuid → 404 EV-6). Unwraps the bundle via **`extractBundle()`**,
50
+ which locates the bundle by its **structural fingerprint** (the `elements` array) across
51
+ `raw` / `raw.data` / `raw.data.<route-key>` — because V2 nests a scripted-API return value one
52
+ level deeper, under a route-keyed slot of `data` (e.g. `data.meta`, `data.surfaces`). On a malformed
53
+ payload it falls back to an empty-elements bundle so the page never crashes.
45
54
  - **`evaluateSurfaceRule`** — the Tier-1 rule evaluator: a **frozen `all/any/none` + `{field,op,value}`
46
55
  grammar** that **throws on an unknown op** (generalized from the SalesOrders
47
56
  `buildPatchedTenantFields`/`evaluateEnableRule`/`resolveFlag` helpers). Evaluated client-side against
@@ -61,13 +70,14 @@ legacy pre-Surface fallback branches at the wired screens removed). Legacy per-s
61
70
 
62
71
  ## What's wired
63
72
 
64
- Login + sales-orders list + the sales-order modal + the **Items LIST** screen. The SalesOrder action
73
+ Login + sales-orders list + the sales-order modal + the **Items**, **VendorItems**, and
74
+ **Inventory** LIST screens. The SalesOrder action
65
75
  bar reproduces `orderViewFields.json` approve/deny/approvalWorkflow/viewLog/editOrder by delegating
66
76
  to the existing `onActionClick`/`onOpenLog` handlers. TABLE-type surfaces stay on the existing
67
77
  TableView pipeline via `tableViewSlug` (see [meta-driven-table-data](meta-driven-table-data.md)) —
68
78
  the Surface layer references the TableView, never replaces it.
69
79
 
70
- ## Per-screen migration recipe (proven on SalesOrders + Items)
80
+ ## Per-screen migration recipe (proven on SalesOrders + Items + VendorItems + Inventory)
71
81
 
72
82
  The screen-by-screen full-app refactor follows one recipe:
73
83
  1. Seed the screen's surface(s) in a `Core/*SurfaceSeed.sql` — a TABLE surface that references the
@@ -82,8 +92,51 @@ Keep TABLE rendering on the existing TableView pipeline; only the chrome (action
82
92
  moves to Surface. A screen with no client/role/lang variance ships a single DEFAULT bundle (no
83
93
  Client overrides), as Items did.
84
94
 
95
+ **VendorItems (screen 2)** followed the recipe exactly: `vendor-items-list` (TABLE → TableView 10,
96
+ recordId 19) + `vendor-items-list-actions` (Refresh / Columns[hidden+disabled] / New Vendor Item),
97
+ page+view-model rewired to resolve title+buttons from the bundle, dropped the
98
+ `useClientFields`/`vendorItemsPageFields` dependency, deleted `vendorItemsPageFields.json` + its
99
+ `fieldsConfig/index.ts` entry.
100
+
101
+ **Inventory (screen 3) — presentation-only, by decision.** Only the page title, header buttons
102
+ (Group by / Refresh), the Group-by modal copy, and the entity icon/color tag map moved to Surface.
103
+ The `inventoryGroupings` `groupings[].levels[]` topology
104
+ (`fetchSlug`/`role`/`additionalDataSlug`/`urlParamKey` driving `GenericNestedTables`) is **data-fetch
105
+ wiring, not presentation**, so per the config/logic boundary it **stays as JSON**. The
106
+ `inventory-list` surface carries `groupByLabels` + `entityTags` in its surface `config`, with
107
+ **recordId+tableViewId NULL** (the page has no single backing Record — it composes
108
+ purchase-orders/items/units via per-level fetchSlugs). Labels + `entityTags` were stripped from both
109
+ DEFAULT and NYCHH `inventoryGroupings.json` (groupings kept) and marked `@deprecated`/optional in the
110
+ `InventoryGroupingsConfig` type.
111
+
112
+ **The config/logic line for migration:** move presentation (titles, button labels, icons, colors,
113
+ modal copy, visibility/enable) to Surface; **leave data-fetch wiring (fetch slugs, roles, URL params,
114
+ nested-table topology) in JSON/code.** Not every screen reduces to a generic `SurfaceSection` swap —
115
+ see SalesOrders below.
116
+
117
+ ## SalesOrders remaining migration — bespoke, NOT a generic SurfaceSection swap (deferred)
118
+
119
+ The SalesOrders detail migration is the plan's explicitly-deferred "blessed escape hatch"/approval
120
+ work — it is bespoke-component + business-logic work, not a generic swap. `SalesOrderSummaryGrid` is
121
+ fully tenant-data-driven through three **bespoke renderers**: `detailSection` (blox `DetailSection` +
122
+ custom `renderField`), `locationCard` (`OrderSectionCard`, structured contact data), and `totalsCard`
123
+ (blox `BaseTotals` **with client-specific recurring-lease business logic**:
124
+ `_totalLease !== 0 → recurringKey` fields). The generic `SurfaceSection` (label/value) cannot
125
+ represent `BaseTotals`/`OrderSectionCard`, and the early-seeded order-details/ship-to/bill-to/
126
+ order-summary SECTION surfaces do **not** cleanly map onto the grid's arbitrary client-keyed section
127
+ model. A faithful migration needs a **registered-renderer approach** keyed off surface element config
128
+ (`detailSection`/`locationCard`/`totalsCard` → their components), with the lease logic staying in
129
+ code/`_underscore`, plus reconciling those seeds and the Compass approval-modal state machine.
130
+ **Deferred to a dedicated design-first session.**
131
+
85
132
  ## Gotchas
86
133
 
134
+ - **Scripted-API GET return values land under `data.<scriptRoute>`, not `data` directly.** V2 nests a
135
+ scripted-API return value inside the envelope under a route-keyed slot of `data` (e.g. `data.meta`,
136
+ `data.surfaces`), one level deeper than a normal data fetch. The bundle hook unwraps this with
137
+ `extractBundle()` by structural fingerprint (the `elements` array). Consumers (`SurfaceSection`,
138
+ `SurfaceActionBar`) are hardened to bail to `null` unless `elements` is a real array, so a malformed
139
+ payload never crashes the page. (Root cause of the "s.elements is not iterable" runtime crash.)
87
140
  - **No feature flag, no fallback.** Surface is the enforced path; a missing/broken surface bundle
88
141
  does not silently fall back to legacy JSON — that path is deleted per screen as it migrates.
89
142
  - **Endpoint is query-string form** — `GET /v2/surfaces/meta?slug=...`, not `/surfaces/<slug>/meta`
@@ -94,6 +147,13 @@ Client overrides), as Items did.
94
147
  treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
95
148
 
96
149
  ## Change history
150
+ - 2026-06-29 — Migrated VendorItems (screen 2, recipe-exact) and Inventory (screen 3,
151
+ **presentation-only by decision** — fetch topology stays JSON; only title/buttons/modal copy/entity
152
+ tags moved). Fixed the "s.elements is not iterable" crash: `extractBundle()` now finds the bundle by
153
+ its `elements`-array fingerprint across `raw`/`raw.data`/`raw.data.<route-key>` (V2 nests
154
+ scripted-API returns under `data.<scriptRoute>`), with an empty-bundle fallback + hardened
155
+ SurfaceSection/SurfaceActionBar guards. Recorded SalesOrders detail as deferred bespoke
156
+ (registered-renderer + lease logic + approval-modal) work. (jcardinal)
97
157
  - 2026-06-29 — DECISION: Surface is now enforced, not flagged — removed `VITE_SURFACE_ENABLED`/
98
158
  `SURFACE_ENABLED` (deleted `featureFlag.ts` + all gating/legacy-fallback branches). Fixed
99
159
  `useFetchSurfaceMeta` to the query-string endpoint `/surfaces/meta?slug=` (path form 404'd EV-6).
@@ -18,6 +18,7 @@
18
18
  | [DB-Driven Notification (Internal) Email](features/notification-email.md) | Internal/notification emails (merge-conflict alerts, ops notices — anything system-generated, not client-facing transactional mail) are sent through one worker | worker2/Worker/Notification/Email.php, _underscore/Model/Client/EmailTemplate.php, dbchanges2/Client/2026-06-23a - EmailTemplateWrapper.sql, dbchanges2/Client_True/2026-06-23a - EmailTemplateWrapper.sql |
19
19
  | [Startech Webhook Handler (worker2)](features/startech-webhook-handler.md) | Receives inbound webhook events from Startech (Easeedesk) and creates or updates the corresponding ticket in TOGA 2.0. | worker2/Worker/Startech.php |
20
20
  | [Talos (TOGa IQ) Meeting-Notes Integration & Token Auto-Refresh (consumer)](features/talos-meeting-notes-integration.md) | How a **dev tool / agent consumes Talos (TOGa IQ)** to query the team meeting-notes corpus programmatically. | .claude/skills/plan-ticket/scripts/talos.js |
21
+ | [Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)](features/talos-pricing-automation.md) | The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model` and tools `talos-pricing-ui` docs for the other halves). | worker2/Worker/Talos/Pricing.php, worker2/Database/TalosPricingCrons.sql |
21
22
  | [Team Sprint Management & Reporting](features/team-sprint-management.md) | `_Worker_Team_Sprint` (file `Worker/Team/Sprint.php`) is the engine behind TOGA's internal **development-sprint process and reporting**. | worker2/Worker/Team/Sprint.php |
22
23
  | [Teams Meeting Transcript Export](features/teams-transcript-export.md) | `_Worker_Team_Transcripts` (action `Team/Transcripts/Export`) polls Microsoft Graph for Teams meeting transcripts produced by a set of organizers, classifies ea | worker2/Worker/Team/Transcripts.php, worker2/Config/production.ini, worker2/Database/TeamsTranscriptExports.sql, dbchanges2/Core/2026-06-18a - Teams Transcript Export schedule.sql |
23
24
  | [VAPI Webhook Handler (worker2 — AI-BDR end-of-call processing)](features/vapi-webhook-handler.md) | `_Worker_Vapi` ([worker2/Worker/Vapi.php](worker2/Worker/Vapi.php)) is the **PHP side of the AI-BDR call loop** — the webhook that receives VAPI's end-of-call r | worker2/Worker/Vapi.php, worker2/Worker/Ai/Bdr/Vapi.php |
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: Talos Pricing Automation (worker2 Cron — AWS Actuals, Calibration, Monthly Report)
3
+ framework: "2.0"
4
+ repo: worker2
5
+ project: Worker
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-29
10
+ owners: [jcardinal]
11
+ files:
12
+ - worker2/Worker/Talos/Pricing.php
13
+ - worker2/Database/TalosPricingCrons.sql
14
+ related:
15
+ - ./creating-worker-actions.md
16
+ - ../architecture.md
17
+ - ../../talos/architecture.md
18
+ - ../../talos/features/pricing-cogs-model.md
19
+ - ../../../1.0/apps/tools/features/talos-pricing-ui.md
20
+ ---
21
+
22
+ ## Summary
23
+
24
+ The worker2 half of the **Talos Pricing Platform** (see the talos `pricing-cogs-model`
25
+ and tools `talos-pricing-ui` docs for the other halves). `_Worker_Talos_Pricing` is an
26
+ **abstract** worker class with three monthly cron actions that turn raw usage/cost data
27
+ into per-client margin tracking and a leadership report. All state lives in the **Team DB**
28
+ (9 `Talos*` tables — schema home is the platform architecture doc); the worker reads/writes
29
+ it via `_underscore::DB_TEAM`.
30
+
31
+ This replaces the Excel "Actuals" tab as the monthly recompute mechanism. The COGS token
32
+ economics are unchanged — only the delivery/automation changed.
33
+
34
+ ## How it works
35
+
36
+ `initialize()` registers the Team DB connection via `_underscore::DB_TEAM` (same pattern as
37
+ `_Worker_Team_Transcripts`) so every action has it. The three actions are registered as
38
+ `Core.CronJobs` rows (no code wiring — see `creating-worker-actions`), fired on the **4th of
39
+ the month**, sequenced 06:00 / 07:00 / 08:00 Central so each step's inputs are present
40
+ before the next runs.
41
+
42
+ ### 1. ImportAwsActuals (06:00)
43
+ Calls AWS Cost Explorer `GetCostAndUsage` per client, filtered by `LINKED_ACCOUNT` or a
44
+ cost-allocation **tag** (assumed key `Client` — open item, confirm). Upserts one row per
45
+ `(clientIdentifier, periodMonth)` into `TalosAwsActualsMonthly`. The AWS Cost Explorer SDK
46
+ is vendored in `worker2/vendor`.
47
+
48
+ ### 2. RecomputeMargins (07:00)
49
+ The core calibration step. For each client/month:
50
+ - **Calibration factor** = `AWS_actual / Langfuse_costCorrected` (write to
51
+ `TalosCalibrationMonthly`, `DECIMAL(14,8)`). Always use Langfuse's **cache-corrected**
52
+ cost — Langfuse badly undercounts cache cost (sample: $201 recorded vs $1,088 corrected).
53
+ - **New clients** (no AWS history) use a **global blended factor** = `SUM(aws) / SUM(langfuse)`
54
+ across all clients, until they have their own rolling-3-month factor.
55
+ - Apply the factor to Langfuse's granular per-feature/per-user structure so **structure comes
56
+ from Langfuse, absolute dollars from AWS**.
57
+ - Compute per-client margin vs. the client's min/max band, update the consecutive out-of-band
58
+ **streak**, and only emit a `TalosFeeRecommendations` row once the streak ≥ the client's
59
+ configured `consecutiveMonths`.
60
+
61
+ ### 3. MonthlyReport (08:00)
62
+ Builds an `.xlsx` with **PhpSpreadsheet** (vendored at
63
+ `_underscore/Component/Library/PhpOffice` — first use in worker2) and sends a templated
64
+ leadership email via `_Model_Client_EmailTemplate::send(...)` to the `True` client,
65
+ rendering `{var}` placeholders through AWS SES.
66
+
67
+ ## Gotchas / open items
68
+ - **Idempotent upsert key** — every fact table is keyed `(clientIdentifier, periodMonth)`
69
+ (usage-by-feature adds `toolCategory`), with a UNIQUE on those columns, so re-running a
70
+ cron is safe.
71
+ - **`REPORT_EMAIL_TEMPLATE` is a placeholder UUID** — needs a real `EmailTemplates` record in
72
+ `Client_True` before MonthlyReport sends.
73
+ - **`_Email` attachment support unconfirmed** — verify before relying on the xlsx attachment.
74
+ - **AWS cost-tag key assumed `Client`** — confirm against the actual cost-allocation tag.
75
+ - **Importer contract (other dev):** `TalosUsageMonthly` + `TalosUsageFeatureMonthly` are the
76
+ Langfuse → MySQL targets. Map the Langfuse org tag `org:True` → `clientIdentifier` `"True"`;
77
+ upsert on `(clientIdentifier, periodMonth[, toolCategory])`.
78
+
79
+ ## Security
80
+ - Hardcoded AWS SES SMTP credentials live in `_underscore/Email.php` (pre-existing, not
81
+ introduced here) — flagged for rotation. Location only; no values recorded.
82
+
83
+ ## Change history
84
+ - 2026-06-29 — Built `_Worker_Talos_Pricing` (abstract) with 3 monthly crons — ImportAwsActuals (Cost Explorer), RecomputeMargins (calibrate Langfuse→AWS, rolling-3mo + global-blended fallback, margin/streak/recommendation), MonthlyReport (PhpSpreadsheet xlsx + templated SES email to True). Registers Team DB via `_underscore::DB_TEAM`; crons are `Core.CronJobs` rows on the 4th, 06/07/08:00 CT. Replaces the Excel "Actuals" recompute. (jcardinal)
@@ -12,12 +12,12 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
12
12
  - **walmarttechservices** (Walmart Tech Services) — 1 doc(s) → [1.0/apps/walmarttechservices/INDEX.md](1.0/apps/walmarttechservices/INDEX.md)
13
13
  - **test** (Test) — 12 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
14
14
  - **toga** (TOGa) — 2 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
15
- - **tools** (Tools) — 5 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
15
+ - **tools** (Tools) — 7 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
16
16
 
17
17
  ## 2.0 framework
18
18
 
19
19
  - **_underscore** (_Underscore) _(framework core)_ — 17 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
20
- - **worker2** (Worker) — 21 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
20
+ - **worker2** (Worker) — 22 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
21
21
  - **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
22
22
  - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
23
23
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
@@ -4,11 +4,13 @@ framework: "2.0"
4
4
  apps:
5
5
  - _underscore
6
6
  - tools
7
+ - worker2
8
+ - dbchanges2
7
9
  project: _Underscore
8
10
  client: true
9
11
  type: profile
10
12
  status: active
11
- updated: 2026-06-26
13
+ updated: 2026-06-29
12
14
  owners: [jcardinal]
13
15
  files: []
14
16
  related:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.228",
3
+ "version": "1.0.230",
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",