docs(35): capture phase context
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
# Phase 35: Mail templates and layouts editor - Context
|
||||||
|
|
||||||
|
**Gathered:** 2026-11-06
|
||||||
|
**Status:** Ready for planning
|
||||||
|
|
||||||
|
<domain>
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
</domain>
|
||||||
|
|
||||||
|
<decisions>
|
||||||
|
## 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/<name>.htm` and are named `<plugin>::mail.partials.<name>`.
|
||||||
|
- 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.** `<img src>` 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 `` or `<img src="url" alt="">` 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).
|
||||||
|
|
||||||
|
</decisions>
|
||||||
|
|
||||||
|
<canonical_refs>
|
||||||
|
## 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`).
|
||||||
|
|
||||||
|
</canonical_refs>
|
||||||
|
|
||||||
|
<code_context>
|
||||||
|
## 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 (`<plugin>::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).
|
||||||
|
|
||||||
|
</code_context>
|
||||||
|
|
||||||
|
<specifics>
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
</specifics>
|
||||||
|
|
||||||
|
<deferred>
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
</deferred>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Phase: 35-mail-templates-and-layouts-editor*
|
||||||
|
*Context gathered: 2026-11-06*
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user