toga-ai 1.0.882 → 1.0.883

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.
@@ -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-17
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 — and the docblock lies.** The comment at `_underscore/Email.php`
80
- ~line 320 says `send()` "enqueues a Worker 2 job". **There is no enqueue anywhere in the file.**
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-17
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
@@ -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) — 21 doc(s) → [1.0/apps/tools/INDEX.md](1.0/apps/tools/INDEX.md)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toga-ai",
3
- "version": "1.0.882",
3
+ "version": "1.0.883",
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",