Skip to content

Wire an email-template barrel and move the reminder sweeps onto it - #99

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-69-email-templates-barrel
Sep 1, 2026
Merged

Wire an email-template barrel and move the reminder sweeps onto it#99
os-warren merged 2 commits into
mainfrom
claude/issue-69-email-templates-barrel

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #69

All three steps of the card, not just the barrel.

  1. src/email-templates/index.ts exports a named dulyEmailTemplates array, carrying the same "named array, never Object.values" comment as every other barrel in this repo.
  2. objectstack.config.ts gains exactly two lines — the import and emailTemplates: dulyEmailTemplates — in the same shape as the pre-created empty barrels already listed there.
  3. The three Lead-time reminders, overdue escalation and stagnation alerts #11 sweep strings moved into bundles, and the three notify nodes swapped from inline title/message to template + templateData. The §8 deviation recorded in src/flows/reminders.flow.ts's header is gone with them; no display text is authored in that file any more.

What a "bundle" is here

One (name, locale) ROW, not one template. IEmailService resolves (name, locale) at delivery, so each of the three names carries two rows — en and zh-CN, exactly the two locales i18n.supportedLocales declares. Six rows, three names.

duly.task_lead_time · duly.task_due_soon · duly.task_overdue.

This does not add a translation-coverage check — that is #18's deliverable, and #18's shape-discovering gate sees these rows the moment it lands. The only locale assertion in this PR is that each referenced bundle has an en row (the source-language anchor the app's own defaultLocale: 'en' resolves to); "every supported locale is covered" is deliberately left to #18.

The notifications still send — measured, with the body

test/email-templates.test.ts boots a real kernel with the real automation, job, messaging and email services, drives the real TimeRelativeTrigger sweep (the job service's trigger() runs the same handler the 08:00 cron would), and reads the row the inbox channel wrote:

[email] declared email templates materialized into sys_email_template {"seeded":6,"skipped":0,"total":6}
[messaging] reliable delivery on (outbox + dispatcher, 8 partitions, single-node)

sys_inbox_message.title    = "File the operator's emissions return"
sys_inbox_message.body_md  = "This is now on your list. Due 2026-09-08."

and the same bundle at locale: 'zh-CN' resolves to 这项任务已进入你的待办列表,截止日期 2026-09-08。 — which is the thing this card bought.

Why the assertion is on the body and not on "a notification was emitted": a template that resolves to nothing emits a notification too. Ablated — one character off the name (duly.task_lead_tim) — the flow run log is byte-identical to the green run:

[automation] run flow=duly_task_lead_time_reminder status=completed acted=0 skipped=1 unmeasured=1

The only difference is that no inbox row is ever written: the inbox channel classifies TEMPLATE_NOT_FOUND as PERMANENT, so the delivery dead-letters while the run reports success. Three tests go red on that ablation, including the end-to-end one. Nothing else in the toolchain catches it — pnpm validate parses the bundles (measured: an unrecognized key inside one takes validate to exit 1 naming the key) but never checks a template name on a notify node against them.

Two measured details worth reviewing

{{{…}}} in subject/bodyText, {{…}} in bodyHtml. renderTemplate HTML-escapes a double-brace hole. The inbox writes the rendered subject into title and the rendered text into body_md, neither of which is an HTML document, so the escaping form would put ' on screen for every duty whose subject contains an apostrophe — which title: '{record.subject}' never did. Measured both ways; bodyHtml keeps the escaping form.

bodyText is authored, not derived. Omitted, the service derives it by stripping tags from bodyHtml, which would make the in-app notification body a side effect of the markup. Authored, the sentence the owner reads is the same one the inline message sent before this card.

Five new devDependencies

@objectstack/{service-automation,service-job,service-messaging,plugin-email,trigger-schedule}. email, messaging and job are in PLATFORM_ALWAYS_ON_CAPABILITIES and triggers is already declared, so objectstack dev mounts all five for real — but the CLI owns that list and createStandaloneStack() returns only the datasource/metadata/ObjectQL base, so the suite has to mount them itself to exercise the delivery path the app actually runs on. Dev-only: nothing in src/ imports them.

Gates

pnpm validate && pnpm typecheck && pnpm test && pnpm build — all green at 4ca5549 (the branch head), 27 files / 674 tests. No changeset: this repo has no changeset mechanism.

Landing first as far as origin/main shows (75d086f, unmoved) — #18 merges main and re-runs.

Generated by Claude Code


Generated by Claude Code

os-warren and others added 2 commits September 1, 2026 15:56
Closes the app's only localizable notification path. `src/email-templates/`
now exists with a named `dulyEmailTemplates` array, `objectstack.config.ts`
carries the one `emailTemplates:` entry, and the three reminder sweeps name a
`template` + `templateData` instead of inline English `title`/`message`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
The inbox-row probe waited 15s inside vitest's default 5s test timeout, so a
missing delivery failed with the generic timeout instead of the labelled
message naming what never arrived. Measured on the dangling-name ablation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
@os-warren
os-warren marked this pull request as ready for review September 1, 2026 16:04
@os-warren
os-warren merged commit 88fc780 into main Sep 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A flow cannot send a localizable notification — the app wires no emailTemplates barrel, so only the explicitly non-localizable path exists

1 participant