toga-ai 1.0.205 → 1.0.207

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.
@@ -103,3 +103,13 @@ packages: `phpoffice/phpspreadsheet ^1.6`, `smalot/pdfparser ^0.14.0`.
103
103
  - **Timezone** is always `America/Chicago` (set in `_.php`).
104
104
  - **`__APPROOT__`** is resolved by locating an ancestor folder named `_`; each app must
105
105
  have that `_` folder at its root.
106
+
107
+ ## App_String cross-framework crypto (`encryptWithKey` / `decryptWithKey`)
108
+
109
+ `App_String::encryptWithKey($data, $key)` and `decryptWithKey($data, $key)` (public static) do
110
+ **AES-256-CBC**: random IV via `random_bytes`, returning `base64(iv . ciphertext)` with openssl
111
+ flag `0`. They are **deliberately byte-for-byte interoperable** with the 2.0
112
+ `_String::encryptWithKey` / `decryptWithKey`, so a SAML handoff token encrypted by the 2.0
113
+ gateway decrypts in any 1.0 app (round-trip verified). Net-new methods — no existing method
114
+ changed, no behavior change for existing apps. First consumer: the Tools app's `App_Auth`
115
+ (`1.0/apps/tools/features/saml-sso-auth.md`).
@@ -0,0 +1,8 @@
1
+ # tools (Tools) — 1.0 knowledge
2
+
3
+ | Doc | Summary | Files |
4
+ |-----|---------|-------|
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/_/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
+ | [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 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 |
8
+ | [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/get.php, tools/mvc/login/get.php, tools/mvc/login/post.php, tools/mvc/logout/get.php |
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: Tools (1.0 Internal-Tools App) Architecture
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: architecture
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/index.php
13
+ - tools/_/app/framework.php
14
+ - tools/_/app/frameworkindex.php
15
+ - tools/_/app/auth.php
16
+ - tools/_/app/nav.php
17
+ - tools/common/header.php
18
+ - tools/common/footer.php
19
+ - tools/mvc/get.php
20
+ - tools/mvc/_TEMPLATE/get.php
21
+ - tools/docs/ADDING_A_TOOL.md
22
+ related:
23
+ - ./features/saml-sso-auth.md
24
+ - ./features/persona-gated-navigation.md
25
+ - ./features/developer-tools.md
26
+ - ../library/architecture.md
27
+ - ../library/features/mvc-page-pattern-and-app-skeleton.md
28
+ ---
29
+
30
+ ## Summary
31
+
32
+ **Tools** is a standalone 1.0 (`App_`) application that houses many small internal tools behind
33
+ simple interfaces, gated by Client_True staff persona. It is modeled on `togaview` and depends on
34
+ the `library` core. Auth comes via the SAML gateway `?saml=` handoff; the app owns its own
35
+ session and reads `Client_True` **read-only**. Adding a tool = create `mvc/<route>/get.php`
36
+ (copy `mvc/_TEMPLATE`) + add one `App_Nav::definition()` entry + `App_Auth::requireAuth([...])`.
37
+
38
+ **Critical rules:** Do NOT use bare `<header>` elements in this app — they inherit the legacy
39
+ dashboard stylesheet's `header{position:fixed;width:100vw}` rule and cause horizontal scroll
40
+ (the top header bar was removed for exactly this reason). A nav action's `route` maps **1:1** to
41
+ `mvc/<route>/get.php`. Auth fails **closed** and never auto-creates users; the dev bypass is
42
+ double-gated (`[internal] dev_mode` AND `App_Registry::inDevMode()`). `config.*.ini`
43
+ (incl. `config.prod.ini`) is committed with plaintext production secrets — a known, team-accepted
44
+ risk (see Known issues); never add more secrets and treat these as compromised if leaked.
45
+
46
+ ## Boot & structure
47
+
48
+ Standard 3-line `index.php`: `require '_.php'; App_Framework_Tools::initialize();
49
+ App_Framework_Tools::renderIndex();`. App-specific framework subclasses
50
+ `App_Framework_Tools` / `App_FrameworkIndex_Tools` live in `_/app/`. The 1.0 folder-based MVC
51
+ page pattern is unchanged from the library skeleton (see the library feature doc).
52
+
53
+ `App_Framework_Tools::initialize()` sets the session cookie HttpOnly + SameSite=Lax before
54
+ calling the parent initialize.
55
+
56
+ ## Layout (single-style app)
57
+
58
+ No per-host `stylePath` subfolder — one `assets/css/style.css`. Uses TOGA Technology logos in
59
+ `assets/img` (`toga-brandmark-white.png` in the dark sidebar; `toga-horizontal-colored.png` on
60
+ login/welcome). A fixed left two-column shell: dark left sidebar (`.app-nav`) with brand at top,
61
+ persona-filtered nav in the middle, pinned footer with the user's name + a ghost "Log out"
62
+ button; content area on the right. **No top header bar** (see Critical rules).
63
+
64
+ ## Home route
65
+
66
+ `/` (`mvc/get.php`) renders a persona-filtered dashboard of available tools (tiles grouped by
67
+ folder) when logged in, via `App_Nav::visibleGroups()`, or a welcome/sign-in card when not.
68
+
69
+ ## Adding a tool
70
+
71
+ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one entry to
72
+ `App_Nav::definition()`, and put `App_Auth::requireAuth([...personas])` at the top. Documented in
73
+ `docs/ADDING_A_TOOL.md`. See the App_Nav and App_Auth feature docs.
74
+
75
+ ## Known issues / security
76
+
77
+ - **Plaintext secrets committed to git.** `config.*.ini` (incl. `config.prod.ini`) contains
78
+ plaintext production secrets (RDS master password, SMTP, Payeezy, NetSuite). The team has
79
+ **accepted this for now** (developer decision, 2026-06-25). Future remediation: git-ignore the
80
+ config files and rotate all exposed credentials. (Location only — no values recorded here.)
81
+ - **SSO initiation + replay defense are open items** — see `features/saml-sso-auth.md`.
@@ -0,0 +1,37 @@
1
+ ---
2
+ title: Tools — Developers Folder (UUID & Password Generators)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/mvc/developers/uuid/get.php
13
+ - tools/mvc/developers/password/get.php
14
+ related:
15
+ - ../architecture.md
16
+ - ./persona-gated-navigation.md
17
+ ---
18
+
19
+ ## Summary
20
+
21
+ The first two tools shipped in the Tools app, both under the **Developers** folder and gated to
22
+ personas **Development Team** / **TOGa Technology**. They are the reference example of the
23
+ "one small tool = one `mvc/<route>/get.php`" pattern.
24
+
25
+ ## How it works
26
+
27
+ ### Generate UUID (`mvc/developers/uuid/get.php`)
28
+ - Server generates an RFC 4122 **v4** UUID via `random_bytes(16)` with version/variant bits set.
29
+ - UI is click-to-copy, then instant **client-side** regenerate using `crypto.getRandomValues`.
30
+
31
+ ### Generate Password (`mvc/developers/password/get.php`)
32
+ - Reproduces the standalone `test/team/generate_password.php` script.
33
+ - `App_String::generateSupportCode(8)` produces the password; `App_String::passwordHash()`
34
+ produces the bcrypt hash. The page shows **both** the password and the hash, each copyable.
35
+
36
+ ## Change history
37
+ - 2026-06-25 — Built the first two Developers tools: RFC 4122 v4 UUID generator and a password+bcrypt-hash generator (ported from test/team/generate_password.php), both gated to Development Team / TOGa Technology personas (jcardinal)
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: Tools Persona-Gated Navigation (App_Nav)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/_/app/nav.php
13
+ related:
14
+ - ../architecture.md
15
+ - ./saml-sso-auth.md
16
+ ---
17
+
18
+ ## Summary
19
+
20
+ `App_Nav` is the Tools app's two-level, **persona-gated** navigation. The structure is a single
21
+ **hard-coded `definition()` array** (NOT in the DB), so developers/Claude edit it directly. It
22
+ drives both the left-rail nav and the home dashboard, and is the single source of truth for which
23
+ personas may reach a given route.
24
+
25
+ ## How it works
26
+
27
+ ### Structure
28
+ - Two levels: **folders** (grouping) → **actions** (each loads a page).
29
+ - Each folder and each action carries a `personas` list. An item is visible iff
30
+ `App_Auth::hasAnyPersona()` matches — folder gate plus an optional per-action gate.
31
+
32
+ ### Render surfaces
33
+ - `render()` — emits the filtered left-rail nav with active-route highlighting.
34
+ - `visibleGroups()` — returns the filtered structure as data for the home dashboard tiles.
35
+ - `personasForRoute()` — returns the union of folder+action personas so a page can self-guard
36
+ with `App_Auth::requireAuth(...)` using the same data the nav used.
37
+
38
+ ### Route convention
39
+ An action's `route` (e.g. `/developers/uuid`) maps **1:1** to `mvc/<route>/get.php`. Adding a
40
+ tool means creating that file and adding one entry to `definition()` — see
41
+ `docs/ADDING_A_TOOL.md` and the architecture doc.
42
+
43
+ ## Change history
44
+ - 2026-06-25 — Built App_Nav: hard-coded two-level persona-gated navigation; render() for the left rail, visibleGroups() for the dashboard, personasForRoute() for page self-guarding; route maps 1:1 to mvc/<route>/get.php (jcardinal)
@@ -0,0 +1,76 @@
1
+ ---
2
+ title: Tools SAML SSO Consumer & Persona-Gated Auth (App_Auth)
3
+ framework: "1.0"
4
+ repo: tools
5
+ project: Tools
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - tools/_/app/auth.php
13
+ - tools/mvc/sso/get.php
14
+ - tools/mvc/login/get.php
15
+ - tools/mvc/login/post.php
16
+ - tools/mvc/logout/get.php
17
+ related:
18
+ - ../architecture.md
19
+ - ../../../2.0/apps/saml/features/downstream-integration-contract.md
20
+ - ../../../clients/true/features/users-personas-data-model.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ `App_Auth` is the Tools app's authentication layer: it consumes the SAML gateway `?saml=`
26
+ handoff (see the 2.0 SAML downstream integration contract), establishes its own session, and
27
+ gates every page by **Client_True persona**. The gateway holds no session — session ownership
28
+ is entirely this app's. Auth fails **closed** to a self-contained 401 page; users are **never**
29
+ auto-created.
30
+
31
+ ## How it works
32
+
33
+ ### Consuming the `?saml=` handoff (`mvc/sso/get.php` → `App_Auth`)
34
+ 1. Length-guard `$_GET['saml']`, then base64-decode → `json_decode` → read `payload.client`
35
+ and `payload.user`.
36
+ 2. Decrypt each with `App_String::decryptWithKey()` using config `[saml] api_secret_access_token`,
37
+ with a **dual-key retry** against `api_secret_access_token_previous` (supports gateway key
38
+ rotation). This interoperates byte-for-byte with the 2.0 `_String::encryptWithKey()` that
39
+ produced the token (see the library `App_String` crypto methods).
40
+ 3. Validate the decrypted **client uuid == configured `true_client_uuid`** via `hash_equals`,
41
+ and that both decrypted values match a UUID regex.
42
+ 4. Load the active Client_True user: `WHERE uuid = ? AND isActive = 1`. No match → fail closed.
43
+
44
+ ### Session establishment (`establishSession()`)
45
+ - Calls `session_regenerate_id(true)`, then caches the user and **persona names** in `$_SESSION`.
46
+ - Persona lookup (read-only `db_true`):
47
+ `SELECT p.name FROM Users u JOIN Users_Personas up ON up.userId=u.id JOIN Personas p ON p.id=up.personaId WHERE u.uuid = <escaped>`.
48
+ - The session cookie is set **HttpOnly + SameSite=Lax** in `App_Framework_Tools::initialize()`
49
+ before the parent initialize runs.
50
+
51
+ ### Page guarding
52
+ - `requireAuth([personas])` at the top of each page; `hasAnyPersona()` is `array_intersect`
53
+ against the cached session personas. App_Nav uses the same cached data to filter the nav.
54
+
55
+ ### Dev bypass (double-gated, fails closed in prod)
56
+ `mvc/login/post.php` permits **email-only** login **only** when config `[internal] dev_mode`
57
+ is truthy **and** `App_Registry::inDevMode()` — both must hold. When used it writes a `SECURITY`
58
+ line to `error_log`.
59
+
60
+ ## Gotchas / known issues
61
+
62
+ - **SSO initiation is not implemented in this 1.0 app.** Building the signed AuthnRequest lives
63
+ in 2.0 `_underscore` (`singleSignOnServiceUrl()`); login here points at a configurable
64
+ `[saml] initiation_url` **placeholder**. Open item: needs a gateway initiation entry point or
65
+ a ported builder.
66
+ - **No app-side replay defense.** The handoff token carries no nonce/timestamp the app verifies.
67
+ Recommend the gateway embed `iat` + `jti`.
68
+
69
+ ## Config keys
70
+
71
+ `[database_true]` (read-only Client_True); `[saml]` `api_secret_access_token` /
72
+ `api_secret_access_token_previous`, `initiation_url`, `client_authentication_uuid`,
73
+ `domain_uuid`, `true_client_uuid`; `[internal]` `dev_mode`.
74
+
75
+ ## Change history
76
+ - 2026-06-25 — Built App_Auth: SAML `?saml=` handoff consumer with dual-key decrypt, hash_equals client-uuid check, fail-closed 401, persona-cached session (HttpOnly+SameSite=Lax), and a double-gated dev bypass. Initiation + replay defense left as open items (jcardinal)
@@ -11,5 +11,6 @@
11
11
  | [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 |
12
12
  | [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 |
13
13
  | [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 |
14
+ | [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/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 |
14
15
  | [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 |
15
16
  | [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,7 +6,7 @@ project: _Underscore
6
6
  client: shared
7
7
  type: architecture
8
8
  status: active
9
- updated: 2026-06-11
9
+ updated: 2026-06-25
10
10
  owners: ["jcardinal", "rgirish"]
11
11
  files:
12
12
  - _underscore/_underscore.php
@@ -32,6 +32,9 @@ Minimum PHP 8.1 (enforced in `_underscore.php`). The current architecture is
32
32
  `_Worker_*`). Always use parameterized queries / the `_Db` layer — never interpolate input into SQL.
33
33
  Beware the **lazy-transaction gotcha**: writes issued outside an explicitly committed transaction
34
34
  can be silently dropped — confirm commit semantics before relying on a write.
35
+ The Surface presentation layer replaces `Page::meta()` but `meta()` must NOT be deleted until
36
+ every page is cut over (per-surface, with a parity diff); config describes, PHP decides —
37
+ business logic never moves into Surface config.
35
38
 
36
39
  ## Entry point & boot sequence
37
40
 
@@ -44,6 +47,41 @@ Every 2.0 project's `index.php` is just `<?php require '_underscore.php';`.
44
47
  (parses INI) → `_Time::initialize()` → **`_underscore::initialize()`** (project hook,
45
48
  required) → `_Session::initialize()` → `_Route::initialize()` (web only).
46
49
 
50
+ ## Surface — platform UI presentation/configuration layer
51
+
52
+ A DB-driven UI presentation layer (2026-06-25, CTO-reviewed: AGREE-WITH-ADJUSTMENTS) that
53
+ serves UI config (labels, theme tokens, visibility/enable, icons, tooltips, button bars,
54
+ table quick-actions, status badges, modals) to React frontends so UI changes need no
55
+ front-end deploy and are editable by a future admin app over plain CRUD. Platform-wide
56
+ across all 2.0 apps — not client-specific. Replaces `_Model_Core_Page::meta()`.
57
+
58
+ **Model:** `Surfaces` (a configurable UI region) + `SurfaceElements` (ordered items: ~15
59
+ typed attribute columns + one JSON `config` long-tail) — typed columns, NOT EAV. Behavior
60
+ via an `Actions` catalog (string key + JSON payload; FE registry maps key→handler). i18n via
61
+ `Messages` + per-client `MessageTranslations` (ICU). Theming via semantic `ThemeTokens` (no
62
+ raw CSS in config). `Vocabularies`/`VocabularyTerms` for value→token/label sets. The entire
63
+ cascade lives in ONE sparse `SurfaceOverrides` table (precedence base < client < persona <
64
+ role, language overlay) replacing the ~40-table EAV settings cascade.
65
+
66
+ **Decisions:** typed-columns-not-EAV; `TableViews` kept as the table query/data engine and
67
+ only REFERENCED (Surface type=TABLE → `tableViewSlug`); ACL kept as-is and COMPOSED
68
+ (`aclActionId`), never absorbed; no versioning/draft/publish; server-side cached resolver
69
+ (`_Model_Core_Surface::resolve`, parameterized, zero-write-on-read). Two-tier gating —
70
+ Tier-1 trivial declarative rules (frozen `all/any/none + {field,op,value}` grammar) evaluated
71
+ client-side; Tier-2 business logic computed in PHP returning booleans (lives in
72
+ `_Model_<Client>_*` overrides + interceptors). Config/logic boundary: **config describes,
73
+ PHP decides.** M2M-safe delivery via opt-in `surface=<slug>` request option → response
74
+ `meta.surface`, never `data`. Generic FE machinery destined for `@agilant/toga-blox`.
75
+
76
+ **Rejected alternatives:** a generic EAV settings cascade (the old `*Settings` tables —
77
+ proven near-empty in prod); raw-CSS-in-config (the old "Dummy Fields" anti-pattern).
78
+
79
+ **CTO-mandated guardrails (CI-enforced, not intentions):** rule grammar is a frozen
80
+ allowlist that throws on unknown ops, with a CI test asserting the op-set; defensive
81
+ resolver with logged fallbacks; debug-bundle endpoint ships WITH the resolver; `c_longValue`
82
+ on overrides with strict per-attribute casting; per-surface meta() cutover with a parity
83
+ diff (do NOT delete meta() until every page is migrated); `BLANK_CLIENT_DATABASE` as a CI gate.
84
+
47
85
  ## Naming convention & autoloader
48
86
 
49
87
  Class names map **directly** to file paths: replace each `_` with `/`; the last segment
@@ -285,3 +323,4 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
285
323
 
286
324
  ## Change history
287
325
  - 2026-06-11 — Documented lazy transaction gotcha in `_Database::register()` (rgirish)
326
+ - 2026-06-25 — Added the Surface platform UI presentation/configuration layer (DB-driven UI config replacing `Page::meta()`, CTO-reviewed AGREE-WITH-ADJUSTMENTS) (jcardinal)
@@ -0,0 +1,104 @@
1
+ ---
2
+ title: Surface Resolver (_Model_Core_Surface::resolve — replaces Page::meta)
3
+ framework: "2.0"
4
+ repo: _underscore
5
+ project: _Underscore
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - _underscore/Model/Core/Surface.php
13
+ - _underscore/Model/Core/SurfaceElement.php
14
+ - _underscore/Model/Core/Action.php
15
+ - _underscore/Model/Core/Vocabulary.php
16
+ - _underscore/Model/Core/VocabularyTerm.php
17
+ - _underscore/Model/Core/Message.php
18
+ - _underscore/Model/Client/SurfaceOverride.php
19
+ - _underscore/Model/Client/MessageTranslation.php
20
+ - _underscore/Model/Client/ThemeToken.php
21
+ - _underscore/Model/Core/Page.php
22
+ related:
23
+ - ../../dbchanges2/features/surface-layer-schema.md
24
+ - ../../api2/features/surface-meta-option.md
25
+ - acl-permission-chain.md
26
+ ---
27
+
28
+ ## What it is
29
+
30
+ The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a
31
+ cached resolver, `_Model_Core_Surface::resolve(&$api, string $slug)`, that merges base config +
32
+ cascade overrides + ACL + messages + theme tokens into one flat, serializable bundle. It is the
33
+ **replacement for `_Model_Core_Page::meta()`** and is shipped as a scripted-API method behind
34
+ `GET /v2/surfaces/{slug}/meta`. Platform/shared — not client-specific. Schema:
35
+ [surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
36
+
37
+ ## The 9 models
38
+
39
+ Core: `_Model_Core_{Surface,SurfaceElement,Action,Vocabulary,VocabularyTerm,Message}`.
40
+ Client: `_Model_Client_{SurfaceOverride,MessageTranslation,ThemeToken}`. Each `extends _Model_True`
41
+ with `const DATABASE`/`TABLE`, typed public-prop fields, and array FKs declaring
42
+ `FIELDOPT_FOREIGNKEY_MODEL` (cross-DB FKs are soft — no SQL constraint). Modeled on
43
+ `RecordField.php`/`TableView.php`.
44
+
45
+ ## How `resolve()` works
46
+
47
+ Inputs come from `$api` (the same accessor `_Model_Client_TableView::meta` uses):
48
+ `clientIdentifier`, `personaIds`, `roleIds`, `languageId`; `$slug` is the query arg.
49
+
50
+ 1. Load `Surface` by slug via ORM `->load()` — **parameterized** (fixes the old `LIKE '$slug'`
51
+ injection).
52
+ 2. One query: all `SurfaceElements` for `surfaceId`, ordered by `sortOrder`.
53
+ 3. One query: matching `SurfaceOverrides`; apply **most-specific-last** in a single in-memory pass
54
+ (`ORDER BY (roleId IS NULL) DESC, (personaId IS NULL) DESC`; language filtered to active or NULL).
55
+ Precedence **base < client < persona < role**, language an orthogonal overlay.
56
+ 4. **ACL composition, not absorption** — batch-resolve permissions via
57
+ `_Model_Client_AclActionPermission`; drop elements whose `aclActionId` isn't permitted (ACL stays
58
+ exactly as-is, see [acl-permission-chain](acl-permission-chain.md)).
59
+ 5. Batch-load (one integer-`IN` query each): `Messages`+`MessageTranslations` (referenced keys only),
60
+ `Vocabularies`+`VocabularyTerms`, `Actions`, client `ThemeTokens`.
61
+ 6. `type=TABLE` → emit `{tableViewSlug}` only; the FE keeps calling the existing `TableView::meta`
62
+ table pipeline (referenced, never duplicated).
63
+ 7. Assemble the flat bundle. **Zero INSERTs.**
64
+
65
+ A `debug()` inspector returns the resolved bundle plus the raw cascade layers (shipped with the
66
+ resolver, not "later").
67
+
68
+ ## Caching & invalidation
69
+
70
+ - Disk cache keyed `surface:meta:{app}:{slug}:{client}:{sortedPersonaIds}:{sortedRoleIds}:{lang}`.
71
+ - **Bust on write, never TTL-only.** `postPost/postPut/postPatch/postDelete` interceptors on all 9
72
+ models do a broad client-scoped bust; a write to a **Core** table must bust all clients' bundles
73
+ (or fold a Core-version stamp into the key).
74
+ - **Keep the bust OUTSIDE the DB transaction** (the lazy-transaction write-drop rule).
75
+
76
+ ## Config vs logic boundary (the hard rule)
77
+
78
+ **Config describes; PHP decides.** Two-tier gating:
79
+ - **Tier 1** — trivial declarative gates (`{all|any|none:[{field,op,value}]}`, nestable) shipped as
80
+ JSON on the element and evaluated **client-side** against the loaded record. Grammar is a **frozen
81
+ allowlist** — a new op is an escalation to Tier 2, not a config change.
82
+ - **Tier 2** — business logic (multi-stage approvals, "reviewer assigned", anything touching
83
+ `Approvals`) is **computed in PHP** and returned as a resolved boolean. It lives in
84
+ `_Model_<Client>_*` overrides + `Client_ApiPayloadInterceptor` hooks — the Surface layer does
85
+ **not** absorb business logic.
86
+
87
+ ## Gotchas
88
+
89
+ - **`_Model_Core_Page::meta()` (Model/Core/Page.php, ~1300 lines) is the engine being replaced** and
90
+ is dangerous to extend: it interpolates `LIKE '$slug'` (injection-prone), **runs INSERTs during a
91
+ read**, is N+1, hand-duplicates the 5-layer cascade 6× via UNION, and is uncached. The empirical
92
+ basis for the typed-over-EAV design: in `Client_Compass` the whole EAV settings cascade (`Settings`
93
+ 16-key registry + ~40 `*Settings` tables) is almost all 0 rows, while typed `TableViews`/
94
+ `TableViewFields`/`SectionRecordFields` ARE populated. **Do not delete `meta()`** until every page
95
+ is migrated — cut over per-surface with a parity diff (old bundle vs new bundle).
96
+ - **The resolver must be defensive** — config writes bypass code review/CI, so missing/invalid config
97
+ falls back to sane defaults and never crashes a page. Strict per-`attribute` casting with logged
98
+ fallback on bad `SurfaceOverrides` data.
99
+ - **Cross-DB reads are batched** (DB_CORE vs DB_CLIENT) and FKs are soft — tolerate dangling refs.
100
+
101
+ ## Change history
102
+ - 2026-06-25 — Built the 9 models + `resolve()`/`debug()` as the cached, parameterized,
103
+ zero-write-on-read replacement for `_Model_Core_Page::meta()`; cache-bust interceptors on all 9
104
+ models (outside the transaction). meta() retained until per-page cutover with parity diff. (jcardinal)
@@ -5,6 +5,7 @@
5
5
  | [API (api2 / TOGa API v2) Architecture](architecture.md) | `api2` is the backend powering the public **TOGa 2.0 API**. | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, api2/Component/Api/Cxml/Cxml.php, api2/Component/Api/V2/Response/Response.php, api2/Config/ |
6
6
  | [Language Translation Layer (audience.language + sidecar tables)](features/language-translation-layer.md) | Serves the same TOGa data (Item title/description/longDescription, expanding later) in multiple languages without forking the schema or breaking English consume | api2/Component/Api/V2/V2.php, api2/Component/Api/V2/Response/Response.php, _underscore/Model/Core/Setting.php, _underscore/Model/Core/RecordField.php, _underscore/Model/Core/DefaultGlobalSetting.php, _underscore/Model/Client/ItemTranslation.php, dbchanges2/Client/2026-06-23a - ItemTranslations.sql, dbchanges2/Client/2026-06-23b - ItemTranslationsAcl.sql, dbchanges2/Core/2026-06-23a - RecordFieldsTranslationColumn.sql, dbchanges2/Core/2026-06-23b - ItemTranslationsRecord.sql |
7
7
  | [POST + JSON-body args for scripted APIs](features/scripted-api-post-body-args.md) | The V2 engine can run a Record Script (scripted API) for a **POST** request, and a scripted API can receive its arguments from the **JSON request body** instead | api2/Component/Api/V2/V2.php |
8
+ | [Surface action-state via the surface=<slug> request option (M2M-safe)](features/surface-meta-option.md) | An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state (`isVisible`/`isEnabled`) to a GET response **under `meta.surface | api2/Component/Api/V2/V2.php, _underscore/Model/Core/Surface.php |
8
9
  | [Tickets API (/v2/tickets)](features/tickets-api.md) | The generic ticket endpoint of the 2.0 REST API. | Component/Api/V2/V2.php |
9
10
  | [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | |
10
11
  | [New Environment Configuration & Provisioning (api2)](workflows/environment-configuration-and-provisioning.md) | What it takes for a 2.0 API environment (e.g. | api2/Config/<environment>.ini, api2/Controller/Index.php, dbchanges2/Core/2026-06-16a - DatabaseHosts for new QA QC stage demo environments.sql, _underscore/Route.php |
@@ -0,0 +1,47 @@
1
+ ---
2
+ title: Surface action-state via the surface=<slug> request option (M2M-safe)
3
+ framework: "2.0"
4
+ repo: api2
5
+ project: API
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - api2/Component/Api/V2/V2.php
13
+ - _underscore/Model/Core/Surface.php
14
+ related:
15
+ - ../../_underscore/features/surface-resolver.md
16
+ ---
17
+
18
+ ## What it is
19
+
20
+ An opt-in V2 engine request option, `surface=<slug>`, that attaches per-record UI action state
21
+ (`isVisible`/`isEnabled`) to a GET response **under `meta.surface`, never `data`**. It is the
22
+ M2M-safe delivery mechanism for the platform-wide Surface layer (see
23
+ [surface-resolver](../../_underscore/features/surface-resolver.md)): the same core data API serves
24
+ both the UI and machine-to-machine integrations, so presentation state must never leak into the
25
+ business data that integrations consume.
26
+
27
+ ## How it works
28
+
29
+ When `surface=<slug>` is present on a GET, after loading the record(s) the engine calls
30
+ `_Model_Core_Surface::resolveRecordState($api, $slug, $record)` and attaches per-record
31
+ `{isVisible, isEnabled}` action state under `meta.surface` (single-record and list shapes; a list is
32
+ resolved in one batched pass, per row in `meta`). When the option is **absent** (all M2M/integration
33
+ traffic), behavior is **byte-for-byte unchanged** — pure data, zero extra work. The path is
34
+ **defensive**: it catches `Throwable` and omits `meta.surface` on error rather than failing the call.
35
+
36
+ ## Key rules
37
+
38
+ - **Presentation state lives in `meta.surface`, never `data`.** Integrations get pure business data.
39
+ - **The business rule is enforced at the action endpoint** (the source of truth that also protects
40
+ M2M — e.g. an integration approving a `fulfilled` order is rejected by the same rule). The surface
41
+ option merely **previews** that rule for the UI; it is a read-only preview, not a second copy of
42
+ the logic.
43
+ - **One call, no extra round-trip** — the state pass rides the existing data fetch.
44
+
45
+ ## Change history
46
+ - 2026-06-25 — Added the `surface=<slug>` opt-in option to the V2 engine; attaches per-record action
47
+ state under `meta.surface` (M2M-safe, defensive, absent = unchanged). (jcardinal)
@@ -3,4 +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/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
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/ |
@@ -0,0 +1,90 @@
1
+ ---
2
+ title: Surface Layer Schema (UI presentation/config tables)
3
+ framework: "2.0"
4
+ repo: dbchanges2
5
+ project: Database Changes
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql
13
+ - dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql
14
+ - dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql
15
+ - dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
16
+ - dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql
17
+ - dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql
18
+ - dbchanges2/Client/2026-06-03- BLANK_CLIENT_DATABASE.sql
19
+ related:
20
+ - ../../_underscore/features/surface-resolver.md
21
+ ---
22
+
23
+ ## What it is
24
+
25
+ The persistent schema for the platform-wide **Surface** UI presentation/configuration layer
26
+ (see the `_underscore` [surface-resolver](../../_underscore/features/surface-resolver.md) feature
27
+ for the runtime). DB-driven UI config (labels, colors via tokens, visibility/enable, icons,
28
+ tooltips, button bars, table quick-actions, status badges, modals) so UI changes need no
29
+ front-end deploy and are editable by a future admin app over plain CRUD. **Platform/shared — not
30
+ client-specific** (Compass is only the first seed/test tenant). It replaces the legacy
31
+ `_Model_Core_Page::meta()` EAV settings cascade (the old ~40 `*Settings` tables — almost all empty
32
+ in prod).
33
+
34
+ ## The 9 tables — Core (structural) vs Client (per-tenant deltas)
35
+
36
+ Structural definitions live in **Core** (platform-shared, mirrors `Records`/`RecordFields`);
37
+ per-tenant deltas live in **Client** (mirrors `TableViews`/ACL/`ItemTranslations`).
38
+
39
+ | Table | DB | Purpose |
40
+ |---|---|---|
41
+ | `Surfaces` | Core | A configurable UI region (`type` ENUM TABLE/SECTION/BUTTON_BAR/ROW_ACTIONS/TAB_STRIP/FILTER_SET/MODAL/DRAWER) |
42
+ | `SurfaceElements` | Core | Ordered items: ~15 typed attr columns + one JSON `config` long-tail + `visibilityRule`/`enabledRule` JSON |
43
+ | `Actions` | Core | Behavior action catalog (FE registry keys + JSON payload) |
44
+ | `Vocabularies` | Core | A value-set (status/stage/priority/type/channel) per record |
45
+ | `VocabularyTerms` | Core | value → token/label/order |
46
+ | `Messages` | Core | i18n key catalog + ICU default value |
47
+ | `SurfaceOverrides` | Client | THE single sparse cascade table (+`c_longValue mediumtext`) replacing ~40 EAV tables |
48
+ | `MessageTranslations` | Client | per-language overlay (sidecar pattern) + `c_longValue` |
49
+ | `ThemeTokens` | Client | per-tenant semantic token → value (the only place a hex/icon name lives) |
50
+
51
+ `SurfaceOverrides` carries the entire cascade in one sparse table: scope columns
52
+ `personaId`/`roleId`/`languageId` (NULL = not scoped on that axis), an `attribute` ENUM, and
53
+ `value varchar(255)` + `c_longValue mediumtext`. Precedence **base < client < persona < role**
54
+ with language as an orthogonal overlay; the resolver applies it most-specific-last in one pass.
55
+
56
+ ## Migration files (typed columns, not EAV)
57
+
58
+ CREATE order is FK-safe (`Messages → Actions → Vocabularies → VocabularyTerms → Surfaces →
59
+ SurfaceElements`). Core migrations run first; Client after. The session built/seeded the
60
+ **SalesOrders + login** vertical (not Tickets) as the review proof:
61
+
62
+ - `Core/2026-06-25a - SurfaceCoreTables.sql` — the 6 Core tables.
63
+ - `Core/2026-06-25b - SurfaceRecordsAndFields.sql` — `Core.Records`+`RecordFields` for all 9 tables (reserved id block **2300–2399**; `recordId` via `model=` subselect).
64
+ - `Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql` — Core seed + the `RecordScripts` resolve row.
65
+ - `Client/2026-06-25a - SurfaceClientTables.sql` — the 3 Client tables + `INSERT IGNORE Languages('en','English')`.
66
+ - `Client/2026-06-25b - SurfaceClientSeed.sql` — `ThemeTokens` + `MessageTranslations(en)`.
67
+ - `Client/2026-06-25c - SurfaceClientAcl.sql` — full ACL chain for the 3 CLIENT-aclDatabase records.
68
+
69
+ ## Gotchas
70
+
71
+ - **Cross-DB FKs (Core↔Client) are SOFT** — the ORM declares `FIELDOPT_FOREIGNKEY_MODEL` but **no
72
+ SQL constraint** exists (e.g. `Surfaces.tableViewId`, `SurfaceOverrides.surfaceId`,
73
+ `MessageTranslations.messageId`). This keeps Core/Client migration ordering safe and lets the
74
+ resolver tolerate dangling refs. Never add a real cross-DB constraint.
75
+ - **`BLANK_CLIENT_DATABASE.sql` MUST append** the 3 new Client tables + the `Languages` `en` seed,
76
+ or every newly provisioned client is born broken. This was appended and provisioning-tested.
77
+ Treat it as a CI gate (provision a throwaway client; assert the surface meta endpoint returns the
78
+ seed).
79
+ - **`SurfaceOverrides.value` is a stringly-typed escape hatch** — a long `CONFIG`/label override
80
+ would silently truncate at varchar(255); that is why `c_longValue mediumtext` exists, and the
81
+ resolver MUST cast strictly per `attribute` (bool/int/string/json) with a logged fallback.
82
+ - **Reserved id block 2300–2399** for the Surface `Records`/`RecordFields` rows (Core max was 331);
83
+ use `model=` subselects for `recordId`, never hardcoded ids.
84
+ - **CLIENT-aclDatabase records** (`SurfaceOverrides`, `MessageTranslations`, `ThemeTokens`) need the
85
+ full 4-step ACL chain + field permissions in **each** client DB; resolve `roleId` by subselect.
86
+
87
+ ## Change history
88
+ - 2026-06-25 — Initial schema for the Surface layer: 6 Core + 3 Client tables, `Records`/`RecordFields`
89
+ (ids 2300–2399), SalesOrders+login seed, ACL chain, and `BLANK_CLIENT_DATABASE` append. Typed
90
+ columns chosen over EAV (the old `*Settings` cascade was empirically near-empty in prod). (jcardinal)
@@ -8,3 +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/{slug}/meta` instead of statically-imported JSON f | 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/featureFlag.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 |
@@ -0,0 +1,71 @@
1
+ ---
2
+ title: Surface Frontend (DB-driven UI consumption, src/surface/)
3
+ framework: "2.0"
4
+ repo: toga25-supply
5
+ project: TOGa 2.5 Supply
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - toga25-supply/src/surface/useFetchSurfaceMeta.ts
13
+ - toga25-supply/src/surface/evaluateSurfaceRule.ts
14
+ - toga25-supply/src/surface/actionRegistry.ts
15
+ - toga25-supply/src/surface/SurfaceActionBar.tsx
16
+ - toga25-supply/src/surface/SurfaceSection.tsx
17
+ - toga25-supply/src/surface/resolve.ts
18
+ - toga25-supply/src/surface/types.ts
19
+ - toga25-supply/src/surface/featureFlag.ts
20
+ - toga25-supply/src/surface/index.ts
21
+ - toga25-supply/src/pages/Login/LoginPage.tsx
22
+ - toga25-supply/src/pages/SalesOrders/SalesOrders.tsx
23
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx
24
+ related:
25
+ - ../../_underscore/features/surface-resolver.md
26
+ - meta-driven-table-data.md
27
+ ---
28
+
29
+ ## What it is
30
+
31
+ The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from
32
+ `GET /v2/surfaces/{slug}/meta` instead of statically-imported JSON field bundles. Lives in
33
+ `src/surface/` for now with a `TODO(blox)` to extract the generic machinery into `@agilant/toga-blox`
34
+ so all 2.0 apps inherit it. Backend: [surface-resolver](../../_underscore/features/surface-resolver.md).
35
+
36
+ ## How it works
37
+
38
+ - **`useFetchSurfaceMeta`** — the single fetch-once/React-Query-cached choke point for a surface's
39
+ meta. Loading a different *record* into the same surface reuses cached meta.
40
+ - **`evaluateSurfaceRule`** — the Tier-1 rule evaluator: a **frozen `all/any/none` + `{field,op,value}`
41
+ grammar** that **throws on an unknown op** (generalized from the SalesOrders
42
+ `buildPatchedTenantFields`/`evaluateEnableRule`/`resolveFlag` helpers). Evaluated client-side against
43
+ the loaded record. A new op is an escalation to a backend Tier-2 capability method, never a grammar
44
+ change.
45
+ - **`actionRegistry`** — string action key → existing app handler (the proven `MODAL_RENDERERS`
46
+ pattern). The DB ships keys + payloads; the app supplies the actual handlers.
47
+ - **theme/message resolvers** (`resolve.ts`) — token → CSS and ICU message rendering.
48
+ - **`SurfaceActionBar`/`SurfaceActions` + `SurfaceSection`** — generic renderers.
49
+
50
+ ## What's wired (review proof)
51
+
52
+ Login + sales-orders list + the sales-order modal, all behind the **`SURFACE_ENABLED`** flag (env
53
+ `VITE_SURFACE_ENABLED`, **default OFF**). The SalesOrder action bar reproduces
54
+ `orderViewFields.json` approve/deny/approvalWorkflow/viewLog/editOrder by delegating to the existing
55
+ `onActionClick`/`onOpenLog` handlers. The table stays on the existing TableView pipeline via
56
+ `tableViewSlug` (see [meta-driven-table-data](meta-driven-table-data.md)) — the Surface layer
57
+ references the TableView, never replaces it.
58
+
59
+ ## Gotchas
60
+
61
+ - **Default OFF.** Nothing renders through the Surface path unless `VITE_SURFACE_ENABLED` is set —
62
+ the existing JSON-bundle path remains live until per-surface cutover.
63
+ - **Tier-1 only on the client.** Business-logic gates (Tier-2) arrive as resolved booleans from the
64
+ backend (`meta.surface`); never re-encode business rules in the FE evaluator.
65
+ - **`tsc` not yet run** this session (the private `@agilant/toga-blox` registry needs npm creds);
66
+ treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
67
+
68
+ ## Change history
69
+ - 2026-06-25 — Built `src/surface/` (fetch hook, frozen Tier-1 rule evaluator, action registry, theme/
70
+ message resolvers, action-bar + section renderers); wired login + SalesOrders list + SO modal behind
71
+ the OFF-by-default `SURFACE_ENABLED` flag. Generic machinery destined for toga-blox. (jcardinal)
@@ -12,13 +12,14 @@ _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) — 11 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) — 4 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
15
16
 
16
17
  ## 2.0 framework
17
18
 
18
- - **_underscore** (_Underscore) _(framework core)_ — 15 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
19
+ - **_underscore** (_Underscore) _(framework core)_ — 16 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
19
20
  - **worker2** (Worker) — 15 doc(s) → [2.0/apps/worker2/INDEX.md](2.0/apps/worker2/INDEX.md)
20
- - **api2** (API) — 6 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
21
- - **dbchanges2** (Database Changes) _(framework core)_ — 2 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
21
+ - **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
22
+ - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
22
23
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
23
24
  - **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
24
25
  - **toga2-view** (TOGa View Frontend) — 4 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
@@ -27,7 +28,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
27
28
  - **voice-to-voice** (TOGa Voice) — 4 doc(s) → [2.0/apps/voice-to-voice/INDEX.md](2.0/apps/voice-to-voice/INDEX.md)
28
29
  - **ai-bdr** (AI-BDR) — 4 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
29
30
  - **toga2-commerce** (TOGa Commerce) — 6 doc(s) → [2.0/apps/toga2-commerce/INDEX.md](2.0/apps/toga2-commerce/INDEX.md)
30
- - **toga25-supply** (TOGa 2.5 Supply) — 6 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
31
+ - **toga25-supply** (TOGa 2.5 Supply) — 7 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
31
32
  - **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
32
33
 
33
34
  ## standalone framework
@@ -3,6 +3,7 @@ title: "TOGA Technology"
3
3
  framework: "2.0"
4
4
  apps:
5
5
  - _underscore
6
+ - tools
6
7
  project: _Underscore
7
8
  client: true
8
9
  type: profile
@@ -24,3 +25,6 @@ by staff role (e.g. the planned Toolbox app) reads its `Users` / `Personas` mode
24
25
  - **Client DB:** `Client_True`
25
26
  - **Client identifier:** `True`
26
27
  - **SSO mapper:** `_Model_True_ClientAuthentication` (base; matches by email from NameID)
28
+
29
+ The new **Tools** app (1.0; repo `tools`) reads the `Client_True` DB **read-only** to gate its
30
+ internal tooling by staff persona (see `1.0/apps/tools/`).
@@ -182,5 +182,14 @@
182
182
  "framework": "2.0",
183
183
  "role": "app",
184
184
  "dependsOn": []
185
+ },
186
+ {
187
+ "repo": "tools",
188
+ "project": "Tools",
189
+ "framework": "1.0",
190
+ "role": "app",
191
+ "dependsOn": [
192
+ "library"
193
+ ]
185
194
  }
186
195
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.205",
3
+ "version": "1.0.207",
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",