toga-ai 1.0.221 → 1.0.223
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/worker/INDEX.md +1 -1
- package/knowledge/1.0/apps/worker/features/forecast2-netsuite-reconciliation.md +78 -1
- package/knowledge/2.0/apps/_underscore/INDEX.md +1 -1
- package/knowledge/2.0/apps/_underscore/architecture.md +4 -3
- package/knowledge/2.0/apps/_underscore/features/surface-resolver.md +29 -6
- package/knowledge/2.0/apps/api2/features/surface-meta-option.md +12 -1
- package/knowledge/2.0/apps/dbchanges2/INDEX.md +1 -1
- package/knowledge/2.0/apps/dbchanges2/features/surface-layer-schema.md +21 -7
- package/knowledge/2.0/apps/toga25-supply/INDEX.md +1 -1
- package/knowledge/2.0/apps/toga25-supply/features/surface-frontend.md +47 -14
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
| [Worker (1.0 Framework) Architecture](architecture.md) | `worker` is the legacy (**1.0** `App_` framework) **background-job tier**. | worker/index.php, worker/_/app/framework.php, worker/crons/, worker/schedules/, worker/ebs/cron.worker.php, worker/.ebextensions/035_cron.worker.config |
|
|
6
6
|
| [Compass MA Sales Order Exception Report](features/compass-ma-sales-order-exception-report.md) | A worker cron that emails operations the "Compass Refresh Exception Report" — Compass `MA%` sales orders whose corresponding Office Depot (ODP) sales order has | worker/crons/toga2/compass/workflow/7_generate_ma_sales_order_exception_report.php |
|
|
7
7
|
| [Compass Partial In-Transit & Delivered Emails (per package)](features/compass-partial-in-transit-delivered-emails.md) | Compass USA and Compass Canada send a **per-package** in-transit email (and a matching delivered email) instead of one email listing the whole order. | worker/crons/toga2/compass/update_salesorder_status_from_odp.php, worker/crons/toga2/compasscanada/workflow/3_update_salesorder_status_from_grand_and_toy.php |
|
|
8
|
-
| [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
|
|
8
|
+
| [Forecast2 ↔ NetSuite Reconciliation & Trueup Tooling](features/forecast2-netsuite-reconciliation.md) | CLI tools to **audit** and **repair** drift between the production `Forecast` DB (core2) and NetSuite. | test/@dave/checker.php, test/@dave/looper.php, test/@dave/reconcile_netsuite_totals.php, test/@dave/fixer.php, test/@dave/analyze_netsuite_forecast_diff.php, test/@dave/trueup_sales.php, test/@dave/trueup_open_orders.php, test/@dave/loop_trueup_open_orders.php, test/@dave/trueup_opportunities.php, test/@dave/probe_sales_gap_direct.php, test/@dave/probe_missing_oo_timing.php, test/@dave/probe_missing_oo_createdby.php, test/@dave/probe_drift_so_dates.php, test/@dave/probe_profit_invoices.php, test/@dave/probe_profit_gap.php, worker/crons/toga2/forecast2/common_import_sales_from_netsuite.php, worker/crons/toga2/forecast2/periodic_forecast_discrepancy_fix_open_orders.php, worker/crons/toga2/forecast2/import_open_orders.php, worker/schedules/cron.worker.infrastructure.json |
|
|
9
9
|
| [NetSuite → TOGa Supply Per-Client Sync (thin wrappers)](features/netsuite-togasupply-per-client-sync.md) | Syncs NetSuite transactions (sales orders, purchase orders, invoices, item receipts, item fulfillments, inventory adjustments) into each TOGa Supply (2.0) clien | worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/crons/toga2/netsuite/sync_togasupply_canon.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
|
|
10
10
|
| [Prudential: Send Shipments for the Day report (daily cron)](features/send-shipments-for-the-day.md) | Daily cron (9:00 PM) that emails Prudential and Dell stakeholders an Excel report of all devices shipped that day, including tracking number, serial number, emp | worker/crons/notifications/reports/send_shipments_for_the_day.php |
|
|
11
11
|
| [Onboarding a Client to the NetSuite TOGa Supply Sync](workflows/onboarding-client-to-netsuite-togasupply-sync.md) | How to add a new TOGa 2 client to the per-client NetSuite → TOGa Supply importer (`worker/crons/toga2/netsuite/`). | worker/crons/toga2/netsuite/sync_togasupply.php, worker/crons/toga2/netsuite/common_sync_togasupply.php, worker/schedules/cron.worker.sync.json, dbchanges2/_modules/netsuite/2026-04-01 - Parameters.sql |
|
|
@@ -6,9 +6,11 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-26
|
|
10
10
|
owners: [dfranks]
|
|
11
11
|
files:
|
|
12
|
+
- test/@dave/checker.php
|
|
13
|
+
- test/@dave/looper.php
|
|
12
14
|
- test/@dave/reconcile_netsuite_totals.php
|
|
13
15
|
- test/@dave/fixer.php
|
|
14
16
|
- test/@dave/analyze_netsuite_forecast_diff.php
|
|
@@ -42,6 +44,37 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
42
44
|
|
|
43
45
|
## Key files / entry points
|
|
44
46
|
|
|
47
|
+
- `checker.php [--from --to] [--category sales|openorders|opportunities|all] [--verbose] [--no-banner] [--fix --prod]`
|
|
48
|
+
— **hyper-fast drift detector** using a **moment-fingerprint drill-down** (default window = YTD).
|
|
49
|
+
Instead of materializing every row on both sides and diffing (the OOM trap that gates `reconcile`'s
|
|
50
|
+
per-txn diffs), each side computes a **5-number fingerprint PER TIME BUCKET** entirely in-DB in one
|
|
51
|
+
`GROUP BY`: `n` = COUNT of non-zero-measure records; `rev` = `ROUND(SUM(revenue),2)`;
|
|
52
|
+
`profit` = `ROUND(SUM(profit),2)` (Sales only); `idSum` = `MOD(SUM(MOD(id,P)),P)`;
|
|
53
|
+
`idSq` = `MOD(SUM(MOD(id*id,P)),P)`, where `P = 2147483647` (2³¹−1 Mersenne prime). `(rev,profit)`
|
|
54
|
+
catch **value drift**; `(n,idSum,idSq)` are a **power-sum fingerprint of the id SET** that catches the
|
|
55
|
+
**compensating case** a plain SUM total can't see (a missing txn masked by an extra txn of equal value).
|
|
56
|
+
Power-sums are pure integer arithmetic, so they compute **identically on Oracle-flavored SuiteQL and
|
|
57
|
+
MySQL** (no cross-engine hash-portability problem); `MOD` by the prime keeps the running sums inside
|
|
58
|
+
64-bit. Compared **top-down, descending only where fingerprints disagree**: L1 `GROUP BY` month → L2
|
|
59
|
+
`GROUP BY` day (only inside bad months) → L3 per-id list (`--verbose`, reporting only). A clean system
|
|
60
|
+
**stops at L1 in seconds pulling no rows**. Match identity is the **NetSuite internal id**
|
|
61
|
+
(`netsuiteTransactionInternalId` / `netsuiteSalesOrderInternalId` / `netsuiteOpportunityInternalId`);
|
|
62
|
+
date + category are only **scoping/localization**, never identity. Identity (count + id-moments) is
|
|
63
|
+
computed over **non-zero-measure rows only**, so checker's "in sync" verdict equals fixer.php's "nothing
|
|
64
|
+
to fix" **by construction** (a missing $0 row is a non-issue). On drift it hands `fixer.php` a **tight
|
|
65
|
+
day window** (window handoff, not id handoff — see decision below). `--fix` requires `--prod` (auto-runs
|
|
66
|
+
fixer on the localized window); a **bare run never writes**. Reads the prod core2 **read-replica**
|
|
67
|
+
directly (same pattern as `reconcile_netsuite_totals.php` / `fixer.php` — creds live in
|
|
68
|
+
`worker/config.worker.ini` / `CLAUDE.md`, not reproduced here).
|
|
69
|
+
- `looper.php [--interval=60] [--count=0] [--cmd "<verbatim>"] [--args "<extra checker flags>"] [--no-banner] [--no-fix]`
|
|
70
|
+
— **generic loop runner**, self-contained (no framework bootstrap; shells out via `passthru`). **DEFAULT
|
|
71
|
+
BEHAVIOR (bare `php looper.php`): loops `checker.php --no-banner --fix --prod` every 60s — i.e. it
|
|
72
|
+
continuously AUTO-CORRECTS PRODUCTION** (prints a bold warning Mode line; the once-only LOOPER ASCII
|
|
73
|
+
banner gets the subline "Looper / Checker / Fixer" in fix-prod mode). `--no-fix` gives a **read-only
|
|
74
|
+
detect loop**. `--count 0` = forever; `--cmd` loops a verbatim command instead of checker (no flags
|
|
75
|
+
added); `--args` appends extra checker flags. Suppresses checker's own per-run banner; prints per-run
|
|
76
|
+
dividers with run #, timestamp, exit code, elapsed. Rationale: continuous unattended drift monitoring +
|
|
77
|
+
auto-correction.
|
|
45
78
|
- `reconcile_netsuite_totals.php [from] [to] [--verbose]` — category grand totals NS vs Forecast2
|
|
46
79
|
(Sales, Sales Profit, Open Orders, Opportunities) with deltas. Read-only. Connects explicitly to
|
|
47
80
|
the **prod core2 reader**; NetSuite via SuiteQL SUMs. `--verbose` additionally prints the
|
|
@@ -141,6 +174,18 @@ by reconciling a chosen tranDate range directly against NetSuite.
|
|
|
141
174
|
stays bounded under the existing 1G `memory_limit`. (This corrected a prior docblock claim that `$sqls`
|
|
142
175
|
was never accumulated globally.)
|
|
143
176
|
|
|
177
|
+
- **checker → fixer handoff is WINDOW-based, not id-based (decision).** checker hands fixer a tight day
|
|
178
|
+
window and lets fixer **re-find** the discrepant ids inside it, rather than passing exact ids. Rationale:
|
|
179
|
+
maker≠checker independence (fixer re-derives the drift), and checker has already shrunk the window, so a
|
|
180
|
+
re-scan is cheap. Id-passing only wins when drift is **sparse across a wide span** (it would skip fixer
|
|
181
|
+
re-scanning clean gaps). **Deferred (NOT built):** (a) an `--ids` skip-find path so checker passes exact
|
|
182
|
+
ids; (b) a `--since` "recency-pruned fingerprint" mode that uses NS `lastmodifieddate` to choose which
|
|
183
|
+
`trandate` buckets to fingerprint, walking backward until clean. ⚠ A `--since` mode has **structural blind
|
|
184
|
+
spots**: pure NS **deletions** (orphan Forecast rows) and **old untouched drift** are invisible to a
|
|
185
|
+
lastmodified scan, so the full `trandate` fingerprint stays the authoritative backstop. Forecast tables key
|
|
186
|
+
on `tranDate`, not NS `lastmodifieddate`, so a **symmetric** lastmodified fingerprint isn't possible — it
|
|
187
|
+
would require NS-pull-then-id-lookup or the recency-pruned approach.
|
|
188
|
+
|
|
144
189
|
## Data model
|
|
145
190
|
|
|
146
191
|
`Forecast.Sales`, `Forecast.OpenOrderItems` on the **core2** cluster
|
|
@@ -206,6 +251,24 @@ None — Forecast2 is a single shared dataset.
|
|
|
206
251
|
concentrated **Dec 2025–Apr 2026**, driven by NetSuite re-valuing `costestimate` on older
|
|
207
252
|
invoices that no `lastmodifieddate`-based sync re-pulls. (Revenue reconciles to the penny; only
|
|
208
253
|
profit drifts.)
|
|
254
|
+
- **Open-orders "round-then-sum" vs "sum-then-round" — the most broadly reusable rounding gotcha.**
|
|
255
|
+
`Forecast.OpenOrderItems.revenue`/`profit` is `decimal(14,2)`: Forecast stores each open-order **LINE's**
|
|
256
|
+
revenue (`qtyOpen × rate`) **rounded to cents on store, then sums** (`SUM(round(line,2))`). A NetSuite-side
|
|
257
|
+
aggregate that does `ROUND(SUM(qtyOpen*rate),2)` (**sum-then-round**) diverges by up to ~½¢ **per line** —
|
|
258
|
+
benign rounding noise that **scales with line count**, so **no fixed aggregate-dollar tolerance can separate
|
|
259
|
+
it from real drift** (a busy bucket's benign band can reach whole dollars). This produced an **unfixable
|
|
260
|
+
phantom**: `fixer.php`'s FIND flags such a penny SO, but its FIX compares **per-line**, finds every line
|
|
261
|
+
already correct to the cent, writes nothing — and the SO churns forever in a loop. **FIX: round each line to
|
|
262
|
+
cents BEFORE summing on the NS side** — `SUM(ROUND(((-tl.quantity) - NVL(tl.quantitybilled,0)) * tl.rate, 2))`
|
|
263
|
+
— so NS matches Forecast's `decimal(14,2)` storage exactly; any remaining diff is then genuinely different
|
|
264
|
+
inputs = real drift. Applied to **both** `checker.php` (open-orders `nsSub` SELECT + HAVING) and `fixer.php`
|
|
265
|
+
`findOpenOrderDiscrepancies`. **Affects ONLY open orders** (a computed `qty×rate` product); Sales/Opportunities
|
|
266
|
+
sum pre-rounded `foreignamount`/`projectedtotal` and were already exact. Verified: full 18-month open-orders
|
|
267
|
+
window went from "drift" to cents-exact in sync after the fix. The previously-reported open-orders deltas were
|
|
268
|
+
entirely this artifact, NOT data drift: the ~$0.04 `reconcile` delta, and the per-SO 1¢ on **SO 6870666**
|
|
269
|
+
(NS 852.06 vs FC 852.07). **Correction to any prior note:** the open-orders **2025-04 "unlocalized month" was
|
|
270
|
+
this round-then-sum noise, NOT NS-live-vs-Forecast-replica timing jitter** — the rounding fix eliminated it,
|
|
271
|
+
disproving the earlier replica-jitter hypothesis.
|
|
209
272
|
- **Compare money at 2 decimals.** DB columns store 2dp but PHP `revenue - cost` carries float
|
|
210
273
|
dust (`313.6` vs `313.60000001`); raw `!=` produced thousands of phantom UPDATEs that re-wrote
|
|
211
274
|
identical values (1,839 on one open-orders run). Both tools now compare `round((float)$x, 2)`.
|
|
@@ -297,6 +360,20 @@ None — Forecast2 is a single shared dataset.
|
|
|
297
360
|
|
|
298
361
|
## Change history
|
|
299
362
|
|
|
363
|
+
- 2026-06-26 — **Added `checker.php` (moment-fingerprint drift detector) + `looper.php` (loop runner), and
|
|
364
|
+
found/fixed the open-orders round-then-sum rounding gotcha.** `checker.php` confirms sync in seconds and
|
|
365
|
+
localizes real drift via a 5-number per-bucket fingerprint (`n,rev,profit,idSum,idSq`; power-sums mod the
|
|
366
|
+
2³¹−1 prime → engine-portable) drilled top-down month→day→id, descending only into mismatched buckets,
|
|
367
|
+
pulling no rows on a clean book; hands fixer a tight **day window** (window handoff, not id — decision
|
|
368
|
+
recorded). `--fix` requires `--prod`; bare run never writes. `looper.php` defaults to looping
|
|
369
|
+
`checker --no-banner --fix --prod` every 60s (continuous prod auto-correction; `--no-fix` = read-only).
|
|
370
|
+
**Rounding gotcha:** `OpenOrderItems` `decimal(14,2)` stores each line round-then-sum, so a NS-side
|
|
371
|
+
sum-then-round aggregate diverges up to ~½¢/line (an unfixable phantom that loops fixer forever); fixed by
|
|
372
|
+
rounding each NS line to cents before summing (`SUM(ROUND(qtyOpen*rate,2))`) in both `checker.php` and
|
|
373
|
+
`fixer.php` `findOpenOrderDiscrepancies` — open-orders only (Sales/Opps already exact). This also
|
|
374
|
+
**disproves the earlier replica-jitter hypothesis** for the open-orders 2025-04 unlocalized month — it was
|
|
375
|
+
the same per-line rounding. Deferred: `--ids` skip-find and a `--since` recency-pruned fingerprint (blind to
|
|
376
|
+
NS deletions + old untouched drift). (dfranks)
|
|
300
377
|
- 2026-06-25 — **`trueup_open_orders` Step 4 now lists the exact per-statement SQL** (`<NS SO id> <SQL>`,
|
|
301
378
|
one line per statement) for both live and `--dry-run` passes, so a dry-run is auditable and any
|
|
302
379
|
unexpected insert/update/delete is traceable to its source order. Implemented via a global `$sqlLog`
|
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
| [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 |
|
|
13
13
|
| [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 |
|
|
14
14
|
| [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 |
|
|
15
|
-
| [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 |
|
|
15
|
+
| [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, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql, _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 |
|
|
16
16
|
| [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 |
|
|
17
17
|
| [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-29
|
|
10
10
|
owners: ["jcardinal", "rgirish"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/_underscore.php
|
|
@@ -71,7 +71,7 @@ Tier-1 trivial declarative rules (frozen `all/any/none + {field,op,value}` gramm
|
|
|
71
71
|
client-side; Tier-2 business logic computed in PHP returning booleans (lives in
|
|
72
72
|
`_Model_<Client>_*` overrides + interceptors). Config/logic boundary: **config describes,
|
|
73
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`.
|
|
74
|
+
`meta.surface`, never `data`. Generic FE machinery destined for `@agilant/toga-blox`. **Surface is the enforced presentation path — no feature flag.** The `SURFACE_ENABLED`/`VITE_SURFACE_ENABLED` flag and all legacy pre-Surface fallback branches were removed; legacy per-screen FIELDS config is deleted as each screen migrates (no dual-path fallback). The meta endpoint is query-string form: `GET /v2/surfaces/meta?slug=<slug>`.
|
|
75
75
|
|
|
76
76
|
**Rejected alternatives:** a generic EAV settings cascade (the old `*Settings` tables —
|
|
77
77
|
proven near-empty in prod); raw-CSS-in-config (the old "Dummy Fields" anti-pattern).
|
|
@@ -80,7 +80,7 @@ proven near-empty in prod); raw-CSS-in-config (the old "Dummy Fields" anti-patte
|
|
|
80
80
|
allowlist that throws on unknown ops, with a CI test asserting the op-set; defensive
|
|
81
81
|
resolver with logged fallbacks; debug-bundle endpoint ships WITH the resolver; `c_longValue`
|
|
82
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.
|
|
83
|
+
diff (do NOT delete meta() until every page is migrated); `BLANK_CLIENT_DATABASE` as a CI gate. Reserved Core id blocks: `Records` 333–341, `RecordFields` 2246–2433 (renumbered 2026-06-29, seed files only — a known file-vs-DB id drift in already-provisioned envs, to reconcile by re-provisioning). Anything spanning both id schemes must be id-agnostic (match by `route=`, not id).
|
|
84
84
|
|
|
85
85
|
## Naming convention & autoloader
|
|
86
86
|
|
|
@@ -324,3 +324,4 @@ multi-file UI components (`.php`/`.html`/`.css`/`.js`) invoked as `<_ComponentNa
|
|
|
324
324
|
## Change history
|
|
325
325
|
- 2026-06-11 — Documented lazy transaction gotcha in `_Database::register()` (rgirish)
|
|
326
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)
|
|
327
|
+
- 2026-06-29 — Surface made the enforced (un-flagged) presentation path; reserved id blocks renumbered to Records 333–341 / RecordFields 2246–2433 (known seed-vs-provisioned drift). (jcardinal)
|
|
@@ -6,10 +6,11 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-29
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Surface.php
|
|
13
|
+
- dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
|
|
13
14
|
- _underscore/Model/Core/SurfaceElement.php
|
|
14
15
|
- _underscore/Model/Core/Action.php
|
|
15
16
|
- _underscore/Model/Core/Vocabulary.php
|
|
@@ -30,8 +31,9 @@ related:
|
|
|
30
31
|
The runtime for the platform-wide **Surface** UI presentation layer: 9 `_underscore` models plus a
|
|
31
32
|
cached resolver, `_Model_Core_Surface::resolve(&$api, string $slug)`, that merges base config +
|
|
32
33
|
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
|
|
34
|
-
`GET /v2/surfaces/
|
|
34
|
+
**replacement for `_Model_Core_Page::meta()`** and is shipped as a scripted-API method called as
|
|
35
|
+
`GET /v2/surfaces/meta?slug=<slug>` (RecordScript args come from the **query string**, not the
|
|
36
|
+
path — see gotchas). Platform/shared — not client-specific. Schema:
|
|
35
37
|
[surface-layer-schema](../../dbchanges2/features/surface-layer-schema.md).
|
|
36
38
|
|
|
37
39
|
## The 9 models
|
|
@@ -62,8 +64,8 @@ Inputs come from `$api` (the same accessor `_Model_Client_TableView::meta` uses)
|
|
|
62
64
|
table pipeline (referenced, never duplicated).
|
|
63
65
|
7. Assemble the flat bundle. **Zero INSERTs.**
|
|
64
66
|
|
|
65
|
-
A `
|
|
66
|
-
resolver, not "later").
|
|
67
|
+
A `metaDebug()` inspector returns the resolved bundle plus the raw cascade layers (shipped with the
|
|
68
|
+
resolver, not "later"). It is **not** named `debug()` — see gotchas.
|
|
67
69
|
|
|
68
70
|
## Caching & invalidation
|
|
69
71
|
|
|
@@ -97,8 +99,29 @@ resolver, not "later").
|
|
|
97
99
|
falls back to sane defaults and never crashes a page. Strict per-`attribute` casting with logged
|
|
98
100
|
fallback on bad `SurfaceOverrides` data.
|
|
99
101
|
- **Cross-DB reads are batched** (DB_CORE vs DB_CLIENT) and FKs are soft — tolerate dangling refs.
|
|
102
|
+
- **A scripted-API method MUST NOT collide with a non-static `_Model` base method.** Naming the
|
|
103
|
+
inspector `debug()` caused a fatal *"Cannot make non static method _Model::debug() static"* — the
|
|
104
|
+
framework base `_Model` defines a NON-static `debug()` (Model.php:167) and the scripted-API
|
|
105
|
+
dispatch calls the method statically. Renamed to `metaDebug` and remapped the RecordScript
|
|
106
|
+
(`route 'debug' → phpMethod 'metaDebug'`). Vet any new scripted-API method name against the
|
|
107
|
+
`_Model` base (e.g. `debug`, and check others) before mapping it.
|
|
108
|
+
- **The endpoint is `GET /v2/surfaces/meta?slug=<slug>`, not `/v2/surfaces/<slug>/meta`.** TOGA
|
|
109
|
+
RecordScripts read their args from the **query string**; the path form makes the engine parse the
|
|
110
|
+
slug as a record uuid and return 404 EV-6. Pattern for any RecordScript:
|
|
111
|
+
`/<route>/<scriptRoute>?<arg>=...`.
|
|
112
|
+
- **Core metadata records exposed to the app must grant CORE role Public (id 1) READ.** The meta
|
|
113
|
+
RecordScript is ACL-gated on the `surfaces` Core record, and authenticated app users carry CORE
|
|
114
|
+
role Public(1); a seed that grants only Super User(3)/Base(4) returns 403 EZ-1. Grant Public(1)
|
|
115
|
+
`allowRead` on the `surfaces` record (full AclRecordPermissions → AclLogicGroups →
|
|
116
|
+
AclLogicGroupExpressions → AclRecordExpressions 'all' chain), mirroring how `apps`/`records`/
|
|
117
|
+
`record-fields` already expose Public READ. Only the `surfaces` record gets Public read — the
|
|
118
|
+
admin-CRUD sibling records stay Super-User-only (the resolver reads their tables server-side).
|
|
100
119
|
|
|
101
120
|
## Change history
|
|
102
|
-
- 2026-06-
|
|
121
|
+
- 2026-06-29 — Deploy fixes: renamed the scripted inspector `debug()`→`metaDebug()` (name collided
|
|
122
|
+
with non-static `_Model::debug()`, fatal); corrected the endpoint to query-string form
|
|
123
|
+
`GET /v2/surfaces/meta?slug=` (path form 404'd EV-6); granted CORE Public(1) READ on the `surfaces`
|
|
124
|
+
record so app users stop getting 403 EZ-1 on meta (id-agnostic migration matches by `route='surfaces'`). (jcardinal)
|
|
125
|
+
- 2026-06-25 — Built the 9 models + `resolve()`/`metaDebug()` as the cached, parameterized,
|
|
103
126
|
zero-write-on-read replacement for `_Model_Core_Page::meta()`; cache-bust interceptors on all 9
|
|
104
127
|
models (outside the transaction). meta() retained until per-page cutover with parity diff. (jcardinal)
|
|
@@ -6,7 +6,7 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-29
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- api2/Component/Api/V2/V2.php
|
|
@@ -42,6 +42,17 @@ traffic), behavior is **byte-for-byte unchanged** — pure data, zero extra work
|
|
|
42
42
|
the logic.
|
|
43
43
|
- **One call, no extra round-trip** — the state pass rides the existing data fetch.
|
|
44
44
|
|
|
45
|
+
## Gotchas
|
|
46
|
+
|
|
47
|
+
- **Meta endpoint 403 EZ-1 for normal app users → grant CORE Public(1) READ on the `surfaces`
|
|
48
|
+
record.** Authenticated app users carry CORE role Public (id 1); the meta RecordScript is
|
|
49
|
+
ACL-gated on the `surfaces` Core record, and a seed granting only Super User(3)/Base(4) rejects
|
|
50
|
+
them. Grant Public(1) `allowRead` on `surfaces` only (the admin-CRUD sibling records stay
|
|
51
|
+
Super-User-only — the resolver reads their tables server-side). See
|
|
52
|
+
[surface-resolver](../../_underscore/features/surface-resolver.md) gotchas for the full chain.
|
|
53
|
+
|
|
45
54
|
## Change history
|
|
55
|
+
- 2026-06-29 — Documented the meta-endpoint ACL: normal app users (CORE Public role 1) need
|
|
56
|
+
`allowRead` on the `surfaces` record or meta returns 403 EZ-1. (jcardinal)
|
|
46
57
|
- 2026-06-25 — Added the `surface=<slug>` opt-in option to the V2 engine; attaches per-record action
|
|
47
58
|
state under `meta.surface` (M2M-safe, defensive, absent = unchanged). (jcardinal)
|
|
@@ -3,5 +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
|
+
| [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/Core/2026-06-29a - ItemsSurfaceSeed.sql, dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.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 |
|
|
7
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/ |
|
|
@@ -6,12 +6,14 @@ project: Database Changes
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-06-
|
|
9
|
+
updated: 2026-06-29
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- dbchanges2/Core/2026-06-25a - SurfaceCoreTables.sql
|
|
13
13
|
- dbchanges2/Core/2026-06-25b - SurfaceRecordsAndFields.sql
|
|
14
14
|
- dbchanges2/Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql
|
|
15
|
+
- dbchanges2/Core/2026-06-29a - ItemsSurfaceSeed.sql
|
|
16
|
+
- dbchanges2/Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql
|
|
15
17
|
- dbchanges2/Client/2026-06-25a - SurfaceClientTables.sql
|
|
16
18
|
- dbchanges2/Client/2026-06-25b - SurfaceClientSeed.sql
|
|
17
19
|
- dbchanges2/Client/2026-06-25c - SurfaceClientAcl.sql
|
|
@@ -60,11 +62,13 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
|
|
|
60
62
|
**SalesOrders + login** vertical (not Tickets) as the review proof:
|
|
61
63
|
|
|
62
64
|
- `Core/2026-06-25a - SurfaceCoreTables.sql` — the 6 Core tables.
|
|
63
|
-
- `Core/2026-06-25b - SurfaceRecordsAndFields.sql` — `Core.Records`+`RecordFields` for all 9 tables (
|
|
65
|
+
- `Core/2026-06-25b - SurfaceRecordsAndFields.sql` — `Core.Records`+`RecordFields` for all 9 tables (`Records` ids **333–341**, `RecordFields` ids **2246–2433**; `recordId` via `model=` subselect). Also maps the meta `RecordScript` `route 'debug' → phpMethod 'metaDebug'` (the inspector cannot be named `debug` — see [surface-resolver](../../_underscore/features/surface-resolver.md)).
|
|
64
66
|
- `Core/2026-06-25c - SalesOrderLoginSurfaceSeed.sql` — Core seed + the `RecordScripts` resolve row.
|
|
67
|
+
- `Core/2026-06-29a - ItemsSurfaceSeed.sql` — the Items LIST screen seed: surfaces `items-list` (TABLE → existing `items` TableView id 9, recordId 21) + `items-list-actions` (BUTTON_BAR: refresh/columns/newItem) + Actions + ~11 Messages. Single DEFAULT bundle, no Client overrides.
|
|
68
|
+
- `Core/2026-06-29b - SurfaceMetaPublicReadAcl.sql` — grants CORE role Public(1) `allowRead` on the `surfaces` record (so app users stop getting 403 EZ-1 on meta). **Id-agnostic**: matches the record by `route='surfaces'`, so it works whether the record id is 333 or the legacy 2300.
|
|
65
69
|
- `Client/2026-06-25a - SurfaceClientTables.sql` — the 3 Client tables + `INSERT IGNORE Languages('en','English')`.
|
|
66
70
|
- `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.
|
|
71
|
+
- `Client/2026-06-25c - SurfaceClientAcl.sql` — full ACL chain for the 3 CLIENT-aclDatabase records (recordId refs updated to the 333-block renumber).
|
|
68
72
|
|
|
69
73
|
## Gotchas
|
|
70
74
|
|
|
@@ -79,12 +83,22 @@ SurfaceElements`). Core migrations run first; Client after. The session built/se
|
|
|
79
83
|
- **`SurfaceOverrides.value` is a stringly-typed escape hatch** — a long `CONFIG`/label override
|
|
80
84
|
would silently truncate at varchar(255); that is why `c_longValue mediumtext` exists, and the
|
|
81
85
|
resolver MUST cast strictly per `attribute` (bool/int/string/json) with a logged fallback.
|
|
82
|
-
- **Reserved id
|
|
83
|
-
|
|
86
|
+
- **Reserved id blocks (renumbered 2026-06-29):** Surface `Core.Records` ids **333–341** (9 tables),
|
|
87
|
+
`Core.RecordFields` ids **2246–2433**. (Originally seeded at 2300–2308 / 2400+, renumbered at the
|
|
88
|
+
team's request.) `RecordScripts.recordId` and the Core+Client ACL `recordId` refs were updated to
|
|
89
|
+
match. Use `model=` subselects for `recordId`, never hardcoded ids.
|
|
90
|
+
- **Known file-vs-DB id drift.** The renumber changed the **seed files only** — already-provisioned
|
|
91
|
+
envs still hold the old 2300/2400 ids. This is intentional/known, to be reconciled by
|
|
92
|
+
re-provisioning later. Anything that must work across both (e.g. the Public-READ ACL grant) must
|
|
93
|
+
be **id-agnostic** — match the record by `route='surfaces'`, not by id.
|
|
84
94
|
- **CLIENT-aclDatabase records** (`SurfaceOverrides`, `MessageTranslations`, `ThemeTokens`) need the
|
|
85
95
|
full 4-step ACL chain + field permissions in **each** client DB; resolve `roleId` by subselect.
|
|
86
96
|
|
|
87
97
|
## Change history
|
|
88
|
-
- 2026-06-
|
|
89
|
-
|
|
98
|
+
- 2026-06-29 — Renumbered the reserved seed id blocks (Records 2300-2308→**333–341**, RecordFields
|
|
99
|
+
2400+→**2246–2433**) in the seed files only — known file-vs-DB drift in provisioned envs. Added
|
|
100
|
+
the Items LIST seed (`ItemsSurfaceSeed.sql`) and the id-agnostic CORE Public(1) READ ACL grant on
|
|
101
|
+
`surfaces` (`SurfaceMetaPublicReadAcl.sql`, matches by `route='surfaces'`). (jcardinal)
|
|
102
|
+
- 2026-06-25 — Initial schema for the Surface layer: 6 Core + 3 Client tables, `Records`/`RecordFields`,
|
|
103
|
+
SalesOrders+login seed, ACL chain, and `BLANK_CLIENT_DATABASE` append. Typed
|
|
90
104
|
columns chosen over EAV (the old `*Settings` cascade was empirically near-empty in prod). (jcardinal)
|
|
@@ -8,4 +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/
|
|
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/meta?slug=<slug>` instead of statically-imported J | 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/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, toga25-supply/src/pages/Items/ItemsPage.tsx, toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx, toga25-supply/src/fieldsConfig/index.ts |
|
|
@@ -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-
|
|
9
|
+
updated: 2026-06-29
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/surface/useFetchSurfaceMeta.ts
|
|
@@ -16,11 +16,13 @@ files:
|
|
|
16
16
|
- toga25-supply/src/surface/SurfaceSection.tsx
|
|
17
17
|
- toga25-supply/src/surface/resolve.ts
|
|
18
18
|
- toga25-supply/src/surface/types.ts
|
|
19
|
-
- toga25-supply/src/surface/featureFlag.ts
|
|
20
19
|
- toga25-supply/src/surface/index.ts
|
|
21
20
|
- toga25-supply/src/pages/Login/LoginPage.tsx
|
|
22
21
|
- toga25-supply/src/pages/SalesOrders/SalesOrders.tsx
|
|
23
22
|
- toga25-supply/src/pages/SalesOrders/view/SalesOrderRecordModalLayout/view/sections/SalesOrderTopBar.tsx
|
|
23
|
+
- toga25-supply/src/pages/Items/ItemsPage.tsx
|
|
24
|
+
- toga25-supply/src/pages/Items/viewModel/useItemsPageViewModel.tsx
|
|
25
|
+
- toga25-supply/src/fieldsConfig/index.ts
|
|
24
26
|
related:
|
|
25
27
|
- ../../_underscore/features/surface-resolver.md
|
|
26
28
|
- meta-driven-table-data.md
|
|
@@ -29,14 +31,17 @@ related:
|
|
|
29
31
|
## What it is
|
|
30
32
|
|
|
31
33
|
The frontend consumer of the platform-wide Surface layer — DB-driven UI config fetched from
|
|
32
|
-
`GET /v2/surfaces/
|
|
34
|
+
`GET /v2/surfaces/meta?slug=<slug>` instead of statically-imported JSON field bundles. Lives in
|
|
33
35
|
`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.
|
|
36
|
+
so all 2.0 apps inherit it. Surface is now the **enforced, unconditional presentation path** (no
|
|
37
|
+
feature flag — see below). Backend: [surface-resolver](../../_underscore/features/surface-resolver.md).
|
|
35
38
|
|
|
36
39
|
## How it works
|
|
37
40
|
|
|
38
41
|
- **`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.
|
|
42
|
+
meta. Loading a different *record* into the same surface reuses cached meta. Builds the URL as
|
|
43
|
+
`/surfaces/meta?slug=...` (query string), **not** `/surfaces/<slug>/meta` (the path form makes the
|
|
44
|
+
engine parse the slug as a record uuid → 404 EV-6).
|
|
40
45
|
- **`evaluateSurfaceRule`** — the Tier-1 rule evaluator: a **frozen `all/any/none` + `{field,op,value}`
|
|
41
46
|
grammar** that **throws on an unknown op** (generalized from the SalesOrders
|
|
42
47
|
`buildPatchedTenantFields`/`evaluateEnableRule`/`resolveFlag` helpers). Evaluated client-side against
|
|
@@ -47,25 +52,53 @@ so all 2.0 apps inherit it. Backend: [surface-resolver](../../_underscore/featur
|
|
|
47
52
|
- **theme/message resolvers** (`resolve.ts`) — token → CSS and ICU message rendering.
|
|
48
53
|
- **`SurfaceActionBar`/`SurfaceActions` + `SurfaceSection`** — generic renderers.
|
|
49
54
|
|
|
50
|
-
##
|
|
55
|
+
## Enforced, not flagged
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
Surface is the **unconditional** presentation path. The `VITE_SURFACE_ENABLED` / `SURFACE_ENABLED`
|
|
58
|
+
feature flag was **removed entirely** (`featureFlag.ts` deleted, all gating conditionals and the
|
|
59
|
+
legacy pre-Surface fallback branches at the wired screens removed). Legacy per-screen `FIELDS` config
|
|
60
|
+
+ rendering are deleted as each screen migrates — there is no dual-path fallback.
|
|
61
|
+
|
|
62
|
+
## What's wired
|
|
63
|
+
|
|
64
|
+
Login + sales-orders list + the sales-order modal + the **Items LIST** screen. The SalesOrder action
|
|
65
|
+
bar reproduces `orderViewFields.json` approve/deny/approvalWorkflow/viewLog/editOrder by delegating
|
|
66
|
+
to the existing `onActionClick`/`onOpenLog` handlers. TABLE-type surfaces stay on the existing
|
|
67
|
+
TableView pipeline via `tableViewSlug` (see [meta-driven-table-data](meta-driven-table-data.md)) —
|
|
68
|
+
the Surface layer references the TableView, never replaces it.
|
|
69
|
+
|
|
70
|
+
## Per-screen migration recipe (proven on SalesOrders + Items)
|
|
71
|
+
|
|
72
|
+
The screen-by-screen full-app refactor follows one recipe:
|
|
73
|
+
1. Seed the screen's surface(s) in a `Core/*SurfaceSeed.sql` — a TABLE surface that references the
|
|
74
|
+
screen's existing `TableView` id via `tableViewSlug`, plus a BUTTON_BAR/actions surface + its
|
|
75
|
+
Actions + Messages (e.g. Items: `items-list` TABLE → TableView id 9 / recordId 21, and
|
|
76
|
+
`items-list-actions` refresh/columns/newItem).
|
|
77
|
+
2. Rewire the page + its view-model to consume the surface bundle (`ItemsPage.tsx` /
|
|
78
|
+
`useItemsPageViewModel.tsx`).
|
|
79
|
+
3. **Delete** the legacy `FIELDS` config: the page's `*Fields.json` and its `fieldsConfig/index.ts`
|
|
80
|
+
entries.
|
|
81
|
+
Keep TABLE rendering on the existing TableView pipeline; only the chrome (actions/labels/visibility)
|
|
82
|
+
moves to Surface. A screen with no client/role/lang variance ships a single DEFAULT bundle (no
|
|
83
|
+
Client overrides), as Items did.
|
|
58
84
|
|
|
59
85
|
## Gotchas
|
|
60
86
|
|
|
61
|
-
- **
|
|
62
|
-
|
|
87
|
+
- **No feature flag, no fallback.** Surface is the enforced path; a missing/broken surface bundle
|
|
88
|
+
does not silently fall back to legacy JSON — that path is deleted per screen as it migrates.
|
|
89
|
+
- **Endpoint is query-string form** — `GET /v2/surfaces/meta?slug=...`, not `/surfaces/<slug>/meta`
|
|
90
|
+
(the path form 404s EV-6).
|
|
63
91
|
- **Tier-1 only on the client.** Business-logic gates (Tier-2) arrive as resolved booleans from the
|
|
64
92
|
backend (`meta.surface`); never re-encode business rules in the FE evaluator.
|
|
65
93
|
- **`tsc` not yet run** this session (the private `@agilant/toga-blox` registry needs npm creds);
|
|
66
94
|
treat type-checking as pending. Runtime `GET /v2/surfaces/{slug}/meta` also not yet exercised.
|
|
67
95
|
|
|
68
96
|
## Change history
|
|
97
|
+
- 2026-06-29 — DECISION: Surface is now enforced, not flagged — removed `VITE_SURFACE_ENABLED`/
|
|
98
|
+
`SURFACE_ENABLED` (deleted `featureFlag.ts` + all gating/legacy-fallback branches). Fixed
|
|
99
|
+
`useFetchSurfaceMeta` to the query-string endpoint `/surfaces/meta?slug=` (path form 404'd EV-6).
|
|
100
|
+
Migrated the Items LIST screen (second proof of the per-screen recipe; deleted `itemsPageFields.json`
|
|
101
|
+
+ its fieldsConfig entries). (jcardinal)
|
|
69
102
|
- 2026-06-25 — Built `src/surface/` (fetch hook, frozen Tier-1 rule evaluator, action registry, theme/
|
|
70
103
|
message resolvers, action-bar + section renderers); wired login + SalesOrders list + SO modal behind
|
|
71
104
|
the OFF-by-default `SURFACE_ENABLED` flag. Generic machinery destined for toga-blox. (jcardinal)
|
package/package.json
CHANGED