toga-ai 1.0.258 → 1.0.260

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.
@@ -14,6 +14,6 @@
14
14
  | [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 |
15
15
  | [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 |
16
16
  | [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 |
17
- | [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-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, 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 |
17
+ | [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, _underscore/Model/Client/AclRecordScript.php, _underscore/Model/Core/RecordScript.php, dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql, dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql, dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql, 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 |
18
18
  | [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 |
19
19
  | [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 | |
@@ -6,12 +6,16 @@ project: _Underscore
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-30
10
- owners: [jcardinal]
9
+ updated: 2026-07-01
10
+ owners: [jcardinal, apeterson]
11
11
  files:
12
12
  - _underscore/Model/Core/Surface.php
13
+ - _underscore/Model/Client/AclRecordScript.php
14
+ - _underscore/Model/Core/RecordScript.php
13
15
  - dbchanges2/Core/2026-06-30a - SurfaceMetaGroupAndSalesOrderSections.sql
14
16
  - dbchanges2/Client/2026-06-30a - SurfaceMetaGroupAcl.sql
17
+ - dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql
18
+ - dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql
15
19
  - dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
16
20
  - dbchanges2/Client/2026-06-29c - SurfaceRecordScriptAcl.sql
17
21
  - dbchanges2/Core/2026-06-29c - SurfaceDebugPhpMethodFix.sql
@@ -113,6 +117,32 @@ which surfaced latent resolution gaps in `_buildBundle`/`_resolveElement`:
113
117
  `_Model_<Client>_*` overrides + `Client_ApiPayloadInterceptor` hooks — the Surface layer does
114
118
  **not** absorb business logic.
115
119
 
120
+ ## How a surfaces scripted-API call is authorized (route-level gate)
121
+
122
+ Authorizing a surfaces script (`meta`, `meta-group`, `debug`) is a **two-layer** check. Get the
123
+ first layer wrong and you get a **403 `EZ-1 AUTHORIZATION`** before any element ACL runs.
124
+
125
+ 1. **Route-level dispatch gate — `Client_<tenant>.AclRecordScripts`.** The surfaces dispatcher
126
+ authorizes each API script (route + method + phpMethod) via the **CLIENT-tier**
127
+ `AclRecordScripts` table (cols `uuid`, `recordScriptId` → `Core.RecordScripts`, `roleId` →
128
+ `Client_<tenant>.Roles`). The check is keyed on the JWT's **`id.client.roles`** (client roles),
129
+ **NOT** the platform `core.roles` and **NOT** the Core `AclRecordPermissions` chain.
130
+ `Core.RecordScripts` maps route/method/phpMethod → an ACL "record" (`recordId`). The three
131
+ surfaces scripts: `meta` (GET, phpMethod `meta`), `meta-group` (GET, `metaGroup`), `debug` (GET,
132
+ `metaDebug`). `_Model_Core_Surface::resolve/metaGroup` read the caller's roles from
133
+ **`id.client.roles`** (Surface.php ~lines 56/87).
134
+ 2. **Element-level ACL (runs *inside* `meta`/`metaGroup`, AFTER the route gate passes)** — dropping
135
+ individual surface elements uses a **separate** client-tier table `_Model_Client_AclActionPermission`,
136
+ gated by each `SurfaceElement.aclActionId`. This is the "composition, not absorption" step in
137
+ `resolve()` step 4; it never causes the route-level 403.
138
+
139
+ **Debugging a surfaces 403 EZ-1 (hard-won):** it is the route-level gate — a missing
140
+ `Client_<tenant>.AclRecordScripts` grant resolved by **client role**. Do **not** chase
141
+ `Core.AclRecordPermissions`, `appId` scoping, or `AclLogicGroups`/`AclRecordExpressions` — all were
142
+ investigated and ruled out as red herrings: `appId = NULL` is the normal working pattern, and the
143
+ Core record grants + their logic-group expressions all evaluate `all`/`"1"`. The gate is purely
144
+ "does this client role have an `AclRecordScripts` row for this `recordScriptId`?"
145
+
116
146
  ## Gotchas
117
147
 
118
148
  - **A Core RecordScript needs `Client.AclRecordScripts` rows per role to be invokable — record-level
@@ -167,8 +197,33 @@ which surfaced latent resolution gaps in `_buildBundle`/`_resolveElement`:
167
197
  per-role `Client.AclRecordScripts` dispatch grant. Verified via toga-db — `meta` script id 25 was
168
198
  granted roles 1/3/4 but `meta-group` (id 27) had **none** until the grant ran. Any new surfaces
169
199
  scripted-API method repeats this: record READ never authorizes dispatch.
200
+ - **The meta-group grant is seeded per-tenant, not globally — new tenants repeat the 403.** When
201
+ `meta-group` shipped, the `AclRecordScripts` grant was seeded **only** for `Client_Compass`
202
+ (recordScriptId → roles 1,3,4). `Client_Quad` and `Client_CompassCanada` had **no** grant for the
203
+ meta-group recordScriptId at all — their `meta` was granted to client role 1 (why `meta` worked but
204
+ `meta-group` 403'd for the same user/token). Same-user-different-script + same-script-different-tenant
205
+ is the fingerprint of this gap. Fix (2026-07-01): two `dbchanges2/Client_<tenant>/` migrations
206
+ granting meta-group to **client role 1** (parity with each tenant's existing `meta` grant),
207
+ id-agnostic (resolve `Core.RecordScripts` id by route/method/phpMethod, cross-DB read of `Core.*`
208
+ per the 2026-06-29c ACL pattern) and re-runnable via `NOT EXISTS`. Surface-layer DB changes ship to
209
+ the **beta** environment only. ⚠ **Root-cause not yet closed:** the meta-group grant should be
210
+ added to the `dbchanges2/Client/` **baseline (blank) seed** so newly-cut clients get it
211
+ automatically — otherwise the next migrated tenant hits the identical 403. Only Compass (already
212
+ correct), Quad, and CompassCanada are addressed. Also note **role breadth**: Compass exposes
213
+ surfaces to roles 1,3,4; Quad/CC got role 1 only (parity with their `meta`). If Quad/CC should
214
+ match Compass, a follow-up migration aligning both `meta` and `meta-group` to roles 1,3,4 is needed.
170
215
 
171
216
  ## Change history
217
+ - 2026-07-01 — Documented the full route-level authorization mechanism (new section): the surfaces
218
+ script gate is `Client_<tenant>.AclRecordScripts` keyed on JWT `id.client.roles` (client roles,
219
+ not `core.roles`, not `Core.AclRecordPermissions`); element-drop is a separate
220
+ `_Model_Client_AclActionPermission`/`aclActionId` layer running inside `resolve()` after the route
221
+ gate; captured the red-herring list (`appId=NULL` is normal, Core grants all evaluate `all`/`"1"`).
222
+ Fixed the `meta-group` 403 EZ-1 for **Quad** and **Compass Canada** — the meta-group
223
+ `AclRecordScripts` grant had been seeded for Compass only; added two id-agnostic, NOT-EXISTS-guarded
224
+ `Client_<tenant>/` migrations granting meta-group to client role 1 (beta env only). Flagged the
225
+ open root-cause: add the grant to the `Client/` baseline seed, and decide role breadth (1 vs 1,3,4)
226
+ for Quad/CC. (apeterson)
172
227
  - 2026-06-30 — Added `metaGroup()` (`GET /v2/surfaces/meta-group?slugs=a,b,c`): batched composition
173
228
  over the existing per-slug `resolve()` cache, capped at `META_GROUP_MAX_SLUGS=25`, unknown slug →
174
229
  empty bundle (chosen over a `parentSurfaceId` GROUP row post-CTO-review). Fixed latent
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-25
9
+ updated: 2026-07-01
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/src/fieldsConfig/index.ts
@@ -64,6 +64,22 @@ and the `useClientFields()` hook.
64
64
  `yourKey: BY_CLIENT[clientSlug] ?? DEFAULT_*`. Avoids editing all ~13 role bundles.
65
65
  `DEFAULT_CLIENT_FIELDS` must also carry the new key (ultimate fallback).
66
66
 
67
+ ### De-roling a client×role bundle to client-only
68
+
69
+ Some bundles were role-keyed (`{ADMIN, MANAGER}`) only for historical reasons — the roles held
70
+ byte-identical config. Those are being flattened to **client-only** (part of the ongoing
71
+ backend-driven / de-role goal). `orderViewFields` is now client-only: `COMPASS` and `COMPASSCANADA`
72
+ read their flat client JSON via `mergeOrderView(<client>OrderViewFields)` in
73
+ `src/fieldsConfig/index.ts` instead of reading `.ADMIN`/`.MANAGER` off the file, and
74
+ `FIELDS/COMPASSCANADA/orderViewFields.json` was flattened from `{ADMIN, MANAGER}` to a flat object.
75
+
76
+ When de-roling, watch the read path: `COMPASS`'s JSON was **already flat** but was still being read
77
+ via `.ADMIN` → `undefined` → the client silently fell back to `DEFAULT` and lost its
78
+ `salesOrderDetailsConfig` joins. This is the flat-bundle trap from the gotcha below — the fix is to
79
+ read the bundle itself, not a role key. `approvalViewFields` and `salesOrdersPageFields` remain
80
+ role-keyed (their ADMIN/MANAGER genuinely differ); fully de-roling those and removing `resolveRole`
81
+ is a separate pending task.
82
+
67
83
  ### Hydration (non-serializable bits)
68
84
 
69
85
  Keep JSON **serializable** — strings, enums, arrays. No JSX, no functions, no Tailwind classes.
@@ -143,6 +159,12 @@ modal's chrome from the same per-client JSON, not from the component:
143
159
  `vendorItemRecordViewFields` are record-modal-layout examples living under `src/layout/.../viewModel/FIELDS/`.
144
160
 
145
161
  ## Change history
162
+ - 2026-07-01 — De-roled `orderViewFields` from client×role to **client-only**: COMPASS/COMPASSCANADA
163
+ now read a flat client JSON via `mergeOrderView(...)` in `fieldsConfig/index.ts` (COMPASSCANADA's
164
+ JSON flattened from byte-identical `{ADMIN,MANAGER}`). Fixed a latent bug where COMPASS's
165
+ already-flat JSON was read via `.ADMIN` → `undefined` → silent fallback to DEFAULT (lost its
166
+ `salesOrderDetailsConfig` joins). `approvalViewFields`/`salesOrdersPageFields` stay role-keyed;
167
+ full de-role + `resolveRole` removal remains pending. Type-check clean. (apeterson)
146
168
  - 2026-06-25 — SalesOrder action buttons render by presentation intent: `kind` (icon/text/viewLog) picks the renderer, `variant` a closed color set, `valueKey` is pure click-dispatch; a `componentRegistry` keyed by `kind` hydrates dedicated components (e.g. ViewLogButton). Added the `recordActionFields`-shape gotcha (per-persona blocks + sibling `poNumber` sub-config; walk `actionOptions` only). (apeterson)
147
169
  - 2026-06-25 — Extended to record-modal layouts: per-mode `*ViewFields`/`*EditFields` JSON under `src/layout/.../viewModel/FIELDS/`, JSON-driven modal `header` (modalTag/labelTemplate/statusBadge) + `editItem` button gated via the Flag/Rule engine, and the flat-bundle defensive-default gotcha. (apeterson)
148
170
  - 2026-06-23 — Documented from the `add-client-fields` skill during initial knowledge seed. (apeterson)
@@ -15,12 +15,13 @@ client: compass-canada
15
15
  type: profile
16
16
  status: active
17
17
  updated: 2026-06-30
18
- owners: [jcardinal, bala, tcox]
18
+ owners: [jcardinal, bala, tcox, apeterson]
19
19
  files: []
20
20
  related:
21
21
  - ../compass-usa/profile.md
22
22
  - ../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md
23
23
  - ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
24
+ - ../../2.0/apps/_underscore/features/surface-resolver.md
24
25
  ---
25
26
 
26
27
  ## Summary
@@ -50,6 +51,12 @@ to but distinct from Compass USA. Like Compass USA it spans the **2.0** commerce
50
51
  see [Config-Driven Expedited Shipping Gating](../../2.0/apps/toga2-commerce/features/expedited-shipping-gating.md).
51
52
 
52
53
  ## Notes
54
+ - **Surface layer (beta):** like Quad, `Client_CompassCanada` had **no** `AclRecordScripts` dispatch
55
+ grant for the surfaces `meta-group` script (only `meta`, to client role 1) → `GET /v2/surfaces/meta-group`
56
+ 403'd EZ-1 while `meta` worked. Fixed 2026-07-01 via
57
+ `dbchanges2/Client_CompassCanada/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql` (meta-group →
58
+ client role 1, id-agnostic + NOT-EXISTS-guarded, beta only). Exposes surfaces to role 1 only, vs
59
+ Compass USA's 1/3/4. See [Surface Resolver](../../2.0/apps/_underscore/features/surface-resolver.md).
53
60
  - Customer language preference: `UserGlobalSettings.settingId = 2` (`en` / `fr-CA`); customer-
54
61
  facing emails are sent in EN or FR accordingly.
55
62
  - Assortment (product-grouping) names are served in fr-CA via the `AssortmentTranslations` sidecar
@@ -14,11 +14,12 @@ client: quad
14
14
  type: profile
15
15
  status: active
16
16
  updated: 2026-06-30
17
- owners: ["jcardinal", "bala"]
17
+ owners: ["jcardinal", "bala", "apeterson"]
18
18
  files: []
19
19
  related:
20
20
  - ../../2.0/apps/_underscore/features/tracking-number-bridges.md
21
21
  - ../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md
22
+ - ../../2.0/apps/_underscore/features/surface-resolver.md
22
23
  ---
23
24
 
24
25
  ## Summary
@@ -48,6 +49,13 @@ Client-specific DB change-sets live in `dbchanges2/Client_Quad/`.
48
49
  (`_underscore/Model/Quad/SalesOrder.php`): non-SA orders use the inline machine, `SA%` orders
49
50
  delegate to the base `_Model_Client_SalesOrder`. Contrast Compass, which is shipped-only. See
50
51
  [IF stage lifecycle & order status](../../2.0/apps/_underscore/features/item-fulfillment-stage-lifecycle-and-order-status.md).
52
+ - **Surface layer (beta):** the surfaces `meta-group` script had **no** `AclRecordScripts` dispatch
53
+ grant for `Client_Quad` (only `meta` was granted, to client role 1), so `GET /v2/surfaces/meta-group`
54
+ 403'd (EZ-1) while `meta` worked. Fixed 2026-07-01 via
55
+ `dbchanges2/Client_Quad/2026-07-01a - GrantSurfacesMetaGroupScriptAcl.sql` (grants meta-group to
56
+ client role 1, id-agnostic + NOT-EXISTS-guarded, beta only). Quad exposes surfaces to role 1 only,
57
+ vs Compass's 1/3/4 — revisit if broader roles are needed. See
58
+ [Surface Resolver](../../2.0/apps/_underscore/features/surface-resolver.md).
51
59
  - Tracking data: record 318 (item-level) is currently empty for this client; their tracking
52
60
  populates the IF/shipment level (record 317, ~98% coverage) and unit level (record 319). The
53
61
  rebuilt views use 318 (per the Compass pattern) and will show tracking once item-level
@@ -14,40 +14,41 @@ updated: 2026-06-30
14
14
  # Session: talos-pricing-platform
15
15
  **Date:** 2026-06-30
16
16
  **Project/Repo:** tools (1.0) + worker2 (2.0) + dbchanges2 (2.0 Team DB)
17
- **Task:** Build a data-driven Talos pricing platform (Team-DB schema + tools onboarding/dashboard UI + worker2 cron automation) to replace the Excel calculator; then fix tools SSO login that was failing in production.
17
+ **Task:** Build a data-driven Talos pricing platform (Team-DB schema + tools onboarding/dashboard UI + worker2 cron automation) to replace the Excel calculator; then fix tools SSO login (both consumer and initiate legs) that was failing in production.
18
18
 
19
19
  ---
20
20
 
21
21
  ## What WORKED
22
- - **Schema (Team DB):** `dbchanges2/Team/2026-06-29a - TalosPricingTables.sql` — 9 tables created, SQL-reviewed (0 critical), all 7 important review fixes applied (dtUpdated on fact tables, DECIMAL(6,4) margins, isFeature* boolean naming, DECIMAL(14,8) calibration factors, full column comments, band-range CHECK). Tables live in the **Team** DB; `dbchanges2/Team/` is the migration folder (Team-DB migrations use unqualified table names).
23
- - **worker2 automation:** `worker2/Worker/Talos/Pricing.php` (`_Worker_Talos_Pricing`) + `worker2/Database/TalosPricingCrons.sql` — `php -l` clean. Three monthly cron actions: ImportAwsActuals (Cost Explorer), RecomputeMargins (calibration + margin/streak/recommendation), MonthlyReport (PhpSpreadsheet xlsx + templated email). initialize() registers Team DB via `_underscore::DB_TEAM` (mirrors `_Worker_Team_Transcripts`).
24
- - **tools UI:** nav folder + 6 pages + estimator + CSS, all `php -l` clean. `App_Talos_Estimator` (`tools/_/app/talos/estimator.php`), onboarding get/post, pricing dashboard, usage benchmarks, cost-factors get/post. Live JS estimate + recommended per-band fees on the onboarding form.
25
- - **SSO 500 fix:** `tools/mvc/sso/get.php` `tools_ssoFail()` — added `if (!headers_sent())` guard around `http_response_code(401)`. `php -l` clean. Root cause confirmed: `App_FrameworkIndex::render()` flushes the preloader buffer (`page.php:213`) BEFORE `body()`/`loadFile()`, so headers are already sent when an mvc page runs.
26
- - **SSO key sourcing:** `tools/_/app/auth.php` `App_Auth::decryptHandoffValue()` rewritten to read `API_SECRET_ACCESS_TOKEN` / `API_SECRET_ACCESS_TOKEN_PREVIOUS` from `Core.Parameters` (rotated there) via new cached `handoffKeys()` + `const CORE_DB='db_toga2core'`. `php -l` clean.
27
- - **Crypto format confirmed:** `App_String::encryptWithKey/decryptWithKey` (library/app/string.php:1075-1094) = AES-256-CBC, `base64(iv . base64ciphertext)`, interoperable with 2.0 `_String`. The user's real `?saml=` payload decoded to exactly that shape — proving the SSO failure was a stale-key/config mismatch, not a format bug.
28
- - **Knowledge captured + pushed twice:** pricing platform (6 docs incl. an approved ELEVATED architecture section in `2.0/apps/talos/architecture.md`) and SSO fixes (2 tools feature-doc updates). Both PUSHED to `_main`.
22
+ - **Schema (Team DB):** `dbchanges2/Team/2026-06-29a - TalosPricingTables.sql` — 9 tables, SQL-reviewed (0 critical), all 7 important review fixes applied (dtUpdated on fact tables, DECIMAL(6,4) margins, isFeature* booleans, DECIMAL(14,8) calibration factors, column comments, band-range CHECK). `dbchanges2/Team/` is the Team-DB migration folder (unqualified table names).
23
+ - **worker2 automation:** `worker2/Worker/Talos/Pricing.php` (`_Worker_Talos_Pricing`) + `worker2/Database/TalosPricingCrons.sql` — `php -l` clean. 3 monthly crons: ImportAwsActuals (Cost Explorer), RecomputeMargins (calibration + margin/streak/recommendation), MonthlyReport (PhpSpreadsheet xlsx + EmailTemplate). initialize() registers Team DB via `_underscore::DB_TEAM`.
24
+ - **tools UI:** nav folder + 6 pages + `App_Talos_Estimator` + CSS, all `php -l` clean. Onboarding get/post (live JS estimate + recommended per-band fees), pricing dashboard, usage benchmarks, cost-factors get/post.
25
+ - **SSO 500 fix (consumer leg):** `tools/mvc/sso/get.php` `tools_ssoFail()` — added `if (!headers_sent())` guard around `http_response_code(401)`. Root cause: `App_FrameworkIndex::render()` flushes the preloader buffer (`page.php:213`) BEFORE `body()`/`loadFile()`, so headers are already sent when any mvc page runs.
26
+ - **SSO key sourcing → Core.Parameters (BOTH legs):** `tools/_/app/auth.php` — `decryptHandoffValue()` (consumer) reads keys via new cached `handoffKeys()`; added public `currentApiSecret()` for the initiate leg; `const CORE_DB='db_toga2core'`. `tools/mvc/sso/initiate/get.php` now passes `'apiSecretAccessToken' => App_Auth::currentApiSecret()`. Both `php -l` clean.
27
+ - **Crypto format confirmed:** `App_String::encryptWithKey/decryptWithKey` (library/app/string.php:1075-1094) = AES-256-CBC, `base64(iv . base64ciphertext)`, interoperable with 2.0 `_String`. The real `?saml=` payload decoded to exactly that shape — proving SSO failure was a key/config issue, not a format bug.
28
+ - **"Sign in failed" root cause found + fixed:** that banner is `login/get.php` `?error=1`, set ONLY by `sso/initiate/get.php:33,38` when `App_Sso::initiate()` throws. The initiate leg still read `api_secret_access_token` from config; after it was removed from config (per earlier wrong advice), `App_Sso::initiate()` (library/app/sso.php:64-65, empty() guard) threw InvalidArgumentException → `/login?error=1`. Fixed by sourcing the secret from Core.Parameters on the initiate leg too.
29
+ - **Knowledge captured + pushed (4 times):** pricing platform (6 docs incl. approved ELEVATED `2.0/apps/talos/architecture.md` section), SSO fixes (`saml-sso-auth.md` + `mvc-data-access-patterns.md`), and the SSO-initiate correction (`saml-sso-auth.md` again). All PUSHED to `_main`.
29
30
 
30
31
  ## What did NOT work — DO NOT RETRY THESE
31
- - **`App_Database::escape()` does NOT exist** — the 1.0 escaper is `App_Database::sqlEscape()`. Using `::escape` fatals.
32
- - **Passing `App_Database::query(...)` inline to `buildArrayOfRows()`/`fetchOne()`** triggers PHP "Only variables should be passed by reference" (both take `&$res`); with display_errors on it renders into output. MUST assign the query result to a variable first (the login post.php does this).
33
- - **Reading SSO tokens from `config.production.ini` `[saml]`** — they go stale because the gateway rotates `API_SECRET_ACCESS_TOKEN`; that stale token was why decrypt returned false and login failed. Do not rely on config tokens; use Core.Parameters.
34
- - **Calling `http_response_code()` / `header()` / `session_regenerate_id()` from any page loaded inside `body()`** — the preloader buffer is already flushed, so it fatals/no-ops. Always guard with `headers_sent()`.
35
- - **Writing `[database_team]`/`[database_toga2core]` into config with a guessed host** — avoided deliberately; the 2.0 Core/Team host (`production-core-cluster…us-west-2`, from config.alpha.ini `[database_prod_toga2core]`) differs from the 1.0 prod-cluster and the 2.0 client cluster (`reader1.client.database.togahub.com` = db_true). Don't assume they're the same host.
32
+ - **Removing `api_secret_access_token` from `config.production.ini` `[saml]`** — broke SSO. The INITIATE leg (`sso/initiate/get.php`) reads it from config to encrypt the RelayState; empty → `App_Sso::initiate()` throws `InvalidArgumentException('...missing required parameter: apiSecretAccessToken')` → `/login?error=1` "Sign in failed". DO NOT keep the rotating secret in config; it now comes from Core.Parameters on both legs.
33
+ - **`App_Database::escape()`** does NOT exist — use `App_Database::sqlEscape()`.
34
+ - **Passing `App_Database::query(...)` inline to `buildArrayOfRows()`/`fetchOne()`** (both take `&$res`) — triggers PHP "Only variables should be passed by reference" (renders with display_errors on). Assign the query result to a variable first.
35
+ - **Calling `http_response_code()`/`header()`/`session_regenerate_id()` from a page loaded inside `body()`** — preloader buffer already flushed → fatals/no-ops. Guard with `headers_sent()`.
36
+ - **Writing `[database_team]`/`[database_toga2core]` with a guessed host** — avoided. The 2.0 Core/Team host is `production-core-cluster…us-west-2` (from config.alpha.ini `[database_prod_toga2core]`), distinct from the 1.0 prod-cluster and the 2.0 client cluster (`reader1.client.database.togahub.com` = db_true).
36
37
 
37
38
  ## Not tried yet (candidates for next session)
38
- - Seeding/calibrating `TalosCostFactors` from the real Langfuse aggregates (estimator currently ships placeholder DEFAULTS).
39
- - Wiring the xlsx as an actual email attachment — `_Email` attachment support is unconfirmed (`MonthlyReport` writes xlsx to a temp path; email body has the HTML table meanwhile).
39
+ - Seeding/calibrating `TalosCostFactors` from real Langfuse aggregates (estimator ships placeholder DEFAULTS).
40
+ - Wiring xlsx as an email attachment — `_Email` attachment support unconfirmed (MonthlyReport writes xlsx to temp; email body has HTML table).
40
41
  - Confirming the AWS Cost Explorer cost-allocation tag key (worker assumes `Client`).
41
- - Creating the leadership `EmailTemplates` record in Client_True and replacing the placeholder `REPORT_EMAIL_TEMPLATE` UUID in `_Worker_Talos_Pricing`.
42
- - Verifying the live SSO login end-to-end after the `[database_toga2core]` connection is added + deployed.
42
+ - Creating the leadership `EmailTemplates` record in Client_True and replacing the placeholder `REPORT_EMAIL_TEMPLATE` UUID.
43
+ - Live end-to-end SSO verification after `[database_toga2core]` is configured + deployed.
43
44
 
44
45
  ## Current file state
45
46
  | File | Status | Notes |
46
47
  |------|--------|-------|
47
48
  | dbchanges2/Team/2026-06-29a - TalosPricingTables.sql | created | 9 Talos* tables, reviewed+fixed. Apply to Team DB. |
48
- | worker2/Worker/Talos/Pricing.php | created | `_Worker_Talos_Pricing`; lint clean. REPORT_EMAIL_TEMPLATE is a placeholder UUID. |
49
+ | worker2/Worker/Talos/Pricing.php | created | `_Worker_Talos_Pricing`; lint clean. REPORT_EMAIL_TEMPLATE placeholder UUID. |
49
50
  | worker2/Database/TalosPricingCrons.sql | created | 3 Core.CronJobs seeds (4th of month). Not yet inserted. |
50
- | tools/_/app/nav.php | modified | Added "Talos Pricing" folder (4 links; Cost Factors narrowed to technical personas). |
51
+ | tools/_/app/nav.php | modified | "Talos Pricing" folder (4 links; Cost Factors narrowed to technical personas). |
51
52
  | tools/_/app/talos/estimator.php | created | `App_Talos_Estimator`; placeholder DEFAULT factors. |
52
53
  | tools/mvc/talos/onboarding/get.php + post.php | created | Form + live estimate; writes TalosClients + TalosPricingBands in a txn. |
53
54
  | tools/mvc/talos/pricing/get.php | created | Read-only margin/recommendation dashboard (db_team). |
@@ -55,21 +56,23 @@ updated: 2026-06-30
55
56
  | tools/mvc/talos/factors/get.php + post.php | created | Technical-only cost-factor editor (upsert TalosCostFactors). |
56
57
  | tools/assets/css/style.css | modified | Appended .pricing-table / .badge / form-grid / estimate-readout styles. |
57
58
  | tools/mvc/sso/get.php | modified | headers_sent() guard in tools_ssoFail() (500→graceful 401). |
58
- | tools/_/app/auth.php | modified | decryptHandoffValue() reads keys from Core.Parameters via db_toga2core; added handoffKeys() + CORE_DB const. |
59
+ | tools/_/app/auth.php | modified | decryptHandoffValue() + currentApiSecret() read tokens from Core.Parameters via db_toga2core; handoffKeys() cache; CORE_DB const. |
60
+ | tools/mvc/sso/initiate/get.php | modified | apiSecretAccessToken now from App_Auth::currentApiSecret() (Core.Parameters), not config. |
59
61
 
60
62
  ## Decisions made
61
- - **Pricing model:** flat monthly ORG FEE (the adjustable margin lever) + per-user fee that STEPS by user-count band; band rate schedule locked at signing. On a band crossing the per-user fee auto-steps to the agreed rate, THEN the org fee is adjusted to restore margin. Rejected: pure per-user banded (no lever) and both-vary.
62
- - **Cost methodology:** calibrate Langfuse→AWS (monthly factor = AWS_actual / Langfuse_costCorrected; new clients use a global blended factor). Rejected: AWS-only and Langfuse-only. Always use Langfuse's cache-corrected cost (it undercounts cache badly).
63
- - **Table location:** the **Team** DB (`_underscore::DB_TEAM`) — what the user called "Core.Teams". Tenant link to Core.Clients is a plain indexed column, NOT a hard FK (Team DB may be a separate instance).
63
+ - **Pricing model:** flat monthly ORG FEE (adjustable margin lever) + per-user fee that STEPS by user-count band; band rate schedule locked at signing; on a band crossing the per-user fee auto-steps, THEN org fee is adjusted. Rejected: pure per-user banded; both-vary.
64
+ - **Cost methodology:** calibrate Langfuse→AWS (monthly factor = AWS_actual / Langfuse_costCorrected; new clients use a global blended factor). Always use cache-corrected Langfuse cost. Rejected: AWS-only; Langfuse-only.
65
+ - **Table location:** Team DB (`_underscore::DB_TEAM`). Tenant link to Core.Clients is a plain indexed column, NOT a hard FK (Team DB may be a separate instance).
64
66
  - **Onboarding writes:** direct DB write from the 1.0 tools app (not via api2) for speed.
65
- - **SSO keys from Core.Parameters:** sourced at runtime (rotated) instead of static config, via a `db_toga2core` connection alias; keeps current→previous rotation fallback. Rejected: continuing to read config `[saml]` tokens.
67
+ - **SSO secret source:** the rotating `API_SECRET_ACCESS_TOKEN` (+ `_PREVIOUS`) lives ONLY in `Core.Parameters`, read by BOTH SSO legs (initiate encrypts RelayState; consumer decrypts current→previous) via the `db_toga2core` connection. Config copies go stale on rotation and silently break SSO. `[saml] true_client_uuid` and the IdP URLs STAY in config. Rejected: config-stored tokens.
66
68
 
67
69
  ## Blockers
68
- - **SSO login still fails until the `[database_toga2core]` connection is added to the tools configs and tools is redeployed** — the `db_toga2core` alias is referenced by `App_Auth::CORE_DB` but no `[database_toga2core]` section exists in `config.production.ini` yet (developer is handling creds). Section template: hostname `production-core-cluster.cluster-clwbyqvxdm4q.us-west-2.rds.amazonaws.com`, username awsroot, dbname `Core`.
69
- - All Talos UI pages need the `[database_team]` connection (db_team → Team DB) before they render data (also developer-supplied creds).
70
+ - **SSO login still fails until `[database_toga2core]` is added to the tools configs AND tools is redeployed.** `App_Auth::CORE_DB='db_toga2core'` is referenced by both SSO legs, but no `[database_toga2core]` section exists in `config.production.ini` yet (developer handling creds). Template: hostname `production-core-cluster.cluster-clwbyqvxdm4q.us-west-2.rds.amazonaws.com`, username awsroot, dbname `Core`. DIAGNOSTIC: a **500 at initiate** = db_toga2core unconfigured (query against unregistered alias throws, uncaught); **/login?error=1** = empty/absent token in Core.Parameters.
71
+ - `Core.Parameters.API_SECRET_ACCESS_TOKEN` must be populated and match the gateway; `[saml] true_client_uuid` must remain in config (consumer leg sso/get.php:80-84 reads it).
72
+ - All Talos UI pages need the `[database_team]` connection (db_team → Team DB) before they render data.
70
73
 
71
74
  ## Exact next step
72
- > Add the `[database_toga2core]` section (alias db_toga2core, dbname=Core, host=production-core-cluster…us-west-2) to `tools/config.production.ini` (and the dev configs you test with), deploy tools, then attempt a fresh SSO login at https://tools.togatech.com/sso to confirm `decryptHandoffValue()` succeeds against the live rotated key.
75
+ > Add `[database_toga2core]` (alias db_toga2core, dbname=Core, host=production-core-cluster…us-west-2) to `tools/config.production.ini` (and the dev configs you test with), confirm `Core.Parameters.API_SECRET_ACCESS_TOKEN` is set and `[saml] true_client_uuid` is still present, deploy tools, then click "Sign in with SSO" at https://tools.togatech.com — expect redirect to the IdP (not `/login?error=1`); a 500 at initiate means db_toga2core isn't wired.
73
76
 
74
77
  ---
75
78
  _Saved by /session-save on 2026-06-30_
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.258",
3
+ "version": "1.0.260",
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",