From df5c1e296c310e9e062d8b09e0b33016a02816e5 Mon Sep 17 00:00:00 2001 From: Jonas Miederer Date: Sun, 13 Sep 2026 17:57:42 +0200 Subject: [PATCH] feat: bell-tower favicon, lfx-clean flows, branded HTML mail --- PLAN_NEXT.md | 16 ++ README.md | 3 +- backend/app/auth/mail.py | 137 ++++++++++++++++-- backend/app/flows/input_schema.py | 5 + backend/components/kirche/README.md | 58 ++++++++ backend/flows/intake_form.json | 2 +- backend/scripts/make_flow_fixtures.py | 13 ++ backend/tests/fixtures/flows/intake_form.json | 2 +- backend/tests/test_auth.py | 31 ++++ backend/tests/test_lfx_validate.py | 34 +++++ frontend/public/apple-touch-icon.png | Bin 3448 -> 3128 bytes frontend/public/favicon-32x32.png | Bin 661 -> 679 bytes frontend/public/favicon.svg | 17 +-- frontend/public/icon-192.png | Bin 3380 -> 3168 bytes frontend/public/icon-512.png | Bin 9220 -> 9472 bytes 15 files changed, 288 insertions(+), 30 deletions(-) create mode 100644 backend/components/kirche/README.md create mode 100644 backend/tests/test_lfx_validate.py 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 8eb9c1526d69ac413238153ab2361827b203bbb7..188f78bbeace34f6932aabdbb9d19d23ad292ec2 100644 GIT binary patch literal 3168 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 Qza|I3#K2s?SkEcqAFa~DV*mgE literal 3380 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 V8+e10av%w~pl^0A>n!^3{{{OEY`Xve