diff --git a/PLAN_NEXT.md b/PLAN_NEXT.md index 7261b30..5e85291 100644 --- a/PLAN_NEXT.md +++ b/PLAN_NEXT.md @@ -135,6 +135,22 @@ request); anything else → 403 and the frontend forwards to `/access?email=…` (prefilled). Rate-limited (5/hour), token by mail only, delivery failures never 500 (see §0.4 hardening). +## 0.6 Polish round (bell-tower favicon, lfx-clean flows, HTML mail) + +* Favicon simplified to a stylized bell tower (spire + shaft + bell-arch + knockout); PNGs regenerated via `npm run icons`. +* `lfx validate --level 4` now green for ALL canonical flows + regression + test (`test_lfx_validate.py`). Root cause: required form fields have no + edge (values arrive via the form_values envelope). Fix: generator writes + a single-space sentinel value for required template fields; the form + parser treats whitespace-only values as "no default". L3 + Message-vs-str warnings are known lfx-validator noise (MessageTextInput + accepts Message at runtime). +* New-input authoring docs: `backend/components/kirche/README.md`. +* Magic-link + access-notification mails are branded multipart + (text + HTML): kirchenblau header, serif headline, real button link, + German copy, email-safe tables/inline styles, no external assets. + ## 1. Locked decisions (do not re-ask) * Auth: SMTP magic-link, 15-min single-use tokens (`secrets.token_urlsafe`, diff --git a/README.md b/README.md index 7061d29..9a5ed29 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ needs judgment. Every step is documented and traceable. `LANGFLOW_COMPONENTS_PATH`). `GET /api/flows/{id}/input-schema` derives a JSON Schema + RJSF uiSchema from the deployed version; the webapp renders the matching form automatically. Flows without such a node fall back to a - single textarea. + single textarea. See + `backend/components/kirche/README.md` for authoring new form fields. - **Web UI** — landing page (*Start*), access request (*Zugang*), and the logged-in webapp: *Workflows* (choose + run with dynamic form), *Aufgaben* (approvals), *Verwaltung* (admin: access requests, import, diff --git a/backend/app/auth/mail.py b/backend/app/auth/mail.py index 0b59112..8764f18 100644 --- a/backend/app/auth/mail.py +++ b/backend/app/auth/mail.py @@ -1,4 +1,10 @@ -"""Magic-link delivery. SMTP when configured, dev log fallback otherwise.""" +"""Magic-link delivery. SMTP when configured, dev log fallback otherwise. + +Mail design follows the web branding (see frontend/src/theme.ts): +kirchenblau header, serif headline, white card on paper background. +Email-safe by construction: table layout, inline styles only, system +fonts, no external images (many clients block them / track opens). +""" from __future__ import annotations @@ -10,11 +16,72 @@ from app.settings import settings logger = logging.getLogger(__name__) +# Brand palette (mirrors frontend/src/theme.ts kirchenblau). +BLUE = "#1d4a79" +BLUE_LIGHT = "#eef4fb" +INK = "#162f4c" +MUTED = "#5b6b7c" + +BLUE = "#1d4a79" +BLUE_LIGHT = "#eef4fb" +INK = "#162f4c" +MUTED = "#5b6b7c" + def magic_link(token: str) -> str: return f"{settings.frontend_url.rstrip('/')}/login?token={token}" +def _page(title: str, intro: str, button_label: str, button_url: str | None, note: str) -> str: + """Branded HTML mail body (table layout, inline styles, no images).""" + button = "" + if button_url: + button = ( + '' + f'
' + f'{button_label}
' + ) + return f""" + + + + +
+ + + + + + + +
+ Kirche Einfach +
+

{title}

+

{intro}

+ {button} +
+

+ {note} +

+
+ +""" + + def _send(message: EmailMessage) -> None: with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=10) as smtp: smtp.starttls() @@ -23,6 +90,33 @@ def _send(message: EmailMessage) -> None: smtp.send_message(message) +def _branded_mail( + subject: str, + to_email: str, + headline: str, + intro: str, + button_label: str, + button_url: str, + note: str, +) -> EmailMessage: + """Text + branded HTML alternative; the link is a real button.""" + message = EmailMessage() + message["Subject"] = subject + message["To"] = to_email + message["From"] = settings.mail_from or settings.contact_to or to_email + if settings.contact_to: + message["Reply-To"] = settings.contact_to + message.set_content( + f"{headline}\n\n{intro}\n\n{button_label}:\n{button_url}\n\n{note}\n" + ) + # Email-safe HTML: tables + inline CSS; some clients strip - - - - - - - - - - - - + + + + + diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png index 8eb9c15..188f78b 100644 Binary files a/frontend/public/icon-192.png and b/frontend/public/icon-192.png differ diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png index 02d8724..1a68be3 100644 Binary files a/frontend/public/icon-512.png and b/frontend/public/icon-512.png differ