DB-backed email-template editor for global_admins, replacing the
"Kommt bald" placeholder. Admins can edit invitation, deadline_digest,
and the shared base wrapper for both DE and EN, preview against sample
data, save with versions, and reset to the embedded default.
Backend:
- Migration 026 adds paliad.email_templates (active row per (key, lang))
and paliad.email_template_versions (append-only, retained 20 deep).
- EmailTemplateService — GetActive falls through to the embedded per-
language file when no DB row, Save validates parse + structural
invariants and writes a version, Reset deletes the active row, Restore
copies a version back. Mutations require DB; reads work without.
- MailService now consults the service for body and subject and falls
back to the embedded default if the active row is malformed at parse
time — a corrupt admin save can never wedge the send path.
- Subjects move from Go (buildDigestSubject + inviteSubject) to
text/template strings stored in the (key, lang) row. Default subjects
ship with a {{/* keep this phrasing */}} comment pointing at the
reminder-redesign doc so the SLO framing rationale survives edits.
- Bilingual templates split into per-language files (invitation.de.html
+ .en.html, deadline_digest.de.html + .en.html, base.de.html + .en.html).
No more {{if eq .Lang}} branching inside templates.
- Handlers under /api/admin/email-templates/* gated by the existing
RequireAdminFunc(users) admin middleware, same shape as /admin/team.
Frontend:
- /admin/email-templates list page — three cards (one per template),
each linking to DE + EN editors with their last-modified status.
- /admin/email-templates/{key}?lang=de three-pane editor — subject + body
textarea + variable docs + actions on the left, sandboxed iframe
preview + version log on the right. 500 ms debounced live preview;
save validates server-side (422 on parse error, surfaced inline).
- admin.tsx flips the Email-Templates card from PLANNED to verfügbar.
- 50 new i18n keys (DE + EN) for the editor surface.
Tests: GetActive fallback path, ValidateTemplate happy + sad paths,
SaveRequiresStore on no-DB service, RenderTemplate body + subject
goldens, full SYSTEMAUSFALL/SYSTEM FAILURE subject matrix.
Smoke (knowledge-platform-only run, no DB/auth):
- GET /admin/email-templates → 302 to /login
- GET /api/admin/email-templates → 401
- go build/vet/test clean, bun run build clean
Design: docs/design-email-templates-2026-04-29.md.
47 lines
2.1 KiB
SQL
47 lines
2.1 KiB
SQL
-- Admin Email-Templates editor (t-paliad-072).
|
||
--
|
||
-- Two tables backing the /admin/email-templates UI:
|
||
-- * paliad.email_templates — exactly one active row per (key, lang) pair.
|
||
-- Absence of a row means "use the embedded default shipped with the
|
||
-- binary". Saves UPSERT into this table; resets DELETE the row.
|
||
-- * paliad.email_template_versions — append-only history. One row per
|
||
-- save (and per reset / restore). The service GCs to the most recent
|
||
-- 20 rows per (key, lang) inside the same tx as the save, so this table
|
||
-- stays at most 3 templates × 2 languages × 20 = 120 rows steady-state.
|
||
--
|
||
-- RLS enabled with no policies: same shape as paliad.invitations and
|
||
-- paliad.reminder_log. The Go server bypasses RLS via its direct DB pool;
|
||
-- this denies all PostgREST access (no PostgREST surface today, but kept
|
||
-- closed by default).
|
||
--
|
||
-- key is one of: 'invitation', 'deadline_digest', 'base'. Not enforced via
|
||
-- CHECK because the canonical key set lives in code (internal/services/
|
||
-- email_template_service.go) and changing it shouldn't require a migration.
|
||
|
||
CREATE TABLE paliad.email_templates (
|
||
key text NOT NULL,
|
||
lang text NOT NULL CHECK (lang IN ('de', 'en')),
|
||
subject text NOT NULL DEFAULT '',
|
||
body text NOT NULL,
|
||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||
updated_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||
PRIMARY KEY (key, lang)
|
||
);
|
||
|
||
CREATE TABLE paliad.email_template_versions (
|
||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||
key text NOT NULL,
|
||
lang text NOT NULL CHECK (lang IN ('de', 'en')),
|
||
subject text NOT NULL DEFAULT '',
|
||
body text NOT NULL,
|
||
saved_at timestamptz NOT NULL DEFAULT now(),
|
||
saved_by uuid REFERENCES auth.users(id) ON DELETE SET NULL,
|
||
note text NOT NULL DEFAULT ''
|
||
);
|
||
|
||
CREATE INDEX email_template_versions_key_lang_idx
|
||
ON paliad.email_template_versions (key, lang, saved_at DESC);
|
||
|
||
ALTER TABLE paliad.email_templates ENABLE ROW LEVEL SECURITY;
|
||
ALTER TABLE paliad.email_template_versions ENABLE ROW LEVEL SECURITY;
|