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.
- package/knowledge/1.0/apps/library/architecture.md +10 -0
- package/knowledge/1.0/apps/tools/INDEX.md +8 -0
- package/knowledge/1.0/apps/tools/architecture.md +81 -0
- package/knowledge/1.0/apps/tools/features/developer-tools.md +37 -0
- package/knowledge/1.0/apps/tools/features/persona-gated-navigation.md +44 -0
- package/knowledge/1.0/apps/tools/features/saml-sso-auth.md +76 -0
- 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 +5 -4
- package/knowledge/clients/true/profile.md +4 -0
- package/knowledge/registry.json +9 -0
- package/package.json +1 -1
|
@@ -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-
|
|
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
|
@@ -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)_ —
|
|
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) —
|
|
21
|
-
- **dbchanges2** (Database Changes) _(framework core)_ —
|
|
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) —
|
|
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/`).
|
package/knowledge/registry.json
CHANGED
package/package.json
CHANGED