diff --git a/.planning/phases/35-mail-templates-and-layouts-editor/35-CONTEXT.md b/.planning/phases/35-mail-templates-and-layouts-editor/35-CONTEXT.md new file mode 100644 index 00000000..1ed35b08 --- /dev/null +++ b/.planning/phases/35-mail-templates-and-layouts-editor/35-CONTEXT.md @@ -0,0 +1,187 @@ +# Phase 35: Mail templates and layouts editor - Context + +**Gathered:** 2026-11-06 +**Status:** Ready for planning + + +## Phase Boundary + +Administrators edit the mail the application sends from the admin panel, as in WinterCMS's Mail templates screen: + +- Every template registered through `pact.HasMailTemplates` (and postcard's built-in mail) is listed. +- Each template, layout and partial can get a database override, and the override can be reset to the file version. +- Admins can create custom layouts. +- A Branding page holds structured colours and a logo. +- A shared image library holds uploaded images. +- Drafts can be previewed and test-sent. + +postcard renders the override when one exists and the plugin's file otherwise, so mail that already works keeps working with no change to apps or plugins. + +Not in this phase: mail driver/SMTP settings screens, MailBlocker, newsletters/bulk mail, a WYSIWYG mail body. + + + + +## Implementation Decisions + +### Placement and permissions +- **D-01:** The screens live **inside cabana**, as an internal pseudo-plugin. This is the same pattern as Administrators/Roles in 19.6.1 (`modules/cabana/backend_screens.go`, `modules/cabana/backendscreens/`). Every app gets them with no setup. postcard stays free of admin and cabana code; cabana (or a thin glue layer) supplies postcard with the DB-backed layer through an interface postcard defines. +- **D-02:** **Navigation: a "Mail" section in Settings**, with entries for Mail templates, Mail layouts, Mail partials, Mail branding and Mail images. Links use `pact.HasSettings`, as in 19.6.1 D-02. There is no top-level nav item. +- **D-03:** **Two permission codes**, registered by the cabana pseudo-plugin: + - `system.manage_mail_templates` covers the templates list/form, template preview and test send. + - `system.manage_mail_layouts` covers layouts, partials, Branding and the image library, because these change every mail. + + Neither code is granted to any role by default (as in 19.6.1 D-04). Only superusers reach the screens until someone grants a code. The image library and its insert picker must also be usable from the template form by holders of `system.manage_mail_templates`, either read/pick only or with upload too (planner's call, stated in the plan). — **Reversibility:** costly — the codes end up stored in `backend_user_roles.permissions` / `backend_users.permissions` JSON on every install that grants them. + +### Editing model: templates +- **D-04:** **A template override stores subject + Markdown body + layout.** + - The body stays Markdown with Go `html/template` variables (`{{ .name }}`), the same format as the file. + - The plain-text part is still derived from the body; there is no hand-written text part. + - The layout is a dropdown over all layouts (file and custom). + - Description stays file-owned and read-only. +- **D-05:** **Locale variants stay separate templates.** `activate.htm` and `activate-en.htm` are their own rows. The list has a locale column, parsed from the suffix convention postcard already uses, plus a locale filter. There is no grouping and no new locale model. +- **D-06:** **Reset to the file version** deletes the override row, on templates, file layouts and file partials alike. +- **D-07:** **Drift notice.** When an override is saved, it stores a hash of the file content it was based on. If the plugin file later changes (for example after a plugin upgrade), the override keeps winning, but the list and the form flag "file changed since override". The form offers a way to view the current file version (a diff if cheap) and the reset button. — **Reversibility:** costly — the hash column is part of the override table schema (migration). + +### Editing model: layouts, Branding, partials +- **D-08:** **Layouts use the same override model as templates.** + - Every file layout is listed: postcard's built-in default plus plugin layouts such as sm-user-plugin `layouts/user.htm` and paymentgateway `layouts/paymentgateway.htm`. + - Each file layout can get a DB override with reset. + - Admins can also **create custom layouts** that exist only in the DB. Custom layouts can be deleted (not reset); deleting one still assigned to template overrides is refused, with a count of those templates. + - "Seeded predefined layouts" means a couple of framework-shipped file layouts, for example the existing default plus one or two more such as system/minimal (the planner picks the set). They are not rows copied into the DB at boot. +- **D-09:** **Layouts are edited in a new `codeeditor` form field type**, Winter-style, with syntax highlighting for HTML/CSS/text. It is added to cabana's field registry and to the admin SPA. The layout form has tabs for the HTML wrapper, the text wrapper and the CSS/header. The field type is reusable by later phases. — **Reversibility:** costly — a published field type name in `fields.yaml` that plugins may start using. +- **D-10:** **Branding page**, as in Winter's mail brand settings. + - It is a settings page (cabana settings model) with Winter's brand colour set: body/content background, text, link, the button primary/positive/negative colours, and header/footer colours. + - It also has a **logo** upload, exposed to layouts as `{{ .logoUrl }}`. + - The colours compile into the brand CSS slot that postcard already inlines. +- **D-11:** **Precedence: Branding replaces config brandCss.** While no Branding record has been saved, `mail.brandCss` from config is used, so existing apps are unchanged. Once Branding is saved, its compiled CSS replaces `mail.brandCss`. `mail.css` is unaffected and still applied. +- **D-12:** **Mail partials are in scope, with file + DB override like templates.** + - Files live at `views/mail/partials/.htm` and are named `::mail.partials.`. + - Plugins declare them through a **new additive optional pact capability**. `pact.HasMailTemplates` is not changed: core plugins must not break. + - Partials are used from template bodies and layouts through a Go template call (the planner picks the exact syntax or helper). + - A few built-in partials ship with postcard (button, panel, table). + - Admins override and reset partials; editing them needs `system.manage_mail_layouts`. + + — **Reversibility:** costly — a published pact capability and naming convention that plugins implement. + +### Validation and send-time safety +- **D-13:** **Saves are validated.** Saving a template, layout or partial override parses it, test-renders it with the sample vars (D-15) and runs postcard's existing safety checks (script/iframe/object/embed, inline handlers, `javascript:`/`vbscript:`/`data:` URLs, CR/LF in the subject). Failures are field validation errors. +- **D-14:** **Fallback at send.** If an override still fails to render at send time (for example a variable missing at runtime), postcard logs the failure and sends the file version, so mail delivery doesn't stop. A template with no file (impossible for templates; custom layouts only) falls back to the default layout. + +### Preview and test send +- **D-15:** **Sample variables**: plugins may add optional sample values to a template file. One option is a sample section in the INI header; another is an additive capability (planner's call). Variables without a sample render as a visible placeholder such as `[name]`. Existing template files work unchanged. +- **D-16:** **Live preview of the unsaved draft.** The template, layout and partial forms render the current draft through postcard with sample vars. The HTML part is shown in a **sandboxed iframe**, and a tab shows the plain-text part. Rendering happens on the server through postcard, so preview matches real sends. +- **D-17:** **Test send to my own address only.** A button sends the current draft (with sample vars) to the signed-in admin's email through the configured driver. It needs the same permission as editing that record and is rate-limited per admin. There is no free-text recipient field. + +### Images +- **D-18:** **A shared mail image library.** A "Mail images" list stores images through `lagoon/attach` on the public disk. Any template, layout or partial can use them, and the Branding logo also uses this storage. +- **D-19:** **Delivery via absolute public URLs.** `` uses the attachment's absolute public URL, built from `storage.uploads.public_url` / `app.url`. No CID inline parts; postcard's `RenderedMessage` and drivers are unchanged. +- **D-20:** **An insert button in the editors.** A picker next to the markdown editor (template bodies) and the codeeditor (layouts/partials) inserts `![alt](url)` or `` for a chosen library image. + +### Quality gates +- **D-21:** Unit tests are the last plan (project rule). They cover: + - override precedence and reset; + - drift detection; + - fallback at send; + - save validation; + - Branding precedence over config; + - the partial capability (boot errors, namespace rule); + - the permission split; + - the test-send recipient restriction and rate limit. + + The postcard and cabana READMEs and the affected `docs/` pages are updated in the same commits as API/config/permission changes. The `admin/` changes regenerate `admin/openapi/admin.json` + `schema.d.ts` (`scripts/check-admin-openapi.sh`) and rebuild the committed `modules/boardwalk/dist` (`scripts/check-admin-dist.sh`). + +### Claude's Discretion +- Table names and schema (one overrides table per kind, or a shared one), as long as it is Postgres/GORM with gormigrate migrations owned by the cabana pseudo-plugin, or by postcard if the planner justifies it. +- Exact partial call syntax and the built-in partial set beyond button/panel/table. +- Which extra predefined file layouts ship (D-08). +- Whether the image picker on the template form needs `manage_mail_layouts` for uploads (D-03 note). +- How postcard learns about overrides: a lookup interface checked per send, or a cache invalidated on save. It must be correct with several app instances, so per-send lookup or explicit invalidation, never a stale boot-time snapshot. +- How the SPA codeeditor is built (the library choice needs a STACK.md/decision note per the dependency rule). +- The plan split (lean planning: present the count before writing PLAN.md files). + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Mail layer +- `modules/postcard/README.md` covers the current template/layout format, catalog, drivers, safety checks and config keys (`mail.css`, `mail.brandCss`). +- `modules/postcard/templates.go` and `modules/postcard/mailer.go` hold `Catalog.Register`, `Mailer.Send`, `Activate` and `BootPlugin`, which are the integration point for the override layer. +- `modules/postcard/assets/default.htm` is the built-in default layout (header `==` text `==` HTML). +- `modules/pact/capabilities.go` §`HasMailTemplates` (around line 1271) must stay unchanged; partials and samples go in new additive capabilities. + +### Admin patterns +- `.planning/phases/19.6.1-backend-administrators-screen/19.6.1-CONTEXT.md` covers the cabana internal pseudo-plugin, permission codes registered by cabana, Settings placement via `pact.HasSettings`, and no default role grant. +- `modules/cabana/backend_screens.go` and `modules/cabana/backendscreens/` hold the internal pseudo-plugin and the YAML controllers to mirror. +- `modules/cabana/settings.go` holds the settings model pattern for the Branding page. +- `modules/cabana/field_markdown.go`, `modules/cabana/field_file.go` and `modules/cabana/field_type.go` are where the new `codeeditor` field and the image picker hook in. +- `admin/src/components/form/fields/` holds the SPA field widgets (MarkdownField, FileuploadField); the new CodeEditorField goes here. +- `.planning/notes/2026-10-07-embedded-admin-release-gate.md` is the dist rebuild gate for `admin/` changes. + +### Storage and URLs +- `modules/lagoon/attach/bucket.go` provides `PublicURLBase` (`storage.uploads.public_url`) for absolute image URLs. +- `.planning/research/STACK.md` holds the decided dependency set; a code-editor JS library must be added here or in a decision note. + +### Reference implementation (WinterCMS) +- `../examples/golem15-wintercms-starter` covers Winter's System → Mail templates/layouts/partials/brand settings (`modules/system/models/MailTemplate.php`, `MailLayout.php`, `MailPartial.php`, `MailBrandSetting.php`) for field sets and behaviour parity. + +### Existing mail consumers (must keep working unchanged) +- `../sm-user-plugin/views/mail/` (including `layouts/user.htm` and the locale variants) and `../sm-paymentgateway-plugin/views/mail/` (including `layouts/paymentgateway.htm`). + + + + +## Existing Code Insights + +### Reusable Assets +- `postcard.Catalog` already parses the INI header + `==` file format and resolves layout aliases. Overrides should reuse the same parser so DB content and files share one format. +- postcard's safety checker (rendered-HTML rejection, header CR/LF) is reused for save validation (D-13) and preview. +- The cabana internal pseudo-plugin (19.6.1) has permission registration, Settings links and YAML controllers. +- The cabana settings model plus `settings_secret.go` patterns cover the Branding page. +- `lagoon/attach` handles uploads, public disk storage and `PublicURLBase`. +- The SPA's `PreviewView.vue` and `FormView.vue` are where the preview pane and test-send action go. + +### Established Patterns +- Plugins can only register names in their own namespace (`::mail.*`). Partials follow the same rule. +- Boot errors for missing files, duplicates and unknown aliases. The new partial capability fails the same way. +- Postgres only, GORM + per-plugin gormigrate migrations; YAML via `goccy/go-yaml`. +- Standard library first; any new dependency (code editor in the SPA) needs a research/decision note. +- A docs checker enforces that README/docs identifiers exist. + +### Integration Points +- postcard's render path (`Mailer.Send`) consults an override source before the file catalog, with fallback per D-14. +- `postcard.Activate` / brand CSS resolution consults the Branding record (D-11). +- The cabana field registry plus the SPA `FieldRenderer.vue` gain `codeeditor`. +- `pact` gets an additive capability for partials (and possibly sample vars). + + + + +## Specific Ideas + +- The user explicitly wants **both** Winter's structured Branding page (colours, logo) **and** codeeditor-based layouts, "as WinterCMS kind of has it". +- Winter parity is the guide for field sets (brand colours, template/layout/partial forms) and for keeping `system.manage_mail_templates` as the code name. + + + + +## Deferred Ideas + +- Hand-written plain-text template parts (Winter's `content_text`), not needed now. +- A WYSIWYG/rich-editor mail body. +- CID-embedded inline images (only absolute URLs in v1). +- Test send to an arbitrary address. +- Mail driver/SMTP configuration screen in the admin. + +### Reviewed Todos (not folded) +- **Port MailBlocker and the block-mail checkbox** (`.planning/todos/pending/mailblocker-with-mailing.md`): kept separate. It is a send-path feature owned by sm-user-plugin (MailBlocker table, check before send, `block_mail` checkbox) and stays a todo for later mail work. + + + +--- + +*Phase: 35-mail-templates-and-layouts-editor* +*Context gathered: 2026-11-06* diff --git a/.planning/phases/35-mail-templates-and-layouts-editor/35-DISCUSSION-LOG.md b/.planning/phases/35-mail-templates-and-layouts-editor/35-DISCUSSION-LOG.md new file mode 100644 index 00000000..3147be8a --- /dev/null +++ b/.planning/phases/35-mail-templates-and-layouts-editor/35-DISCUSSION-LOG.md @@ -0,0 +1,91 @@ +# Phase 35: Mail templates and layouts editor - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. +> Decisions are captured in CONTEXT.md — this log preserves the alternatives considered. + +**Date:** 2026-11-06 +**Phase:** 35-mail-templates-and-layouts-editor +**Areas discussed:** Placement & permissions, Editing model, Preview & test send, Images + +--- + +## Todo cross-reference + +| Option | Description | Selected | +|--------|-------------|----------| +| Keep it separate | MailBlocker stays a todo for the later mail work | ✓ | +| Fold it in | Widens the scope to sm-user-plugin and the send path | | + +## Placement & permissions + +| Option | Description | Selected | +|--------|-------------|----------| +| Inside cabana | Internal pseudo-plugin, the 19.6.1 pattern | ✓ | +| A new sm-mail-plugin | Opt-in plugin repo | | +| Inside postcard | postcard would depend on cabana | | + +| Option | Description | Selected | +|--------|-------------|----------| +| Settings → Mail section | Winter-like, through pact.HasSettings | ✓ | +| Settings → System section | Next to Administrators/Roles | | +| Top-level nav item | | | + +| Option | Description | Selected | +|--------|-------------|----------| +| One code | system.manage_mail_templates | | +| Two codes | templates + layouts | ✓ | + +| Option | Description | Selected | +|--------|-------------|----------| +| system.manage_mail_templates + system.manage_mail_layouts | Winter-compatible | ✓ | +| mail.manage_templates + mail.manage_layouts | New namespace | | + +## Editing model + +| Option | Description | Selected | +|--------|-------------|----------| +| Subject + Markdown body + layout | Same as the file format | ✓ | +| Also a hand-written plain-text part | | | +| HTML body (rich editor) | | | + +**Layout editing:** the options were a codeeditor field, a plain textarea, or structured fields. **User's choice (free text):** "I wonder if we could have both 3 and 1. WinterCMS kind of has it - it has 'Branding' section with structured fields for colors etc and codeeditor-based layouts." This became a Branding page plus codeeditor layouts. + +| Option | Description | Selected | +|--------|-------------|----------| +| Defer partials | | | +| Include partials, file + DB | | ✓ | + +| Question | Options | Selected | +|----------|---------|----------| +| Branding contents | Winter colours + logo / colours only / + free CSS | Winter colours + logo | +| Branding vs config | Branding replaces brandCss once saved / config wins / concatenate | Branding replaces once saved | +| Partials shape | Same pattern as templates / built-in only | Same pattern as templates | +| Branding & partials perms | Under manage_mail_layouts / own branding code | Under manage_mail_layouts | +| Plugin file layouts | Same override model / imported to DB at boot | Same override model | +| Drift after file change | Override wins + notice / silently | Override wins + notice | +| Locale variants | Own rows + locale column / grouped tabs | Own rows + locale column | +| Validation | On save + fallback at send / save only, strict at send | On save + fallback at send | + +## Preview & test send + +| Question | Options | Selected | +|----------|---------|----------| +| Preview | Live draft in a sandboxed iframe + text tab / saved only | Live draft in a sandboxed iframe | +| Sample vars | Optional file samples + placeholders / placeholders only / admin JSON | Optional file samples + placeholders | +| Test send | Own address / any address / none | Own address | + +## Images + +| Question | Options | Selected | +|----------|---------|----------| +| Storage | Shared mail image library / per-record attachments | Shared library | +| Delivery | Absolute public URLs / inline CID / per-image toggle | Absolute URLs | +| Placing | Insert button in the editor / copy URL | Insert button | + +## Claude's Discretion + +- Schema layout, partial call syntax, extra predefined layouts, the image picker permission detail, the override lookup/caching mechanism (must be multi-instance safe), the SPA code-editor library (needs a decision note), the plan split. + +## Deferred Ideas + +- Plain-text template parts, a WYSIWYG body, CID images, test send to any address, a mail driver settings screen.