toga-ai 1.0.882 → 1.0.884
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/tools/INDEX.md +1 -0
- package/knowledge/1.0/apps/tools/architecture.md +8 -0
- package/knowledge/1.0/apps/tools/features/email-queue-review.md +54 -0
- package/knowledge/2.0/apps/_underscore/features/email-send-pipeline.md +21 -6
- package/knowledge/2.0/apps/_underscore/features/email-template-sending.md +28 -3
- package/knowledge/2.0/apps/_underscore/features/page-meta-context-field-settings.md +18 -2
- package/knowledge/2.0/apps/toga25-supply/features/meta-driven-table-data.md +42 -11
- package/knowledge/2.0/apps/toga25-supply/features/transfer-orders-page.md +2 -2
- package/knowledge/INDEX.md +1 -1
- package/knowledge/clients/nychh/features/transfer-order-notification-emails.md +8 -1
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
| [Compass User & Persona Admin (tools /compass/users, /compass/personas)](features/compass-user-persona-admin.md) | Two pages in the Tools `developers` nav folder that let staff do, in a browser, the Compass requests that used to mean "email a developer, who writes a `dbchang |
|
|
10
10
|
| [Design Demo Admin](features/design-demo-admin.md) | SSO-gated Tools admin at `/design` that publishes self-contained "Claude Design" HTML exports as versioned demos into the `forward` repo over the GitHub REST AP |
|
|
11
11
|
| [Tools — Developers Folder (UUID & Password Generators)](features/developer-tools.md) | Tools "Developers" folder — the Generate UUID and Generate Password tools (reference example of the one-tool-per-`mvc/<route>/get.php` pattern); open for the ge |
|
|
12
|
+
| [/emails Email Queue Review (Tools → 2.0 Logs_<Client>.Email)](features/email-queue-review.md) | Staff page to browse any 2.0 client's `Logs_<Client>.Email` queue and release/reject `HELD` review-hold emails. |
|
|
12
13
|
| [/errors Curation Console (Tools → shared Core Logs DB)](features/errors-curation-console.md) | Internal-only 1.0 Tools triage/curation screen (`/errors`) for the 2.0 Issue/Event error pipeline, reading the shared Core Logs DB via `db_toga2logs`; curates, |
|
|
13
14
|
| [GitHub Audit (monthly code-churn report)](features/github-audit.md) | Internal monthly code-churn report at `/developers/github-audit` (lines added/removed across every `agilantsolutions` repo for one month); open for the two-phas |
|
|
14
15
|
| [Legacy Email Notifier (tools /email-migration/notify)](features/legacy-email-notifier.md) | SSO-gated admin tool at `/email-migration/notify` that emails employees a digest of mail still hitting their legacy domains (asks senders to switch to `@togatec |
|
|
@@ -79,9 +79,17 @@ Create `mvc/<folder>/<tool>/get.php` (copy `mvc/_TEMPLATE/get.php`), add one ent
|
|
|
79
79
|
|
|
80
80
|
## Gotchas
|
|
81
81
|
|
|
82
|
+
- **Bare `<header>` violated again (2026-09-23, /emails)** — use a `<div>` with a page class; see Critical rules.
|
|
83
|
+
- **The app-wide input rule outranks class selectors.** `style.css` ~line 769 styles inputs with five chained `:not()`; that specificity beats a page's class selector. A page restyling its inputs must repeat the same `:not()` chain under its page scope.
|
|
84
|
+
- **`.app-content` caps width at 960px** unless the page root has `.tool--wide`.
|
|
85
|
+
- **`.btn-secondary` and `.btn-secondary-outline` are RED (danger).** There is no neutral button class; do not use them for non-destructive actions.
|
|
86
|
+
- **CSRF is not a token.** `App_Page::validateCrossSiteRequestForgery` checks Origin/Referer only; POST handlers rely on it.
|
|
82
87
|
- **⚠ `tools` runs PHP 8, not 7.2 — check the vhost, not the framework label.** Despite being a 1.0 `App_` app, its vhost lives in the PHP 8 Apache (`DocumentRoot "C:/WWW/tools"`), and `App_Compass::tenantKey(mixed $key)` uses the `mixed` type hint (PHP 8.0+). The trap: `mixed` **passes** `php -l` on 7.2 (parsed as a class name) and only fatals at runtime — "Argument 1 passed to f() must be an instance of mixed, string given". So linting `tools` on 7.2 fails in the dangerous direction: it accepts code that would die.
|
|
83
88
|
- **Plaintext secrets were publicly web-readable (fixed 2026-07-24; rotation still owed).** `config.*.ini` (incl. `config.prod.ini`) holds plaintext production secrets (RDS master password, SMTP, Payeezy, NetSuite). The front controller routed through `index.php` on a `RewriteCond !-f` rule **only**, which is not access control — a direct `GET /config.production.ini` served the file **verbatim**. Fixed 2026-07-24 by adding `<FilesMatch>` `Require all denied` blocks (sensitive extensions incl. `.ini`, and any `config.*`) — see the [1.0 back-end security standard](../../standards/backend-php.md). **Action still owed:** rotate every secret in `config.production.ini` — the file was reachable, so treat all of it as compromised. (Location only — no values recorded here.)
|
|
84
89
|
- **Hardcoded GitHub Personal Access Token committed in plaintext (rotation owed).** `.ebextensions/020_setup_git_libraries.config` contains a GitHub PAT in plaintext, committed to the repo and therefore exposed in git history. **Action owed:** rotate the token and move it to an EB environment property (read via `getenv()`/`$_SERVER`/`$_ENV` per the deploy workflow's clear_env gotcha); a rotation does not un-expose git history. (Location only — no value recorded here.)
|
|
85
90
|
- **SSO initiation + replay defense are open items** — see `features/saml-sso-auth.md`.
|
|
86
91
|
- **~~Unproxied third-party polling from the browser~~ (resolved 2026-08-18).** The monitor column used to pull ~250 KB from OneUptime cross-origin every poll (per open tab) and hardcode the status-page ID in the HTML. It is now served by a same-origin server-side proxy `/v2/monitors/status/` (`App_Clickup_Monitors`) that fetches, reduces to ~1 KB and caches ~5s (APCu), with the status-page ID in config — one upstream fetch shared across all tabs, no cross-origin dependency. See `features/oneuptime-monitor-status-panel.md`. **Framework gotcha found building it (prod incident CV-2):** never call `curl_close()` in App_ code — it is deprecated in PHP 8.5 and the strict error handler promotes that `E_DEPRECATED` to a fatal (500). It is a no-op since PHP 8.0; drop it (or `unset($ch)`).
|
|
87
92
|
- **Framework-level Sentry error reporting is an open gap.** Prod printed "Sentry is not installed" because `sentry/sentry` was missing from `composer.json` (added `^4.10`; sibling apps use `^4.10`/`^4.19`). But even with Sentry in `vendor/`, the 1.0 framework does **not** auto-require `vendor/autoload.php` (the `App_Talos_S3` pattern `require_once`s it explicitly), so framework-level Sentry reporting for Tools still does not load. Any code path that needs a composer package must `require_once` the autoloader itself.
|
|
93
|
+
|
|
94
|
+
## Change history
|
|
95
|
+
- 2026-09-23 — Added gotchas from the /emails build: input :not chain, 960px cap/.tool--wide, red btn-secondary, Origin/Referer CSRF. (jcardinal)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: /emails Email Queue Review (Tools → 2.0 Logs_<Client>.Email)
|
|
3
|
+
framework: "1.0"
|
|
4
|
+
repo: tools
|
|
5
|
+
project: Tools
|
|
6
|
+
client: shared
|
|
7
|
+
type: feature
|
|
8
|
+
status: active
|
|
9
|
+
updated: 2026-09-23
|
|
10
|
+
owners: ["jcardinal"]
|
|
11
|
+
files:
|
|
12
|
+
- tools/_/app/emails.php
|
|
13
|
+
- tools/mvc/emails/get.php
|
|
14
|
+
- tools/mvc/emails/post.php
|
|
15
|
+
- tools/_/app/nav.php
|
|
16
|
+
- tools/assets/css/style.css
|
|
17
|
+
related:
|
|
18
|
+
- ../architecture.md
|
|
19
|
+
- ../../../../2.0/apps/_underscore/features/email-send-pipeline.md
|
|
20
|
+
- ../../../../2.0/apps/_underscore/features/email-template-sending.md
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Summary
|
|
24
|
+
|
|
25
|
+
Staff page to browse any 2.0 client's `Logs_<Client>.Email` queue and release/reject `HELD`
|
|
26
|
+
review-hold emails. Nav: Developers group, Development Team persona only.
|
|
27
|
+
|
|
28
|
+
## Key files / entry points
|
|
29
|
+
|
|
30
|
+
- `tools/_/app/emails.php` — `App_Emails`: client list, queue queries, actions.
|
|
31
|
+
- `tools/mvc/emails/get.php` / `post.php` — page + actions.
|
|
32
|
+
- `tools/assets/css/style.css` — `.tool--emails` block.
|
|
33
|
+
|
|
34
|
+
## How it works
|
|
35
|
+
|
|
36
|
+
1. Client list from Core (`db_toga2core`). Queue queries go over `db_toga2logs` against one
|
|
37
|
+
`Logs_<Client>` db named from `Core.Clients.logDatabaseId`, checked against
|
|
38
|
+
`^Logs_[A-Za-z0-9]+$`. Never joins Core with Logs.
|
|
39
|
+
2. Status tabs with counts, search, pagination, split-pane list/detail.
|
|
40
|
+
3. Actions:
|
|
41
|
+
- **Approve** — guarded `UPDATE ... WHERE status='HELD'`, requires `affectedRows === 1`; sets
|
|
42
|
+
`PENDING`, `dtReviewed`, `reviewedBy` = staff email.
|
|
43
|
+
- **Reject** — `HELD` → `CANCELLED`, same guard.
|
|
44
|
+
- **Send copy** — new `PENDING` row + attachments via `INSERT ... SELECT`, PHP v4 uuid per row.
|
|
45
|
+
4. Deep link `/emails?client=&emailId=` is the target of `[_underscore] email_review_url`.
|
|
46
|
+
|
|
47
|
+
## Gotchas / known issues
|
|
48
|
+
|
|
49
|
+
- Preview is an `<iframe sandbox="" srcdoc>` — the **only** XSS barrier for stored email HTML.
|
|
50
|
+
Never add `allow-scripts`.
|
|
51
|
+
- CSRF is `App_Page` Origin/Referer check, not a token (see architecture).
|
|
52
|
+
|
|
53
|
+
## Change history
|
|
54
|
+
- 2026-09-23 — Created: browse queue, Approve/Reject held emails, Send copy. (jcardinal)
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: ["bala", "tcox", "dfranks", "apeterson"]
|
|
9
|
+
updated: 2026-09-23
|
|
10
|
+
owners: ["bala", "tcox", "dfranks", "apeterson", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Email.php
|
|
13
13
|
- _underscore/String.php
|
|
@@ -39,6 +39,9 @@ SMTP** using PHPMailer, then flips each row to `SENT` or `FAILED`. So a 2.0 emai
|
|
|
39
39
|
unless both `setClientIdentifier(...)` and a From address (`fromEmailAddress`) are set. The
|
|
40
40
|
`Logs_[Client]` databases live on a separate cluster (`production-logs-*`), so `send()` points
|
|
41
41
|
`DB_CLIENT_LOGS` at the correct `Logs_[Client]` schema before inserting.
|
|
42
|
+
- Setters (2026-09-23): `setIsHeld(true)` writes the row as `HELD` instead of `PENDING`;
|
|
43
|
+
`setEmailTemplateId()` / `setReviewOfEmailId()` fill the matching columns. `send()` returns the
|
|
44
|
+
**int `Logs_<Client>.Email` id**.
|
|
42
45
|
- `worker2/Worker/Infrastructure/Email/Send.php` — `Run()` fetches up to 100 `PENDING` rows for
|
|
43
46
|
the client, and `sendEmail()` transmits each via SES SMTP. Constants: `STATUS_PENDING`,
|
|
44
47
|
`STATUS_SENT`, `STATUS_FAILED`, `MAX_RETRIES`.
|
|
@@ -54,6 +57,17 @@ SMTP** using PHPMailer, then flips each row to `SENT` or `FAILED`. So a 2.0 emai
|
|
|
54
57
|
4. On success the row flips to `SENT`; on failure `retryCount` is incremented and the row is
|
|
55
58
|
retried on subsequent runs until `MAX_RETRIES` (3), after which it is marked `FAILED`.
|
|
56
59
|
|
|
60
|
+
### Statuses + review columns (2026-09-23)
|
|
61
|
+
|
|
62
|
+
`status` enum = `PENDING` / `SENT` / `FAILED` / `HELD` / `CANCELLED`. The Send cron reads only
|
|
63
|
+
`PENDING`, so `HELD` and `CANCELLED` rows are never sent (worker2 unchanged). Columns:
|
|
64
|
+
`emailTemplateId` (after `dtCreated`), `reviewOfEmailId` (self FK, `ON DELETE SET NULL` — links a
|
|
65
|
+
review copy to the held original), `dtReviewed` (after `dtSent`), `reviewedBy` (after
|
|
66
|
+
`failureReason`). Migration: `dbchanges2/Logs_Client/2026-09-23a - EmailReview.sql` — must run
|
|
67
|
+
**before** the `_underscore` deploy or every `send()` fails. Who writes `HELD`: see
|
|
68
|
+
[email-template-sending.md](email-template-sending.md) *Review hold*; who releases it: Tools
|
|
69
|
+
[/emails](../../../../1.0/apps/tools/features/email-queue-review.md).
|
|
70
|
+
|
|
57
71
|
### `send()` resolves the log database itself — from the clientIdentifier
|
|
58
72
|
|
|
59
73
|
`send()` does not take a database name. It resolves `Logs_<Client>` by joining
|
|
@@ -76,10 +90,8 @@ derive one from the other).
|
|
|
76
90
|
|
|
77
91
|
## Gotchas / known issues
|
|
78
92
|
|
|
79
|
-
- **It is a POLL, not an enqueue
|
|
80
|
-
|
|
81
|
-
`send()` only INSERTs a `PENDING` row (`Logs_<Client>.Email`, `status` enum
|
|
82
|
-
`PENDING`/`SENT`/`FAILED`), and `worker2/Worker/Infrastructure/Email/Send.php` loops the clients
|
|
93
|
+
- **It is a POLL, not an enqueue.** (The old docblock claiming `send()` "enqueues a Worker 2 job"
|
|
94
|
+
was corrected 2026-09-23.) `send()` only INSERTs a `PENDING` (or `HELD`) row, and `worker2/Worker/Infrastructure/Email/Send.php` loops the clients
|
|
83
95
|
and `SELECT`s `PENDING` rows. Two consequences: nothing is queued anywhere but the database, and
|
|
84
96
|
**a `PENDING` row written from a local machine is never delivered**, because the worker polls the
|
|
85
97
|
sandbox/prod clusters, not your laptop. Local "the email never arrived" is expected — verify by
|
|
@@ -190,6 +202,9 @@ derive one from the other).
|
|
|
190
202
|
for a hotfix.
|
|
191
203
|
|
|
192
204
|
## Change history
|
|
205
|
+
- 2026-09-23 — Review hold: `HELD`/`CANCELLED` statuses, `emailTemplateId`/`reviewOfEmailId`/
|
|
206
|
+
`dtReviewed`/`reviewedBy` columns, `setIsHeld`/`setEmailTemplateId`/`setReviewOfEmailId`;
|
|
207
|
+
`send()` now returns the log id; stale "enqueues" docblock fixed. (jcardinal)
|
|
193
208
|
- 2026-09-17 — Three gotchas from building the NYCHH transfer-order emails, no code change: the
|
|
194
209
|
`Logs_<Client>.Email` `body` column holds the **finished HTML**, so a new email is verified by
|
|
195
210
|
reading the row rather than an inbox; **`send_debug_emails_to` redirects at QUEUE time only** (the
|
|
@@ -6,7 +6,7 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
9
|
+
updated: 2026-09-23
|
|
10
10
|
owners: ["jcardinal", "bala", "mhammontree", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Client/EmailTemplate.php
|
|
@@ -65,6 +65,29 @@ sends via `_Email`. The only client-specific input the send actually needs is th
|
|
|
65
65
|
4. Replace `{key}` placeholders in subject and body from the `$args` variadic map
|
|
66
66
|
(`replaceTemplateVariables`), then `send()`.
|
|
67
67
|
|
|
68
|
+
### Review hold (`EmailTemplates.isReviewRequired`, 2026-09-23)
|
|
69
|
+
|
|
70
|
+
The **template** decides (not `_Email`; direct `_Email` callers are never held). When
|
|
71
|
+
`isReviewRequired = 1`, `dispatch()` calls `_Email::send()` twice:
|
|
72
|
+
1. The real email, real recipients, `setIsHeld(true)` → row status `HELD` (Send cron skips it).
|
|
73
|
+
2. A `PENDING` copy, subject prefixed `[REVIEW] `, sent **only** to the template's `toCcBcc =
|
|
74
|
+
REVIEW` addresses. Body gets a yellow banner inserted after `<body>` listing the real
|
|
75
|
+
To/CC/BCC + held id, plus an approve link if `[_underscore] email_review_url` is set (Tools
|
|
76
|
+
`/emails?client=&emailId=`). Linked via `reviewOfEmailId`.
|
|
77
|
+
|
|
78
|
+
Release/reject happens in Tools [/emails](../../../../1.0/apps/tools/features/email-queue-review.md).
|
|
79
|
+
Rules:
|
|
80
|
+
- **Fail loud:** review on + no `REVIEW` rows → throws `Email template <uuid> requires review but
|
|
81
|
+
has no REVIEW addresses configured`; nothing is queued. Unknown `toCcBcc` → throws.
|
|
82
|
+
- `REVIEW` addresses are never merged into real To/CC/BCC.
|
|
83
|
+
- Out of scope by decision: no expiry of held rows, no body/PII hiding, no time-sensitive guard.
|
|
84
|
+
- Rejected design: an approve button inside the email (mail scanners pre-fetch links). Runner-up:
|
|
85
|
+
single held row, preview only in Tools.
|
|
86
|
+
|
|
87
|
+
Migrations: `Core/2026-09-23e - EmailTemplateReviewRecordField.sql` (RecordFields 2632 on
|
|
88
|
+
Records 202), `Client/2026-09-23a - EmailTemplateReview.sql`, `Logs_Client/2026-09-23a -
|
|
89
|
+
EmailReview.sql`. Deploy: SQL (Logs_Client first) → `_underscore` → tools.
|
|
90
|
+
|
|
68
91
|
Both `sendEmail()` and `send()` are thin wrappers that forward to `dispatch()`, so the API
|
|
69
92
|
and non-API paths run identical code — no behavioral drift between them.
|
|
70
93
|
|
|
@@ -76,10 +99,10 @@ gets a real error instead of a `true`/`false` it would otherwise ignore.
|
|
|
76
99
|
|
|
77
100
|
## Data model
|
|
78
101
|
|
|
79
|
-
- `EmailTemplates` (client DB): `uuid`, `isActive`, `sendFromEmailAddress`, `sendFromName`,
|
|
102
|
+
- `EmailTemplates` (client DB): `uuid`, `isActive`, `isReviewRequired`, `sendFromEmailAddress`, `sendFromName`,
|
|
80
103
|
`priority`, `subject`, `body`.
|
|
81
104
|
- `EmailTemplateOutgoingEmailAddress` (client DB): `emailTemplateId`, `emailAddress`,
|
|
82
|
-
`toCcBcc` (`TO`/`CC`/`BCC`).
|
|
105
|
+
`toCcBcc` (`TO`/`CC`/`BCC`/`REVIEW`).
|
|
83
106
|
|
|
84
107
|
## Client variations
|
|
85
108
|
|
|
@@ -207,6 +230,8 @@ worker method) in-process instead.
|
|
|
207
230
|
|
|
208
231
|
## Change history
|
|
209
232
|
|
|
233
|
+
- 2026-09-23 — Added the review hold (`isReviewRequired` + `REVIEW` addresses → `HELD` real email +
|
|
234
|
+
`[REVIEW]` copy); fail-loud on missing REVIEW rows / unknown `toCcBcc`. (jcardinal)
|
|
210
235
|
- 2026-09-17 — Added two caller-side gotchas found building the NYCHH transfer-order emails:
|
|
211
236
|
`replaceTemplateVariables()` does **no escaping** (plain `str_replace`, and the Send worker forces
|
|
212
237
|
HTML mode), so a stored value containing `<` arrives as live markup and every scalar must be
|
|
@@ -6,8 +6,8 @@ project: _Underscore
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-09-
|
|
10
|
-
owners: [tcox, bala, jcardinal]
|
|
9
|
+
updated: 2026-09-24
|
|
10
|
+
owners: [tcox, bala, jcardinal, apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- _underscore/Model/Core/Page.php
|
|
13
13
|
- _underscore/Model/Client/TableView.php
|
|
@@ -62,6 +62,18 @@ For a **joined** column, a `ClientRecordFieldSettings` row with a null `contextR
|
|
|
62
62
|
|
|
63
63
|
**Check which page slug the frontend ACTUALLY requests.** Every label and ACL lookup is filtered on the page's `Core.PageRecords`, so the records must be attached to **the page the frontend asks for** — not always the page you built. Elite's `service-requests` view is requested as **`meta?page=sales-orders-listing`** (Elite reuses that page), so records 99 (Tickets) and 36 (ServiceRequestTypes) had to be attached to `sales-orders-listing`, **not** only to the new `service-requests-listing` page. Attaching them to the new page alone changed nothing. **Read the Network tab first** — mis-diagnosed twice before the actual request URL settled it. A missing header is a *page-attachment* question, and only the real request identifies the page.
|
|
64
64
|
|
|
65
|
+
**The Core default label chain (verified against production Core, 2026-09-24).** For a base (non-custom, context-free) field the whole chain is Core-only:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Pages --< PageRecords >-- Records --< RecordFields
|
|
69
|
+
|
|
|
70
|
+
DefaultRecordFieldSettings (settingId = 'label-singular')
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`Core.Settings` id **5** = `label-singular`; `Core.Apps` id **1** = `supply`. Client/Persona/User/Language `RecordFieldSettings` only *override* this; the Core default already labels the field for **every** tenant. So seeding headers for a new page is **one `dbchanges2` `Core/` file** — no per-tenant fan-out and no cross-cluster split. (Worked example: `Core/2026-09-24a - BundlesListingPageMeta.sql`.)
|
|
74
|
+
|
|
75
|
+
**`Sections` are a different path — and listing pages have none.** `Sections` / `SectionRecordFields` / `ClientSectionRecordFieldSettings` do not feed table column labels. In prod and sandbox **every** `*-listing` page has **0** `Sections` rows; only `*-view` pages (`items-view`, `sales-orders-view`) have any. "This page has no Sections" is normal and is never the reason a header is missing.
|
|
76
|
+
|
|
65
77
|
**The `Core.PageRecords` gate.** `Page.php` assembles `settings.fields` from an **INNER JOIN** on `Core.RecordFields` (~L652) gated by **`Core.PageRecords`**: only fields of records *attached to the page* are emitted. A correct settings row does nothing until **both** the labelled field's record **and** the context record are attached to the page. Sequencing:
|
|
66
78
|
1. **The `isset()` guard in `Page.php` (below) goes first.** Until it ships, adding context rows is actively dangerous — a context row fatals **every** page that contains the labelled field's record but not the context record.
|
|
67
79
|
2. Then the `Core.PageRecords` attachments.
|
|
@@ -77,6 +89,7 @@ For a **joined** column, a `ClientRecordFieldSettings` row with a null `contextR
|
|
|
77
89
|
|
|
78
90
|
## Gotchas
|
|
79
91
|
|
|
92
|
+
- **Missing headers: check `PageRecords` + `DefaultRecordFieldSettings`, never `Sections`.** Listing pages have zero Sections by design.
|
|
80
93
|
- **A page-meta 500 is not necessarily about that page.** Trace it to the offending `ClientRecordFieldSettings` row via the array key in the message (it is a `Core.RecordFields` id), not via the slug you requested.
|
|
81
94
|
- **Error-handler strictness is a deploy-time behavior change.** Warnings survivable for years become 500s the moment the strict handler ships; expect a cluster of "new" failures in old code after such a deploy.
|
|
82
95
|
- Do not "fix" this by deleting the client's settings row — that removes a real client customization. The guard belongs in the framework.
|
|
@@ -87,3 +100,6 @@ For a **joined** column, a `ClientRecordFieldSettings` row with a null `contextR
|
|
|
87
100
|
- [TableView field/column metadata](../../api2/features/tableview-field-metadata.md)
|
|
88
101
|
- [Error reporting (Issue/Event)](error-reporting-issue-event.md)
|
|
89
102
|
- [Elite supply2 TableView config drift](../../../clients/elite/features/supply2-tableview-config-drift.md)
|
|
103
|
+
|
|
104
|
+
## Change history
|
|
105
|
+
- 2026-09-24 — Documented the Core-only label chain (`Pages`→`PageRecords`→`RecordFields`→`DefaultRecordFieldSettings` `label-singular`, setting id 5) and that `Sections` are a separate path listing pages never have. (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-09-
|
|
9
|
+
updated: 2026-09-24
|
|
10
10
|
owners: [apeterson]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/pages/SalesOrders/viewModel/useSalesOrdersPageViewModel.tsx
|
|
@@ -16,7 +16,8 @@ files:
|
|
|
16
16
|
- toga25-supply/src/pages/Bundles/viewModel/useBundlesPageViewModel.tsx
|
|
17
17
|
- toga-blox-npm/dist/hooks/useFetchPageMeta.d.ts
|
|
18
18
|
- toga-blox-npm/dist/hooks/useFetchTablePageMeta.d.ts
|
|
19
|
-
- toga-blox-npm/dist/hooks/useAssignTableFieldLabels.
|
|
19
|
+
- toga-blox-npm/dist/components/Table/hooks/useAssignTableFieldLabels.js
|
|
20
|
+
- _underscore/Model/Core/Page.php
|
|
20
21
|
- toga-blox-npm/dist/components/Table/hooks/useTableData.d.ts
|
|
21
22
|
related:
|
|
22
23
|
- ../architecture.md
|
|
@@ -79,17 +80,39 @@ The view model then returns `salesOrdersTableMeta` (after column-visibility adju
|
|
|
79
80
|
- **Table slug** (`tableState.slug`) identifies the *table* and is used for **both** the table-meta fetch (step 3) and the table-data fetch (step 5) — they must match so the columns and the rows describe the same table. In Sales Orders this slug is derived from the route pathname via `useServerTableUrlState`, not read out of `pageMeta`.
|
|
80
81
|
- `useAssignTableFieldLabels` is the bridge: it overlays the page meta's per-field labels (`pageMeta.settings.fields[table][field].label`) onto the table meta's fields, so page-scoped copy wins without re-fetching.
|
|
81
82
|
|
|
82
|
-
### 🚨 There is NO label in the table meta —
|
|
83
|
+
### 🚨 There is NO label in the table meta — labels come from Core page meta
|
|
83
84
|
|
|
84
|
-
`_Model_Client_TableView::meta()` emits, per field: `slug`, `settings{table, field, contextTable, contextField}`, `tableViewField`, `type`, `precision` and the `is*` flags — **and no `label`.** Every column header in this app is therefore supplied by the *page* meta and grafted on in step 4.
|
|
85
|
+
`_Model_Client_TableView::meta()` emits, per field: `slug`, `settings{table, field, contextTable, contextField}`, `tableViewField`, `type`, `precision` and the `is*` flags — **and no `label`.** `Core.TableViewFields` has no label column at all. Every column header in this app is therefore supplied by the *page* meta and grafted on in step 4.
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
**The label chain (verified against production Core, 2026-09-24):**
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Pages --< PageRecords >-- Records --< RecordFields
|
|
91
|
+
|
|
|
92
|
+
DefaultRecordFieldSettings (settingId = 'label-singular')
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
- `_Model_Core_Page::meta()` (`_underscore/Model/Core/Page.php`, the "fields settings" block ~L640-745) scopes which `RecordFields` a page exposes through **`PageRecords`** — the join table, **not** `Sections`.
|
|
96
|
+
- The label value is `Core.DefaultRecordFieldSettings` where `settingId` = the `label-singular` setting (`Core.Settings` id **5**; `Core.Apps` id **1** = `supply`). Per-tenant overrides layer on top via `Client/Persona/User/LanguageRecordFieldSettings` in the client DB, but the **Core default carries the label for every tenant**.
|
|
97
|
+
- **So seeding labels is a Core-only change** — one `dbchanges2` `Core/` file, no per-tenant fan-out, no cross-cluster split.
|
|
98
|
+
- `Sections` / `SectionRecordFields` / `ClientSectionRecordFieldSettings` are a **separate** settings path with nothing to do with table column labels.
|
|
99
|
+
|
|
100
|
+
**Trap — "the page has no Sections" is a dead end.** In prod (and sandbox) **every** `*-listing` page has **0 rows in `Sections`**; only the `*-view` pages (`items-view`, `sales-orders-view`) have any (5 each). Missing Sections is normal for a listing page and is never why labels are missing. Time was lost chasing this on 2026-09-24.
|
|
101
|
+
|
|
102
|
+
**Two failure shapes in `useAssignTableFieldLabels` that look identical on screen** (`toga-blox-npm/dist/components/Table/hooks/useAssignTableFieldLabels.js`):
|
|
103
|
+
|
|
104
|
+
| Cause | Code path | Result |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `pageMeta` is null (no page-meta record) | `if (!pageFields) return table` | fields carry **no `label` key at all** |
|
|
107
|
+
| `pageMeta` present but the page slug is wrong / field unmatched | `const label = labelSingular \|\| fieldSlug` | label is stamped as the **raw slug** |
|
|
108
|
+
|
|
109
|
+
Pointing a page at the **wrong** page-meta slug is therefore not harmless — the merge runs, matches nothing, and writes slugs in as labels. Both render as camelCase headers, so the symptom does not tell you which one you have; check the `pageMeta` response.
|
|
87
110
|
|
|
88
111
|
**Interim workaround (Transfer Orders, 2026-08-28):** a slug humanizer applied to the **merged** meta — `sourceLocation` → "Source Location" — deliberately after the merge so headers and the Columns modal cannot drift. It cannot recover a label the slug does not contain: the design's "TO Number" is unreachable from slug `number`.
|
|
89
112
|
|
|
90
|
-
**
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
**Preferred fix for a new list page (decided 2026-09-24): seed the Core page meta.** One `dbchanges2` `Core/` migration with a `<slug>-listing` `Pages` row + `PageRecords` link + `DefaultRecordFieldSettings` `label-singular` rows. This was previously mis-estimated as expensive (assumed per-tenant `SectionRecordFields` fan-out across ~30 client DBs plus a cross-cluster split) — **it is not; it is Core-only, one file.** Prefer it over the `humanizeSlug` fallback, which stays valid only where the slug already reads correctly. A third option, extending the **list surface** with per-column `FIELD` elements carrying a `labelMessageId`, additionally makes headers translatable through `MessageTranslations` — something the page-meta route cannot do. See [surface-frontend](./surface-frontend.md).
|
|
114
|
+
|
|
115
|
+
Worked example (Bundles, TRUE-82137, 2026-09-24) — `dbchanges2` `Core/2026-09-24a - BundlesListingPageMeta.sql`: `Pages(bundles-listing, appId 1)`, `PageRecords` → `Records` id 43 (bundles), and 5 `DefaultRecordFieldSettings` `label-singular` rows (`number` 265 → Number, `name` 248 → Name, `description` 1446 → Description, `isActive` 1356 → Active, `sortOrder` 1314 → Sort Order). The `Records` and `RecordFields` ids already existed in prod, so no team-reserved ids were needed.
|
|
93
116
|
|
|
94
117
|
### 🚨 Never hardcode a table meta in a view model — it fakes the flags too
|
|
95
118
|
|
|
@@ -104,9 +127,14 @@ Inventory page), and called `useTablePageMeta` but **threw the fetched result aw
|
|
|
104
127
|
**masked** the fact that the real served meta had them all off (the flags were NULL in
|
|
105
128
|
`TableViewFields`). The page looked configured while the database said otherwise.
|
|
106
129
|
|
|
107
|
-
Use the fetched server meta
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
Use the fetched server meta. A hardcoded meta is never a workaround for missing data; it hides the
|
|
131
|
+
data problem.
|
|
132
|
+
|
|
133
|
+
The label half was closed on **2026-09-24**, and it takes **two changes that must land together**:
|
|
134
|
+
`useBundlesPageViewModel.tsx:40` `"inventory"` → `"bundles-listing"` (commit `87b4442`), **and** the
|
|
135
|
+
Core seed `dbchanges2 Core/2026-09-24a - BundlesListingPageMeta.sql` (commit `89d88d7`). Shipping the
|
|
136
|
+
slug alone leaves the page with no page-meta record; shipping the seed alone leaves it fetching
|
|
137
|
+
`inventory`.
|
|
110
138
|
|
|
111
139
|
### What's in the table meta (the columns)
|
|
112
140
|
|
|
@@ -157,7 +185,10 @@ App-side glue (NOT in toga-blox):
|
|
|
157
185
|
- **`additionalData` must be in the query key** (it is, inside `useTableData`) — required for WHERE-filtered modal tables or React Query serves a stale unfiltered first render.
|
|
158
186
|
- **Never hardcode a table meta object in a view model.** It silently fakes `isSortable`/`isFilterable`/`isCopyable` and hides a real `TableViewFields` gap; always render the fetched meta.
|
|
159
187
|
- **toga-blox edits need a rebuild** — the app consumes `dist/`; editing only `src` is a silent no-op.
|
|
188
|
+
- **Listing pages have zero `Sections` rows by design** — never diagnose missing headers by looking at `Sections`.
|
|
189
|
+
- **A wrong page-meta slug is worse than none** — the merge stamps raw slugs in as labels; it looks the same on screen as having no page meta.
|
|
160
190
|
- **A new page with no page-meta record has no column labels at all** — the table meta carries no `label` and the merge hook no-ops on a null `pageMeta`, so headers *and* the Columns modal render raw camelCase slugs. Plan the label source before building the screen (see above).
|
|
161
191
|
|
|
162
192
|
## Change history
|
|
193
|
+
- 2026-09-24 — Corrected the column-label source: labels come from `Core.DefaultRecordFieldSettings` (`label-singular`) scoped by `Pages`→`PageRecords`→`RecordFields`, **not** from `Sections`; listing pages legitimately have zero Sections. Recorded the two `useAssignTableFieldLabels` failure shapes (null pageMeta vs. wrong slug → raw slug stamped as label), corrected the cost of seeding page meta to Core-only/one-file, and closed the Bundles label gap (slug fix + `Core/2026-09-24a`, TRUE-82137). (apeterson)
|
|
163
194
|
- 2026-09-23 — Added the hardcoded-table-meta anti-pattern found on the Bundles page: it produced raw slug headers and, worse, faked `isSortable`/`isFilterable`/`isCopyable` as true, masking NULL flags in `TableViewFields` (TRUE-82137). (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-09-
|
|
9
|
+
updated: 2026-09-24
|
|
10
10
|
owners: [apeterson, bala, "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- toga25-supply/src/pages/TransferOrders/view/CreateTransferOrderModal/hooks/useCreateTransferOrder.ts
|
|
@@ -191,7 +191,7 @@ One `SurfaceOverrides` row, `IS_VISIBLE = '1'`, **client-wide** (`personaId`/`ro
|
|
|
191
191
|
|
|
192
192
|
`src/pages/TransferOrders/`, wired into `ROUTE_REGISTRY` in `src/routes.tsx` as `transfer-orders`.
|
|
193
193
|
- **The TableView slug comes from the surface** (`listSurfaceMeta.surface.tableViewSlug`, falling back to a local constant), so re-pointing the page at a different view is a data change. The page never derives its identity from the route.
|
|
194
|
-
- **Column labels are humanized from the slug.** There is no `transfer-orders-listing` page-meta record, so blox's label merge contributes nothing and headers would render raw camelCase. The humanizer is applied to the **merged** meta so headers and the Columns-visibility modal cannot drift. It cannot produce the design's "TO Number" from slug `number` — see [meta-driven-table-data](./meta-driven-table-data.md)
|
|
194
|
+
- **Column labels are humanized from the slug.** There is no `transfer-orders-listing` page-meta record, so blox's label merge contributes nothing and headers would render raw camelCase. The humanizer is applied to the **merged** meta so headers and the Columns-visibility modal cannot drift. It cannot produce the design's "TO Number" from slug `number`. Seeding a `transfer-orders-listing` page-meta record is the fix, and it is cheap (Core-only, one `dbchanges2` file) — see [meta-driven-table-data](./meta-driven-table-data.md).
|
|
195
195
|
|
|
196
196
|
### The record modal — two fetches, one modal element
|
|
197
197
|
|
package/knowledge/INDEX.md
CHANGED
|
@@ -15,7 +15,7 @@ _Auto-generated by `knowledge.js index`. Do not hand-edit._
|
|
|
15
15
|
- **test** (Test) — 16 doc(s) → [1.0/apps/test/INDEX.md](1.0/apps/test/INDEX.md)
|
|
16
16
|
- **toga** (TOGa) — 3 doc(s) → [1.0/apps/toga/INDEX.md](1.0/apps/toga/INDEX.md)
|
|
17
17
|
- **store** (Store) — 3 doc(s) → [1.0/apps/store/INDEX.md](1.0/apps/store/INDEX.md)
|
|
18
|
-
- **tools** (Tools) —
|
|
18
|
+
- **tools** (Tools) — 22 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
|
|
19
19
|
|
|
20
20
|
## 2.0 framework
|
|
21
21
|
|
|
@@ -7,7 +7,7 @@ client: nychh
|
|
|
7
7
|
type: client-feature
|
|
8
8
|
status: active
|
|
9
9
|
updated: 2026-09-23
|
|
10
|
-
owners: ["bala", "rgirish"]
|
|
10
|
+
owners: ["bala", "rgirish", "jcardinal"]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Client/Nychh/TransferOrderEmails.php
|
|
13
13
|
- _underscore/Model/Nychh/TransferOrder.php
|
|
@@ -231,6 +231,12 @@ Cost of the split, stated so nobody is surprised later:
|
|
|
231
231
|
|
|
232
232
|
## Gotchas / known issues
|
|
233
233
|
|
|
234
|
+
- **Submitted uses the review hold** (template "Transfer Order - Submitted", `Client_Nychh`
|
|
235
|
+
`EmailTemplates` id 3, `isReviewRequired = 1`) — first prod user; see
|
|
236
|
+
[email-template-sending.md](../../../2.0/apps/_underscore/features/email-template-sending.md).
|
|
237
|
+
Turning it on **without** a `REVIEW` address row fails `Client/Nychh/TransferOrderEmails/SendSubmitted`
|
|
238
|
+
(prod Logs.Issue Y6) — expected fail-loud. Fix: add the `REVIEW` row, retry the job.
|
|
239
|
+
|
|
234
240
|
- **⚠ There is NO `try`/`catch` anywhere in this feature, by design.**
|
|
235
241
|
`sendTransferOrderEmail()` returns `void` and **throws `_Exception`** when
|
|
236
242
|
`_Model_Client_EmailTemplate::send()` returns `false` (which happens only when the template row is
|
|
@@ -260,6 +266,7 @@ loaded out of the dbchanges2 file so an unresolved `{placeholder}` fails a test.
|
|
|
260
266
|
unescaped values and both detail-box defects were all found there, not in production.
|
|
261
267
|
|
|
262
268
|
## Change history
|
|
269
|
+
- 2026-09-23 — Submitted template put on review hold; missing REVIEW row → job failure note. (jcardinal)
|
|
263
270
|
|
|
264
271
|
- 2026-09-23 — **The greeting now addresses whoever raised the order, not the site contact.**
|
|
265
272
|
`readTransferOrderForEmail()` selects `CreatedByUsers.firstName AS submittedByFirstName`, and
|
package/package.json
CHANGED