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.
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -0
- package/knowledge/2.0/apps/_underscore/architecture.md +40 -1
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +104 -0
- package/knowledge/2.0/apps/api2/INDEX.md +1 -0
- package/knowledge/2.0/apps/api2/features/surface-meta-option.md +47 -0
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -0
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +90 -0
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -0
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +71 -0
- package/knowledge/INDEX.md +3 -3
- package/package.json +1 -1
|
@@ -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-
|
|
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)
|
package/knowledge/INDEX.md
CHANGED
|
@@ -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)_ —
|
|
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) —
|
|
21
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
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