toga-ai 1.0.201 → 1.0.203

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.
@@ -6,6 +6,7 @@
6
6
  | [Diagnostic Dialog — View Recommended Services Routing](features/diagnostic-dialog-view-recommended-services.md) | `App_Model_Toga_Diagnostic::initializeDiagnosticDialog()` renders the device modal used across all TOGa service request views. | library/app/model/toga/diagnostic.php |
7
7
  | [Elite Freshservice Sync (library)](features/elite-freshservice-sync.md) | `App_Api_Toga2` in `library/app/api/toga2.php` orchestrates bidirectional sync between TOGA 2 and TOGaDesk. | library/app/api/toga2.php |
8
8
  | [Branded HTML Email Templates (App_Email_Template)](features/email-templates.md) | `App_Email_Template` (`app/email/template.php`) is the base class for branded HTML emails in the 1.0 (`App_`) framework. | library/app/email/template.php, library/app/email/agilant.php |
9
+ | [1.0 MVC Page Pattern & New-App Skeleton](features/mvc-page-pattern-and-app-skeleton.md) | This is the **reusable recipe for standing up a new 1.0 (`App_`) application** and for adding pages to one — the folder-based MVC routing, the page lifecycle, t | library/app/framework.php, library/app/frameworkindex.php, library/app/mvc.php, library/app/database.php, library/app/model.php, library/app/config.php |
9
10
  | [NetSuite SuiteQL/REST API Reference](features/netsuite-suiteql-api-reference.md) | General working reference for the Agilant NetSuite integration: how to authenticate, how SuiteQL behaves, and the confirmed schema of the tables/columns/codes w | library/app/api/netsuite/rest.php, library/ssl/netsuite_ec_key.pem |
10
11
  | [NetSuite SuiteQL/REST Shim — Field Semantics](features/netsuite-suiteql-rest-shim.md) | `App_Api_Netsuite_Rest` is the REST/SuiteQL replacement for the deprecated NetSuite SOAP toolkit. | library/app/api/netsuite/rest.php |
11
12
  | [Startech PC Matic B2B Sync (library)](features/startech-pcmaticb2b-sync.md) | `library/app/api/toga2.php` handles bidirectional ticket sync for PC Matic B2B between TOGaDesk 1.0 and TOGA 2.0. | library/app/api/toga2.php |
@@ -0,0 +1,113 @@
1
+ ---
2
+ title: 1.0 MVC Page Pattern & New-App Skeleton
3
+ framework: "1.0"
4
+ repo: library
5
+ project: Library
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - library/app/framework.php
13
+ - library/app/frameworkindex.php
14
+ - library/app/mvc.php
15
+ - library/app/database.php
16
+ - library/app/model.php
17
+ - library/app/config.php
18
+ related:
19
+ - ../architecture.md
20
+ - ../../togaview/architecture.md
21
+ ---
22
+
23
+ ## Summary
24
+
25
+ This is the **reusable recipe for standing up a new 1.0 (`App_`) application** and for adding
26
+ pages to one — the folder-based MVC routing, the page lifecycle, the model/DB conventions, and
27
+ the minimal new-app skeleton. The `library` core supplies all of it; an app contributes only its
28
+ `index.php`, config, `_/app/` framework subclasses, `mvc/` pages, and `common/` templates.
29
+ togaview is the reference implementation (see its architecture doc for the multi-tenant
30
+ specifics); this doc captures the framework-level pattern that any new 1.0 app reuses.
31
+
32
+ ## How it works
33
+
34
+ ### Folder-based, classless routing
35
+ A URL maps directly to a file on disk — **the file path IS the route**, there is no routing
36
+ config and no controller classes. `App_MVC::parseRoute()` turns the request URI into
37
+ `mvc/<folder…>/<httpmethod>.php`, matching **longest-to-shortest** so overlapping paths resolve
38
+ to the most specific folder.
39
+
40
+ ```
41
+ GET /task_list/ajax -> mvc/task_list/ajax/get.php
42
+ POST /login -> mvc/login/post.php
43
+ ```
44
+
45
+ - `<httpmethod>` is `get` or `post`, taken from the HTTP verb (overridable via a `_method` POST param).
46
+ - `get.php` renders a page; `post.php` does writes then redirects via `App_MVC::routeTo()`
47
+ (`header('Location: …'); exit;`).
48
+ - Because matching is longest-to-shortest, keep route folders distinct to avoid surprise matches.
49
+
50
+ ### Entry point (3 lines)
51
+ ```php
52
+ require '_.php'; // bootstraps library core + autoloader
53
+ App_Framework_<App>::initialize(); // env, config, DB links, session, Sentry
54
+ App_Framework_<App>::renderIndex(); // resolves stylePath + dispatches the page
55
+ ```
56
+ App-specific framework classes live in `<app>/_/app/` and extend the library bases
57
+ `App_Framework` (`framework.php`) and `App_FrameworkIndex` (`frameworkindex.php`).
58
+
59
+ ### Page lifecycle (no template engine)
60
+ `App_FrameworkIndex::body()` loads the matched mvc file **inline** between
61
+ `common/<stylePath>/header.php` and `common/<stylePath>/footer.php`. There is no master-template
62
+ engine — pages are inline PHP + HTML. `stylePath` is selected by `$_SERVER['HTTP_HOST']` in
63
+ `framework.php::renderIndex()`, giving per-brand CSS under `assets/css/<stylePath>/`. Pages guard
64
+ auth themselves by checking `$_SESSION` at the top.
65
+
66
+ ### Adding a page
67
+ Create `mvc/<page>/get.php` (and `post.php` if it writes). Scaffold from `mvc/_TEMPLATE/`.
68
+
69
+ ### Models
70
+ Extend `App_Model`, named `App_Model_<Db>_<Table>`, declaring `const TABLENAME`, `const DATABASE`,
71
+ and `public $field = self::FIELDTYPE_*` for each column. Field types: `PRIMARYKEY`, `INT`, `CHAR`,
72
+ `DATETIME`, `BLOB`, `BLOBSTORAGE`, `FOREIGNKEY`, `SERIALIZED`, `DATETIME_CREATED`,
73
+ `DATETIME_UPDATED`.
74
+
75
+ ```php
76
+ class App_Model_Toga_Client extends App_Model {
77
+ const TABLENAME = 'Clients';
78
+ const DATABASE = 'db_toga';
79
+ public $id = self::FIELDTYPE_PRIMARYKEY;
80
+ public $clientName = self::FIELDTYPE_CHAR;
81
+ }
82
+ ```
83
+ Usage: `new App_Model_X($id)` loads a row; `->save()` persists; `App_Model_X::lookupValueByField()`.
84
+
85
+ ### Databases (multiple are first-class)
86
+ DB access is mysqli via `App_Database::query($sql, 'db_<name>')`. **Multiple databases are
87
+ first-class:** each `[database_<name>]` section in `config.<env>.ini` is registered as registry
88
+ key `db_<name>` by `App_Database::registerDatabaseConnect()`. SELECTs **auto-route to
89
+ `<link>_read`** when the host is an RDS cluster (read-cluster support). Escape with
90
+ `App_Database::sqlEscape()`; read results with `buildArrayOfRows()` / `fetchRow()`.
91
+
92
+ ### Config
93
+ `App_Config` does `parse_ini_file(config.<ENVIRONMENT>.ini)`; the environment comes from
94
+ `getenv('ENVIRONMENT')` (defaults to `'worker'` — an unset env loads the wrong file). Sections:
95
+ `[internal]`, `[database_*]`, `[email]`, `[external]`, `[modules]`, `[api]`.
96
+
97
+ > **Secrets are currently stored in plaintext ini files** — a known practice, not a recommended
98
+ > one. Document where a credential lives, never its value.
99
+
100
+ ## Minimal new-app skeleton
101
+ ```
102
+ index.php # the 3-line entry point
103
+ config.<env>.ini # per-environment config + DB sections
104
+ mvc/_TEMPLATE/get.php # page scaffold
105
+ mvc/<page>/{get,post}.php # one folder per route
106
+ common/<stylePath>/{header,footer}.php # brand shell wrapped around each page
107
+ common/404.php
108
+ assets/ # css/js per stylePath
109
+ ```
110
+ The app depends on the `library` core repo (bootstrapped via `_.php`).
111
+
112
+ ## Change history
113
+ - 2026-06-25 — Documented the 1.0 folder-based MVC page pattern, model/multi-DB conventions, and the minimal new-app skeleton, discovered while planning the new "Toolbox" 1.0 app (reference: togaview) (jcardinal)
@@ -3,4 +3,5 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [saml Architecture](architecture.md) | The `saml` repo is the SAML 2.0 / SSO gateway for all TOGa applications. | saml/index.php, saml/_.php, saml/Controller/Index.php, saml/Config/production.ini, saml/.platform/hooks/prebuild/git.sh, saml/.ebextensions/git.php |
6
+ | [SAML Downstream Integration Contract](features/downstream-integration-contract.md) | The contract a **downstream TOGa app** implements to authenticate users through the SAML gateway (`saml.togahub.com`). | saml/Controller/Index.php, _underscore/Model/Core/ClientAuthentication.php, _underscore/Model/True/ClientAuthentication.php |
6
7
  | [Rate SAML User Provisioning](features/rate-user-provisioning.md) | When a Rate user authenticates via SSO, `_Model_Rate_ClientAuthentication::getAuthenticatedSsoUser()` is called by the saml gateway. | _underscore/Model/Rate/ClientAuthentication.php, _underscore/Model/Rate/User.php, _underscore/Model/Rate/Customer.php, _underscore/Model/Rate/Contact.php |
@@ -7,7 +7,7 @@ client: shared
7
7
  type: architecture
8
8
  status: active
9
9
  updated: 2026-06-11
10
- owners: ["rgirish"]
10
+ owners: ["rgirish", "jcardinal"]
11
11
  files:
12
12
  - saml/index.php
13
13
  - saml/_.php
@@ -80,9 +80,11 @@ To onboard a new SSO client, add the class in `_underscore` — not in this repo
80
80
  ## Gotchas / known issues
81
81
 
82
82
  - **`_Database::register()` auto-starts a lazy transaction (since Apr 2 2026).** Any code that registers DBs then writes must call `_Database::transactionCommit()` before exiting — otherwise MySQL silently rolls back all writes on connection close. The `/acs` handler calls `transactionCommit()` after `getAuthenticatedSsoUser()` succeeds and before the redirect. See `_underscore/Database.php:48`.
83
- - **SAML signature verification is commented out.** The IdP X509 cert verification block is disabled. The only gate is the encrypted RelayState. Re-enabling per-IdP signature verification is a priority security improvement.
83
+ - **SAML signature verification is commented out — this is the repo's top security priority.** The IdP X509 cert verification block in the `/acs` flow is disabled, so the gateway accepts any well-formed, status-success `SAMLResponse` without proving it was signed by the expected IdP. The only remaining gate is the encrypted RelayState, which authenticates the *originating TOGa request* — not the *asserting IdP*. Threat model: an attacker who can craft or replay a `SAMLResponse` (or a malicious/compromised IdP) can forge an identity assertion for any user, since nothing binds the assertion to a trusted signing key. Re-enabling per-IdP X509 signature verification is the single highest-impact security fix for this repo.
84
+ - **Plaintext secrets in source — move to AWS SSM Parameter Store.** `saml/Config/production.ini` stores credentials in plaintext, and a Sentry DSN is hardcoded in `saml/Controller/Index.php`. Both are checked into the repo. Migrate these to AWS SSM Parameter Store (SecureString) and load at runtime; the source tree should reference parameter names only, never literal credential values.
84
85
  - **SLS not implemented.** `/sls` falls through to the default banner.
85
86
  - **`set_exception_handler(null)` at top of `saml()`.** Unhandled exceptions output raw PHP errors. All exceptions from `getAuthenticatedSsoUser()` are caught and sent to Sentry.
86
87
 
87
88
  ## Change history
88
89
  - 2026-06-11 — Added `transactionCommit()` before redirect; wrapped `getAuthenticatedSsoUser()` in try/catch with Sentry; echo+exit on auth failure (rgirish)
90
+ - 2026-06-25 — Sharpened signature-verification gotcha with explicit threat model and flagged it as the repo's top security priority; documented plaintext secrets in `production.ini` + hardcoded Sentry DSN in `Controller/Index.php` with SSM Parameter Store recommendation (jcardinal)
@@ -0,0 +1,81 @@
1
+ ---
2
+ title: SAML Downstream Integration Contract
3
+ framework: "2.0"
4
+ repo: saml
5
+ project: SAML SSO Gateway
6
+ client: shared
7
+ type: feature
8
+ status: active
9
+ updated: 2026-06-25
10
+ owners: [jcardinal]
11
+ files:
12
+ - saml/Controller/Index.php
13
+ - _underscore/Model/Core/ClientAuthentication.php
14
+ - _underscore/Model/True/ClientAuthentication.php
15
+ related:
16
+ - ../architecture.md
17
+ - ../../_underscore/architecture.md
18
+ - ../../../clients/rate/features/saml-sso.md
19
+ ---
20
+
21
+ ## Summary
22
+
23
+ The contract a **downstream TOGa app** implements to authenticate users through the SAML gateway
24
+ (`saml.togahub.com`). The architecture doc covers the gateway internals; this doc is the
25
+ **integration recipe** for an app that wants SSO — how to initiate the flow, what the gateway
26
+ hands back, and how to consume it. Reusable for any new SSO-consuming app.
27
+
28
+ ## How it works
29
+
30
+ ### SP-initiated flow
31
+ 1. The downstream app calls
32
+ `_Model_Core_ClientAuthentication::singleSignOnServiceUrl(uuid, domainUuid, urlParameters)`
33
+ to build a signed SAML AuthnRequest (HTTP-Redirect binding). The RelayState
34
+ (`{v:1, time, domain, urlParameters}`) is encrypted with the Core `API_SECRET_ACCESS_TOKEN`
35
+ and carries a 300-second TTL.
36
+ 2. The user authenticates at the client IdP, which POSTs the `SAMLResponse` to the gateway `/acs`.
37
+ 3. `/acs` decrypts RelayState (supports key rotation via `API_SECRET_ACCESS_TOKEN_PREVIOUS`),
38
+ resolves Core `Domain → Client → Environment`, **dynamically registers the client DBs**
39
+ (`DB_CLIENT`, `DB_CLIENT_LOGS`, `DB_CLIENT_ARCHIVE`), then calls the client-specific mapper
40
+ `_Model_<ClientIdentifier>_ClientAuthentication::getAuthenticatedSsoUser($assertion)` to
41
+ resolve or create the user.
42
+ - Base `_Model_True_ClientAuthentication` matches by **email** (from the NameID).
43
+ - Compass matches `c_hrEmpUsername`; Rate matches `borrowerID`.
44
+
45
+ ### Handoff back to the downstream app
46
+ The gateway redirects the browser to:
47
+ ```
48
+ <appDomain>?saml=<base64(json)>
49
+ ```
50
+ where the JSON is:
51
+ ```json
52
+ {
53
+ "auth": "encrypted-user-uuid",
54
+ "payload": { "client": "encryptWithKey(client.uuid)", "user": "encryptWithKey(user.uuid)" }
55
+ }
56
+ ```
57
+ all encrypted under the Core `API_SECRET_ACCESS_TOKEN`. An optional `?urlParameters` is forwarded
58
+ through.
59
+
60
+ The downstream app then: base64-decodes the `saml` param, JSON-parses it, decrypts the client and
61
+ user UUIDs with `_String::decryptWithKey()`, loads the user, and **establishes its own session**.
62
+ The gateway holds no session — session ownership is entirely the downstream app's.
63
+
64
+ ### Integrating a NEW downstream app
65
+ 1. Ensure an SSO config row exists in `_Model_Core_ClientAuthentications_Sso`.
66
+ 2. Implement/extend `_Model_<Client>_ClientAuthentication::getAuthenticatedSsoUser()` in
67
+ `_underscore` (the gateway repo is not modified — see the per-client extension pattern in the
68
+ architecture doc).
69
+ 3. Initiate SSO via `singleSignOnServiceUrl()`.
70
+ 4. Consume the `?saml=` handoff and decrypt the UUIDs with `API_SECRET_ACCESS_TOKEN`.
71
+
72
+ ## Gotchas / known issues
73
+
74
+ - **Lazy-transaction write-drop.** `_Database::register()` auto-starts a lazy transaction (since
75
+ Apr 2026). Any `/acs` write path (e.g. user auto-creation) must call
76
+ `_Database::transactionCommit()` before exit or the writes are silently rolled back.
77
+ - **Gateway holds no session** — every downstream app must build and own its own session from the
78
+ decrypted handoff.
79
+
80
+ ## Change history
81
+ - 2026-06-25 — Documented the downstream integration contract (SP-initiated flow, `?saml=` handoff shape, and the 4 onboarding steps), discovered while planning a new SSO-consuming app (reference: saml) (jcardinal)
@@ -3,7 +3,8 @@
3
3
  | Doc | Summary | Files |
4
4
  |-----|---------|-------|
5
5
  | [TOGa 2.5 Supply — Architecture](architecture.md) | `toga25-supply` ("TOGa 2.5 Supply") is the **React/TypeScript frontend** for the 2.0 Supply application — an iteration and improvement of `toga2-supply`. | toga25-supply/src/main.tsx, toga25-supply/src/App.tsx, toga25-supply/src/routes.tsx, toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/layout/, toga25-supply/src/pages/, toga25-supply/src/hooks/useTableCellInteractions.ts |
6
- | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md |
6
+ | [Action-Button Rule Engine (Flag / Rule grammar)](features/action-button-rule-engine.md) | A declarative, fully config-driven rule engine that resolves the boolean-ish flags (`isEnabled`, `isVisible`, `isComplete`) on SalesOrder action-button options. | toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts, toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts, toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx, toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx |
7
+ | [Client-Configurable Fields (useClientFields / fieldsConfig)](features/client-configurable-fields.md) | The mechanism for config that **varies by client** (or client × role) — field overrides, filter buttons, group-by options, column pickers, layout toggles — with | toga25-supply/src/fieldsConfig/index.ts, toga25-supply/src/fieldsConfig/useClientFields.ts, toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/viewModel/FIELDS/, toga25-supply/src/pages/Inventory/README.md, toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/, toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/ |
7
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 |
8
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 |
9
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 |
@@ -0,0 +1,119 @@
1
+ ---
2
+ title: Action-Button Rule Engine (Flag / Rule grammar)
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: [apeterson]
11
+ files:
12
+ - toga25-supply/src/pages/SalesOrders/helpers/evaluateEnableRule.ts
13
+ - toga25-supply/src/pages/SalesOrders/helpers/buildPatchedTenantFields.ts
14
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/viewModel/useSalesOrderRecordModalLayoutModel.tsx
15
+ - toga25-supply/src/pages/SalesOrders/view/SalesOrderApprovalModalsLayout/viewModel/useApprovalModalViewModel.tsx
16
+ related:
17
+ - client-configurable-fields.md
18
+ - ../architecture.md
19
+ ---
20
+
21
+ ## What it is
22
+
23
+ A declarative, fully config-driven rule engine that resolves the boolean-ish flags
24
+ (`isEnabled`, `isVisible`, `isComplete`) on SalesOrder action-button options. Every such
25
+ flag is a **`Flag`** — either a static `true`/`false` or a **`Rule`** tree evaluated against
26
+ the order's runtime state. Because the entire grammar is data, the backend can ship a rule
27
+ tree verbatim and adding or re-gating a button becomes a pure config change with no frontend
28
+ code edit. This is the realization of the long-stated goal that SalesOrders action-button
29
+ config be completely field-driven and backend-suppliable.
30
+
31
+ ## How it works
32
+
33
+ The engine lives in `src/pages/SalesOrders/helpers/evaluateEnableRule.ts` and is applied by
34
+ `buildPatchedTenantFields.ts`.
35
+
36
+ ### The grammar
37
+
38
+ ```ts
39
+ type Flag = boolean | Rule | undefined;
40
+
41
+ type Rule =
42
+ | { all: Rule[] } // AND — every child true
43
+ | { any: Rule[] } // OR — at least one child true
44
+ | { not: Rule } // NOT
45
+ | { field: string; op: FieldOp; value? } // dot-path predicate into RuleContext
46
+ | { type: string }; // named domain primitive (rare)
47
+
48
+ type FieldOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte"
49
+ | "in" | "nin" | "truthy" | "falsy";
50
+ ```
51
+
52
+ - **Field predicates** address into the `RuleContext` by dot-path (e.g. `order._status`,
53
+ `currentStage.ApprovalTemplateStages.step`). `getByPath` walks the path with optional
54
+ chaining; a missing path yields `undefined`.
55
+ - **`RuleContext`** is `{ order, currentStage?, stages? }`. `currentStage`/`stages` carry
56
+ approval-workflow state when available.
57
+ - **Named rules** (`{ type }`) are bespoke leaf checks that a field predicate cannot express
58
+ — they live in the `NAMED_RULES` registry. Currently only `stepTwoAssigned` (walks the
59
+ approval stages to confirm a step-2 assignee exists). An unknown `type` resolves to `true`.
60
+
61
+ ### Resolution primitives
62
+
63
+ - `evaluateRule(rule, ctx): boolean` — recursive evaluator over the combinators.
64
+ - `resolveFlag(flag, ctx, fallback = false): boolean` — the single primitive behind every
65
+ flag. Static booleans pass through; an object is treated as a `Rule` and evaluated;
66
+ `undefined` returns `fallback`. The same prop can therefore be static for one client and
67
+ rule-driven for another with **no code change**.
68
+
69
+ ### Applying flags to tenant config
70
+
71
+ `buildPatchedTenantFields({ tenantFields, context })` maps over
72
+ `tenantFields.recordActionFields.actionOptions` and patches each option's resolvable flags:
73
+
74
+ - `isEnabled` — via `resolveActionEnabled` (see back-compat below).
75
+ - `isVisible` and `isComplete` — only patched when the key is present on the option, each via
76
+ `resolveFlag(..., false)`.
77
+
78
+ It returns a new `tenantFields` with a patched `recordActionFields.actionOptions`. Callers:
79
+ `useSalesOrderRecordModalLayoutModel.tsx` and `useApprovalModalViewModel.tsx`.
80
+
81
+ ### Back-compat shim
82
+
83
+ The previous single-purpose `EnableRule` grammar (`alwaysEnabled`, `requireStepTwoAssigned`,
84
+ `requireOrderField`, `statusIn`) is mapped onto the generic engine via `evaluateEnableRule`,
85
+ so existing configs keep working while they migrate to `Rule`. `resolveActionEnabled` picks
86
+ the path:
87
+
88
+ 1. `isEnabled` is an object → treat as a `Rule`, `resolveFlag`.
89
+ 2. else a separate `enableRule` exists → legacy `evaluateEnableRule`.
90
+ 3. else → return the static `isEnabled`.
91
+
92
+ Retire the shim once all configs express flags as `Rule`.
93
+
94
+ ## Adding or re-gating a button
95
+
96
+ 1. **Pure config change (preferred):** edit the action option's `isEnabled` / `isVisible` /
97
+ `isComplete` in the per-client `recordActionFields` config to a static boolean or a `Rule`
98
+ tree. No code edit. The backend can ship the same tree.
99
+ 2. **New primitive check only:** if a gate genuinely can't be expressed as field predicates
100
+ + combinators (e.g. it must walk approval stages), add one entry to `NAMED_RULES` and
101
+ reference it as `{ type: "yourCheck" }`. This is the only expected reason to edit the
102
+ engine file.
103
+
104
+ ## Gotchas
105
+
106
+ - **`recordActionFields` is the current key** (renamed from `approvalActionFields` /
107
+ `orderViewFields`-era naming). `buildPatchedTenantFields` reads
108
+ `recordActionFields.actionOptions`; missing config falls back to `[]`.
109
+ - **`isVisible` / `isComplete` default to `false`** when present-but-undefined, while
110
+ `isEnabled` falls through to its static value. Don't assume a missing flag means "shown".
111
+ - **The table row menu (`RowActionButtons.tsx`) reads raw config directly** and is
112
+ intentionally out of scope for rule-driven enablement — it does not go through
113
+ `buildPatchedTenantFields`.
114
+ - **Prefer the `Rule` grammar over magic named keys.** Anything expressible as a field
115
+ comparison must stay config-only; adding to `NAMED_RULES` is the rare exception, not the
116
+ default.
117
+
118
+ ## Change history
119
+ - 2026-06-25 — Documented the generic Flag/Rule engine: combinators, field predicates, named-rule registry, `resolveFlag`/`buildPatchedTenantFields`, and the legacy `EnableRule` back-compat shim. (apeterson)
@@ -6,7 +6,7 @@ project: TOGa 2.5 Supply
6
6
  client: shared
7
7
  type: feature
8
8
  status: active
9
- updated: 2026-06-23
9
+ updated: 2026-06-25
10
10
  owners: [apeterson]
11
11
  files:
12
12
  - toga25-supply/src/fieldsConfig/index.ts
@@ -14,9 +14,12 @@ files:
14
14
  - toga25-supply/src/pages/SalesOrders/viewModel/FIELDS/
15
15
  - toga25-supply/src/pages/Inventory/viewModel/FIELDS/
16
16
  - toga25-supply/src/pages/Inventory/README.md
17
+ - toga25-supply/src/layout/ItemRecordModalLayout/viewModel/FIELDS/
18
+ - toga25-supply/src/layout/VendorItemRecordModalLayout/viewModel/FIELDS/
17
19
  related:
18
20
  - ../architecture.md
19
21
  - column-visibility.md
22
+ - action-button-rule-engine.md
20
23
  ---
21
24
 
22
25
  ## What it is
@@ -28,10 +31,13 @@ and the `useClientFields()` hook.
28
31
 
29
32
  ## How it works
30
33
 
31
- 1. **Per-client JSON lives next to the consuming page**, under `FIELDS/<CLIENT>/`
32
- (e.g. `src/pages/SalesOrders/viewModel/FIELDS/COMPASS/salesOrdersPageFields.json`). There is
33
- **always a `DEFAULT/`** folder every client falls back to. `<CLIENT>` must match the hostname
34
- `clientSlug` (uppercase `COMPASS`, `NYCHH`, …).
34
+ 1. **Per-client JSON lives next to the consuming page _or layout_**, under `FIELDS/<CLIENT>/`
35
+ (e.g. `src/pages/SalesOrders/viewModel/FIELDS/COMPASS/salesOrdersPageFields.json`, or for a
36
+ record-modal layout `src/layout/ItemRecordModalLayout/viewModel/FIELDS/COMPASS/itemRecordViewFields.json`).
37
+ There is **always a `DEFAULT/`** folder every client falls back to. `<CLIENT>` must match the
38
+ hostname `clientSlug` (uppercase — `COMPASS`, `COMPASSCANADA`, `NYCHH`, …). Record-modal layouts
39
+ typically split their config per modal mode — `*ViewFields.json`, `*EditFields.json`,
40
+ `*CreateFields.json` — one file per mode, per client.
35
41
  2. **`src/fieldsConfig/index.ts`** imports those JSON files and wires them into the
36
42
  `ClientFields` bundle — a normalized `FIELDS[clientSlug][role]` map merged over `DEFAULT`.
37
43
  3. **`useClientFields()`** resolves the active `clientSlug` (from `useHostnameStore`) + `role`
@@ -64,6 +70,22 @@ registry that maps enum → real value (e.g. `MODAL_RENDERERS: Record<ModalKey,
64
70
  This is the one place app-owned JSX is grafted back onto client-authored config. Clients never
65
71
  author JSX.
66
72
 
73
+ ### Record-modal header & action config
74
+
75
+ Record-modal layouts (`ItemRecordModalLayout`, `VendorItemRecordModalLayout`) now drive the
76
+ modal's chrome from the same per-client JSON, not from the component:
77
+
78
+ - **`header`** — `{ modalTag: { icon, iconStyle, title }, labelTemplate, statusBadge }`.
79
+ `labelTemplate` is a `"{dotted.path} literal"` string resolved against the record (same
80
+ `fillTemplate` convention as `optionLabelTemplate`). `statusBadge` is
81
+ `{ field, activeLabel, inactiveLabel }` — the component reads `record[field]` (e.g.
82
+ `isActive`) and renders the matching label. The component bakes in no titles or labels.
83
+ - **`editItem`** — a `{ valueKey, kind: "button", label, icon, isVisible, isEnabled }` action
84
+ block. `isVisible` / `isEnabled` are `Flag`s, so the Edit button is gated declaratively per
85
+ client (e.g. visible+enabled for vendor items, hidden for the DEFAULT item view). These flags
86
+ resolve through the same engine as SalesOrder action buttons — see
87
+ [action-button-rule-engine.md](action-button-rule-engine.md).
88
+
67
89
  ## Adding a new client-configurable field
68
90
 
69
91
  1. Author the serializable JSON under `FIELDS/DEFAULT/<thing>.json` + a folder per overriding client.
@@ -81,8 +103,18 @@ author JSX.
81
103
  - **New client *file* = no code change** (just import/wiring). **New render style/modal type
82
104
  (new enum value) = code change** in the hydration registry — that's the intended boundary
83
105
  between client config and app rendering.
106
+ - **Flat (un-wrapped) bundles need a fallback that tolerates the missing `DEFAULT` key.** Some
107
+ bundles wrap their config under a top-level `DEFAULT` key, others are authored flat. In
108
+ `fieldsConfig/index.ts`, derive the default defensively rather than assuming the wrapper exists:
109
+ `const DEFAULT_ORDER_VIEW_FIELDS = (defaultBundle as any).DEFAULT ?? (defaultBundle as any) ?? {};`
110
+ — fall through to the bundle itself, then `{}`, so a flat JSON file does not resolve to `undefined`.
84
111
  - Worked examples: `salesOrdersPageFields` (client × role) and `inventoryGroupings` (client-only +
85
- hydration; full write-up in `src/pages/Inventory/README.md`).
112
+ hydration; full write-up in `src/pages/Inventory/README.md`); `itemRecordViewFields` /
113
+ `vendorItemRecordViewFields` are record-modal-layout examples living under `src/layout/.../viewModel/FIELDS/`.
86
114
 
87
115
  ## Change history
116
+ - 2026-06-25 — Extended to record-modal layouts: per-mode `*ViewFields`/`*EditFields` JSON under `src/layout/.../viewModel/FIELDS/`, JSON-driven modal `header` (modalTag/labelTemplate/statusBadge) + `editItem` button gated via the Flag/Rule engine, and the flat-bundle defensive-default gotcha. (apeterson)
88
117
  - 2026-06-23 — Documented from the `add-client-fields` skill during initial knowledge seed. (apeterson)
118
+ - 2026-06-25 — Client field JSON also lives under `src/layout/<Modal>/viewModel/FIELDS/<CLIENT>/`
119
+ (record-modal layouts; split per mode: View/Edit/Create). Documented the defensive
120
+ `?? (defaultBundle) ?? {}` fallback for flat (un-`DEFAULT`-wrapped) bundles. (apeterson)
@@ -4,7 +4,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
4
4
 
5
5
  ## 1.0 framework
6
6
 
7
- - **library** (Library) _(framework core)_ — 8 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
7
+ - **library** (Library) _(framework core)_ — 9 doc(s) → [1.0/apps/library/INDEX.md](1.0/apps/library/INDEX.md)
8
8
  - **worker** (Worker) — 10 doc(s) → [1.0/apps/worker/INDEX.md](1.0/apps/worker/INDEX.md)
9
9
  - **togadesk** (TOGa Desk) — 8 doc(s) → [1.0/apps/togadesk/INDEX.md](1.0/apps/togadesk/INDEX.md)
10
10
  - **togaview** (TOGa View) — 6 doc(s) → [1.0/apps/togaview/INDEX.md](1.0/apps/togaview/INDEX.md)
@@ -20,14 +20,14 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
20
20
  - **api2** (API) — 6 doc(s) → [2.0/apps/api2/INDEX.md](2.0/apps/api2/INDEX.md)
21
21
  - **dbchanges2** (Database Changes) _(framework core)_ — 2 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
- - **saml** (SAML SSO Gateway) — 2 doc(s) → [2.0/apps/saml/INDEX.md](2.0/apps/saml/INDEX.md)
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)
25
25
  - **toga2-hub** (TOGa Hub) — 2 doc(s) → [2.0/apps/toga2-hub/INDEX.md](2.0/apps/toga2-hub/INDEX.md)
26
26
  - **talos** (TOGa IQ) — 6 doc(s) → [2.0/apps/talos/INDEX.md](2.0/apps/talos/INDEX.md)
27
27
  - **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
28
  - **ai-bdr** (AI-BDR) — 4 doc(s) → [2.0/apps/ai-bdr/INDEX.md](2.0/apps/ai-bdr/INDEX.md)
29
29
  - **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) — 5 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
30
+ - **toga25-supply** (TOGa 2.5 Supply) — 6 doc(s) → [2.0/apps/toga25-supply/INDEX.md](2.0/apps/toga25-supply/INDEX.md)
31
31
  - **toga-blox** (TOGa Blox) — 7 doc(s) → [2.0/apps/toga-blox/INDEX.md](2.0/apps/toga-blox/INDEX.md)
32
32
 
33
33
  ## standalone framework
@@ -51,6 +51,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
51
51
  - **Quad Graphics** (`quad`) → [clients/quad/INDEX.md](clients/quad/INDEX.md)
52
52
  - **Rate** (`rate`) → [clients/rate/INDEX.md](clients/rate/INDEX.md)
53
53
  - **Tow Foundation** (`tow-foundation`) → [clients/tow-foundation/INDEX.md](clients/tow-foundation/INDEX.md)
54
+ - **TOGA Technology** (`true`) → [clients/true/INDEX.md](clients/true/INDEX.md)
54
55
  - **Walmart Client Profile** (`walmart`) → [clients/walmart/INDEX.md](clients/walmart/INDEX.md)
55
56
  - **Wiss, Janney, Elstner Associates, Inc.** (`wje`) → [clients/wje/INDEX.md](clients/wje/INDEX.md)
56
57
 
@@ -0,0 +1,6 @@
1
+ # Client: TOGA Technology `true`
2
+
3
+ | Doc | Framework | Summary | Files |
4
+ |-----|-----------|---------|-------|
5
+ | [True Users / Personas Data Model](features/users-personas-data-model.md) | 2.0 | The `Client_True` schema models internal/staff users and their **personas** — used to gate UI and navigation by staff role in downstream tooling. | |
6
+ | [TOGA Technology](profile.md) | 2.0 | "True" is TOGa's own **internal / staff tenant** — the base client whose data lives in the `Client_True` schema in production. | |
@@ -0,0 +1,45 @@
1
+ ---
2
+ title: True Users / Personas Data Model
3
+ framework: "2.0"
4
+ project: _Underscore
5
+ client: true
6
+ type: client-feature
7
+ status: active
8
+ updated: 2026-06-25
9
+ owners: [jcardinal]
10
+ files: []
11
+ related:
12
+ - clients/true/profile.md
13
+ - 2.0/apps/saml/features/downstream-integration-contract.md
14
+ ---
15
+
16
+ ## Summary
17
+
18
+ The `Client_True` schema models internal/staff users and their **personas** — used to gate UI and
19
+ navigation by staff role in downstream tooling. After SSO resolves a user, an app looks up that
20
+ user's personas and drives visibility from persona membership.
21
+
22
+ ## How it works
23
+
24
+ ### Tables
25
+ - **`Users`** — key columns: `id`, `uuid` (char, unique), `email` (varchar, unique), `firstName`,
26
+ `lastName`, `displayName`, `isActive` (tinyint, default `1`), `password` (char, nullable), plus
27
+ FKs `contactId`, `clientId`, `locationId`, `userTypeId`, `supervisorUserId`.
28
+ - **`Personas`** — `id`, `uuid`, `name`, `number`.
29
+ - **`Users_Personas`** (many-to-many bridge) — `id`, `uuid`, `userId`, `personaId`.
30
+
31
+ ### Live personas (production)
32
+ TOGa Technology, Development Team, Legal Team, Contact Center, Operations, Executive, Audit.
33
+
34
+ ### Usage pattern for downstream apps
35
+ After SSO resolves the user, look up their personas via
36
+ `Users JOIN Users_Personas JOIN Personas`, then drive UI / navigation visibility by persona
37
+ membership.
38
+
39
+ ## Gotchas / known issues
40
+
41
+ - **Cache personas in session.** Cache the resolved persona names in `$_SESSION` at login to avoid
42
+ a per-request DB read for visibility checks.
43
+
44
+ ## Change history
45
+ - 2026-06-25 — Documented the Client_True Users/Personas data model and persona-gated UI usage pattern, discovered via prod DB schema inspection while planning persona-gated internal tooling (jcardinal)
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: "TOGA Technology"
3
+ framework: "2.0"
4
+ apps:
5
+ - _underscore
6
+ project: _Underscore
7
+ client: true
8
+ type: profile
9
+ status: active
10
+ updated: 2026-06-25
11
+ owners: [jcardinal]
12
+ files: []
13
+ related:
14
+ - clients/true/features/users-personas-data-model.md
15
+ ---
16
+
17
+ ## Summary
18
+
19
+ "True" is TOGa's own **internal / staff tenant** — the base client whose data lives in the
20
+ `Client_True` schema in production. It is also the default SSO identity mapper
21
+ (`_Model_True_ClientAuthentication`, which matches users by email). Internal tooling that gates UI
22
+ by staff role (e.g. the planned Toolbox app) reads its `Users` / `Personas` model.
23
+
24
+ - **Client DB:** `Client_True`
25
+ - **Client identifier:** `True`
26
+ - **SSO mapper:** `_Model_True_ClientAuthentication` (base; matches by email from NameID)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.201",
3
+ "version": "1.0.203",
4
4
  "description": "TOGA Technology Team Claude Knowledge System — shared AI coding harness with skills, knowledge base CLI, and project installer for Claude Code.",
5
5
  "keywords": [
6
6
  "claude",