toga-ai 1.0.205 → 1.0.206

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.
@@ -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)
@@ -15,10 +15,10 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
15
15
 
16
16
  ## 2.0 framework
17
17
 
18
- - **_underscore** (_Underscore) _(framework core)_ — 15 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
18
+ - **_underscore** (_Underscore) _(framework core)_ — 16 doc(s) → [2.0/apps/_underscore/INDEX.md](2.0/apps/_underscore/INDEX.md)
19
19
  - **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)
20
+ - **api2** (API) — 7 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
21
+ - **dbchanges2** (Database Changes) _(framework core)_ — 3 doc(s) → [2.0/apps/dbchanges2/INDEX.md](2.0/apps/dbchanges2/INDEX.md)
22
22
  - **toga2-supply** (TOGa Supply) — 3 doc(s) → [2.0/apps/toga2-supply/INDEX.md](2.0/apps/toga2-supply/INDEX.md)
23
23
  - **saml** (SAML SSO Gateway) — 3 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
24
24
  - **toga2-view** (TOGa View Frontend) — 4 doc(s) → [2.0/apps/toga2-view/INDEX.md](2.0/apps/toga2-view/INDEX.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.205",
3
+ "version": "1.0.206",
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",