Biz Admin Leads Dashboard — CRM-artiges /admin für Supabase
Erstellt eine /admin-Seite im 1CRM-Stil in einem Next.js Projekt (App Router). 3 Tabs — Übersicht (KPI + Diagramme), Kunden (Tabelle + Filter), E-Mail-Marketing (Erstellen + Massenversand + Verlauf).
Philosophie: Einfacher als 1CRM (ein gemeinsames Passwort, kein Multi-User), aber mit ausreichend Diagrammen + Deltas + E-Mail-Marketing für 1-2 Benutzer eines Unternehmens. Der gesamte Zustand wird in Supabase gespeichert, die Dateien werden unabhängig vom bestehenden Projekt erstellt.
/admin → Popup "Admin-Passwort eingeben" → Tabs rufen /api/admin/{leads|dashboard|campaigns} auf
→ jede Anfrage sendet Header x-admin-pass
→ serverseitiger Timing-sicherer Vergleich mit ADMIN_PASSWORD env
Das Passwort wird im React-State gespeichert — ein Seitenneuladen zeigt das Popup erneut an (beabsichtigt, einfach, kein Session-Cookie nötig).
Nur Supabase — die KV-Variante wurde entfernt. Grund: campaigns + campaign_sends benötigen SQL-Joins + service_role zur RLS-Umgehung; KV unterstützt das nicht nativ.
Wann dieser Skill verwendet werden sollte
- Das Projekt hat bereits Sepay-Zahlungen und Lead-Speicherung über
lib/leads-supabase.ts(ausbiz-setup-sepay-paymentmit Supabase) → benötigt eine Ansichtsebene + Dashboard + E-Mail-Marketing. - Der Benutzer möchte eine CRM-ähnliche Seite: KPI-Karten mit Delta, Liniendiagramm nach Tagen, Donut-Konvertierung, E-Mail-Marketing für registrierte Kunden.
NICHT verwenden, wenn:
- Das Projekt noch keine
lib/leads-supabase.tshat → Benutzer zuerst/biz-setup-sepay-payment(mit Supabase) ausführen lassen. - Das Projekt Vercel KV anstelle von Supabase verwendet → dieser Skill unterstützt KEIN KV mehr. Der Benutzer hat nur 2 Optionen: zu Supabase migrieren oder manuell portieren.
- Mehrere Admins mit Rollen/Berechtigungen benötigt werden → dieser Skill verwendet ein gemeinsames Passwort, ungeeignet.
- Detailliertes E-Mail-Tracking (Öffnen/Klicks pro Empfänger) benötigt wird → der Skill verfolgt nur gesendet/fehlgeschlagen. Der Benutzer benötigt Resend/SendGrid/Brevo mit Tracking.
Workflow (6 Phasen)
Phase 0: ERKENNE Next.js App Router + überprüfe lib/leads-supabase.ts + überprüfe SMTP-Env
Phase 1: FRAGE nach Benutzerpasswort (Standard 123456) — ABSCHLUSS
Phase 2: INSTALLIERE npm-Abhängigkeit `recharts`
Phase 3: FÜHRE supabase-migration-admin.sql im Supabase SQL-Editor aus — ABSCHLUSS
Phase 4: SCHREIBE ADMIN_PASSWORD in .env.local
Phase 5: ERSTELLE 9 Codedateien (5 lib + 4 route + 1 page)
Phase 6: TESTE 3 Ebenen (curl dashboard, UI Smoke, Testkampagne senden)
Phase 0 — Erkennung + Abhängigkeiten überprüfen
# Überprüfe Next.js + App Router
test -f next.config.js -o -f next.config.mjs -o -f next.config.ts && echo "Next.js: ✓"
test -d app && echo "App Router: ✓" || { echo "✗ App Router erforderlich. Dieser Skill unterstützt Pages Router nicht."; exit 1; }
# Überprüfe Supabase Lead Store
test -f lib/leads-supabase.ts && echo "Supabase Store: ✓" || { echo "✗ lib/leads-supabase.ts fehlt — führe zuerst /biz-setup-sepay-payment mit Supabase aus"; exit 1; }
test -f lib/leads-store.ts && echo "Provider-unabhängiger Wrapper: ✓"
test -f lib/supabase-admin.ts && echo "Service-Role Client: ✓"
# Überprüfe SMTP-Env (von biz-email-setup)
grep -E "^SMTP_HOST=" .env.local && echo "SMTP_HOST: ✓" || { echo "✗ SMTP-Env fehlt — führe zuerst /biz-email-setup aus"; exit 1; }
grep -E "^SMTP_USER=" .env.local
grep -E "^SMTP_PASS=" .env.local
grep -E "^MAIL_FROM=" .env.local
# Überprüfe Pakete
grep '"nodemailer"' package.json && echo "nodemailer: ✓"
grep '"@supabase/supabase-js"' package.json && echo "@supabase: ✓"
Wenn lib/leads-supabase.ts fehlt:
⚠ Ich sehe, dass das Projekt noch keinen Supabase Lead Store hat. Bitte führe zuerst
/biz-setup-sepay-payment(mit Supabase-Backend) aus, um die Zahlung + die Tabelleleads+ RLS einzurichten. Dann komm zurück, und ich erstelle das Admin-Dashboard.
Wenn SMTP-Env fehlt:
⚠ Ich sehe, dass SMTP noch nicht konfiguriert ist. Der Tab E-Mail-Marketing benötigt SMTP zum Senden. Bitte führe zuerst
/biz-email-setupaus, um einen Anbieter auszuwählen (Gmail/Resend SMTP/Brevo/SendGrid/Zoho...), dann komm zurück, und ich erstelle das Admin-Dashboard. Ich kann auch ohne den E-Mail-Tab fortfahren — bestätigst du das?
Zusammenfassung für den Benutzer:
Ich habe erkannt: Next.js App Router ✓, Supabase Store ✓, SMTP eingerichtet ✓. Ich werde:
- Nach dem Passwort für /admin fragen
rechartsinstallieren (für Liniendiagramm + Donut)- SQL-Migration ausführen, um Tabellen
campaigns+campaign_sendsin Supabase zu erstellenADMIN_PASSWORDin .env.local schreiben- 9 Dateien erstellen (5 lib + 4 route + 1 page)
- Testanleitung geben
Bereit für Phase 1?
Phase 1 — Nach Passwort fragen (ABSCHLUSS)
Frage genau so:
Welches Passwort möchtest du für die /admin-Seite verwenden?
- Standardmäßig schlage ich vor:
123456- Oder füge ein stärkeres Passwort für die Produktion ein (≥12 Zeichen, mit Buchstaben + Zahlen + Sonderzeichen)
Bitte Passwort einfügen (oder "123456" für die Standardeinstellung eingeben):
Warte auf Antwort des Benutzers. Verwende das vom Benutzer eingefügte Passwort. Bei Leerzeichen → verwende 123456.
Gehe NICHT zu Phase 2, bis der Benutzer das Passwort bestätigt hat.
Phase 2 — recharts installieren
# Paketmanager erkennen
test -f pnpm-lock.yaml && PM=pnpm
test -f yarn.lock && PM=yarn
test -z "$PM" && PM=npm
$PM add recharts # oder: pnpm add recharts / yarn add recharts
nodemailer wurde bereits von /biz-email-setup installiert. Nicht erneut installieren.
Phase 3 — SQL-Migration ausführen (ABSCHLUSS)
Anleitung für den Benutzer:
Öffne das Supabase Dashboard → SQL Editor → Neue Abfrage → füge den gesamten Inhalt der folgenden Datei ein → klicke auf Ausführen:
[paste templates/supabase-migration-admin.sql hier]Dieses SQL erstellt 2 neue Tabellen:
campaigns(Kampagnenkopf) +campaign_sends(Log pro Empfänger). Es verändert NICHT die vorhandene Tabelleleads.Nach der Ausführung überprüfe am Ende des SQL — du solltest
campaigns: 0undcampaign_sends: 0sehen.
Warte auf die Bestätigung des Benutzers "SQL ausgeführt" und fahre dann mit Phase 4 fort.
Phase 4 — .env.local schreiben
Füge an .env.local an:
# Admin /admin page password
ADMIN_PASSWORD=<benutzer-passwort-aus-phase-1>
Wenn ADMIN_PASSWORD bereits vorhanden ist → frage den Benutzer, ob überschrieben werden soll (Standard: NEIN).
Stelle sicher, dass .env.local in .gitignore ist:
grep -qxF ".env.local" .gitignore 2>/dev/null || echo ".env.local" >> .gitignore
Phase 5 — 9 Dateien erstellen
lib/admin-auth.ts ← Vorlage lib-admin-auth.ts
lib/admin-stats.ts ← Vorlage lib-admin-stats.ts
lib/campaigns.ts ← Vorlage lib-campaigns.ts
lib/email-render.ts ← Vorlage lib-email-render.ts
lib/mailer.ts ← Vorlage lib-mailer.ts
app/admin/page.tsx ← Vorlage app-admin-page.tsx
app/api/admin/leads/route.ts ← Vorlage app-api-admin-leads-route.ts
app/api/admin/dashboard/route.ts ← Vorlage app-api-admin-dashboard-route.ts
app/api/admin/campaigns/route.ts ← Vorlage app-api-admin-campaigns-route.ts
E-Mail-Render-Modul: lib/email-render.ts ist ein REINES Modul (importiert kein nodemailer) — enthält textToHtml / wrapEmailHtml / personalize / renderCampaignEmail. lib/mailer.ts exportiert diese Funktionen erneut + fügt SMTP-Transport hinzu. Diese Trennung ermöglicht es app/admin/page.tsx (Client-Komponente), die Render-Hilfsfunktion für die Vorschau-Schaltfläche zu importieren, ohne nodemailer in das Client-Bundle zu ziehen.
Hinweis, wenn das Projekt bereits lib/mailer.ts von biz-email-setup hat: NICHT überschreiben — erstelle den Mailer dieses Skills als lib/admin-mailer.ts und ändere den Import in app/api/admin/campaigns/route.ts zu @/lib/admin-mailer. lib/email-render.ts ist immer sicher (eigener Name, keine Konflikte).
Hinweis zu Import-Aliasen: Alle Vorlagen verwenden @/lib/.... Überprüfe, ob das Projekt tsconfig.json paths "@/*": ["./*"] hat (Next.js Standard). Wenn nicht, ändere zu relativen Imports.
Hinweis zu lib/leads-store.ts: Der API-Route leads importiert listLeads aus @/lib/leads-store (Provider-unabhängiger Wrapper von biz-setup-sepay-payment). Wenn der Benutzer zuvor KV eingerichtet und dann umbenannt hat → überprüfe, ob der Wrapper auf Supabase verweist.
Datenflussdiagramm
Browser (admin/page.tsx)
│ fetch (Header x-admin-pass)
├──► GET /api/admin/dashboard?days=30 → lib/admin-stats.ts → leads Tabelle → { kpi, timeseries, deltas }
├──► GET /api/admin/leads?status=... → lib/leads-store.ts → leads Tabelle → { leads, stats }
├──► GET /api/admin/campaigns → lib/campaigns.ts → campaigns Tabelle → { campaigns: [...] }
└──► POST /api/admin/campaigns (body hat bodyFormat: 'text' | 'html')
└─► lib/campaigns.queryAudience(leads.where status/days)
└─► lib/email-render.renderCampaignEmail(format, body) → { html, text } (einmal rendern)
└─► lib/campaigns.createCampaign (campaigns Zeile)
└─► Schleife (200ms Verzögerung):
lib/mailer.personalize(html/text pro Empfänger)
lib/mailer.sendOne(to, subject, html)
lib/campaigns.logCampaignSend (campaign_sends Zeile)
└─► lib/campaigns.setCampaignStatus (endgültige Zählungen + sentAt)
return { successCount, failCount }
Client-Vorschau (OHNE Serveraufruf): admin/page.tsx importiert lib/email-render direkt,
"Vorschau"-Button rendert { html } + personalize(Vorlage) → zeigt in <iframe srcDoc sandbox>.
Phase 6 — 3 Testebenen
Test 1 — Curl Dashboard Endpunkt
PASS="<passwort-aus-phase-1>"
# Falsches Passwort → 401
curl -i http://localhost:3000/api/admin/dashboard -H "x-admin-pass: wrong"
# Erwartet: 401, {"error":"invalid_password"}
# Richtiges Passwort → 200 mit KPI + Zeitreihe
curl -s http://localhost:3000/api/admin/dashboard?days=7 -H "x-admin-pass: $PASS" | jq '.period, .deltas'
# Erwartet: period {days, leads, paid, revenue, conversionRate}, deltas {leadsPct, ...}
Test 2 — UI Smoke im Browser
1. pnpm dev (oder yarn/npm)
2. Öffne http://localhost:3000/admin
3. Sieh Popup "Admin-Seite" → falsches Passwort eingeben → "Falscher Code, bitte versuche es erneut"
4. Richtiges Passwort eingeben → Dashboard wird angezeigt
5. Tab Übersicht: 4 KPI-Karten, Liniendiagramm, Donut, Periodenauswahl 7d/30d/90d funktioniert
6. Tab Kunden: Tabelle + Filter + CSV-Export sichtbar
7. Tab E-Mail-Marketing: Formular + Verlauf (zunächst leer) sichtbar
8. Tab E-Mail-Marketing: Inhalt eingeben → auf "👁 Vorschau" klicken → Modal zeigt E-Mail
Render (Umschalter Normaler Text ↔ HTML, um beide Modi zu bestätigen)
Test 3 — Testkampagne an E-Mail-Eigentümer senden
WICHTIG: Wähle Zielgruppe
last_days= 7 mit gesetztem Passwort, Body mit{{name}}. Sende Test an einen einzelnen Lead in der DB (Test-Lead des Eigentümers), um den Ablauf zu bestätigen, bevor der eigentliche Massenversand erfolgt.
1. Gehe zu Tab E-Mail-Marketing
2. Kampagnenname: "Test senden"
3. Betreff: "Test-E-Mail von /admin — hast du sie erhalten?"
4. Zielgruppe: "Nach letzten Tagen" → 7 Tage
5. Inhalt:
Hallo {{name}},
Dies ist eine Test-E-Mail von der Admin-Seite, um zu bestätigen, dass SMTP funktioniert.
Wenn du diese erhältst, werde ich mit dem Versand echter Kampagnen beginnen.
6. Klicke auf "👁 Vorschau" → bestätige korrekte Anzeige → schließe Modal
7. Klicke auf "Jetzt senden" → bestätige
8. Warte 10-30s (200ms × Anzahl Empfänger)
9. Überprüfe:
- Grünes Banner "X/X E-Mails gesendet (0 Fehler)"
- Verlaufstabelle hat neue Zeile mit Status "Gesendet"
- Eigentümer checkt Posteingang → sieht E-Mail mit ersetztem {{name}}
Endausgabe
✓ /admin CRM-artig mit Supabase + recharts + E-Mail-Marketing erstellt
📁 Neue Dateien (9):
lib/
admin-auth.ts (Timing-sicherer Passwortcheck)
admin-stats.ts (KPI + Zeitreihe + Deltas)
campaigns.ts (CRUD + Zielgruppenabfrage)
email-render.ts (rein: textToHtml + wrapEmailHtml + renderCampaignEmail — gemeinsam genutzt von Server + Client Vorschau)
mailer.ts (nodemailer Transport + Re-Export von email-render + Opt-Out Footer)
app/admin/page.tsx (1CRM 3-Tab-UI — Übersicht / Kunden / E-Mail-Marketing — Body Text/HTML + Vorschau)
app/api/admin/
leads/route.ts (GET Liste + Filter)
dashboard/route.ts (GET KPI + Zeitreihe)
campaigns/route.ts (GET Verlauf + POST Massenversand)
🗄 Supabase Migration:
- Tabellen campaigns + campaign_sends (RLS deny-all, service_role bypass)
- Du hast templates/supabase-migration-admin.sql ausgeführt ✓
📦 NPM-Abhängigkeiten:
- recharts (neu installiert)
- nodemailer (bereits von biz-email-setup vorhanden)
🔐 Env:
- ADMIN_PASSWORD in .env.local geschrieben
- .gitignore enthält jetzt .env.local
- SMTP_* + MAIL_FROM von biz-email-setup werden wiederverwendet
🧪 Tests:
1. ✓ Curl Dashboard Endpunkt (Test 1)
2. ✓ UI Smoke im Browser (Test 2)
3. ✓ Testkampagne an E-Mail-Eigentümer gesendet (Test 3)
🔧 Deine TODO-Liste:
1. (Für Produktion) ADMIN_PASSWORD zu Vercel-Env-Variablen hinzufügen:
vercel env add ADMIN_PASSWORD production
2. Neu bereitstellen: vercel --prod
3. Öffne https://<domain>/admin → Passwort eingeben → bestätige, dass 3 Tabs funktionieren
4. Erste echte Kampagne senden: kleine Zielgruppe wählen (last_days 7), um
SMTP-Rate-Limit des Anbieters zu überprüfen, dann auf 'all' hochskalieren
Vorlagen
| Datei | Beschreibung |
|---|---|
templates/supabase-migration-admin.sql | Migration erstellt campaigns + campaign_sends |
templates/lib-admin-auth.ts | checkAdminPass() Timing-sicherer Vergleich |
templates/lib-admin-stats.ts | getDashboardData(days) — KPI + Zeitreihe + Deltas |
templates/lib-campaigns.ts | CRUD campaigns + queryAudience() |
templates/lib-email-render.ts | Reines Modul — textToHtml() + wrapEmailHtml() + personalize() + renderCampaignEmail() (text/HTML). Importiert kein nodemailer → Client-Komponente kann für Vorschau verwendet werden |
templates/lib-mailer.ts | nodemailer Transport + sendOne() + sleep() + Re-Export der Render-Hilfsfunktion von email-render |
templates/app-admin-page.tsx | 1CRM-Seite mit Sidebar + 3 Tabs + recharts. E-Mail-Tab hat Formatumschalter Text/HTML + Vorschau-Button (Modal-iframe) |
templates/app-api-admin-leads-route.ts | Leads-Tabellen-API (ruft listLeads() auf) |
templates/app-api-admin-dashboard-route.ts | Dashboard-Analytics-API |
templates/app-api-admin-campaigns-route.ts | GET Liste + POST Erstellen und Senden |
Anti-Pattern (vermeiden)
- ❌ Passwort mit
===vergleichen → Timing-Angriff. Vorlage verwendetcrypto.timingSafeEqualüberlib/admin-auth.ts. - ❌ Passwort im Quellcode hartcodieren → vergessen zu ändern → Produktion mit Test-Passwort. Immer über
process.env.ADMIN_PASSWORD. - ❌ Passwort über URL-Query-String senden (
?pass=123456) → Leak in Browser-Verlauf + Access-Logs. Headerx-admin-passverwenden. - ❌ Separate Login-Seite + Session-Cookie + JWT erstellen wenn der Benutzer nur einfaches Admin benötigt. Inline-Popup reicht aus.
- ❌ Passwort in localStorage speichern → JS kann lesen, XSS-Leak. Vorlage speichert in React-State (geht bei Neuladen verloren, beabsichtigt).
- ❌ Test in Phase 6 überspringen → Benutzer könnte defekte Kampagnen an echte Kunden senden. Führe immer Test 1, 2, 3 durch.
- ❌ Annehmen, dass der Benutzer
pnpmverwendet → Erkenne Paketmanager von Lock-Dateien. - ❌ E-Mail-Render-Logik in den API-Route einbetten → Duplikation zwischen Server (senden) und Client (Vorschau). Trenne in
lib/email-render.ts. - ❌ bodyFormat als Tag nicht speichern →
campaigns.body_formatin der Migration, damit der Verlauf weiß, wie der Inhalt angezeigt wird (HTML vs. Text).