feat(engagement): the template editor, the trigger catalog and the send log (engagement Phase 5b) #173
Reference in New Issue
Block a user
No description provided.
Delete Branch "feature/engagement-template-editor"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Engagement Phase 5b. Phase 5a gave templates a table, a renderer and nine seeded rows; nothing could change one. This is the screen that lets an operator change one without being able to break the mail the system depends on — plus the two screens §7.1 Q4 promised Phase 5.
Design of record:
docs/website/ENGAGEMENT.md§4.6.2, with an As built — 5b section landing in RunicGateway/docs#TBD. Operator guide in RunicGateway/runicgateway.com#TBD.Scope: three screens, not one
The Phase 5 body names only the editor. Q4's answer and §6.2 both promise "Triggers, Templates and the send log" in Phase 5, so all three shipped. Triggers is a read-only render of two endpoints Phase 2 already serves and cost no server work; the send log is one paged route over a table that has been filling since Phase 4a and whose index was built for it. Leaving either out would have left the nav group half-built and G15 — "no send log, no delivery status, no audit" — open with the rows already on disk.
The decisions, settled with the org lead before any code
component. Email blocks render on the serverstatuswas unenforceddraft/publishedshipped in 5a and nothing read it — an operator who saved a draft kept mailing itrenderByKeyrequirespublished, else the shipped seed. A draft now means what the word meanstrigger_id NOT NULLcore.admin.test-send. No schema change, no nullable columnprotectedblocked deletion; nothing stopped deleting a template a rule usesAnd the correction that changed the most code. §4.6.2 introduces duplicate as how a
protectedtemplate is customized. The schema comment says the opposite — "Editable, NOT deletable" — and the org lead's ruling is the schema's: a default is edited in place.protectednow blocks deletion and nothing else.Three things the plan did not know
email.itemList.variableholds a bare name, so a digest pointed atitmeswould have saved clean and arrived empty — the one variable mistake a reader of the template cannot see. Blocks now optionally declarevariables(props); the editor makes that field a<select>over the trigger's list variables, so the typo is unavailable.seed_keyloses its variable palette, so duplicatingnotify.eventwould have been refused for the{{title}}/{{intro}}it was copied with — the one action §4.6.2 offers, refusing itself. The copy inherits it;customized = 1is what the seeder actually reads.validateEmailBlocksreturns{ valid, errors }, not an array — and the first version of the model tested it with.length, so block validation never ran at all. Caught by the test asserting a preview refuses an unknown prop.One out-of-scope fix, approved
The live walk found that Phase 4a validated a rule's template key against
/^[a-z0-9][a-z0-9-]{0,63}$/— no dot — while every template key has one. No rule could name any template that exists, so §4.6.2's whole duplicate-and-point-a-rule-at-it workflow was unreachable. The org lead approved fixing it here; both models now read one pattern fromengagement/templates.The CSP surface
§4.6.2 called the preview "the one genuinely new CSP surface". Three things hold it:
<iframe sandbox="" srcDoc>(noallow-scripts, noallow-same-origin, opaque origin); the HTML crosses as a JSON string, never as a document from this origin; and blocks are validated-then-sanitized on write.client/test/emailTemplates.test.jsis the sibling test it asked for — it reads the attribute, not the file, because the first version failed on the comment explaining there is noallow-scripts.Verified
version: 2against the server's1, and deleting a registration.mailerpath; every refusal;address_hashnever leaving the server; and the end-to-end duplicate → point a rule → 409 on delete walk that the Phase 4a fix unblocked.api.vsapi.admin.) and a pill that wrapped mid-phrase; both fixed.🤖 Generated with Claude Code
https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
Phase 5a gave templates a table, a renderer and nine seeded rows; nothing could change one. This is the screen that lets an operator change one without being able to break the mail the system depends on — plus the two screens Q4 promised Phase 5: Triggers (read-only, from the registries) and the Send Log, which closes G15. The shape follows from one fact: a mail body is rendered by the SERVER, so the preview is too, and framed rather than redrawn in React. A client-side renderer would be a second implementation of the one artifact that matters, agreeing with the send path on the day it was written and drifting from the first Outlook fix on. Settled with the org lead before any code: a shipped default is edited IN PLACE (`protected` blocks deletion and nothing else, `customized = 1` keeps the edit); duplicate is the only way to a new template; `renderByKey` now requires `published`; a test send is logged under a synthetic `core.admin.test-send`; and a template a rule points at refuses deletion with a 409 naming the rules. Three things the plan did not know, found by building it: - The undeclared-variable check cannot be a token scan. `email.itemList.variable` holds a BARE name, so a digest pointed at `itmes` would have saved clean and arrived empty. Blocks now declare `variables(props)`; the editor makes that field a select over the trigger's list variables so the typo is unavailable. - A duplicate that drops `seed_key` loses its variable palette, so duplicating `notify.event` would have been refused for the tokens it was copied with — the one action §4.6.2 offers, refusing itself. The copy inherits it; `customized` is what the seeder actually reads. - `validateEmailBlocks` returns `{ valid, errors }`, not an array, and the first version tested it with `.length` — so block validation never ran at all. Also fixes a Phase 4a defect the live walk found, with the org lead's approval: a rule's template key was checked against a pattern with no dot in it, so no rule could name any template that exists — §4.6.2's whole duplicate-and-point-a-rule-at-it workflow was unreachable. Both models now read one pattern. Verified against the running stack: real multipart mail into a mailpit catcher including an unsaved draft, the draft/published arms both ways through the real mailer path, every refusal, and the end-to-end duplicate → rule → 409 walk. Server 1428 tests green, client 324. Co-Authored-By: Claude <noreply@anthropic.com>