MBO-Tech-IT-Webseite/docs/superpowers/plans/2026-07-21-familyguard-flye...

633 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# FamilyGuard Flyer-Download Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Der FamilyGuard-Flyer wird über ein E-Mail-Gate herunterladbar: Interessent gibt E-Mail-Adresse ein (mit DSGVO-Einwilligung), bekommt sofort einen Download-Button + eine Bestätigungsmail mit Link, und Jonny (jonny@mbo-tech-it.de) wird per Mail benachrichtigt.
**Architektur:** Neue API-Route `POST /api/familyguard-flyer` validiert Eingabe, speichert den Lead in einer neuen Supabase-Tabelle `flyer_downloads`, verschickt zwei Mails über die bestehende `lib/mailer.ts`-Infrastruktur (SMTP mit Queue-Fallback) und liefert die Download-URL zurück. Eine neue Client-Komponente `FlyerDownloadForm` kapselt Formular + Erfolgs-Zustand und wird einmalig auf der FamilyGuard-Seite eingebunden; die Homepage-Kachel verlinkt per Anchor dorthin.
**Tech Stack:** Next.js 15 App Router, TypeScript (`strict: true`), Supabase (`@supabase/supabase-js`), Nodemailer (bestehend in `lib/mailer.ts`), Tailwind CSS.
## Global Constraints
- Kein Test-Framework im Projekt (kein Jest/Vitest, keine `*.test.ts`-Dateien existieren) — Verifikation erfolgt manuell über `npx tsc --noEmit`, `npm run dev` und `curl`, wie es auch bei allen bestehenden API-Routen (z. B. `app/api/contact/route.ts`) gehandhabt wurde.
- `@/*` ist auf das Root-Verzeichnis aliased.
- `JSX.Element` nicht verwenden — `ReactElement` aus `react` importieren, falls ein Rückgabetyp explizit gebraucht wird.
- Neue Server-seitige E-Mail-Funktionen folgen exakt dem bestehenden Muster in `lib/mailer.ts` (`sendWithFallback`-Helper, gleiches HTML-Layout/Branding).
- Die Migration für `flyer_downloads` wird **nicht** als Datei unter `modules/` abgelegt (dieser Ordner ist Jonnys projektübergreifender Wiederverwendungs-Katalog für generische Module) und auch nicht als SQL-Datei im Repo getrackt (kein anderer Content-Table hat das) — das SQL wird in Task 2 dokumentiert und von Jonny manuell im Supabase SQL-Editor ausgeführt.
- Git-Commits nach jedem Task, wie in diesem Projekt bisher gehandhabt (kein `--no-verify`, keine `-i`-Flags).
---
### Task 1: Flyer-PDF nach `public/downloads/` verschieben
**Files:**
- Move: `docs/MBO_FamilyGuard_Flyer_01.pdf``public/downloads/MBO_FamilyGuard_Flyer_01.pdf`
**Interfaces:**
- Produziert: Statische URL `/downloads/MBO_FamilyGuard_Flyer_01.pdf`, die von Task 4 (API-Response) und Task 5 (Download-Button) referenziert wird.
- [ ] **Step 1: Ordner anlegen und Datei verschieben**
```bash
mkdir -p public/downloads
git mv docs/MBO_FamilyGuard_Flyer_01.pdf public/downloads/MBO_FamilyGuard_Flyer_01.pdf
```
- [ ] **Step 2: Verifizieren, dass die Datei unter der neuen URL ausgeliefert wird**
```bash
npm run dev
```
Dann im Browser (oder mit curl in einem zweiten Terminal) prüfen:
```bash
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/downloads/MBO_FamilyGuard_Flyer_01.pdf
```
Erwartet: `200`
- [ ] **Step 3: Commit**
```bash
git add public/downloads/MBO_FamilyGuard_Flyer_01.pdf docs/MBO_FamilyGuard_Flyer_01.pdf
git commit -m "feat: move FamilyGuard flyer PDF to public/downloads for direct serving"
```
---
### Task 2: Supabase-Tabelle `flyer_downloads` + Typdefinition
**Files:**
- Modify: `lib/supabase.ts` (Database-Typ ergänzen, nach dem `kontakt_social`-Block, vor der schließenden `};` der `Tables`)
**Interfaces:**
- Produziert: Tabellentyp `flyer_downloads` mit `Row`/`Insert`/`Update`, den Task 4 (API-Route) für `db.from("flyer_downloads").insert(...)` benötigt.
- [ ] **Step 1: SQL manuell im Supabase SQL-Editor ausführen**
Dieser Schritt kann nicht automatisiert werden (lokale `.env.local` enthält nur Platzhalter-Credentials, kein echter Zugriff auf die Produktions-Supabase-Instanz). Jonny führt folgendes SQL in seiner Supabase-Instanz aus:
```sql
CREATE TABLE IF NOT EXISTS flyer_downloads (
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
email TEXT NOT NULL,
flyer TEXT NOT NULL DEFAULT 'familyguard',
dsgvo_einwilligung BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_flyer_downloads_email ON flyer_downloads(email);
CREATE INDEX IF NOT EXISTS idx_flyer_downloads_created_at ON flyer_downloads(created_at DESC);
ALTER TABLE flyer_downloads DISABLE ROW LEVEL SECURITY;
```
Verifikation (im SQL-Editor):
```sql
SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'flyer_downloads';
```
Erwartet: 5 Zeilen (`id`, `email`, `flyer`, `dsgvo_einwilligung`, `created_at`).
- [ ] **Step 2: Datenbank-Typ in `lib/supabase.ts` ergänzen**
In `lib/supabase.ts` direkt vor der schließenden `};` des `Tables`-Objekts (nach dem `kontakt_social`-Eintrag, aktuell endet dieser Block bei `Relationships: [];\n };`) folgenden Eintrag einfügen:
```ts
flyer_downloads: {
Row: {
id: number;
email: string;
flyer: string;
dsgvo_einwilligung: boolean;
created_at: string;
};
Insert: {
id?: number;
email: string;
flyer?: string;
dsgvo_einwilligung: boolean;
created_at?: string;
};
Update: {
email?: string;
flyer?: string;
dsgvo_einwilligung?: boolean;
};
Relationships: [];
};
```
- [ ] **Step 3: TypeScript-Check**
```bash
npx tsc --noEmit
```
Erwartet: keine Fehler.
- [ ] **Step 4: Commit**
```bash
git add lib/supabase.ts
git commit -m "feat: add flyer_downloads table type to Supabase schema"
```
---
### Task 3: Mailer-Funktionen für Flyer-Benachrichtigung und Flyer-Link
**Files:**
- Modify: `lib/mailer.ts` (zwei neue Exporte am Ende der Datei ergänzen, nach `sendeKontaktEmail`)
**Interfaces:**
- Konsumiert: `sendWithFallback` (bereits in `lib/mailer.ts` definiert, Zeile 40).
- Produziert: `sendeFlyerBenachrichtigung(data: { email: string }): Promise<{ sent: boolean; queued: boolean }>` und `sendeFlyerLink(data: { email: string }): Promise<void>`, die Task 4 (API-Route) importiert.
- [ ] **Step 1: Funktionen in `lib/mailer.ts` ergänzen**
Am Ende der Datei (nach der schließenden `}` von `sendeKontaktEmail`) einfügen:
```ts
export async function sendeFlyerBenachrichtigung(
data: { email: string }
): Promise<{ sent: boolean; queued: boolean }> {
const html = `
<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"></head>
<body style="font-family:system-ui,sans-serif;color:#e2e8f0;max-width:600px;margin:0 auto;padding:0">
<div style="background:#18212f;padding:20px 24px;border-bottom:2px solid #f97316">
<h1 style="color:#f97316;margin:0;font-size:20px;font-weight:700">MBO Tech IT</h1>
<p style="color:rgba(255,255,255,0.6);margin:4px 0 0;font-size:13px">FamilyGuard Flyer-Download</p>
</div>
<div style="padding:24px;background:#1e2a3b">
<h2 style="margin:0 0 16px;font-size:18px;color:#f8fafc">Neuer Flyer-Download</h2>
<p style="color:#94a3b8;margin:0 0 16px;line-height:1.6">
Ein Interessent hat den MBO FamilyGuard Flyer angefordert.
</p>
<table style="width:100%;border-collapse:collapse">
<tr><td style="padding:6px 0;color:#94a3b8;width:120px">E-Mail</td><td style="padding:6px 0"><a href="mailto:${data.email}" style="color:#60a5fa">${data.email}</a></td></tr>
</table>
</div>
<div style="padding:12px 24px;background:#111925;border-top:1px solid rgba(255,255,255,0.1)">
<p style="margin:0;font-size:11px;color:#64748b">MBO Tech IT · ${process.env.APP_URL ?? "https://mbo-tech-it.de"}</p>
</div>
</body>
</html>`;
return sendWithFallback(
{
from: `"MBO Tech IT" <${process.env.SMTP_FROM}>`,
to: "jonny@mbo-tech-it.de",
replyTo: data.email,
subject: `Neuer FamilyGuard-Flyer-Download: ${data.email}`,
text: `Neuer Flyer-Download\n\nE-Mail: ${data.email}`,
html,
},
`FamilyGuard-Flyer-Benachrichtigung ${data.email}`
);
}
export async function sendeFlyerLink(data: { email: string }): Promise<void> {
const downloadLink = `${process.env.APP_URL ?? "https://mbo-tech-it.de"}/downloads/MBO_FamilyGuard_Flyer_01.pdf`;
const html = `
<!DOCTYPE html>
<html lang="de">
<head><meta charset="UTF-8"></head>
<body style="font-family:system-ui,sans-serif;color:#e2e8f0;max-width:600px;margin:0 auto;padding:0">
<div style="background:#18212f;padding:20px 24px;border-bottom:2px solid #f97316">
<h1 style="color:#f97316;margin:0;font-size:20px;font-weight:700">MBO Tech IT</h1>
<p style="color:rgba(255,255,255,0.6);margin:4px 0 0;font-size:13px">MBO FamilyGuard</p>
</div>
<div style="padding:24px;background:#1e2a3b">
<h2 style="margin:0 0 16px;font-size:18px;color:#f8fafc">Ihr FamilyGuard-Flyer</h2>
<p style="color:#94a3b8;margin:0 0 24px;line-height:1.6">
Vielen Dank für Ihr Interesse an MBO FamilyGuard. Den Flyer mit allen Details
finden Sie hier zum Download:
</p>
<a href="${downloadLink}"
style="display:inline-block;background:#f97316;color:#fff;font-weight:700;padding:12px 24px;border-radius:8px;text-decoration:none;font-size:15px">
Flyer herunterladen
</a>
<p style="color:#64748b;font-size:12px;margin:24px 0 0">
Fragen? Rufen Sie uns an: <a href="tel:+4917193451093" style="color:#f97316">+49 171 9345193</a>
</p>
</div>
<div style="padding:12px 24px;background:#111925;border-top:1px solid rgba(255,255,255,0.1)">
<p style="margin:0;font-size:11px;color:#64748b">MBO Tech IT · ${process.env.APP_URL ?? "https://mbo-tech-it.de"}</p>
</div>
</body>
</html>`;
await sendWithFallback(
{
from: `"MBO Tech IT" <${process.env.SMTP_FROM}>`,
to: data.email,
subject: "Ihr MBO FamilyGuard Flyer",
text: `Hallo,\n\nvielen Dank für Ihr Interesse an MBO FamilyGuard. Den Flyer finden Sie hier:\n${downloadLink}\n\nMBO Tech IT`,
html,
},
`FamilyGuard-Flyer-Link ${data.email}`
);
}
```
- [ ] **Step 2: TypeScript-Check**
```bash
npx tsc --noEmit
```
Erwartet: keine Fehler.
- [ ] **Step 3: Commit**
```bash
git add lib/mailer.ts
git commit -m "feat: add mailer functions for FamilyGuard flyer notification and link"
```
---
### Task 4: API-Route `POST /api/familyguard-flyer`
**Files:**
- Create: `app/api/familyguard-flyer/route.ts`
**Interfaces:**
- Konsumiert: `sendeFlyerBenachrichtigung`, `sendeFlyerLink` (aus Task 3), `createServiceClient` (aus `lib/supabase.ts`), Tabelle `flyer_downloads` (aus Task 2).
- Produziert: `POST /api/familyguard-flyer` — Request `{ email: string, dsgvoEinwilligung: boolean }`, Response `{ ok: true, downloadUrl: string }` oder `{ ok: false, error: string }`. Wird von Task 5 (`FlyerDownloadForm`) konsumiert.
- [ ] **Step 1: Route-Datei erstellen**
```ts
import { NextResponse } from "next/server";
import { sendeFlyerBenachrichtigung, sendeFlyerLink } from "@/lib/mailer";
import { createServiceClient } from "@/lib/supabase";
const FLYER_DOWNLOAD_URL = "/downloads/MBO_FamilyGuard_Flyer_01.pdf";
function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
export async function POST(request: Request) {
let body: { email?: string; dsgvoEinwilligung?: boolean };
try {
body = await request.json();
} catch {
return NextResponse.json({ ok: false, error: "Ungültige Anfrage" }, { status: 400 });
}
const email = body.email?.trim() ?? "";
if (!isValidEmail(email)) {
return NextResponse.json(
{ ok: false, error: "Bitte geben Sie eine gültige E-Mail-Adresse ein" },
{ status: 400 }
);
}
if (body.dsgvoEinwilligung !== true) {
return NextResponse.json(
{ ok: false, error: "Bitte stimmen Sie der Datenverarbeitung zu" },
{ status: 400 }
);
}
try {
const db = createServiceClient();
await db.from("flyer_downloads").insert({ email, dsgvo_einwilligung: true });
} catch (err) {
console.error("[FamilyGuard-Flyer] Supabase insert error:", err);
}
await sendeFlyerBenachrichtigung({ email });
sendeFlyerLink({ email }).catch((err) =>
console.error("[FamilyGuard-Flyer] Flyer-Mail an Interessenten fehlgeschlagen:", err)
);
return NextResponse.json({ ok: true, downloadUrl: FLYER_DOWNLOAD_URL });
}
```
- [ ] **Step 2: TypeScript-Check**
```bash
npx tsc --noEmit
```
Erwartet: keine Fehler.
- [ ] **Step 3: Manueller Funktionstest gegen den Dev-Server**
```bash
npm run dev
```
In einem zweiten Terminal:
```bash
curl -s -X POST http://localhost:3000/api/familyguard-flyer \
-H "Content-Type: application/json" \
-d '{"email":"test@beispiel.de","dsgvoEinwilligung":true}'
```
Erwartet: `{"ok":true,"downloadUrl":"/downloads/MBO_FamilyGuard_Flyer_01.pdf"}` (oder ein Fehler zu SMTP/Supabase im Server-Log, falls in der lokalen Umgebung keine echten Zugangsdaten hinterlegt sind — das ist unschädlich, siehe Global Constraints).
Zusätzlich Validierung prüfen:
```bash
curl -s -X POST http://localhost:3000/api/familyguard-flyer \
-H "Content-Type: application/json" \
-d '{"email":"keine-email","dsgvoEinwilligung":true}'
```
Erwartet: `{"ok":false,"error":"Bitte geben Sie eine gültige E-Mail-Adresse ein"}` mit Status 400.
- [ ] **Step 4: Commit**
```bash
git add app/api/familyguard-flyer/route.ts
git commit -m "feat: add API route for FamilyGuard flyer email-gate download"
```
---
### Task 5: Komponente `FlyerDownloadForm`
**Files:**
- Create: `components/FlyerDownloadForm.tsx`
**Interfaces:**
- Konsumiert: `POST /api/familyguard-flyer` (aus Task 4).
- Produziert: `export default function FlyerDownloadForm()` (keine Props), gerendert mit `id="flyer-download"` als Root-Element. Wird von Task 6 in `app/pakete/familyguard/page.tsx` eingebunden.
- [ ] **Step 1: Komponente erstellen**
```tsx
"use client";
import { useState } from "react";
type Status = "idle" | "loading" | "success" | "error";
export default function FlyerDownloadForm() {
const [email, setEmail] = useState("");
const [dsgvo, setDsgvo] = useState(false);
const [status, setStatus] = useState<Status>("idle");
const [errorMsg, setErrorMsg] = useState("");
const [downloadUrl, setDownloadUrl] = useState("");
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setStatus("loading");
setErrorMsg("");
try {
const res = await fetch("/api/familyguard-flyer", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, dsgvoEinwilligung: dsgvo }),
});
const data = await res.json();
if (!res.ok || !data.ok) {
setErrorMsg(data.error ?? "Unbekannter Fehler");
setStatus("error");
} else {
setDownloadUrl(data.downloadUrl);
setStatus("success");
}
} catch {
setErrorMsg("Netzwerkfehler bitte versuchen Sie es erneut.");
setStatus("error");
}
}
return (
<div
id="flyer-download"
className="mb-16 p-6 sm:p-8 rounded-2xl bg-white dark:bg-gray-900 border border-slate-200 dark:border-gray-800 scroll-mt-24"
>
<div className="text-center mb-6">
<span className="text-orange-400 font-mono text-xs font-bold tracking-[0.25em] uppercase">
Flyer
</span>
<h3 className="text-2xl font-black text-slate-900 dark:text-white mt-1 mb-2">
FamilyGuard-Flyer herunterladen
</h3>
<p className="text-slate-600 dark:text-slate-400 text-sm max-w-xl mx-auto">
Alle Infos zu MBO FamilyGuard kompakt als PDF tragen Sie Ihre E-Mail-Adresse
ein und laden Sie den Flyer direkt herunter.
</p>
</div>
{status === "success" ? (
<div className="flex flex-col items-center text-center">
<p className="text-slate-700 dark:text-slate-300 text-sm mb-4">
Fertig! Der Flyer wurde Ihnen außerdem per E-Mail zugeschickt.
</p>
<a href={downloadUrl} download className="btn-primary inline-flex px-6 py-3 text-sm">
Flyer jetzt herunterladen
</a>
</div>
) : (
<form onSubmit={handleSubmit} className="max-w-md mx-auto space-y-4">
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="max@beispiel.de"
className="w-full px-4 py-3 rounded-xl bg-slate-50 dark:bg-[#111925] border border-slate-300 dark:border-gray-700 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-slate-600 focus:outline-none focus:border-orange-500/60 focus:ring-1 focus:ring-orange-500/20 transition-colors"
/>
<label className="flex items-start gap-2 text-xs text-slate-600 dark:text-slate-400">
<input
type="checkbox"
required
checked={dsgvo}
onChange={(e) => setDsgvo(e.target.checked)}
className="mt-0.5"
/>
<span>
Ich bin damit einverstanden, dass meine E-Mail-Adresse zum Zweck des
Flyer-Versands gespeichert wird. Details in der{" "}
<a href="/datenschutz" className="text-orange-400 hover:underline">
Datenschutzerklärung
</a>
.
</span>
</label>
{status === "error" && (
<div className="flex items-start gap-3 px-4 py-3 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
<span>{errorMsg || "Anfrage fehlgeschlagen. Bitte versuchen Sie es erneut."}</span>
</div>
)}
<button
type="submit"
disabled={status === "loading"}
className="btn-primary w-full py-3 text-sm disabled:opacity-60 disabled:cursor-not-allowed"
>
{status === "loading" ? "Wird gesendet..." : "Flyer herunterladen"}
</button>
</form>
)}
</div>
);
}
```
- [ ] **Step 2: TypeScript-Check**
```bash
npx tsc --noEmit
```
Erwartet: keine Fehler.
- [ ] **Step 3: Commit**
```bash
git add components/FlyerDownloadForm.tsx
git commit -m "feat: add FlyerDownloadForm component"
```
---
### Task 6: `FlyerDownloadForm` in die FamilyGuard-Seite einbinden
**Files:**
- Modify: `app/pakete/familyguard/page.tsx:1-3` (Import ergänzen), `app/pakete/familyguard/page.tsx:204-206` (Komponente einfügen)
**Interfaces:**
- Konsumiert: `FlyerDownloadForm` (aus Task 5).
- [ ] **Step 1: Import ergänzen**
In `app/pakete/familyguard/page.tsx` nach der bestehenden Import-Zeile `import Logo from "@/components/Logo";` (Zeile 3) einfügen:
```tsx
import FlyerDownloadForm from "@/components/FlyerDownloadForm";
```
- [ ] **Step 2: Komponente zwischen Preis-Block und Bottom-Callout einfügen**
Den bestehenden Codeblock
```tsx
{/* Bottom callout */}
<div className="p-6 rounded-2xl border border-blue-500/20 bg-blue-500/5 text-center">
```
ersetzen durch:
```tsx
<FlyerDownloadForm />
{/* Bottom callout */}
<div className="p-6 rounded-2xl border border-blue-500/20 bg-blue-500/5 text-center">
```
- [ ] **Step 3: TypeScript-Check**
```bash
npx tsc --noEmit
```
Erwartet: keine Fehler.
- [ ] **Step 4: Manueller Test im Browser**
```bash
npm run dev
```
`http://localhost:3000/pakete/familyguard` öffnen, zum Flyer-Download-Bereich scrollen, Formular ohne Checkbox absenden (Browser-Validierung muss greifen wegen `required`), dann mit gültiger E-Mail + Checkbox absenden und prüfen, dass der Download-Button erscheint und `http://localhost:3000/pakete/familyguard#flyer-download` direkt zur Sektion springt.
- [ ] **Step 5: Commit**
```bash
git add app/pakete/familyguard/page.tsx
git commit -m "feat: embed FlyerDownloadForm on FamilyGuard page"
```
---
### Task 7: Link zur Flyer-Sektion in der Homepage-Promo-Kachel
**Files:**
- Modify: `components/FamilyGuardPromo.tsx:42-49`
**Interfaces:**
- Konsumiert: Anchor `#flyer-download` auf `/pakete/familyguard` (aus Task 6).
- [ ] **Step 1: Dritten Button ergänzen**
Den bestehenden Block
```tsx
<div className="flex flex-col sm:flex-row gap-3">
<Link href="/pakete/familyguard" className="btn-primary inline-flex justify-center px-6 py-3 text-sm">
FamilyGuard entdecken
</Link>
<a href="#contact" className="btn-secondary inline-flex justify-center px-6 py-3 text-sm">
Jetzt beraten lassen
</a>
</div>
```
ersetzen durch:
```tsx
<div className="flex flex-col sm:flex-row gap-3">
<Link href="/pakete/familyguard" className="btn-primary inline-flex justify-center px-6 py-3 text-sm">
FamilyGuard entdecken
</Link>
<Link
href="/pakete/familyguard#flyer-download"
className="btn-secondary inline-flex justify-center px-6 py-3 text-sm"
>
Flyer herunterladen
</Link>
<a href="#contact" className="btn-secondary inline-flex justify-center px-6 py-3 text-sm">
Jetzt beraten lassen
</a>
</div>
```
- [ ] **Step 2: TypeScript-Check**
```bash
npx tsc --noEmit
```
Erwartet: keine Fehler.
- [ ] **Step 3: Manueller Test im Browser**
```bash
npm run dev
```
`http://localhost:3000/` öffnen, in der FamilyGuard-Promo-Kachel auf „Flyer herunterladen" klicken, prüfen dass die FamilyGuard-Seite direkt bei der Flyer-Download-Sektion landet.
- [ ] **Step 4: Commit**
```bash
git add components/FamilyGuardPromo.tsx
git commit -m "feat: link homepage FamilyGuard promo to flyer download section"
```
---
## Nach Abschluss aller Tasks
- `npm run build` einmal komplett durchlaufen lassen, um sicherzustellen, dass Produktions-Build fehlerfrei ist.
- Jonny muss die SQL-Migration aus Task 2 in der echten Supabase-Instanz ausführen und `SMTP_*`/`APP_URL`-Env-Variablen auf dem Server prüfen, damit die Mails im Produktivbetrieb tatsächlich zugestellt werden.