Tableau de bord des leads Biz Admin — Style CRM /admin pour Supabase
Génère une page /admin de type 1CRM dans un projet Next.js (App Router). 3 onglets — Vue d'ensemble (KPI + graphiques), Clients (tableau + filtres), Marketing par e-mail (création + envoi en masse + historique).
Philosophie : Plus simple que 1CRM (1 mot de passe commun, pas de multi-utilisateurs), mais avec suffisamment de graphiques, de deltas et de marketing par e-mail pour un entrepreneur de 1 à 2 personnes. Tout l'état est stocké dans Supabase, le fichier de structure est indépendant du projet existant.
/admin → popup "Saisir le mot de passe d'administration" → les onglets appellent /api/admin/{leads|dashboard|campaigns}
→ chaque requête envoie l'en-tête x-admin-pass
→ comparaison Timing-safe côté serveur avec la variable d'environnement ADMIN_PASSWORD
Le mot de passe est stocké dans l'état React — rafraîchir la page = la popup réapparaît (volontaire, simple, pas de cookie de session).
Supabase uniquement — la variante KV a été supprimée. Raison : les campagnes et les envois de campagnes ont besoin de jointures SQL + service_role pour contourner RLS ; KV ne les prend pas en charge nativement.
Quand utiliser cette compétence
- Le projet a déjà le paiement Sepay et le stockage des leads via
lib/leads-supabase.ts(debiz-setup-sepay-paymenten choisissant Supabase) → besoin d'une couche de visualisation, d'un tableau de bord et d'un marketing par e-mail. - L'utilisateur souhaite une page de style CRM : cartes KPI avec delta, graphique linéaire par jour, donut de conversion, envoi d'e-mails promotionnels aux clients déjà enregistrés.
NE PAS utiliser quand :
- Le projet n'a pas encore
lib/leads-supabase.ts→ demander à l'utilisateur d'exécuter/biz-setup-sepay-payment(choisir Supabase) d'abord. - Le projet utilise Vercel KV au lieu de Supabase → cette compétence NE prend PLUS en charge KV. L'utilisateur a seulement 2 options : migrer vers Supabase ou porter manuellement.
- Besoin de multi-administrateurs avec rôles/permissions → compétence à 1 mot de passe commun, inadaptée.
- Besoin de suivi détaillé des e-mails (ouverture/clic par destinataire) → la compétence ne suit que l'envoi/l'échec. L'utilisateur a besoin de Resend/SendGrid/Brevo en masse avec suivi.
Workflow (6 phases)
Phase 0 : DÉTECTER Next.js App Router + vérifier lib/leads-supabase.ts + vérifier les variables d'environnement SMTP
Phase 1 : DEMANDER le mot de passe utilisateur (par défaut 123456) — GATE
Phase 2 : INSTALLER la dépendance npm `recharts`
Phase 3 : EXÉCUTER supabase-migration-admin.sql dans l'éditeur SQL Supabase — GATE
Phase 4 : ÉCRIRE ADMIN_PASSWORD dans .env.local
Phase 5 : GÉNÉRER 9 fichiers de code (5 lib + 4 route + 1 page)
Phase 6 : TESTER 3 niveaux (curl du point de terminaison dashboard, test UI superficiel, envoi d'une campagne test)
Phase 0 — Détection + vérification des dépendances
# Vérifier 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 "✗ Besoin de l'App Router. Cette compétence ne supporte pas le Pages Router."; exit 1; }
# Vérifier le stockage Supabase des leads
test -f lib/leads-supabase.ts && echo "Stockage Supabase : ✓" || { echo "✗ Fichier lib/leads-supabase.ts manquant — exécutez /biz-setup-sepay-payment en choisissant Supabase d'abord"; exit 1; }
test -f lib/leads-store.ts && echo "Wrapper agnostique du fournisseur : ✓"
test -f lib/supabase-admin.ts && echo "Client de rôle service : ✓"
# Vérifier les variables d'environnement SMTP (de biz-email-setup)
grep -E "^SMTP_HOST=" .env.local && echo "SMTP_HOST : ✓" || { echo "✗ Variables d'environnement SMTP manquantes — exécutez /biz-email-setup d'abord"; exit 1; }
grep -E "^SMTP_USER=" .env.local
grep -E "^SMTP_PASS=" .env.local
grep -E "^MAIL_FROM=" .env.local
# Vérifier les paquets
grep '"nodemailer"' package.json && echo "nodemailer : ✓"
grep '"@supabase/supabase-js"' package.json && echo "@supabase : ✓"
Si lib/leads-supabase.ts est manquant :
⚠ Je vois que le projet n'a pas encore de stockage de leads Supabase. Veuillez d'abord exécuter
/biz-setup-sepay-payment(choisir le backend Supabase) pour configurer le paiement + la tableleads+ RLS. Revenez ensuite, je générerai le tableau de bord admin.
Si les variables d'environnement SMTP sont manquantes :
⚠ Je vois que SMTP n'est pas configuré. L'onglet Marketing par e-mail nécessite SMTP pour envoyer. Veuillez d'abord exécuter
/biz-email-setuppour choisir un fournisseur (Gmail/Resend SMTP/Brevo/SendGrid/Zoho...), puis revenez, je générerai l'admin. Je peux continuer sans l'onglet email — vous confirmez ?
Résumé pour l'utilisateur :
J'ai détecté : Next.js App Router ✓, stockage Supabase ✓, SMTP configuré ✓. Je vais :
- Vous demander le mot de passe pour /admin
- Installer
recharts(pour les graphiques linéaires + donut)- Exécuter la migration SQL pour créer les tables
campaigns+campaign_sendsdans Supabase- Écrire
ADMIN_PASSWORDdans .env.local- Générer 9 fichiers (5 lib + 4 route + 1 page)
- Vous guider pour les tests
Prêt pour la Phase 1 ?
Phase 1 — Demander le mot de passe (GATE)
Demander exactement :
Quel mot de passe souhaitez-vous utiliser pour la page /admin ?
- Par défaut, je propose :
123456- Ou collez un mot de passe plus fort pour la production (≥12 caractères, avec lettres + chiffres + caractères spéciaux)
Collez le mot de passe (ou tapez "123456" pour utiliser la valeur par défaut) :
Attendre la réponse de l'utilisateur. Prendre le mot de passe collé par l'utilisateur. Vide → utiliser 123456.
NE PAS passer à la Phase 2 tant que l'utilisateur n'a pas confirmé le mot de passe.
Phase 2 — Installer recharts
# Détecter le gestionnaire de paquets
test -f pnpm-lock.yaml && PM=pnpm
test -f yarn.lock && PM=yarn
test -z "$PM" && PM=npm
$PM add recharts # ou : pnpm add recharts / yarn add recharts
nodemailer est déjà installé par /biz-email-setup. Ne pas réinstaller.
Phase 3 — Exécuter la migration SQL (GATE)
Instructions pour l'utilisateur :
Ouvrez le tableau de bord Supabase → Éditeur SQL → Nouvelle requête → collez tout le contenu du fichier ci-dessous → cliquez sur Exécuter :
[coller le contenu de templates/supabase-migration-admin.sql ici]Ce SQL crée 2 nouvelles tables :
campaigns(en-tête de campagne) +campaign_sends(journal par destinataire). Il NE touche PAS à la tableleadsexistante.Une fois exécuté, vérifiez à la fin du SQL — vous devez voir
campaigns: 0etcampaign_sends: 0.
Attendre que l'utilisateur confirme "SQL exécuté" puis passer à la Phase 4.
Phase 4 — Écrire dans .env.local
Ajouter à .env.local :
# Mot de passe de la page /admin
ADMIN_PASSWORD=<mot-de-passe-collé-par-l'utilisateur-phase-1>
Si ADMIN_PASSWORD existe déjà → demander à l'utilisateur s'il veut écraser (par défaut NON).
S'assurer que .env.local est dans .gitignore :
grep -qxF ".env.local" .gitignore 2>/dev/null || echo ".env.local" >> .gitignore
Phase 5 — Générer 9 fichiers
lib/admin-auth.ts ← templates/lib-admin-auth.ts
lib/admin-stats.ts ← templates/lib-admin-stats.ts
lib/campaigns.ts ← templates/lib-campaigns.ts
lib/email-render.ts ← templates/lib-email-render.ts
lib/mailer.ts ← templates/lib-mailer.ts
app/admin/page.tsx ← templates/app-admin-page.tsx
app/api/admin/leads/route.ts ← templates/app-api-admin-leads-route.ts
app/api/admin/dashboard/route.ts ← templates/app-api-admin-dashboard-route.ts
app/api/admin/campaigns/route.ts ← templates/app-api-admin-campaigns-route.ts
Module de rendu d'email : lib/email-render.ts est un module PUR (n'importe pas nodemailer) — contient textToHtml / wrapEmailHtml / personalize / renderCampaignEmail. lib/mailer.ts réexporte ces fonctions + ajoute le transport SMTP. Cette séparation permet à app/admin/page.tsx (composant client) d'importer les helpers de rendu pour le bouton Aperçu sans attirer nodemailer dans le bundle client.
Note si le projet a déjà lib/mailer.ts de biz-email-setup : NE PAS écraser — générer le mailer de cette compétence sous lib/admin-mailer.ts et modifier l'import dans app/api/admin/campaigns/route.ts vers @/lib/admin-mailer. lib/email-render.ts est toujours sûr (nom unique, ne touche à rien).
Note sur l'alias d'import : tous les modèles utilisent @/lib/.... Vérifier que le projet a les chemins tsconfig.json "@/*": ["./*"] (par défaut Next.js). Sinon, les remplacer par des imports relatifs.
Note sur lib/leads-store.ts : La route API des leads importe listLeads depuis @/lib/leads-store (wrapper agnostique du fournisseur de biz-setup-sepay-payment). Si l'utilisateur a configuré KV auparavant puis l'a renommé -> vérifier que le wrapper pointe vers Supabase.
Schéma du flux de données
Navigateur (admin/page.tsx)
│ fetch (en-tête x-admin-pass)
├──► GET /api/admin/dashboard?days=30 → lib/admin-stats.ts → table leads → { kpi, timeseries, deltas }
├──► GET /api/admin/leads?status=... → lib/leads-store.ts → table leads → { leads, stats }
├──► GET /api/admin/campaigns → lib/campaigns.ts → table campaigns → { campaigns: [...] }
└──► POST /api/admin/campaigns (corps avec bodyFormat: 'text' | 'html')
└─► lib/campaigns.queryAudience(leads.where status/days)
└─► lib/email-render.renderCampaignEmail(format, body) → { html, text } (rendu 1 fois)
└─► lib/campaigns.createCampaign (ligne campaigns)
└─► boucle (throttle 200ms) :
lib/mailer.personalize(html/text par destinataire)
lib/mailer.sendOne(to, subject, html)
lib/campaigns.logCampaignSend (ligne campaign_sends)
└─► lib/campaigns.setCampaignStatus (comptes finaux + sentAt)
return { successCount, failCount }
Aperçu client (N'APPELS PAS le serveur) : admin/page.tsx importe lib/email-render directement,
le bouton "Aperçu" rendu { html } + personalize(modèle) → affiché dans <iframe srcDoc sandbox>.
Phase 6 — Tester 3 niveaux
Test 1 — Curl du point de terminaison du tableau de bord
PASS="<mot-de-passe-de-la-phase-1>"
# Mauvais mot de passe → 401
curl -i http://localhost:3000/api/admin/dashboard -H "x-admin-pass: wrong"
# Attendu : 401, {"error":"invalid_password"}
# Bon mot de passe → 200 avec KPI + séries temporelles
curl -s http://localhost:3000/api/admin/dashboard?days=7 -H "x-admin-pass: $PASS" | jq '.period, .deltas'
# Attendu : period {days, leads, paid, revenue, conversionRate}, deltas {leadsPct, ...}
Test 2 — Test UI superficiel dans le navigateur
1. pnpm dev (ou yarn/npm)
2. Ouvrir http://localhost:3000/admin
3. La popup "Page d'administration" apparaît → saisir un mauvais mot de passe → "Mauvais code, veuillez réessayer"
4. Saisir le bon mot de passe → accéder au tableau de bord
5. Onglet Vue d'ensemble : voir 4 cartes KPI, graphique linéaire, donut, sélecteur de période 7j/30j/90j fonctionnel
6. Onglet Clients : voir le tableau + filtres + export CSV
7. Onglet Marketing par e-mail : voir le formulaire de création + historique (vide au début)
8. Onglet Marketing par e-mail : saisir du contenu → cliquer sur "👁 Aperçu" → la modale affiche l'email
rendu (basculer entre les formats Texte simple ↔ HTML pour confirmer les 2 modes)
Test 3 — Envoyer une campagne test à l'e-mail du propriétaire
IMPORTANT : Choisir l'audience
last_days= 7 avec le mot de passe défini, corps avec{{name}}. Envoyer un test à un seul lead dans la base de données (lead test du propriétaire) pour confirmer le flux avant d'envoyer en masse.
1. Aller dans l'onglet Marketing par e-mail
2. Nom de la campagne : "Test d'envoi"
3. Sujet : "Email test depuis /admin — l'avez-vous reçu ?"
4. Audience : "Selon les jours les plus récents" → 7 jours
5. Contenu :
Bonjour {{name}},
Ceci est un email test depuis la page admin pour confirmer que SMTP fonctionne.
Si vous le recevez, je commencerai à envoyer de vraies campagnes.
6. Cliquer sur "👁 Aperçu" → confirmer que l'email s'affiche correctement → fermer la modale
7. Cliquer sur "Envoyer maintenant" → confirmer
8. Attendre 10-30s (200ms × nombre de destinataires)
9. Vérifier :
- Bannière verte "X/X e-mails envoyés (0 erreur)"
- Tableau d'historique avec une nouvelle ligne de statut "Envoyé"
- Le propriétaire vérifie sa boîte de réception → voir l'email avec le nom remplaçant {{name}}
Résultat final
✓ Tableau de bord /admin de style CRM généré avec Supabase + recharts + marketing par e-mail
📁 Nouveaux fichiers (9) :
lib/
admin-auth.ts (vérification du mot de passe Timing-safe)
admin-stats.ts (KPI + séries temporelles + deltas)
campaigns.ts (CRUD + requête d'audience)
email-render.ts (pur : textToHtml + wrapEmailHtml + renderCampaignEmail — partagé entre serveur et aperçu client)
mailer.ts (transporteur nodemailer + réexport de email-render + pied de page de désabonnement)
app/admin/page.tsx (UI à 3 onglets de type 1CRM — Vue d'ensemble / Clients / Marketing par e-mail — corps texte/HTML + Aperçu)
app/api/admin/
leads/route.ts (GET liste + filtre)
dashboard/route.ts (GET KPI + séries temporelles)
campaigns/route.ts (GET historique + POST envoi en masse)
🗄 Migration Supabase :
- Tables campaigns + campaign_sends (RLS deny-all, contournement service_role)
- Vous avez exécuté templates/supabase-migration-admin.sql ✓
📦 Dépendance NPM :
- recharts (nouvellement installé)
- nodemailer (déjà présent depuis biz-email-setup)
🔐 Env :
- ADMIN_PASSWORD écrit dans .env.local
- .gitignore inclut désormais .env.local
- SMTP_* + MAIL_FROM réutilisés depuis biz-email-setup
🧪 Test :
1. ✓ Curl du point de terminaison du tableau de bord (Test 1)
2. ✓ Test UI superficiel dans le navigateur (Test 2)
3. ✓ Envoi d'une campagne test à l'e-mail du propriétaire (Test 3)
🔧 À faire par vous :
1. (Si déploiement en production) Ajouter ADMIN_PASSWORD aux variables d'environnement Vercel :
vercel env add ADMIN_PASSWORD production
2. Redéployer : vercel --prod
3. Ouvrir https://<domaine>/admin → saisir le mot de passe → confirmer que les 3 onglets fonctionnent
4. Premier envoi de campagne réelle : choisir une petite audience (last_days 7) pour vérifier
la limite de débit SMTP du fournisseur avant de passer à 'all'
Modèles
| Fichier | Description |
|---|---|
templates/supabase-migration-admin.sql | Migration créant campaigns + campaign_sends |
templates/lib-admin-auth.ts | checkAdminPass() comparaison Timing-safe |
templates/lib-admin-stats.ts | getDashboardData(days) — KPI + séries temporelles + deltas |
templates/lib-campaigns.ts | CRUD campagnes + queryAudience() |
templates/lib-email-render.ts | Module pur — textToHtml() + wrapEmailHtml() + personalize() + renderCampaignEmail() (texte/HTML). N'importe pas nodemailer → utilisable par le composant client pour l'aperçu |
templates/lib-mailer.ts | Transporteur nodemailer + sendOne() + sleep() + réexport des helpers de rendu depuis email-render |
templates/app-admin-page.tsx | Page de style 1CRM avec barre latérale + 3 onglets + recharts. Onglet email avec bascule de format texte/HTML + bouton Aperçu (modale iframe) |
templates/app-api-admin-leads-route.ts | API de la table des leads (appelle listLeads()) |
templates/app-api-admin-dashboard-route.ts | API d'analyse du tableau de bord |
templates/app-api-admin-campaigns-route.ts | GET liste + POST créer et envoyer |
Anti-patrons (à ne pas faire)
- ❌ Comparer le mot de passe avec
===→ attaque temporelle. Le modèle utilisecrypto.timingSafeEqualvialib/admin-auth.ts. - ❌ Codage en dur du mot de passe dans la source → oublier de changer → déploiement en production avec un mot de passe de test. Toujours via
process.env.ADMIN_PASSWORD. - ❌ Envoi du mot de passe dans la chaîne de requête URL (
?pass=123456) → fuite dans l'historique du navigateur + les journaux d'accès. Utiliser l'en-têtex-admin-pass. - ❌ Créer une page de connexion séparée + cookie de session + JWT alors que l'utilisateur a juste besoin d'un admin simple. La popup en ligne suffit.
- ❌ Stocker le mot de passe dans localStorage → JS peut le lire, fuite XSS. Le modèle le stocke dans l'état React (perdu au rafraîchissement, intentionnel).
- ❌ Ignorer la vérification des dépendances (sauts de Phase 0) → plantage au moment de l'exécution parce que
listLeads()ousuppabaseAdminn'existent pas. - ❌ Envoyer en masse avec un throttling trop rapide → les fournisseurs SMTP rejettent ou mettent sur liste noire. L'intervalle de 200 ms est sécurisé ; si l'utilisateur a besoin de plus rapide, il doit passer à Resend/SendGrid API (en dehors du périmètre de cette compétence).
- ❌ Permettre des pièces jointes dans les campagnes → complexité inutile pour ce niveau. Les pièces jointes sont hors du périmètre.
- ❌ Supposer que
lib/leads-store.tsexiste → doit être vérifié dans la Phase 0.
⚠ Rappel : Si l'utilisateur demande une fonctionnalité hors du périmètre (multi-admin, pièces jointes, segmentation avancée, A/B testing), refuser poliment et suggérer de développer manuellement ou d'utiliser un service spécialisé (Mailchimp, Brevo, Customer.io).