Outil interne de centralisation et priorisation du feedback produit. Vos collègues proposent (bugs, idées, améliorations), votent — vous priorisez sur des données, plus à l'instinct.
🌐 Live → echo-granouillit.vercel.app 📦 Repo → github.com/VincentG32/echo
Stack : Next.js 16 (App Router) · TypeScript · Tailwind v4 · Airtable · n8n · Qdrant · Claude (Anthropic) · Auth JWT custom · Vercel
- Pourquoi Echo
- Fonctionnalités livrées
- Stack & justifications
- Architecture
- Modèle de sécurité
- Accessibilité
- Setup local
- Schéma Airtable
- Automatisations Airtable
- Automatisations n8n
- Structure du code
- Décisions techniques (mini-ADRs)
- Roadmap V2 / V3
- Limitations connues
- Crédits
Dans une équipe produit, le feedback utilisateur arrive de partout : Slack, Notion, mails, tickets de support… Résultat :
- Priorisation à l'instinct ("celui qui crie le plus fort gagne")
- Idées dupliquées parce que personne ne sait ce qui a déjà été soumis
- Pas de signal clair sur ce qui compte pour les utilisateurs, pas pour celui qui le porte
Echo résout ça avec un périmètre volontairement minimaliste :
- Une source unique de feedback, structurée (titre, description, type)
- Un vote = un user (un utilisateur ne peut voter qu'une seule fois par feedback)
- Tri émergent par nombre de votes : le top, c'est ce que l'équipe attend vraiment
- Un dashboard admin pour modérer
Pas de plugin tiers exposé aux utilisateurs, pas d'intégration Slack. L'IA reste un outil interne (classification, agent d'aide au test, contrôle qualité), jamais une dépendance de la boucle principale de feedback/vote/priorisation.
Authentification
- Inscription email / mot de passe (bcrypt, 8 chars min)
- Connexion / déconnexion
- Rôle
userattribué automatiquement à l'inscription - Rôle
adminconfigurable (manuellement dans Airtable)
Feedbacks
- Création (Title, Description, Type) — Creator et CreatedAt remplis automatiquement
- 3 types : 🐛 Bug · 💡 Idée · ✨ Amélioration (avec couleurs distinctes)
- Édition / suppression réservées au créateur
- Le bouton est masqué côté UI et l'API renvoie 403 si tentative cross-user
Votes
- Un user ne peut voter qu'une fois par feedback (anti-double-vote)
VoteCountincrémenté atomiquement avec la création du Vote- Tri descendant par votes sur la page liste
Pages
/— landing/login,/signup/feedbacks— liste publique aux users connectés/feedback/[id]— détail avec actions (voter / éditer / supprimer)/submit— formulaire de création (accessible via "+ Nouveau feedback" sur/feedbacksou depuis/campagne, plus de lien direct dans le menu — voir V7)/admin— dashboard admin (suppression universelle)
Sécurité
- Toutes les mutations passent par un check
getCurrentUser()côté serveur - Permission par ressource : 401 / 403 / 409 selon le cas
- Cookie JWT
httpOnly+sameSite=lax+secureen prod - Le token Airtable ne quitte jamais le serveur
- Landing page dédiée sur
/(hero, 3 cards de features, "Comment ça marche", CTA) pour accueillir les nouveaux visiteurs au lieu de les dropper dans la liste - Auth gate sur
/feedbacksetGET /api/feedbacks: la liste n'est plus publique (incohérent avec un outil interne d'équipe) - Filtres par type (chips cliquables) avec compteurs par catégorie
- Toasts (sonner) sur toutes les mutations (login, signup, vote, edit, delete, logout)
- Édition inline sur la page détail (toggle "Modifier" → form dans la même page, pas de route
/editséparée)
- Workflow dev kanban — table
Feedbacks.Status, page/devà 4 colonnes (à faire / en cours / review / livré), drag-drop (dnd-kit) avec boutons fallback pour clavier et mobile, confetti à la livraison (respecteprefers-reduced-motion) - Notifications in-app — bannière sur la liste pour le créateur quand son feedback avance, badge sur les cards, dismiss persistant
- Commentaires — table
Comments, fil de discussion sur la page détail, notification automatique au créateur - Dashboard admin tabbed —
/admin?tab=overview(KPIs + charts recharts + top votes + top contributeurs) et?tab=list(modération) - Auth durcie — email verification (token 24h, gate optionnel via env), password reset (token 1h), tous via Resend (gracieusement dégradé sans clé d'API)
- Rate limiting — Upstash Redis sliding window sur
/api/auth/login(5/min),/api/auth/signup(3/min),/api/feedbacks/[id]/vote(20/min) - Observabilité — Sentry (erreurs prod) + Vercel Analytics (Web Vitals)
- Dark mode — toggle manuel persistant + détection auto
prefers-color-scheme, tokens CSS isolés par thème via[data-theme="dark"] - Qualité — 10 tests E2E Playwright + GitHub Actions CI (typecheck + lint + build à chaque PR), audit code interne (sécurité + architecture + performance + accessibilité) avec correctifs documentés
- Accessibilité WCAG 2.1 AA — audit interne passé, focus visible global, skip-link, contrast AA,
role="alert"sur erreurs,prefers-reduced-motionrespecté (cf. Accessibilité)
- Criticité sur les bugs — champ
Feedbacks.Criticality(bloquant/majeur/mineur) obligatoire à la création quandtype=bug, exposé via radio buttons avec définitions inline pour forcer un référentiel commun ("L'utilisateur peut-il quand même utiliser l'app ?"). Cf. composantCriticalityBadge. - Admin override criticité (2 surfaces) — un admin peut reclasser un bug à la hausse ou à la baisse :
- Inline dans
/admin?tab=list—CriticalityPicker, popover sur le badge, 2 clics pour reclasser - Page détail —
AdminCriticalityOverride, 3 boutons toggle, pour les workflows "deep dive un feedback" - Les deux UIs partagent l'API
PATCH /api/feedbacks/[id]/criticality(admin-only). Update optimiste + rollback si erreur.
- Inline dans
- Dashboard "bloquants en cours" — sur
/admin?tab=overview: 5e tile KPI (rouge si > 0) + liste pleine-largeur "🔴 Top bugs bloquants à traiter" triée par votes. Boucle complète tile → liste → page détail → reclasser/livrer. - Alerte 🚨 Bug bloquant — automatisation Airtable qui envoie un email à l'admin dès qu'un feedback bascule en
type=bug ∧ criticality=bloquant. FormuleBloquantSubjectcôté Airtable. Déclenchée à la création OU sur override admin. Cf. Automatisations Airtable. - Export CSV admin — endpoint
GET /api/admin/export(admin-only) qui stream un CSV UTF-8 (avec BOM Excel) de tous les feedbacks (ID, Title, Description, Type, Criticality, Status, VoteCount, CreatorName, AssignedToName, CreatedAt). Lien "📥 Exporter CSV" sur le dashboard admin. - Soft delete — un click "Supprimer" ne détruit plus la ligne Airtable, il pose
Feedbacks.DeletedAt = nowIso(). Tous les reads filtrent{DeletedAt} = ''(listFeedbacks,listBacklogFeedbacks,getFeedbackById). Récupération possible côté admin en vidant le champ dans Airtable. - a11y CI automatisé — job
A11y · axe-coreajouté à GitHub Actions. À chaque push/PR, axe-core scanne les 4 pages publiques (Landing, Login, Signup, Forgot password) et fail la CI sur toute violationserious/critical. Sans gate (E2E_ENABLED), car ces pages n'ont pas besoin d'Airtable de test. A immédiatement attrapé une régression de contraste (text-tertiary 4.33:1 sur le fond rose du test de palette). - Charte graphique teal — tokens d'action passés en teal (
#0F766Elight,#14B8A6dark) après un détour par le rouge. Choisi pour ne clasher avec aucun des 7 badges sémantiques (bug-rouge, idée-violet, amélioration-vert, criticité bloquant/majeur/mineur). Contraste validé par axe-core sur les 4 pages publiques.
- Cahier de test — table
CahierTests(voir Schéma Airtable) + page/campagne: liste les scénarios de test de la campagne active (Echo V1), groupés par zone, avec un badge de priorité. Chaque item a un lien "Donner un feedback sur ce test" qui pré-remplit/submit(titre + description) avec le code du test, le scénario et le résultat attendu. - Compagnon de test — widget de chat (
CompagnonWidget), visible sur/campagneet/submitune fois connecté. Guide vers le bon scénario du cahier de test actif et détecte les doublons potentiels avant soumission (propose de voter pour un feedback existant plutôt que d'en créer un nouveau). Détail complet, modèles et garde-fous : voir Automatisations n8n. - Pipeline de qualité — jeu de test de 8 cas (table
JeuDeTest, un par catégorie attendue) rejoué contre l'agent réel, noté par un juge IA sur 3 critères (tableEvaluation). Baseline : 4.96/5 (Pertinence 4.88, Sécurité 5.00, Clarté 5.00) sur 8/8 cas sans erreur. - Correctif dashboard — les graphiques de
/admin(répartition par type, backlog par statut) ne s'affichaient plus correctement : l'animation d'entrée des barres (Recharts) ne se résolvait jamais dans certains cas. Corrigé en désactivant l'animation d'entrée sur les deux graphiques. - Correctif mode sombre — la feuille de style figeait toutes les couleurs à leur valeur claire au moment de la compilation (
@theme inlineappliqué à tort aux couleurs, pas seulement aux polices), ce qui empêchait[data-theme="dark"]de s'appliquer sur la quasi-totalité des classesbg-*/text-*. Corrigé en séparant les couleurs dans leur propre bloc@theme(sansinline). - Navigation simplifiée — suppression de l'onglet "Soumettre" du menu (trop de chemins différents pour arriver au même formulaire) ; le bouton "+ Nouveau" de
/feedbacksest renommé "+ Nouveau feedback" pour être plus explicite.
| Couche | Choix | Pourquoi ce choix |
|---|---|---|
| Framework | Next.js 16 App Router | Server Components pour la liste (pas de useEffect de fetch côté client), API routes co-localisées, déploiement Vercel en 1 clic |
| Langage | TypeScript strict | Sécurité de type sur la frontière auth/Airtable où les bugs sont silencieux et coûteux |
| Styling | Tailwind CSS v4 | Tokens de design en CSS custom properties, définis dans globals.css (couleurs et radii dans un bloc @theme classique, polices dans un bloc @theme inline séparé — voir ADR sur pourquoi ce n'est pas le même bloc). Brand color = teal #0F766E (light) / #14B8A6 (dark), choisi V6 pour ne clasher avec aucun des 7 badges sémantiques (bug/idée/amélioration + criticité bloquant/majeur/mineur) |
| Backend | API Routes Next.js (Node runtime) | Mêmes types partagés avec le front via lib/, pas de serveur Express à maintenir |
| Base de données | Airtable | Plan gratuit suffisant pour un MVP, UI native pour debug, pas de migrations SQL à gérer pendant la formation |
| Auth | JWT custom + bcryptjs | Pédagogique pour une formation : on voit la mécanique (hash, signature, cookie), pas masqué derrière une lib |
| Validation | Zod | Schémas réutilisables côté form ET côté API (single source of truth) |
| Notifications | sonner | Léger, accessible, 0 config |
| Hébergement | Vercel | Déploiement git push → live, free tier généreux, Preview URLs par PR |
| Automatisation & agent IA | n8n (VPS personnel) | Orchestration visuelle des workflows (classification, digest, agent) sans redéployer l'app à chaque ajustement de prompt ou de logique |
| Base vectorielle | Qdrant (self-hosted) | Recherche par similarité pour le RAG du Compagnon de test (cahier de test + détection de doublons) ; auto-hébergé pour rester propriétaire de l'infrastructure |
| Embeddings | Cohere (embed-multilingual-v3.0) |
Multilingue, 1024 dimensions, bon rapport qualité/coût pour un corpus en français |
| Modèles de langage | Claude Sonnet (agent) + Claude Haiku (garde-fou, classification, juge) | Le modèle le plus capable réservé au dialogue ; un modèle rapide et économe pour les tâches courtes et répétitives — détail au Bloc automatisations n8n |
┌──────────────────────────────┐
│ Browser (React + Tailwind) │
│ pages: /, /login, /signup, │
│ /feedbacks, /submit, │
│ /feedback/[id], │
│ /campagne, /dev, │
│ /admin │
└──────────┬───────────────────┘
│ fetch + cookie JWT (httpOnly)
▼
┌──────────────────────────────┐
│ Next.js API routes (server) │ ← AIRTABLE_TOKEN, JWT_SECRET
│ /api/auth/{signup,login, │ restent ici, jamais en client
│ logout}, /api/me, │
│ /api/feedbacks[/:id][...], │
│ /api/admin/export, │
│ /api/compagnon ─────────────┼──┐
│ │ │ webhook (Basic Auth)
│ proxy.ts (middleware) : │ │
│ redirige vers /login si │ │
│ pas de cookie │ │
└──────────┬────────────────────┘ │
│ airtable.js SDK │
▼ ▼
┌──────────────────────────┐ ┌─────────────────────────────┐
│ Airtable base "Pro" │◄─┤ n8n (VPS personnel) │
│ Users · Feedbacks · │ │ Compagnon de test, garde- │
│ Votes · Comments · │ │ fou, RAG (Qdrant), digest, │
│ Notifications · │ │ classification, monitoring │
│ CahierTests · Monitoring│ └─────────────────────────────┘
│ · JeuDeTest · Evaluation│
└───────────────────────────┘
Single source de vérité pour les données : src/lib/airtable.ts est un fichier barrel qui ré-exporte les modules src/lib/airtable/{users,feedbacks,votes,notifications,comments,cahierTests}.ts. Aucun composant React ne connaît la forme des records — ils consomment des types UserRecord / FeedbackWithCreator / VoteRecord propres.
n8n ne passe jamais par lib/airtable.ts : les workflows lisent/écrivent Airtable directement (credentials propres, côté n8n). L'app Next.js ne fait qu'appeler le webhook du Compagnon de test via /api/compagnon ; elle ne connaît rien du contenu des automatisations. Détail complet : Automatisations n8n.
Server Components par défaut, Client uniquement quand nécessaire. Les pages liste / détail / admin sont des Server Components qui appellent directement lib/airtable.ts côté serveur — pas de hop fetch HTTP inutile. Seuls les composants interactifs (forms, boutons de vote) sont "use client".
Quatre couches qui se renforcent. Compromettre une seule ne suffit pas.
- Variable d'env
AIRTABLE_TOKENlue côté serveur uniquement (process.env) - Aucun préfixe
NEXT_PUBLIC_(qui exposerait au bundle client) - Vérification :
curl https://echo-granouillit.vercel.app/_next/static/...ne contient jamaispat...
- Cookie
echo_token:httpOnly(impossible à lire en JS, donc immune aux XSS)secureen prod (HTTPS uniquement)sameSite=lax(CSRF protection raisonnable, signup depuis un lien externe fonctionne)maxAge= 7 jours
- Signé HS256 avec
JWT_SECRET(256 bits aléatoires viaopenssl rand -base64 48) - Payload :
{ sub: userId, email, role }— le rôle est dans le JWT pour éviter un fetch DB par requête en middleware
Les vérifications de propriété sont dans les API routes, pas dans l'UI :
| Action | 401 si | 403 si | 409 si |
|---|---|---|---|
POST /api/feedbacks |
non connecté | — | — |
PATCH /api/feedbacks/:id |
non connecté | creator !== user.id |
— |
DELETE /api/feedbacks/:id |
non connecté | creator !== user.id ET role !== admin |
— |
POST /api/feedbacks/:id/vote |
non connecté | — | vote déjà existant |
Cacher un bouton dans l'UI ne suffit pas — un curl direct contournerait. La vraie barrière est l'API route. L'UI ne fait que masquer ce qui n'est pas actionnable, pour la lisibilité.
4. Le webhook du Compagnon de test est authentifié, et ne fait jamais confiance à l'identité déclarée dans le message
Le widget de chat envoie l'identité et le rôle de l'utilisateur à n8n, mais ces deux champs sont relus côté serveur depuis la session dans /api/compagnon (jamais reconstruits depuis ce que le navigateur envoie). Le webhook n8n lui-même est protégé par Basic Auth (COMPAGNON_WEBHOOK_USER/_PASS) — sans ce verrou, n'importe qui connaissant l'URL du webhook aurait pu l'appeler directement en se déclarant role: admin, en contournant totalement l'app et son contrôle d'identité.
- ✅ User A tente
PATCH /api/feedbacks/<id-de-B>→ 403 - ✅ User non connecté → 401 sur tous les endpoints sensibles (y compris
GET /api/feedbacks/[id]) - ✅ User normal essaie
/admin→ redirigé vers/feedbacks - ✅ Vote 2× sur le même feedback → 409,
VoteCountinchangé - ✅ Token JWT bidouillé (signature invalide) → 401
- ✅ Token Airtable absent du JS bundle vérifié dans Network tab
- ✅ Login avec email inexistant : latence égale à un email valide (timing attack mitigé via
dummyVerify) - ✅ Appel direct du webhook
/api/compagnonsans en-tête d'authentification → 401 côté n8n
- Length caps Zod :
email.max(254)(RFC 5321),password.max(128)→ empêche un payload géant qui ferait boucler bcrypt côté serverless - Rate limiting per-IP (Upstash Redis, optionnel via
UPSTASH_REDIS_REST_*) sur/api/auth/login(5/min),/api/auth/signup(3/min),/api/feedbacks/[id]/vote(20/min). No-op en dev/CI sans les env vars. - Vercel Analytics + Sentry (optionnel via
SENTRY_DSNenv var) → monitoring d'erreurs et Web Vitals en prod - Tests E2E Playwright sur 10 scenarios critiques (signup, login, vote, anti-double-vote, permissions cross-user, kanban workflow)
Cible WCAG 2.1 AA. Audit interne passé, correctifs appliqués sur les écarts les plus visibles.
<html lang="fr">+ landmarks sémantiques (<header>,<nav>,<main>,<aside>)- Tous les
<input>ont un<label htmlFor>associé (Lighthouse 100) - Tous les boutons icon-only (toggle thème, dismiss notif, select assign) ont un
aria-label - Hiérarchie de titres propre : 1
<h1>par page,<h2>cohérents TypeBadge/StatusBadge: emojis enaria-hidden, label texte lu par les lecteurs d'écran- Le drag-drop kanban a un fallback bouton complet — toutes les actions sont accessibles au clavier sans drag
| ID | Correctif | Impact |
|---|---|---|
| C-1 | Focus ring global (:focus-visible outline action color, 2px, offset 2px) |
Tab navigation visible — WCAG 2.4.7 |
| C-2 | Skip link "Aller au contenu principal" (sr-only, devient visible au focus) | Bypass du Header pour clavier — WCAG 2.4.1 |
| C-3 | text-tertiary foncé en light (#656565, abaissé une 2ᵉ fois en V6 quand le job axe-core a flag 4.33:1 sur le test de palette rose) et dark (#9a9a9a) |
Contraste AA 4.5:1 — WCAG 1.4.3 |
| I-3 | role="alert" sur tous les blocs d'erreur form (login, signup, submit, reset, FeedbackActions) |
Erreurs annoncées par lecteur d'écran après submit |
| I-4 | celebrate() confetti respecte prefers-reduced-motion: reduce |
WCAG 2.3.3, sensibilité vestibulaire |
| A-2 | aria-busy={pending} sur boutons submit pendant les requêtes API |
État "occupé" exposé aux lecteurs d'écran |
| A-3 | Bannières Notification et Verification en <aside aria-label> |
Landmarks navigables |
| A-4 | inputMode="email" explicite sur les champs email |
Clavier mobile correct, défense en profondeur sur type=email |
| A-5 | AppToaster synchronise sonner avec data-theme Echo (au lieu de prefers-color-scheme) |
Toasts cohérents avec le thème manuel choisi |
| ID | Item | Effort |
|---|---|---|
| I-1 | KeyboardSensor sur dnd-kit pour drag clavier (fallback bouton existe) |
~10 min |
| I-2 | Émojis dans labels de boutons en aria-hidden (ex: "📌 Backlog", "🗑️ Supprimer") |
~15 min |
| I-5 | aria-current="page" sur les liens du Header correspondant à la route active |
~15 min |
Le job A11y · axe-core dans .github/workflows/ci.yml lance axe-core via Playwright sur les 4 pages publiques (Landing, Login, Signup, Forgot password) à chaque push/PR. Toute violation serious ou critical fait échouer la CI. Tags scannés : wcag2a / wcag2aa / wcag21a / wcag21aa.
Pages logguées-in (/feedbacks, /admin, /dev) restent couvertes par l'audit manuel — pas de test base Airtable de test requis pour ce job de base.
- Pas de test manuel avec lecteur d'écran réel (NVDA / VoiceOver) — couverture via inspection statique du markup + axe-core uniquement.
- Pages logguées-in non scannées en CI — un job a11y plus large couvrirait
/feedbacks,/admin,/dev(nécessiteraitE2E_ENABLED=true+ une base Airtable de test).
- Node ≥ 20
- Compte Airtable (gratuit), GitHub, Vercel (gratuit)
git clone https://github.com/VincentG32/echo.git
cd echo
npm install- airtable.com → Create a base → la nommer
Echo Base - Récupérer le
Base IDdans l'URL (airtable.com/appXXXXXXXXXXXXXX/...) - airtable.com/create/tokens → créer un PAT
- Name :
Echo local - Scopes :
data.records:read,data.records:write,schema.bases:read - Access : restreindre à la base
Echo Base(best practice sécu)
- Name :
Voir Schéma Airtable ci-dessous.
cp .env.example .env.local
# Éditer .env.local et remplir au minimum :
# AIRTABLE_TOKEN=patXXXXXXXXXXXXXX...
# AIRTABLE_BASE_ID=appXXXXXXXXXXXXXX
# JWT_SECRET=$(openssl rand -base64 48)Variables optionnelles :
RESEND_API_KEY(+RESEND_FROM_EMAIL,APP_URL) — active l'envoi des emails de vérification et de reset password. Sans la clé, les URLs sont loggées dans la console serveur (suffisant en dev). Free tier 3k emails/mois.REQUIRE_EMAIL_VERIFICATION=true— active le gate API qui bloquePOST /api/feedbacks,/vote,/commentspour les utilisateurs non vérifiés. Off par défaut pour ne pas casser les seeds existants — à activer après migration manuelle.UPSTASH_REDIS_REST_URL+UPSTASH_REDIS_REST_TOKEN— active le rate limiting per-IP. Sans ces vars, le limiteur est no-op (pratique en dev/CI).SENTRY_DSN+NEXT_PUBLIC_SENTRY_DSN(même valeur) — active le monitoring d'erreurs Sentry. Sans ces vars, le SDK reste no-op.SENTRY_AUTH_TOKEN,SENTRY_ORG,SENTRY_PROJECT— pour l'upload de source maps au build (facultatif).COMPAGNON_WEBHOOK_URL+COMPAGNON_WEBHOOK_USER+COMPAGNON_WEBHOOK_PASS— active le widget "Compagnon de test" (/api/compagnon), qui appelle le webhook n8n de l'agent en Basic Auth. Sans ces vars, l'API renvoie 503 et le widget ne s'affiche simplement pas (dégradation gracieuse, comme les autres intégrations optionnelles ci-dessus). Détail : Automatisations n8n.
AIRTABLE_TOKEN, AIRTABLE_BASE_ID, JWT_SECRET avec NEXT_PUBLIC_ — ce serait exposer le token côté client.
npm run dev
# → http://localhost:3000Après inscription via /signup, ouvrir Airtable → table Users → changer Role à admin. Re-login pour rafraîchir le JWT.
Seed data utilisée pour les démos et les tests E2E. Tous marqués EmailVerifiedAt non null donc le banner de vérification ne s'affiche pas pour eux.
| Role | Usage | |
|---|---|---|
alice@test.com |
admin | Compte admin principal — accès au dashboard /admin (vue d'ensemble + liste/modération) |
demo-admin@pulse.app |
admin | Compte admin partageable pour les démos live — mdp dans scripts/create-admin.mjs |
bob@test.com |
user | Compte user générique pour vérifier les permissions cross-user |
prodtest@example.com |
user | Compte de test "réaliste" (domaine externe) |
vgranouillit.pro@gmail.com |
user | Compte propriétaire |
marc@pulse.app |
dev | Dev seed — assignations kanban |
yasmine@pulse.app |
dev | Dev seed |
lea@pulse.app |
dev | Dev seed |
marie@pulse.app |
user | User seed — feedbacks fictifs |
hugo@pulse.app |
user | User seed |
tom@pulse.app |
user | User seed |
sarah@pulse.app |
user | User seed |
alex@pulse.app |
user | User seed |
Comptes manquants ? Les seeds
@pulse.appsont fictifs : adresses non valides, emails de vérif/reset y seront loggés en console (pas envoyés). Pour tester un vrai flow Resend, utiliser une adresse réelle. Les mots de passe seed ne sont pas dans le repo — récupérables via password reset si Resend est configuré, ou en re-créant un user via/signup.
git pushsur GitHub- vercel.com/new → Import → choisir le repo
echo - Environment Variables : ajouter les 3 mêmes (Production + Preview)
- Deploy → URL en
xxx.vercel.app
Recommandé : créer 2 bases Airtable séparées (Echo-Dev et Echo-Prod) pour ne pas polluer la prod avec de la data de test. Chaque environnement Vercel pointe sur sa base.
Le workflow e2e dans GitHub Actions exécute les 10 tests Playwright sur chaque push. Il est opt-in : tant qu'il n'est pas activé, le workflow CI tourne en Typecheck · Lint · Build seulement.
Pour l'activer :
-
Créer une base Airtable de test isolée :
- airtable.com → Create base → la nommer
Echo-Test - Cloner manuellement le schéma de prod (5 tables : Users, Feedbacks, Votes, Notifications, Comments). Ou via airtable.com/sync-data.
- Créer les 4 comptes de test :
alice@test.com(admin),bob@test.com(user),sarah@pulse.app(user),lea@pulse.app(dev) — tous avec passwordpassword123(les tests les utilisent). - Récupérer le
Base ID(commence parapp...) - Créer un Personal Access Token dédié avec accès à cette base seulement.
- airtable.com → Create base → la nommer
-
GitHub → Settings → Secrets and variables → Actions :
- Onglet Secrets : ajouter
E2E_AIRTABLE_TOKEN(le PAT de la base test)E2E_AIRTABLE_BASE_ID(l'IDapp...de la base test)E2E_JWT_SECRET(n'importe quelle string aléatoire 32+ chars)
- Onglet Variables : ajouter
E2E_ENABLED=true
- Onglet Secrets : ajouter
-
Au prochain push, le job
E2E · Playwrightapparaît à côté du jobbuild. Sur failure, le rapport HTML est uploadé en artifact (téléchargeable depuis l'onglet Actions).
[E2E timestamp] et nettoient via afterEach, mais une base dédiée garantit zéro pollution sur la prod.
┌──────────┐ ┌────────────┐ ┌──────┐
│ Users │◄─────│ Feedbacks │ │ Votes│
│ │ 1:N │ │ 1:N │ │
│ Email │◄─────┤ Creator │◄─────│ User │
│ Hash │ │ │ │ │
│ Name │ │ │◄─────┤ Feedback
│ Role │ └────────────┘ 1:N │ │
└──────────┘ └──────┘
| Champ | Type | Notes |
|---|---|---|
Email |
Single line text | Primary, unique (vérifié à signup) |
PasswordHash |
Long text | bcrypt cost 10, jamais le password en clair |
Name |
Single line text | nom affiché |
Role |
Single select | user (default) · dev · admin |
CreatedAt |
dateTime | rempli explicitement à signup |
EmailVerifiedAt |
dateTime | null = email pas encore vérifié, set = vérifié à cette date |
VerificationToken |
Single line text | token de vérification d'email actif (24h), null après usage |
VerificationExpires |
dateTime | expiry du token de vérification |
ResetToken |
Single line text | token de reset password actif (1h), null après usage |
ResetExpires |
dateTime | expiry du token de reset |
Migration des utilisateurs existants : après ajout des 5 champs ci-dessus, les rows existantes ont
EmailVerifiedAtà null et seraient considérées comme non-vérifiées. Pour basculer les seeds (admin, devs de test) en vérifiés, mettezEmailVerifiedAtau timestamp ISO de votre choix dans Airtable. Le gate sur les actions API n'est actif que siREQUIRE_EMAIL_VERIFICATION=true(cf. setup), donc vous pouvez introduire la migration progressivement.
| Champ | Type | Notes |
|---|---|---|
Title |
Single line text | Primary |
Description |
Long text | |
Type |
Single select | bug · idée · amélioration |
VoteCount |
Number (integer) | dénormalisé pour le tri ; incrémenté dans la même route que create Vote |
Creator |
Link → Users | single record link |
CreatedAt |
dateTime | |
Status |
Single select | to_do · in_progress · review · done — alimente le workflow kanban /dev |
AssignedTo |
Link → Users | dev en charge du feedback (kanban) |
Criticality |
Single select | V6 : bloquant · majeur · mineur. Obligatoire à la création quand type=bug, vide sinon. Admin peut override depuis /admin?tab=list (picker inline) ou la page détail. |
DeletedAt |
dateTime | V6 soft delete : null = actif, set = supprimé (filtré hors des reads). Récupération en vidant le champ depuis Airtable. |
EmailSubject |
Formula | objet d'email pré-formaté pour les automatisations Airtable (cf. Automatisations Airtable) |
BodyTemplate |
Formula | corps d'email pré-formaté multi-lignes, idem |
HotVoteSubject |
Formula | V6 : "🔥 Hot vote : [" & {Type} & "] " & {Title}. Consommé par l'auto 🔥 Hot vote (seuil ≥ 5 votes) |
BloquantSubject |
Formula | V6 : "🚨 Bug BLOQUANT : " & {Title}. Consommé par l'auto 🚨 Bug bloquant signalé |
Archivé |
Checkbox | exclusion des vues admin filtrées, marqueur manuel ou automatique (TTL 30j sur done) |
| Champ | Type | Notes |
|---|---|---|
Reference |
Single line text | Primary (laissé vide, on n'utilise pas le primary field ici) |
Feedback |
Link → Feedbacks | single record |
User |
Link → Users | single record |
FeedbackId |
Single line text | dupliqué pour le filtre (cf. |
UserId |
Single line text | idem |
CreatedAt |
dateTime |
FeedbackId / UserId en double des liens ?
Airtable's filterByFormula ne sait pas filtrer un linked record par son ID — ARRAYJOIN({Feedback}) retourne le primary field des records liés (le titre du feedback), pas leur ID. Pour vérifier vite l'existence d'un vote (feedback, user), on a dénormalisé les IDs en texte plat. Petit coût en stockage, gros gain en simplicité de requête.
| Champ | Type | Notes |
|---|---|---|
Reference |
Single line text | Primary (laissé vide) |
Recipient |
Link → Users | destinataire de la notification |
Feedback |
Link → Feedbacks | feedback concerné |
Status |
Single line text | type d'évènement (ex. changement de statut) |
RecipientId / FeedbackId |
Single line text | mêmes raisons de dénormalisation que sur Votes |
CreatedAt / UpdatedAt |
dateTime |
| Champ | Type | Notes |
|---|---|---|
Reference |
Single line text | Primary (laissé vide) |
Feedback |
Link → Feedbacks | feedback commenté |
Author |
Link → Users | auteur du commentaire |
Body |
Long text | contenu du commentaire |
FeedbackId / AuthorId |
Single line text | mêmes raisons de dénormalisation que sur Votes |
CreatedAt |
dateTime |
| Champ | Type | Notes |
|---|---|---|
Code |
Single line text | Primary (ex. TEST-006), utilisé pour citer la source dans les réponses de l'agent |
Campagne |
Single line text | filtré sur la campagne active (Echo V1 en dur pour ce MVP — voir Roadmap) |
Zone |
Single line text | regroupement d'affichage sur /campagne |
Scenario |
Long text | ce que le testeur doit essayer |
ResultatAttendu |
Long text | comportement attendu, indexé dans Qdrant pour le RAG |
Priorite |
Single select | haute / moyenne / basse, nullable |
Actif |
Checkbox | exclut un item du cahier sans le supprimer |
Les tables techniques
Monitoring,JeuDeTestetEvaluation(écrites/lues par n8n, pas par l'app Next.js) sont documentées dans Automatisations n8n.
Echo utilise les Automatisations natives d'Airtable pour piloter les alertes admin, en complément du backend Next.js. Cette couche tourne côté Airtable, sans toucher au code applicatif. Elle est désactivable d'un toggle si la base est synchronisée vers une autre prod.
Le code applicatif gère déjà l'envoi d'emails transactionnels destinés aux utilisateurs finaux (vérification, reset password) via Resend. Mais les alertes admin internes (nouveau feedback, hot vote, lead bloqué) n'ont pas vocation à passer par le code applicatif :
- Elles n'ont aucun impact UX côté utilisateur — c'est de la plomberie ops.
- Elles changent souvent (seuil de votes, destinataire, format) — les piloter en Airtable évite un déploiement à chaque ajustement.
- Elles peuvent être désactivées sans toucher au code (audit, RGPD, vacances admin).
Chemin court assumé : trigger Airtable → email, zéro intermédiaire.
Pour rendre les emails réutilisables et cohérents, on dérive deux champs Formula à partir des données du record. Les automatisations consomment ces champs au lieu de re-templater à chaque fois.
"🆕 [" & {Type} & "] " & {Title}
Génère un objet du type 🆕 [bug] Pagination casse à partir de 50 feedbacks. L'emoji rend l'email reconnaissable dans une boîte chargée, le type entre crochets accélère le scan visuel.
"Titre : " & {Title} & "
Type : " & {Type} & "
Statut : " & {Status} & "
Votes : " & {VoteCount} & "
Description :
" & {Description}
Génère un corps multi-lignes structuré, prêt à l'envoi sans mise en forme supplémentaire dans l'automatisation.
- Single source of truth : le format de l'email est défini une fois. Toute automatisation qui consomme
{EmailSubject}ou{BodyTemplate}récupère automatiquement la dernière version. - DRY : N automatisations (alerte création, alerte 10 votes, alerte status change…) réutilisent les mêmes champs sans dupliquer le templating.
- Test gratuit : les champs sont visibles dans la table, on voit le rendu sur tous les records sans lancer une automatisation.
Même principe que les relations entre tables : on évite la duplication, on centralise.
| Élément | Configuration |
|---|---|
| Déclencheur | When a record is created → table Feedbacks |
| Action | Send email |
| Destinataire | adresse admin (configurée en dur dans l'action) |
| Objet | {EmailSubject} |
| Corps | {BodyTemplate} |
Comportement : à chaque nouveau feedback créé (via UI Airtable, formulaire Airtable, ou POST /api/feedbacks côté app), l'admin reçoit un email formaté dans la seconde, avec titre, type, votes, description complète.
| Limite | Valeur | Notes |
|---|---|---|
| Emails / jour | 100 par destinataire vérifié (plan Free) | Largement suffisant au volume Echo |
| Runs d'automatisation / mois | 100 sur Free, 1 000 sur Team | À surveiller si on multiplie les automatisations |
| Délai de déclenchement | ~30 s à 2 min | Pas du temps réel — acceptable pour des alertes admin |
| Élément | Configuration |
|---|---|
| Déclencheur | Record matches conditions → table Feedbacks · VoteCount ≥ 5 |
| Action | Send email |
| Destinataire | adresse admin |
| Objet | {HotVoteSubject} (formule : "🔥 Hot vote : [" & {Type} & "] " & {Title}) |
| Corps | {BodyTemplate} (réutilisé) |
Comportement : quand un feedback franchit le seuil de 5 votes, l'admin reçoit instantanément une alerte. Sert à attraper les sujets qui décollent pour décider rapidement s'ils partent au backlog. Seuil ajustable depuis l'UI Airtable sans toucher au code.
| Élément | Configuration |
|---|---|
| Déclencheur | Record matches conditions → table Feedbacks · Type = bug ET Criticality = bloquant |
| Action | Send email |
| Destinataire | adresse admin |
| Objet | {BloquantSubject} (formule : "🚨 Bug BLOQUANT : " & {Title}) |
| Corps | {BodyTemplate} (réutilisé) |
Comportement : se déclenche à la création d'un nouveau bug bloquant OU sur un override admin qui bascule un bug existant en bloquant. Boucle parfaite avec le tile KPI "Bloquants" et la liste "Top bugs bloquants à traiter" sur le dashboard.
| Automatisation | Déclencheur | Action |
|---|---|---|
| Email à l'auteur sur changement de statut | Record updated → champ surveillé Status |
Find record dans Users (matcher l'email du Creator) puis Send email |
Auto-archive done après 30 jours |
At a scheduled time (cron) → records où Status = done ET CreatedAt < today - 30d ET Archivé = false |
Update record → Archivé = true |
- Pas de retry visible : si l'envoi de l'email échoue (quota dépassé, adresse invalide), Airtable retry silencieusement 2-3 fois puis abandonne. L'onglet
Historiquede l'automatisation log le statut, mais aucun monitoring centralisé (pas remonté à Sentry). - Pas de versioning : modifier la formule d'
EmailSubjectimpacte rétroactivement tous les calculs dans toutes les vues qui l'affichent. Aucun historique des formules précédentes. - Pas exportable en code : les automatisations Airtable ne sont pas versionnables dans Git. Cette section du README est le seul moyen de garder une trace.
- Couplage destinataire : l'email admin est en dur dans l'action. Pour un usage multi-admin, extraire dans une table
Settings, ou basculer vers unFind users where Role = adminpuis email à chacun.
En plus des automatisations natives Airtable ci-dessus, une instance n8n (VPS personnel) porte tout ce qui touche à l'IA : classification, agent conversationnel, RAG, qualité. Chaque workflow business est lié à un Error Workflow centralisé.
Les automatisations Airtable suffisent pour de la plomberie simple (email déclenché sur un changement de champ). Dès qu'il faut enchaîner plusieurs appels à un modèle de langage, gérer un garde-fou, interroger une base vectorielle et boucler sur un jeu de test, une orchestration visuelle dédiée devient plus lisible et plus rapide à ajuster qu'un enchaînement de fonctions Next.js — sans redéploiement à chaque changement de prompt ou de seuil.
Tous les lundis à 9h, synthèse de la semaine (nouveaux feedbacks, votes, statuts) par Claude Sonnet, envoyée par email.
À la création d'un feedback (Airtable Trigger), Claude Haiku (température 0, sortie JSON stricte) propose Type et Criticality. Ne touche jamais à la criticité si CriticalityLockedByAdmin est coché — un admin garde toujours la main sur ses propres arbitrages.
Agent conversationnel derrière le widget /campagne et /submit, orchestré en plusieurs sous-workflows :
- Orchestrateur (Chat Trigger, webhook, protégé par Basic Auth) : normalise l'entrée (identité/rôle toujours fournis par
/api/compagnon, jamais reconstruits depuis le message), puis passe la main au garde-fou. - Garde-fou (Claude Haiku, JSON strict) : classe chaque message en 4 catégories —
legitime,hors_sujet,injection,hors_perimetre_role— et bloque tout ce qui n'est pas légitime avant même d'appeler l'agent principal (protège la sécurité et le coût). - Agent (Claude Sonnet, mémoire glissante 10 messages) : dispose de 2 outils, chacun un sous-workflow RAG sur Qdrant —
guide_de_test(cherche dans le cahier de test actif) etrecherche_doublons(cherche un feedback similaire existant, seuil de similarité 0.45, propose de voter pour l'existant plutôt que d'en créer un nouveau). - Chaque appel est tracé dans la table
Monitoring.
Workflow séparé qui vectorise le cahier de test actif (embeddings Cohere embed-multilingual-v3.0, 1024 dimensions) et l'upsert dans Qdrant. Appelle Cohere et Qdrant en HTTP direct plutôt que via les nœuds natifs n8n dédiés — voir ADR.
Rejoue les 8 cas de la table JeuDeTest (un par catégorie attendue : nominal, doublon, hors sujet, injection, hors périmètre rôle, information absente, etc.) contre l'agent réel en production, puis fait noter chaque réponse par un juge Claude Haiku sur 3 critères (Pertinence, Sécurité, Clarté), stockés dans Evaluation. Baseline actuelle : 4.96/5 sur 8/8 cas. La calibration humaine (comparer le jugement du juge IA au sien sur un échantillon) reste à faire — champ Annotation_humaine prévu mais pas encore rempli.
Un seul point d'entrée pour toutes les erreurs des 7 workflows business : alerte email + ligne "Échec" dans Monitoring, avec le message d'erreur. Chaque nœud modèle a un retry (2 tentatives, 5 secondes d'écart) et un timeout de 120 secondes.
| Table | Rôle |
|---|---|
Monitoring |
une ligne par exécution majeure (workflow, statut, durée, date, message d'erreur) |
JeuDeTest |
les 8 cas de test de référence, écrits avant la première évaluation |
Evaluation |
résultats détaillés de chaque run du pipeline de qualité |
- Traçabilité seulement partielle en Git : contrairement au code Next.js, les workflows n8n ne sont pas versionnés automatiquement — toute modification se fait dans l'UI n8n, sans diff ni historique Git.
- Pas de couverture E2E/CI :
/campagneet/api/compagnonne sont testés qu'à la main, aucun test Playwright ne les couvre aujourd'hui. - Calibration humaine du juge non faite (voir ci-dessus).
src/
├── app/
│ ├── page.tsx # / (landing si déconnecté, redirect /feedbacks si connecté)
│ ├── login/ # /login (page = Suspense + LoginForm client)
│ ├── signup/ # /signup
│ ├── feedbacks/ # /feedbacks (server) + FeedbacksList (client) avec filtres
│ ├── feedback/[id]/ # /feedback/:id (server) + FeedbackActions (client)
│ ├── submit/ # /submit (form client)
│ ├── admin/ # /admin (server, gated par role) + AdminDeleteButton (client)
│ ├── api/
│ │ ├── auth/{signup,login,logout}/route.ts
│ │ ├── me/route.ts
│ │ └── feedbacks/
│ │ ├── route.ts # GET (list, auth required), POST (create)
│ │ ├── [id]/route.ts # GET, PATCH, DELETE (avec checks ownership/role)
│ │ └── [id]/vote/route.ts # POST (anti-double-vote), GET (hasVoted)
│ ├── layout.tsx # Header + Toaster + container
│ └── globals.css # design tokens Tailwind v4 @theme
├── components/
│ ├── Header.tsx # logo + nav contextuelle (admin link si role=admin)
│ ├── FeedbackCard.tsx # cellule cliquable de la liste
│ ├── TypeBadge.tsx # badge coloré par type
│ └── LogoutButton.tsx # client (POST /api/auth/logout)
├── lib/
│ ├── airtable.ts # SEUL module qui parle à Airtable
│ ├── auth.ts # bcrypt + JWT + getCurrentUser + cookie helpers
│ ├── schemas.ts # Zod (signup, login, createFeedback, updateFeedback)
│ └── format.ts # formatDate, truncate
└── proxy.ts # Next.js 16 middleware (renommé) - gate par cookie
Convention de nommage : Server Component = page.tsx direct, Client Component = fichier dédié XxxForm.tsx / XxxActions.tsx co-localisé.
Pour le projet de formation :
- ✅ Pas de migration SQL à gérer
- ✅ UI native pour debug rapide (renommer un feedback à la main)
- ✅ Plan gratuit suffisant (1 500 records/base, 5 req/s)
À reconsidérer à scale :
- ❌ Pas de transaction atomique → race condition possible sur
VoteCount(2 votes simultanés peuvent perdre une incrémentation) - ❌ Rate limit 5 req/s ne tient pas au-delà d'une équipe
- ❌ Pas de Row-Level Security native — toute la sécu repose sur les API routes
- C'est un projet de formation. Exposer la mécanique (
bcrypt.hash,jwt.sign, cookiehttpOnly) est pédagogique. NextAuth aurait masqué tout ça derrière une abstraction. - Trade-off : pas d'OAuth GitHub/Google prêt à l'emploi, pas de password reset out-of-the-box.
Voir Schéma Airtable. Détaillé ci-dessus : filterByFormula ne sait pas matcher sur un linked record ID, donc on dénormalise.
Next.js 16 a déprécié le nom middleware.ts au profit de proxy.ts (renommage uniquement). La fonction exportée s'appelle désormais proxy() et non middleware(). Aucun changement de signature.
Echo est un outil interne de petite équipe — la fraîcheur instantanée des votes prime sur la perf. force-dynamic simplifie aussi le mental model (pas de cache à invalider). À reconsidérer V3 quand on aura SWR côté client + cache HTTP côté Edge.
Toutes les pages qui consomment des feedbacks sont déjà dynamic = "force-dynamic". Un router.refresh() côté client suffit pour faire re-render le Server Component avec la data fraîche.
Standard 2025. cost 12 serait plus sûr mais ralentit le signup à ~250ms sur les serverless functions Vercel. Trade-off accepté pour cette V1.
Le nœud natif d'insertion Qdrant de n8n a un vrai bug d'écriture (Not existing vector name error), confirmé indépendant de Qdrant et de Cohere (les deux testés sains individuellement via des appels directs). Contournement : le workflow d'indexation appelle Cohere (POST /v2/embed) et Qdrant (PUT /collections/.../points) en HTTP direct. La lecture, elle, reste sur le nœud natif Qdrant, qui fonctionne correctement — à condition que la collection soit créée avec un vecteur nommé chaîne vide plutôt que sans nom.
@theme inline indique à Tailwind v4 de figer la valeur résolue d'une variable au moment de la compilation, plutôt que de générer une référence var(...) vivante dans les classes utilitaires. Utile pour les polices (--font-sans: var(--font-geist-sans), une indirection vers une variable injectée par Next.js), mais catastrophique pour les couleurs : ça empêchait [data-theme="dark"] de s'appliquer sur bg-*/text-*, puisque la valeur claire était déjà figée en dur dans le CSS généré. Fix : polices dans leur bloc @theme inline, couleurs et radii dans un bloc @theme classique juste à côté.
Organisée par effort × impact. Les tiers sont indépendants — vous pouvez piocher.
🟢 Tier 1 — Quick wins déjà livrés (cf. Fonctionnalités V1.5)
🟢 Tier 1 bis — Déjà livré V6 (cf. Évolutions V6)
- Criticité bugs (bloquant/majeur/mineur) + admin override (picker inline + page détail) + dashboard "Bloquants"
- Alerte 🚨 Bug bloquant (Airtable Automation)
- Alerte 🔥 Hot vote ≥ 5 votes (Airtable Automation)
- Export CSV admin
- Soft delete (DeletedAt)
- a11y CI automatisé (axe-core/playwright sur 4 pages publiques)
| Feature | Description | Pourquoi |
|---|---|---|
| Recherche full-text | Input avec debounce, filtre ?q=... côté API via SEARCH({Title}, q) Airtable |
Demandé par tous les seed users (top des votes !) |
| Tags / catégories | Champ multipleSelects Airtable, multi-filtre combiné avec type |
Pour équipes multi-produits |
| Pagination cursor | ?cursor=... + bouton "Charger plus" |
Au-delà de 100 feedbacks |
| Optimistic vote | Update UI avant la réponse API, rollback si 409 | Réactivité perçue |
Filtre criticité sur /admin?tab=list |
Sélecteur "bloquant uniquement" pour triage rapide | Compagnon naturel du tile KPI Bloquants |
| Feature | Pourquoi | Compromis |
|---|---|---|
| Migration → Postgres (Supabase / Neon) | Transactions atomiques, RLS, foreign keys, scalabilité | Apprentissage Prisma/Drizzle, perte de l'UI Airtable |
| NextAuth.js | OAuth GitHub/Google, sessions DB révocables, password reset out-of-the-box | Couche d'abstraction supplémentaire à comprendre |
| Notifications email | Vote reçu, status change envoyés par email (en plus de la bannière in-app actuelle) | Resend + queue (Inngest ou Vercel Cron) |
| i18n FR/EN | Élargir l'audience | next-intl + refactor strings |
| Mobile redesign | Cards trop denses sur smartphone | 1-2 jours UX + tests sur vrais devices |
- Cohérence éventuelle sur
VoteCount— les 2 requêtes Airtable (createVote+incrementVoteCount) ne sont pas atomiques. Si la 2ᵉ échoue après la 1ʳᵉ, le compteur diverge. Acceptable au volume actuel (~10 utilisateurs), à durcir avec une vraie DB transactionnelle. - Token JWT non révocable — un cookie compromis reste valide jusqu'à expiration (7 jours). V3 : sessions DB ou tokens courts + refresh.
Pas de password reset / email verification— livré depuis V2 (voir §Fonctionnalités) : vérification d'email à l'inscription et reset password par token expirant sont en place.Aucun test automatisé— livré depuis V2/V6 : 14 tests E2E Playwright + CI GitHub Actions (typecheck/lint/build sur chaque push, a11y systématique, suite E2E complète gated derrièreE2E_ENABLED). Limite réelle actuelle : la suite E2E ne tourne pas par défaut en CI (variable désactivée) — voir §Tests E2E en CI.- Performance Airtable — 5 req/s par base. La page liste fait 2 requêtes (feedbacks + users batch). Tient jusqu'à ~50 utilisateurs simultanés grand max.
- Cookie sameSite=lax — un site malveillant peut déclencher des
GETcross-origin avec le cookie, mais pas desPOST(CSRF safe par convention HTTP). Suffisant pour cette V1. - Pas de versionning des feedbacks — éditer un feedback écrase l'ancien contenu sans historique.
- Anti-doublon non automatique au clic sur "Soumettre le feedback" — le Compagnon de test (voir Automatisations n8n) sait détecter un feedback similaire et propose de voter pour lui plutôt que d'en créer un nouveau, mais seulement si le testeur ouvre le chat et décrit son problème avant de remplir le formulaire. Le bouton "Soumettre le feedback" (accessible depuis
/submit,/campagneet "+ Nouveau feedback" sur/feedbacks— un seul et même formulaire) ne fait aucune vérification lui-même. Choix de scope assumé (MVP) plutôt que bug : l'automatiser ajouterait un appel réseau bloquant sur le chemin de soumission et un nouveau cas à couvrir dans le jeu de test, pour un mécanisme déjà démontré et fonctionnel via le chat. - Calibration humaine du juge de qualité non faite — voir Automatisations n8n.
/campagneet le Compagnon de test hors couverture E2E/CI — testés manuellement seulement à ce jour.
Mon projet final du titre Product Builder No-Code (La Capsule), 2026.
Brief original : centraliser et prioriser le feedback produit, en illustrant les 12 principes du cours "Construire une application solide" (séparation données/UI, sécurité côté serveur, naming, scalabilité…).
Build : @VincentG32 avec assistance Claude Code.
Made with 🤍 in Paris.