Changelog Batimio
Toutes les modifications notables de ce projet sont documentées dans ce fichier.
Le format suit Keep a Changelog et le projet suit la gestion sémantique de versions majeur.mineur.patch.
- majeur : rupture publique (API, parcours utilisateur, schéma DB non rétro-compatible)
- mineur : livraison d'un nouveau Sprint ou d'une nouvelle fonctionnalité
- patch : correction de bug ou ajustement mineur sans nouvelle feature
[1.17.5] — 2026-04-23
Corrigé — Consolidation SERP de marque : canonical + Schema.org + favicon unique
Le SERP Google pour batimio montrait 3 résultats distincts (/, /pro, /client/demande) avec un mix HTTP / HTTPS et des favicons différents. Conséquence : pas de consolidation en "résultat de marque" (homepage + sitelinks), dilution de l'autorité de domaine entre URLs perçues comme indépendantes. Trois fixes concourants :
feat(seo)metadataBasedans app/layout.tsx — fixehttps://batimio.comcomme base de toutes les URLs absolues de metadata (OG, canonical, etc.). Empêche que Next.js émette des URLs ambigues.feat(seo)Canonical explicite sur la homepage (app/page.tsx) →<link rel="canonical" href="https://batimio.com/">. Signal fort à Google que l'URL unique de la homepage est celle-ci (pas dewww., pas dehttp://, pas de paramètres).feat(seo)Schema.org JSON-LDOrganization+WebSitesur la homepage — alimente le Knowledge Graph Google, condition pour qu'un SERP de marque consolidé apparaisse à terme (3-6 mois après installation d'une notoriété minimum).fix(seo)Favicon unifié : suppression depublic/icons/icon.svg(doublon 512×512 incohérent avecapp/icon.svg32×32). Manifest PWA mis à jour pour pointer sur/icon.svgunique. Élimine la divergence de favicon entre pages indexées par Google à des dates différentes.
Note stratégique
Ces corrections accélèrent la consolidation mais ne la garantissent pas. Google met en général 2-6 mois pour unifier un SERP de marque après la mise en place des signaux. Facteurs complémentaires requis : backlinks vers batimio.com (signal de marque), trafic régulier, absence d'erreurs d'indexation sur le domaine.
Sécurité (audit v1.17.5)
- ✅ Aucune nouvelle surface, uniquement des métadonnées statiques côté client.
- ✅ Aucun secret nouveau, aucune dépendance ajoutée.
- ✅ Le
dangerouslySetInnerHTMLpour le JSON-LD injecte du contenu constant (pas d'input utilisateur) — pas de XSS.
[1.17.4] — 2026-04-23
Corrigé — Sitemap : segments entreprises-N.xml + fiabilisation anti-timeout
fix(seo)Renommageartisans-N.xml→entreprises-N.xmlpour cohérence avec le path canonique/entreprise/<slug>(v1.17.3). Un URL devenue incohérente dans un sitemap (pointer vers/entreprise/dans un fichier nomméartisans-N) confuse les crawlers.fix(seo)SEGMENT_SIZEpasse de 45 000 à 1 000 pour tuer le problème de timeout Vercel. Avant : chaque segment faisait 45 requêtes Supabase séquentielles (PostgREST cap à 1000 lignes par réponse) → 4.5 s + cold start → timeout aléatoire sur 5 segments / 6 côté Googlebot. Après : 1 seule requête par segment (.range(N*1000, N*1000+999)), temps de réponse < 300 ms garanti.- Conséquence : 258 segments
entreprises-0..257.xmlau lieu de 6. Google préfère de nombreux petits sitemaps à quelques gros (meilleure parallélisation de crawl + récupération d'erreur granulaire par fichier). construireSegmentEntreprisessimplifié : plus de boucle de pagination interne, juste unerange()+ unorder('slug')pour la stabilité.
Sécurité (audit v1.17.4)
- ✅ Aucune nouvelle surface. Validation d'input par regex stricte
/^entreprises-(\d+)\.xml$/inchangée. - ✅ Aucun secret nouveau, aucune dépendance ajoutée.
[1.17.3] — 2026-04-23
Modifié — Path canonique /entreprise/ (ex-/artisan/)
feat(seo)Les fiches passent de/artisan/<slug>à/entreprise/<slug>pour s'aligner sur les 2 concurrents SEO dominants : annuaire-entreprises.data.gouv.fr/entreprise/ et pappers.fr/entreprise/. Le terme/entreprise/est aussi plus neutre que/artisan/— compatible avec l'extension future à d'autres corps de métier non-artisanaux.- Rétro-compat 301 :
app/artisan/[slug]/page.tsxdevient une simple redirection permanente vers/entreprise/[slug]pour préserver le jus SEO de toute URL/artisan/déjà indexée ou partagée (aucune ne devrait l'être depuis le sitemap, mais garde-fou défensif). - Mise à jour de tous les liens internes :
app/recherche/page.tsx,app/[metier]/[ville]/page.tsx,app/[metier]/[ville]/[prestation]/page.tsx,components/shared/ArtisansMap.tsx,components/shared/SchemaOrg.tsx(URLs Schema.org + canonicals). - Sitemap :
app/sitemap/[segment]/route.tsémet désormais/entreprise/<slug>. - Test E2E smoke mis à jour :
tests/e2e/smoke-browser.spec.ts. CLAUDE.md§20 mis à jour avec le nouveau path canonique et la double rétro-compat (slug + path).
Sécurité (audit v1.17.3)
- ✅ Pas de nouvelle surface (simple rename de route + redirect).
- ✅ La RLS
public_read_activesurartisansreste la seule barrière ; s'applique identiquement sur les deux paths. - ✅ Aucun secret nouveau, aucune dépendance ajoutée.
Breaking change (mineur, compensé par 301)
Les URLs /artisan/<slug> ne renvoient plus directement le contenu : elles redirigent en 301 vers /entreprise/<slug>. Les backlinks externes et URLs indexées continuent de fonctionner (Google propagera le 301 sur le re-crawl suivant).
[1.17.2] — 2026-04-23
Corrigé — Sitemap : /sitemap.xml répondait 404 en prod
fix(seo)Remplacement de l'approchegenerateSitemapsde Next.js par des route handlers manuels. La fonctiongenerateSitemapscassait au build ou au routing en prod (v1.17.1 a livré un/sitemap.xmlqui répondait 404). L'implémentation manuelle élimine toute magie framework.- Nouvelle structure :
```
/sitemap.xml → index (<sitemapindex>)
/sitemap/static.xml → pages statiques + métier×ville + prestations
/sitemap/artisans-0.xml → artisans 0 à 44999
/sitemap/artisans-1.xml → artisans 45000 à 89999
... (autant de segments que nécessaire)
```
- Fichiers : app/sitemap.xml/route.ts (index) + [app/sitemap/[segment]/route.ts](app/sitemap/[segment]/route.ts) (segments). Ancien app/sitemap.ts supprimé.
- Tolérance aux pannes : si la DB est injoignable au moment du build, l'index dégrade à 1 segment plutôt que de servir un 500. Un sitemap incomplet > pas de sitemap du tout.
- Cache HTTP explicite :
Cache-Control: public, max-age=86400, s-maxage=86400+revalidate = 86400Next.js.
Sécurité (audit v1.17.2)
- ✅ Aucune nouvelle table ni colonne, aucun secret nouveau.
- ✅ Route handlers publics par conception (sitemap = contenu public destiné aux crawlers).
- ✅ Input
segmentvalidé par regex stricte/^artisans-(\d+)\.xml$/— pas d'injection possible dans la lecture DB.
[1.17.1] — 2026-04-23
Corrigé — Sitemap segmenté pour indexer 100% des artisans
fix(seo)Sitemap segmenté viagenerateSitemaps(Next 14.2+) — la prod Batimio contient 257 429 fiches artisan (confirmé après migration 022), alors que l'ancienapp/sitemap.tslimitait arbitrairement la sortie à 5 000 URLs. Google ne pouvait donc voir que 1,9 % du corpus. Au-dessus de 50 000 URLs par fichier, le standard XML Sitemaps est cassé de toute façon.- Nouvelle architecture produite automatiquement par Next :
```
/sitemap.xml → index (<sitemapindex>, liste les segments)
/sitemap/0.xml → pages statiques + listings metier×ville + prestations
/sitemap/1.xml → artisans 0–44999
/sitemap/2.xml → artisans 45000–89999
... (6 segments pour 257k)
```
- Paramètres :
SEGMENT_SIZE = 45000(marge sous la limite XML 50k), pagination interne par chunksPG_CHUNK = 1000(limite PostgREST Supabase),revalidate = 86400(24h — inutile de régénérer 6+ sitemaps à chaque requête Googlebot). - Ordre stable
.order('slug')pour que les segments ne se chevauchent pas entre deux régénérations.
Sécurité (audit v1.17.1)
- ✅ Pas de nouvelle table ni colonne ; lecture publique via RLS
public_read_activedéjà en place. - ✅ Utilisation du client admin (service_role) justifiée — la page est publique, on contourne la RLS pour servir les fiches
non_revendiqueede façon cohérente avec l'existant. - ✅ Aucun secret nouveau, aucune dépendance ajoutée.
Note stratégique
Cette correction débloque la stratégie SEO v1.17.0 : sans sitemap segmenté, les slugs SIREN bien formés ne servaient qu'à 2 % des artisans. Désormais 100 % du corpus est crawlable.
[1.17.0] — 2026-04-23
Ajouté — Slugs SEO format annuaire-entreprises / societe.com
feat(seo)Migration 022 — les slugs des fiches artisan passent du format historique<nom>-<4-derniers-SIRET>(ex:roman-bat-5001) au format standard du Registre National des Entreprises :<nom>-<SIREN>(ex:roman-bat-900616475).- Format aligné sur annuaire-entreprises.data.gouv.fr, societe.com, pappers.fr → Google reconnaît désormais l'identifiant SIREN comme signal fort et peut classer Batimio comme source alternative à ces annuaires publics.
- Gestion des collisions multi-établissement : si plusieurs SIRET partagent un même SIREN, on suffixe avec le NIC (5 derniers chars du SIRET) →
roman-bat-900616475-00012. feat(seo)Fonction Postgrespublic.slug_seo(raison_sociale, siren): exposée pour réutilisation (enrichissement, triggers futurs).feat(seo)Rétro-compatibilité 301 : tous les anciens slugs sont archivés dansartisans.slug_legacy. La page/artisan/[slug]fait unpermanentRedirect301 vers le slug canonique si l'URL correspond à un ancien format → préservation du jus SEO de toute URL déjà indexée par Google.
Modifié
scripts/sirene-import-idf-full.ts— nouveau format appliqué à chaque nouvelle fiche importée depuis la base Sirène INSEE V3.lib/supabase/types.ts— ajout deslug_legacyet de la fonctionslug_seodans les types générés.
Note stratégique — Objectif "battre annuaire-entreprises sur Google"
Ce changement de slug est un prérequis nécessaire mais pas suffisant pour dépasser annuaire-entreprises.data.gouv.fr sur une requête "<nom de l'artisan>". Pour y arriver il faudra encore :
1. Sprint 12 (v1.2) — enrichissement INPI / RGE / BODACC pour densifier le contenu de chaque fiche (dirigeants, certifs, historique) au-dessus du niveau de data.gouv.fr.
2. Backlinks — acquisition de liens entrants (presse artisanat, partenaires CAPEB/FFB, blog Batimio).
3. Soumission Google Search Console du nouveau sitemap après migration pour accélérer le re-crawl.
4. Core Web Vitals — LCP / CLS / INP optimisés (Sprint 11 SEO programmatique).
Sécurité (audit v1.17.0)
- ✅ RLS — Pas de nouvelle table créée. La colonne
slug_legacyhérite automatiquement des policies existantes surartisans(lecture publique conditionnée àstatut IN ('non_revendiquee', 'actif')viapublic_read_active). - ✅ Secrets — Aucune nouvelle clé ni variable d'environnement.
- ✅ Routes API — Aucune route ajoutée ; la page
/artisan/[slug]reste publique par conception. - ✅ Injection — Le fallback
slug_legacyutilise.eq('slug_legacy', slug)du client Supabase (paramètres échappés). - ✅ Dépendances — Aucune nouvelle dépendance ;
permanentRedirectimporté denext/navigation(déjà présent).
[1.16.5] — 2026-04-23
Ajouté — Profil client enrichi
feat(ui)components/client/PhaseProgressBar.tsx— barre de progression compacte des 6 phases du workflow Sprint 18 (version liste, complémentaire deWizard6Etapespleine taille). Gère les états hors-pipeline (annulée avec hachures, en-sommeil avec badge ⏸)feat(ui)/client/profilaffiche désormais pour chaque demande une barre de progression visuelle des 6 phases (cadrage → solde) à la place du simple badge de phasefeat(ui)Titre de demande éditable en ligne via le composantEditableDemandeTitre(bouton ✏️ → champ texte → ✓/✕). Mise à jour immédiate aprèsrouter.refresh(). Désactivé si la demande est annuléefeat(api)PATCH /api/demande/[id]/titre— nouvelle route. Auth + ownership (client_id === auth.uid()) + validation Zod (titre trimé, 3-200 chars)
Règle d'affichage des demandes annulées (Sprint 18 complément)
feat(ui)Dans/client/profil:- Demande annulée sans artisan contacté → cachée immédiatement de la liste (le client n'a embêté personne, pas besoin de traçabilité)
- Demande annulée avec au moins un artisan contacté → visible 30 jours après
annulee_at, puis cachée (transparence pour les artisans consultés, purge automatique après délai raisonnable) - Indicateur en bas de liste : « X demande(s) annulée(s) masquée(s) »
Sécurité (audit v1.16.5)
- ✅ Nouvelle route
/api/demande/[id]/titre: auth vérifiée, ownership contrôlé explicitement, input validé par Zod (pas d'injection possible — React échappe le rendu) - ✅ Pas de nouvelle table ni colonne → RLS déjà actif sur
demandes - ✅ Aucune clé/secret nouveau, aucune dépendance ajoutée
[1.16.4] — 2026-04-23
Corrigé — Cohérence visuelle + wizard étape 2
fix(ui)/client/demande/artisans— refonte visuelle pour matcher la page diagnostic (style devis) :<article>bordé + shadow, en-tête enbg-gray-50/60avec numéro de consultation + date, section « Intervention » en 2 colonnes (libellé + adresse), encart bleu multi-consultation, liste des artisans en rows séparés pardivide-y(plus de cartes imbriquées)fix(ui)Wizard forcé sur phaseconsultationdès qu'on ouvre/client/demande/artisans. L'étape 2/6 s'active visuellement immédiatement (l'utilisateur sait où il en est) au lieu de rester sur « cadrage » tant qu'aucune mission n'est créée en DBfeat(ui)ArtisanCardaccepte désormais un propvariant: 'card' | 'row'. En moderow, la carte n'a plus de bordure/ombre et s'affiche comme une ligne de liste — évite définitivement le problème de cartes imbriquées quand utilisé dans un<article>déjà bordé
Sécurité (audit v1.16.4)
- ✅ Modifications purement UI, aucune nouvelle table / route API / secret client / dépendance
[1.16.3] — 2026-04-23
Corrigé
fix(ui)Cartes artisans imbriquées sur/client/demande/artisans(régression v1.15.5) —ArtisanCardavait son propre wrapperrounded-xl border+ un bouton « Contacter » stub, et je l'avais enveloppé dans un second wrapper bordé + un vraiContacterArtisanButton. Résultat : double bordure + deux boutons. Fix :ArtisanCardaccepte désormais un propaction: ReactNode(slot) et est le seul niveau de carte. Le bouton stub interne est supprimé
Ajouté — Gamification du seuil de confiance IA
feat(ia)Seuil de confiance abaissé de 90 → 80 (lib/phases.ts+ migration Postgres 021fn_calculer_phase_demande). L'ancien seuil 90 était trop strict pour un particulier qui ne connaît pas toutes les dimensions exactes. Constante exportéeSEUIL_CONFIANCE_CONSULTATION = 80feat(ui)Nouveau composantcomponents/client/ScoreConfianceProgress.tsx— affiche sur la page diagnostic une barre de progression vers le seuil 80 + message gamifié selon l'état :< 80: « Encore X points pour débloquer l'étape 2 » + rappel que répondre aux questions fait monter le score≥ 80sans artisan contacté : « ✓ Score atteint ! Tu peux contacter des artisans » + CTA fort vers/client/demande/artisans≥ 80avec mission active : « 🎉 Étape 2 débloquée »- Rend visible à l'utilisateur la règle qui jusqu'ici était implicite et bloquante (bug UX : on atteignait 100/100 sans comprendre pourquoi on restait visuellement « bloqué au cadrage »)
Sécurité (audit v1.16.3)
- ✅ Pas de nouvelle table — migration 021 modifie uniquement une fonction Postgres existante (RLS non applicable)
- ✅ Aucun nouveau secret côté client
- ✅ Aucune nouvelle route API exposée
- ✅
npm audit: inchangé - ✅ Middleware et auth flow inchangés
⚠️ Action manuelle
Appliquer db/migrations/021_confiance_threshold_80.sql via Supabase SQL Editor (courte, idempotente, pas de colonne modifiée — juste la fonction fn_calculer_phase_demande).
[1.16.2] — 2026-04-22
🔐 Sécurité — patch RLS pour 5 tables manquées ou non appliquées
Après application de la migration 019, la vérification SELECT tablename FROM pg_tables WHERE schemaname='public' AND rowsecurity=false a révélé 5 tables encore ouvertes :
notifications,mission_etapes,devis_ecarts— étaient dans la 019 mais l'ALTER TABLE ENABLE RLSn'a pas pris (exécution partielle probable côté SQL Editor)codes_naf— catalogue NAF du seed Sirène, ratée dans l'audit initial_migrations_applied— table interne créée par le scriptmigrate-cloud.ts
#### Migration 020
fix(security)db/migrations/020_security_rls_patch.sql— ré-applique défensivementENABLE ROW LEVEL SECURITY+ policies pour les 5 tables manquantes. UtiliseDROP POLICY IF EXISTS+CREATE POLICYpour être idempotentenotifications:destinataire = auth.uid()(propre utilisateur)mission_etapes: participants de la mission en lecture, artisan en écrituredevis_ecarts: artisan voit ses propres écartscodes_naf: catalogue public SELECT (true)_migrations_applied: admin-only (aucune policy, service_role uniquement)
Action manuelle : appliquer 020_security_rls_patch.sql via Supabase SQL Editor, puis re-vérifier :
```sql
SELECT tablename FROM pg_tables
WHERE schemaname = 'public' AND rowsecurity = false;
```
Résultat attendu : 0 ligne.
[1.16.1] — 2026-04-22
🔐 Sécurité — activation RLS sur toutes les tables publiques
Contexte : alerte Supabase du 2026-04-22 « Table publicly accessible — Anyone with your project URL can read, edit, and delete all data because Row-Level Security is not enabled ». Audit interne → 16 tables sur 22 étaient exposées en lecture/écriture via anon (l'URL du projet + la clé anonyme suffisait). Seules artisans, demandes, missions, messages, demande_messages, parrainages avaient RLS actif.
#### Migration 019
fix(security)db/migrations/019_security_rls_all_public_tables.sql— active RLS sur les 16 tables restantes et applique des politiques minimalistes selon 4 régimes d'accès :
A. Admin-only (aucune policy → seul service_role accède)
sirene_imports,stripe_events,enrichissement_jobs,analytics_events
B. Catalogues publics (SELECT public, écriture service_role)
seo_pages,prestations_types,normes,materiaux_catalogue,tarifs_reference
C. Enrichissement profil artisan (SELECT public pour fiche /artisan/[slug], écriture service_role)
artisan_dirigeants,artisan_certifications,artisan_annonces_legales
D. Propre à l'utilisateur (filtrage auth.uid())
disponibilites— artisan voit/modifie ses créneaux, public voit les créneauxtype='disponible'pour réservationnotifications—destinataire = auth.uid()mission_etapes— client OU artisan de la mission, écriture artisan seulementdevis_ecarts— artisan voit ses propres écarts estimé/réel
#### Impact applicatif
- Aucun changement code : toutes les routes API utilisent déjà
createAdminClient()(service_role) qui bypass RLS. Les Server Components qui lisent viacreateClient()(cookies auth) sont couverts par les policiesauth.uid() - Les pages publiques SEO continuent d'afficher normes, matériaux, certifs artisan (policies B+C)
- Vérification post-migration : exécuter dans SQL Editor
```sql
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE schemaname = 'public' AND rowsecurity = false;
```
Doit renvoyer 0 ligne.
Ajouté — Consigne projet « Audit sécurité à chaque évolution »
Mémoire système enregistrée : à chaque bump de version ou nouvelle feature, exécuter une checklist sécurité (RLS, secrets côté client, Zod sur routes API, npm audit, webhooks signés, middleware, dépendances) avant le commit. Toute vulnérabilité identifiée doit être corrigée dans le même commit — jamais différée.
⚠️ Action manuelle
Appliquer la migration 019 via Supabase SQL Editor (comme pour la 018) ou npm run migrate:cloud si SUPABASE_DB_URL est configurée.
[1.16.0] — 2026-04-22
Ajouté — Split paiement Stripe acompte 30 % / solde 70 %
Complète la finalisation du Sprint 18 en remplaçant le paiement unique par
un paiement en deux temps, conforme aux pratiques du BTP :
- Acompte 30 % capturé immédiatement à la rétention de l'artisan par le client
- Solde 70 % en capture manual (escrow Stripe), libéré à la clôture du chantier
#### Base de données
feat(db)db/migrations/018_sprint18_stripe_split.sql— nouvelles colonnes surmissions :acompte_payment_intent_id,acompte_montant_ttc,acompte_paiement_statut,acompte_paye_atsolde_payment_intent_id,solde_montant_ttc,solde_paiement_statut,solde_paye_at,solde_libere_at- CHECK contraintes sur les statuts (
en_attente,autorise,paye,libere,rembourse,echec) - Index sur
acompte_paiement_statutetsolde_paiement_statut - Migration idempotente (
IF NOT EXISTS)
#### Librairie Stripe
feat(stripe)lib/stripe/escrow.tsrefondue :creerAcompte(montantTotal, artisanAccountId, missionId, pourcentage=0.3)→capture_method: 'automatic'creerSolde(montantTotal, artisanAccountId, missionId, pourcentage=0.7)→capture_method: 'manual'(escrow)libererSolde(paymentIntentId, montant)→paymentIntents.capture()- Constante exportée
POURCENTAGE_ACOMPTE = 0.3 - Compat descendante :
creerEscrow/libererEscrowconservés (deprecated) pour les missions pré-v1.16
#### Flux applicatif
feat(api)POST /api/demande/[id]/choisir— à la rétention d'un artisan, crée les deux PaymentIntents :- Détermination du montant total (devis artisan final > tarif_total_ttc > fourchette_max demande)
- Acompte PI + Solde PI (mode mock si Stripe non configuré pour dev local)
stripe_payment_intent_idpointe désormais sur le PI solde (compat descendante car c'est le PI libéré à la clôture)feat(api)POST /api/paiement/[id]/valider—libererSolderemplacelibererEscrow. Litsolde_payment_intent_iden priorité, fallback legacy surstripe_payment_intent_id. MAJsolde_paiement_statut='libere'+ dates
#### Interface client
feat(ui)Nouveau composantcomponents/client/PaiementSplit.tsx— affiche les deux lignes (acompte / solde) avec montant, statut coloré (en attente / autorisé / payé / libéré), dates de paiement / libération, total TTCfeat(ui)/client/demande/[id](hub) — encart<PaiementSplit>sous l'artisan retenu dès que les PI sont créésfeat(ui)/client/mission/[id]— même encart au-dessus des actions de mission
#### Tests
test(unit)tests/unit/escrow-split.test.ts— 7 tests : pourcentages par défaut, calculs acompte/solde, tolérance d'arrondi, pourcentage custom, commission 4 % sur libération- Total : 197 tests verts (vs 190 en v1.15.5)
⚠️ Action manuelle requise après déploiement
La migration 018 doit être appliquée sur Supabase Cloud. Deux options :
Option A — Script (si SUPABASE_DB_URL est configurée dans .env.local)
```bash
npm run migrate:cloud
```
Option B — Dashboard (recommandé si pas de SUPABASE_DB_URL)
1. Supabase Cloud → projet Batimio → SQL Editor
2. Coller le contenu de db/migrations/018_sprint18_stripe_split.sql
3. Exécuter
Tant que la migration n'est pas appliquée, POST /api/demande/[id]/choisir rencontrera une erreur column "acompte_..." does not exist. Les missions créées avant v1.16 continuent à fonctionner sans interruption grâce à la compat descendante.
[1.15.5] — 2026-04-22
Ajouté — Sprint 18 (partie finale) : multi-consultation + pré-visite artisan + choix client
Finalise le Sprint 18. Le cycle complet de la demande est désormais
opérationnel : plusieurs artisans peuvent être consultés en parallèle, chacun
propose une pré-visite gratuite, ajuste le devis IA après visite, puis le
client compare et retient celui qu'il souhaite. Les artisans non retenus
sont notifiés.
#### API consultation multi-artisan
feat(api)POST /api/consultation— lance une consultation{demande_id, artisan_id}. Crée une mission enstatut='devis_envoye',statut_consultation='en_cours', sans créneau. Copiediagnostic_detaildansdevis_ia_initial(référence intangible pour comparaison future). Passe la demande en phaseconsultation. Notifie l'artisan par email (nouvelle_demande)feat(client)/client/demande/artisans— ancien lien « Réserver » remplacé par bouton Contacter (multi-artisan possible). Marquage « Déjà contacté » sur les artisans déjà sollicités. Nouveau lien vers le hub/client/demande/[id]
#### Workflow pré-visite artisan
feat(api)POST /api/mission/[id]/visite/proposer— l'artisan propose 1 à 3 créneaux.statut_visite='proposee', email clientfeat(api)POST /api/mission/[id]/visite/confirmer— le client confirme un des créneaux proposés.statut_visite='confirmee', email artisan avec date + adressefeat(api)POST /api/mission/[id]/visite/realiser— l'artisan clôture la visite, soumet notes + devis ajusté (devis_artisan_finalavecmontant_total_ttc,motif_modification).statut_visite='realisee',statut_consultation='devis_envoye'feat(pro)/pro/demandes/[id]— composantVisiteActionsaffiché quand l'artisan a une mission de consultation active. Formulaire datetime-local pour créneaux + éditeur devis final avec ancrage sur durées/matériaux IA
#### Choix client
feat(api)POST /api/demande/[id]/choisir— le client retient un artisan (mission_id). La mission passestatut_consultation='retenu'+statut='confirme'. Les autres missions de la demande passent ennon_retenu+non_retenu_notif_at. Demande → phasevalidation. Email automatique envoyé aux artisans non retenus (templateconsultation_non_retenue)feat(client)Nouveau composantRetenirArtisanButtonavec confirmation modale
#### Page hub unifiée
feat(client)Nouvelle page/client/demande/[id]— hub unifié de la demande. Liste toutes les consultations avec :- Nom artisan, ville, note moyenne, nombre d'avis
- Badge IA (initial / modifié)
- Statut de visite coloré (en attente / proposée / confirmée / réalisée)
- Créneaux à confirmer (composant
ChoisirCreneauVisite) si l'artisan a proposé - Prix du devis (IA initial ou artisan final, labellisé)
- Motif d'ajustement de l'artisan
- Bouton « Retenir cet artisan » dès qu'un devis final est soumis
- Signalisation visuelle « Retenu » / « Non retenu » une fois le choix effectué
#### Templates email
feat(email)3 nouveaux templates Resend :visite_proposee,visite_confirmee,consultation_non_retenue— branding Batimio, CTA vers le hub, ton professionnel
#### Reporté à v1.16
- Split Stripe acompte (30 %) / solde (70 %) sur
mission_choisie— le paiement escrow actuel reste en place (paiement unique, libération à la clôture). La scission nécessite une refonte delib/stripe/escrow.tset fera l'objet d'un sprint dédié
[1.15.4] — 2026-04-22
Corrigé — Dialogue IA sans répétition + estimatif stable
Sur la demande d926e62d-… (clôture 18 m parpaing, maçon), l'IA posait des
questions déjà répondues et le devis variait de plusieurs milliers d'euros
entre deux tours. Cause racine : app/api/demande/[id]/preciser n'envoyait
à Claude que le dernier lot de réponses, pas l'historique complet, et
recalculait l'estimation from scratch à chaque tour.
fix(ia)app/api/demande/[id]/preciser/route.ts— Nouvel historiquediagnostic_detail.qa_historyappend-only (liste[{q, r}]), migré automatiquement depuis le legacydemandes.mesures. Indexation par question normalisée (lowercase + unaccent + ponctuation dépouillée) pour dédoublonner même si l'IA reformulefix(ia)Le prompt envoyé à Claude contient désormais l'intégralité des Q&R passées dans un bloc clairement délimité, avec instruction stricte « ne jamais re-poser une question déjà présente »fix(ia)Ancrage du devis : durée, matériaux et nombre d'actions du round précédent sont injectés comme référence. Instruction Claude : rester dans ±15 % sauf spécificité lourde justifiant un changementfix(ia)Filtre post-traitement : si Claude renvoie malgré tout une question doublon, elle est silencieusement retirée avant stockage
Ajouté — Magic link 100 % batimio.com (plus de domaine supabase.co)
feat(auth)Nouvelle routeapp/api/auth/magic-link/route.ts— génère le token viasupabase.auth.admin.generateLinkpuis construit elle-même l'URL surhttps://batimio.com/auth/confirm?token_hash=…. Envoi de l'email via Resend avec template Batimio (magic_link). Création automatique de l'utilisateur s'il n'existe pas (email_confirm: true→ pas de double email)feat(auth)app/auth/login/page.tsx— bascule vers la nouvelle route. Affiche désormais l'erreur?error=…renvoyée par le callback (invalid_code, invalid_token…) de façon lisiblefeat(auth)Nouveau template emailmagic_linkavec CTA Batimio, fallback texte du lien, avertissement anti-phishing- Bénéfices : URL 100 % batimio.com dans la boîte mail, plus de PKCE (marche cross-device : ouvrir l'email sur mobile puis cliquer marche même si la demande a été faite depuis desktop), branding Batimio complet
Dashboard Supabase — action complémentaire (optionnelle)
Cette livraison ne nécessite aucun changement de template côté Supabase Dashboard (on n'utilise plus les templates Supabase). Côté dashboard, s'assurer uniquement que :
- Authentication → URL Configuration → Site URL =
https://batimio.com - Authentication → URL Configuration → Redirect URLs contient
https://batimio.com/auth/confirm
[1.15.3] — 2026-04-22
Ajouté — Sprint 18 (partie 2) : wizard omniprésent + devis préliminaire + profil enrichi
Complète la partie 1 (Sprint 18.0 de v1.15.0). Rend le cycle de vie de la
demande lisible à chaque étape côté client et formalise le cadrage sous
forme de devis préliminaire (étape 1), conformément à la promesse
« l'artisan fixe le devis final après visite ».
#### Wizard des 6 phases visible partout
feat(ui)components/shared/WizardDemande.tsx— Wrapper serveur qui résout la phase d'une demande (viademande_idoumission_id) et rend<Wizard6Etapes>. Accepte aussi unephaseForceequand la demande n'existe pas encorefeat(ui)/client/demande— Wizard figé sur « Cadrage » avant la soumission initialefeat(ui)/client/demande/artisans— Wizard rendu depuisdemande_idfeat(ui)/client/demande/reservation— Wizard rendu depuisdemande_idfeat(ui)/client/demande/confirmation— Wizard rendu depuismission_id(résolution mission → demande)feat(ui)/client/mission/[id]— Wizard rendu depuis la mission courante
#### Page diagnostic → format devis préliminaire (étape 1)
feat(ui)Refonte complète de/client/demande/diagnosticsous forme de devis à entête structurée : numéro de devis (8 premiers chars de l'ID), date d'émission, bloc Émetteur (Batimio IA) / Chantier (intervention + adresse), description du besoin, tableau des interventions (#, description, durée), liste des matériaux, totaux HT / TVA 10 % / TTC, mentions légales (valeur non contractuelle + référence CGI 279-0 bis)feat(ui)Écran d'erreur dédié sidiagnostic_detail.erreurest présent (rend l'ID de la demande exploitable sans exposer de stack trace)- La section « Questions complémentaires IA » reste affichée en dessous du devis
#### Page profil enrichie
feat(ui)/client/profilliste désormais toutes les demandes du client connecté (100 dernières, triées antéchronologique), avec icône de phase, label de phase courante, corps de métier, fourchette tarifaire et lien direct vers le devisfeat(ui)Informations personnelles : email + date d'inscription + ID utilisateurfeat(ui)Lien complémentaire vers/client/historique(missions confirmées)
#### Tests
test(unit)tests/unit/claude-config.test.ts— assertion maxTokens mise à jour (1500 → 4000, cf. v1.15.2)- Total : 190/190 tests passent
[1.15.2] — 2026-04-22
Corrigé
fix(ia)CLAUDE_CONFIG.maxTokenspasse de 1500 → 4000. Cause racine identifiée sur la demandea55e0381-…(clôture 18 m parpaing + portails + muret, maçon) : la réponse JSON était tronquée par la limite de tokens, provoquant « La réponse IA n'est pas au format JSON attendu » (cf.diagnostic_detail.erreuren base). Un devis maçonnerie complexe dépasse régulièrement 1500 tokens à cause du volume d'actions, matériaux et questions dimensionnellesfix(api)/api/agent— extraction JSON plus tolérante : après retrait des blocs markdown, on isole la sous-chaîne entre le premier{et le dernier}pour neutraliser un éventuel préambule/postambule ajouté par le LLM malgré l'instruction « sans preamble »fix(api)Log d'erreur enrichi : longueur de la réponse +stop_reasonClaude + extrait tronqué (500 chars début / 500 fin) pour diagnostiquer rapidement les cas atypiques. Lestop_reasonest aussi renvoyé dans le message d'erreur côté client
[1.15.1] — 2026-04-22
Ajouté
feat(client)Page/client/profilaffiche désormais l'email, la date d'inscription et l'identifiant de l'utilisateur connecté (au lieu du placeholder « À implémenter »)feat(api)/api/agent: la lignedemandesest insérée dès le début du traitement pour que l'ID soit disponible même si l'analyse échoue. L'erreur est tracée dansdiagnostic_detail
Corrigé
fix(client)L'URL/client/demande?id=<uuid>contient désormais l'ID de la demande dès la soumission (UUID généré côté client viacrypto.randomUUID, propagé à l'API pour servir de clé d'insertion)fix(api)En cas d'échec d'analyse, la réponse inclut ledemande_idet (en dev) le message d'erreur sous-jacent, ce qui permet de diagnostiquer un cas comme « Clôture 18 m en parpaing / Maçon » sans perdre la tracefix(ui)Le bandeau d'erreur de la page « Nouvelle demande » affiche l'ID de la demande pour faciliter le debug côté support
[1.15.0] — 2026-04-21
Ajouté — Sprint 18 (partie 1) : workflow en 6 phases visible + gestion demande client
Première livraison du Sprint 18. Matérialise visuellement le cycle de vie
d'une demande (6 phases officielles) et donne au client les commandes de
gestion de base (annuler, mettre en sommeil, réveiller).
Les 6 phases du pipeline :
1. Cadrage — L'IA pose des questions jusqu'à atteindre 90 % de confiance
2. Consultation — Plusieurs artisans proposent une pré-visite et ajustent le devis IA
3. Validation — Le client choisit un devis, fixe le créneau, verse l'acompte
4. Travaux — L'artisan retenu réalise les travaux
5. Fin de chantier — Travaux clôturés, facture émise
6. Solde payé — Solde libéré vers l'artisan
Hors pipeline : annulee, en_sommeil.
#### Base de données (migration 017)
feat(db)Colonnesdemandes.phase,phase_updated_at,mission_choisie_id,en_sommeil_since,en_sommeil_motif,annulee_at,annulee_motiffeat(db)Colonnesmissionspour workflow visite + devis IA/modifié :
statut_visite, visite_creneaux_proposes, visite_debut_at, visite_fin_at, visite_notes, devis_ia_initial, devis_ia_initial_at, devis_artisan_final, devis_artisan_final_at, devis_modification_motif, statut_consultation, non_retenu_notif_at
feat(db)Fonction Postgresfn_calculer_phase_demande(uuid): détermine la phase courante depuis les données- Migration appliquée sur Supabase Cloud + local
#### Composants et UI
feat(ui)components/shared/Wizard6Etapes.tsx— Wizard horizontal responsive, affiche les 6 étapes avec état (done / current / pending / hors_pipeline), lignes de liaison qui se colorent à mesure que l'on progressefeat(ui)components/shared/BadgeIA.tsx— Badge explicite qui signale clairement au client qui a produit le devis : « Devis généré par l'IA », « Validé par l'artisan », « Ajusté par l'artisan »feat(ui)components/client/GererDemande.tsx— Panneau d'actions client : mettre en sommeil, réveiller, annuler. Avec confirmations natives et capture de motif optionnelfeat(lib)lib/phases.ts— Source unique de vérité : types, ordres, labels FR, couleurs,calculerPhase()miroir de la fonction Postgres
#### Endpoints
feat(api)POST /api/demande/:id/annuler— Marque la demande annulée, retire les consultations en cours, valide ownership via Supabase Authfeat(api)POST /api/demande/:id/sommeil— Avecaction: 'pause' | 'reveil'. Le réveil restaure la phase précédente en analysant les données (cadrage / consultation / validation)
#### Intégration
feat/client/demande/diagnosticaffiche désormais en tête le wizard 6 étapes + le badge IA, et propose en bas le panneau de gestion de la demandefeatSi la demande est annulée ou en sommeil, les boutons « Voir les artisans » sont masqués (évite actions incohérentes)- Backward-compatible : si
demande.phasen'est pas encore renseigné (anciennes demandes),calculerPhase()la déduit des données
À venir Sprint 18 partie 2 (v1.15.1+)
- Multi-artisans consultés avec tableau comparatif des devis côté client
- Workflow pré-visite artisan (proposer créneau, confirmer, visiter, corriger le devis)
- Choix client d'un devis + notification des artisans non retenus
- Split acompte / solde via Stripe Connect
- Page
/client/demande/[id]unifiée (hub de la demande)
[1.14.3] — 2026-04-21
Ajouté — Timeline chronologique illustrée des interventions
feat(diagnostic)SchémaActionArtisanstructuré remplace le tableau de strings plat. Champs :ordre,etape,duree_min,icone(emoji),descriptionoptionnellefeat(prompt)Le prompt Claude oblige désormais des étapes strictement chronologiques avec durées réalistes et icône emoji (🧱 🔧 ⚡ 🎨 🏗️ 🪜 📏 …)feat(ui)/client/demande/diagnosticet/pro/demandes/[id]affichent une timeline visuelle :- Badge « Étape N »
- Icône dans un cercle à gauche
- Durée formatée en badge à droite (
30 min/1 h 30/2 j 4 h) - Description en ligne secondaire
compatBackward-compatible vianormaliserActions()— les anciens diagnostics enstring[]s'affichent en timeline avecduree_min: 0feat(validation)Zod schema dans/api/agentaccepte l'union des 2 formats
Corrigé — Ceinture de sécurité sur RepondreQuestions
fix(ui)Ajout d'unekeyprop calculée sur le contenu des questions dans la page parent → force le remount complet du composant quand les questions changent, en complément duuseEffectde v1.14.2. Évite les cas edge où le state pourrait rester bloqué.
[1.14.2] — 2026-04-20
Corrigé — Formulaire de précisions client qui se bloquait après la 1re réponse
fix(ui)RepondreQuestions.tsx : le state local (reponses,enCours,erreur) n'était pas réinitialisé quand le serveur retournait de nouvelles questions viarouter.refresh(). Conséquences :- Les anciennes réponses s'affichaient dans les nouveaux textarea (placeholder incorrect)
- Les champs restaient désactivés (
disabled={enCours}à true) - Le bouton « Mise à jour du diagnostic » restait grisé
- Solution :
useEffectqui observe une signature stable du tableauquestions(join('\u0001')) et remet à zéro le state à chaque changement de contenu.
Corrigé — La confiance IA ne remontait pas après réponses
fix(preciser)Le prompt disait « révise à la hausse » mais Claude pouvait renvoyer une confiance stagnante ou en baisse quand il identifiait encore des ambiguïtés. Double correctif :
1. Prompt renforcé : instruction stricte « confiance DOIT augmenter de MINIMUM 10 points » + contexte envoyé (confiance précédente + nombre de réponses fournies)
2. Plancher post-traitement : si Claude renvoie confiance < confiance_precedente + 10, on clampe à ce plancher (plafonné à 100). Garantit la monotonie.
[1.14.1] — 2026-04-20
Ajouté — Le client peut répondre directement aux questions IA
feat(client)ComposantRepondreQuestions.tsx : formulaire inline avec une zone de texte par question posée par l'IAfeat(api)RoutePOST /api/demande/:id/preciser :- Reçoit les réponses textuelles du client
- Fusionne dans
demandes.mesures(clésq1,q2, …) en préservant les mesures existantes - Relance Claude avec contexte enrichi (question → réponse en paires Q/R)
- Met à jour
diagnostic_detail,diagnostic_label,confiance_ia, durées, matériaux, fourchettes tarifaires - Recalcule la fourchette DVF avec les durées révisées
feat(ui)Page/client/demande/diagnosticintégrée avec le formulaire- Rafraîchit automatiquement (
router.refresh()) après envoi pour afficher le nouveau diagnostic - Bouton « Affiner le diagnostic avec mes réponses »
- Si l'IA estime qu'il reste des questions non couvertes, elle peut en poser de nouvelles → cycle itératif
Sécurité
- Vérification
demande.client_id === auth.uid()quand un user est connecté : un tiers ne peut pas préciser la demande d'un autre
[1.14.0] — 2026-04-20
Ajouté — Sprint 17 : Devis adaptatif (questions ou devis, 2 formats)
feat(devis-adaptatif)Pipeline unifié qui évalue si la description est suffisante pour chiffrer, puis renvoie soit des questions ciblées soit directement le devislib/claude/devis-adaptatif.ts: prompt Claude + fonctionevaluerEtGenererDevis()- Deux statuts de sortie :
questions(3–8 questions typées) oupret(DevisNormeResult) - Questions catégorisées (
dimensions,materiaux,acces,support,finitions) et priorisées (bloquant/important/nice_to_have) - Types d'input :
mesure(avec unité),choix,oui_non,text,photo feat(api)RoutePOST /api/devis/adaptatif- Fusionne les réponses dans
demandes.mesures(persistance multi-passe) - Persiste le devis dans
missions.devis_normédès questatus=pret - Peut être appelée N fois jusqu'à convergence
feat(pdf)lib/pdf/devis-html.tsdeux modes de rendu HTML A4simplified(envoi client) : langage clair, pas de références matériau, durées en heures, DTU en codes seulement, signatures prestataire/clientdetailed(usage interne pro) : toutes colonnes (référence, niveau MO, tarif horaire, durée minutes), DTU avec extraits article par article- Commutation visible dans la toolbar écran entre les deux vues
feat(api)/api/mission/:id/devis?format=simplified|detailedaccepte le paramètre de rendufeat(ui)Composant Reactcomponents/pro/DevisAdaptatif.tsx- Bouton « Évaluer et générer le devis »
- Formulaire dynamique avec inputs typés selon
DevisQuestion.type - Badge « Requis » sur questions bloquantes + désactivation submit tant que bloquantes non remplies
- 4 boutons sortie : client HTML / client PDF / pro HTML / pro PDF
feat(ui)/pro/devis/[id]refondue autour du composant adaptatif,DevisFormlegacy conservé en fallback
Séparation stricte matériaux / main d'œuvre
Les deux formats séparent clairement les deux postes :
- Bloc Fournitures et matériaux (table avec désignation, qté, prix unit., total HT)
- Bloc Main d'œuvre (étapes ordonnées avec durée et coût)
- Bloc Totaux ventilés : Fournitures HT + MO HT + TVA + TTC
Basé sur les DTU
- Chaque devis cite exclusivement les DTU stockés en DB (
normestable, 103 entrées) - Validation côté serveur : les références matériaux doivent exister dans
materiaux_catalogue, sinon elles sont filtrées avant persistance - Tarifs horaires tirés de
tarifs_reference(IDCC 1596 + FFB IDF)
Roadmap CLAUDE.md
- Section 19 « ROADMAP V1.1+ » étendue avec Sprint 17 détaillé
- Timeline synthétique mise à jour jusqu'à v1.14
[1.13.2] — 2026-04-20
Corrigé
fix(agent)Enumcorps_metierétendu aux 9 métiers du catalogue (plombier, électricien, menuisier, peintre, maçon, plaquiste, carreleur, charpentier, couvreur) + normalisation tolérante (accents, pluriels, synonymes comme "maconnerie"/"toiture"). Avant, une demande maçon renvoyait "Données invalides" car Claude répondait "maçon" avec cédille mais le schéma Zod attendait "macon" sans.fix(i18n)CGU + page confidentialité : restauration des accents français (é, è, ê, ç, à) et des espaces insécables avant les:/%/ unités, conformément aux règles typographiques de l'Imprimerie nationale.
[1.13.1] — 2026-04-20
Corrigé
fix(auth)Middleware capture le?code=orphelin sur toute page hors/auth/*et redirige vers/auth/callback(a215872). Défense en profondeur pour le cas où un magic link pointe sursite_urlracine sans passer explicitement par le callback.
[1.13.0] — 2026-04-20
Ajouté — Documents HTML/PDF imprimables (devis + lettre de mission)
feat(docs-pdf)Génération HTML A4 imprimable du devis (a29aa8b)- Route
GET /api/mission/:id/devisavec?print=1auto-trigger dialogue d'impression - Mentions légales complètes : L.111-1 / L.242-5 Code de la consommation + L.221-18 rétractation + L.441-10 pénalités de retard
- Champs signature prestataire + « Bon pour accord » client
- Normes DTU/AFNOR citées en bloc dédié
- Pied de page SAS RHEXO
feat(docs-pdf)Refactor complet du rendu HTML de la lettre de mission- Toolbar écran avec bouton « Imprimer / PDF »
- Date de génération + numéro de version visibles
- Mémo synthétique en callout en haut
- Matériel complet en table avec références fournisseurs
featlib/pdf/branding.ts : source unique pour infos éditeur SAS RHEXOfeatlib/pdf/css-print.ts : CSS commun A4 +@page+@media printfeatUI/pro/devis/[id] : 2 boutons « Prévisualiser » + « Télécharger PDF »featUI/pro/chantier/[id] : 4 boutons (Lettre HTML + PDF, Devis HTML + PDF)- Choix technique : pas de Playwright/Puppeteer → « Print → Save as PDF » natif navigateur, compatible Alpine
[1.12.0] — 2026-04-20
Ajouté — Catalogue DTU complet (95 normes supplémentaires)
feat(dtu)Migration 016 : 95 nouvelles normes DTU/NF ajoutées au catalogue (e361a91)- Couvreur : 19 DTU (40.x complet + 43.x étanchéité)
- Plombier : 18 DTU (60.x, 61.1, 64.1, 65.x, 75.1)
- Menuisier : 14 DTU (33, 34, 35, 36, 37, 39, 51.x, 55)
- Maçon : 14 DTU (13.x, 14.1, 20.x, 21, 23.1, 26.x, 42.1)
- Plaquiste : 11 DTU (25.x, 27.1, 45.x, 58.x)
- Peintre : 10 DTU (42.1, 44.1, 53.x, 59.x)
- Électricien : 8 normes (NF C 15-100, DTU 68.3, 70.1, NF C 14-100, 16-600)
- Charpentier : 7 DTU (31.x, 32.x, 41.2)
- Carreleur : 2 DTU (52.1, 52.2)
- Total : 103 normes en DB (8 Sprint 15 + 95 Sprint 19)
feat(dtu)Lettre de mission branchée sur la tablenormes- Nouvelle fonction
chargerNormesContexte()qui lit les DTU applicables au corps de métier - Prompt Claude instruit strictement de ne citer QUE les DTU fournis (pas d'invention)
- Format standardisé « DTU XX.Y — Titre (§article) »
[1.11.0] — 2026-04-19
Ajouté — Script de migration Supabase Cloud
feat(migrate)scripts/migrate-cloud.tsavec postgres.js (deppostgres ^3.4.5)featTable_migrations_appliedpour idempotencefeatnpm scriptsmigrate:cloudetmigrate:cloud:dryfeatLectureSUPABASE_DB_URLdepuis.env.localfeatGestion des migrations déjà appliquées (pré-marquage manuel possible)
[1.10.0] — 2026-04-19
Ajouté — Mentions légales SAS RHEXO
featCGU section 1 « Éditeur du site » avec SAS RHEXO complet + hébergeursfeatRenumérotation des sections 2-8 CGUfeatConfidentialité : SAS RHEXO comme responsable du traitementfeatConfidentialité section cookies mise à jour (3 catégories avec consentement par catégorie)
Corrigé
fix(ui)Sauts de ligne CGU + confidentialité (classesproseinertes sans plugin typography → remplacées par styles Tailwind explicites via attribute selectors)
[1.9.0] — 2026-04-19
Ajouté — Recherche par ville
featChamp « Ville » remplace « Code postal » sur/recherchefeatParse automatique : accepte « Versailles », « 78000 » ou « Versailles (78000) »featAffichage format « Ville (CP) » partout :/recherche+/[metier]/[ville]compatParamètre URLcp=conservé pour rétro-compatibilité
[1.8.0] — 2026-04-19
Ajouté — Bannière de consentement cookies (RGPD)
feat(consent)components/shared/ConsentBanner.tsx : bannière fixe + bouton flottant de réouverture (1325292)feat(consent)lib/consent.ts : state localStorage + events réactifsfeat(consent)lib/consent-hook.ts : hookuseConsent()viauseSyncExternalStorefeat(consent)Mode « Personnaliser » avec toggles par catégorie (Essentiel / Analytics / Marketing)feat(consent)Refus aussi visible que l'acceptation (conformité CNIL)feat(consent)GoogleAnalytics gated : GA4/GTM/Ads ne chargent QUE si consent donnéfeatGA4 configuré avecanonymize_ip: true+cookie_flags: SameSite=None;Secure
Fichiers clés
components/shared/ConsentBanner.tsxcomponents/shared/GoogleAnalytics.tsxlib/consent.ts+lib/consent-hook.ts
[1.7.0] — 2026-04-19
Ajouté — Intégration Google (GA4 + GTM + Ads + Search Console)
feat(google)components/shared/GoogleAnalytics.tsx(d7895ce)- Scripts conditionnels GA4 + GTM + Google Ads
- Pas de rendu si aucune variable d'env Google renseignée
featMetadata verification Google Search Console + Bing Webmasterfeatlib/analytics/google.ts : helperssendGaEvent,sendAdsConversion,GaEventsfeatÉvénements métier pré-câblés :demandeCreee,rdvReserve,paiementConfirme,abonnementSouscritenvAjout des 5 variables Google dans.env.example :NEXT_PUBLIC_GA_MEASUREMENT_ID,NEXT_PUBLIC_GTM_ID,NEXT_PUBLIC_GOOGLE_ADS_ID,GOOGLE_SITE_VERIFICATION,BING_SITE_VERIFICATION
Infrastructure (non-code)
- Configuration Resend SMTP sur Supabase Auth (délivrabilité magic link)
- Configuration DNS
batimio.com : SPF racine + DMARC relaxed - Site URL Supabase corrigé :
http://localhost:3000→https://batimio.com - Redirect URL allowlist : prod + dev local +
batimio.fr - Migrations 010 à 016 appliquées sur Supabase Cloud
choix techniquePas de Playwright/Puppeteer → "Print → Save as PDF" natif navigateur, compatible Alpine
Ajouté — Catalogue DTU complet (95 normes)
feat(dtu)Migration 016 : 95 nouvelles normes DTU/NF ajoutées au catalogue (e361a91)- Couvreur : 19 DTU (40.x complet + 43.x étanchéité)
- Plombier : 18 DTU (60.x, 61.1, 64.1, 65.x, 75.1)
- Menuisier : 14 DTU (33, 34, 35, 36, 37, 39, 51.x, 55)
- Maçon : 14 DTU (13.x, 14.1, 20.x, 21, 23.1, 26.x, 42.1)
- Plaquiste : 11 DTU (25.x, 27.1, 45.x, 58.x)
- Peintre : 10 DTU (42.1, 44.1, 53.x, 59.x)
- Électricien : 8 normes (NF C 15-100, DTU 68.3, 70.1, NF C 14-100, 16-600)
- Charpentier : 7 DTU (31.x, 32.x, 41.2)
- Carreleur : 2 DTU (52.1, 52.2)
- Total : 103 normes en DB (8 Sprint 15 + 95 cette version)
feat(dtu)Lettre de mission branchée sur la tablenormes- Nouvelle fonction
chargerNormesContexte()qui lit les DTU applicables au corps de métier - Prompt Claude instruit strictement de ne citer QUE les DTU fournis (pas d'invention)
- Format standardisé "DTU XX.Y — Titre (§article)"
Ajouté — Bannière de consentement cookies (RGPD)
feat(v1.6.0)Banner de consentement avec catégories Analytics / Marketing (1325292)lib/consent.ts: state localStorage + events réactifslib/consent-hook.ts: hookuseConsent()viauseSyncExternalStorecomponents/shared/ConsentBanner.tsx: bannière + bouton flottant réouverture- Mode "Personnaliser" avec toggles par catégorie
- Refus aussi visible que l'acceptation (conformité CNIL)
featGoogleAnalytics gated : GA4/GTM/Ads ne chargent QUE si consent donnéfeatGA4anonymize_ip: true+cookie_flags: SameSite=None;Secure
Ajouté — Recherche par ville (Sprint 5 refonte)
featChamp "Ville" remplace "Code postal" sur/recherchefeatParse automatique : accepte "Versailles", "78000" ou "Versailles (78000)"featAffichage format "Ville (CP)" partout :/recherche+/[metier]/[ville]compatParamètre URLcp=conservé pour rétro-compatibilité
Ajouté — Mentions légales SAS RHEXO
featCGU section 1 "Éditeur du site" avec SAS RHEXO complet + hébergeursfeatRenumérotation sections 2-8 CGUfeatConfidentialité : SAS RHEXO comme responsable du traitementfeatConfidentialité section cookies mise à jour (3 catégories)
Ajouté — Script de migration Supabase Cloud
feat(migrate)scripts/migrate-cloud.tsavec postgres.js (deppostgres ^3.4.5)featTable_migrations_appliedpour idempotencefeatnpm scriptsmigrate:cloudetmigrate:cloud:dryfeatLectureSUPABASE_DB_URLdepuis.env.local
Ajouté — Google Analytics / GTM / Ads / Search Console
feat(google)components/shared/GoogleAnalytics.tsx(d7895ce)- Scripts conditionnels GA4 + GTM + Google Ads
- Pas de rendu si aucune variable d'env renseignée
featMetadata verification Google Search Console + Bing Webmasterfeatlib/analytics/google.tshelpers :sendGaEvent,sendAdsConversion,GaEventsfeatÉvénements métier pré-câblés :demandeCreee,rdvReserve,paiementConfirme,abonnementSouscritenvAjout des 5 variables Google dans.env.example
Corrigé
fix(auth)Middleware catch?code=orphelin et redirige vers/auth/callback(a215872)- Cas d'edge : magic link sans
emailRedirectTopointe sursite_urlracine - Défense en profondeur : le middleware préserve le path d'origine comme
redirect= fix(ui)Sauts de ligne CGU + confidentialité (classesproseinertes → styles Tailwind explicites)fix(lint)Échappement apostrophes (react/no-unescaped-entities)fix(lint)Date.now()remplacé parnew Date().getTime()(react-hooks/purity)
Migrations DB (10 à 16 poussées sur Supabase Cloud)
- Migration 010 — SEO programmatique (tables
seo_pages+prestations_types) - Migration 011 — Enrichissement INPI/RGE/BODACC (3 tables + fonction
calc_score_completude) - Migration 012 — Dialogue IA adaptatif (table
demande_messages+ RLS) - Migration 013 — Lettre de mission (colonnes sur
missions+ tablemission_etapes) - Migration 014 — Devis normé DTU (tables
normes,materiaux_catalogue,tarifs_reference,devis_ecarts) - Migration 015 — Analytics (table
analytics_events+ 3 vues agrégées) - Migration 016 — Catalogue DTU complet (95 normes seed)
Infrastructure
- Configuration Resend SMTP sur Supabase Auth (délivrabilité magic link)
- Configuration DNS
batimio.com: SPF racine + DMARC relaxed (délivrabilité email) - Site URL Supabase corrigé :
http://localhost:3000→https://batimio.com - Redirect URL allowlist : prod + dev local +
batimio.fr
[1.6.0] — 2026-04-19
Ajouté — Sprint 16 : Analytics + redirection .fr + changelog
feat(sprint-16)Analytics pages vues, provenance, funnels de conversion (cb4053f)- Table
analytics_events+ 3 vues agrégées (pageviews, sources, funnels) - 4 funnels prédéfinis : demande / revendication / abonnement / recherche
- Tracking serveur (trackServerEvent) et client (sendBeacon + session anonyme localStorage)
- Route
POST /api/analyticsavec extraction UTM + geo headers Vercel - Composant
AnalyticsProvider(page_view auto sur changement de route) - Dashboard
/admin/analytics(KPIs, top pages, sources, funnels) - RGPD-compatible : pas de cookie, session ID anonyme
featRedirection 301batimio.fr→batimio.comvianext.config.ts redirects()featPage publique/changelogconvertitCHANGELOG.mden HTMLfeatBadge version du footer cliquable vers/changelog
Corrigé
fix(lint)Échappement apostrophes (react/no-unescaped-entities) sur 3 fichiersfix(lint)Date.now()remplacé parnew Date().getTime()dans composant asyncfix(lint)Variableartisaninutilisée remplacée parvoid artisanData
[1.5.0] — 2026-04-19
Ajouté — Sprint 15 : Devis DTU/AFNOR + quantitatifs
feat(sprint-15)Référentiel normes DTU/AFNOR (8 normes seed) (8686a29)feat(sprint-15)Catalogue matériaux (15 refs plomberie seed)feat(sprint-15)Barème tarifs horaires IDF (IDCC 1596 + FFB) par métier × niveaufeat(sprint-15)Génération IA devis normé (lib/claude/devis-norme.ts) avec validation refsfeat(sprint-15)RoutePOST /api/devis/norme+ persistancemissions.devis_norméfeat(sprint-15)Tabledevis_ecartspour la boucle d'amélioration estimé vs réel
[1.4.0] — 2026-04-19
Ajouté — Sprint 14 : Lettre de mission artisan
feat(sprint-14)Génération IA lettre synthétique (5 infos clés mobile)feat(sprint-14)Génération IA lettre détaillée (étapes + matériel + DTU)feat(sprint-14)Rendu HTML imprimable A4 avec échappement XSSfeat(sprint-14)RoutePOST /api/mission/[id]/lettreavec versioning + historiquefeat(sprint-14)Tablemission_etapes(check-list chantier cochable)
[1.3.0] — 2026-04-19
Ajouté — Sprint 13 : Dialogue IA adaptatif
feat(sprint-13)Machine à états conversationnelle (5 états + escalade)feat(sprint-13)Questions complémentaires typées (text/choix/mesure/photo/oui_non)feat(sprint-13)Tabledemande_messages+ RLS clientfeat(sprint-13)Seuil confiance 85% + limite 5 tours avant escaladefeat(sprint-13)RoutePOST /api/dialogueavec tracking tokens + coût EURfeat(sprint-13)Composant React clientDialogueIA.tsx
[1.2.0] — 2026-04-19
Ajouté — Sprint 12 : Enrichissement INPI / RGE / BODACC
feat(sprint-12)Client API INPI RNE (auth OAuth + cache token + rate-limit)feat(sprint-12)Client open data RGE (data.gouv.fr) + BODACC annonces légalesfeat(sprint-12)Tablesartisan_dirigeants,artisan_certifications,artisan_annonces_legalesfeat(sprint-12)Fonction SQLcalc_score_completude+ trigger autofeat(sprint-12)Workerscripts/enrich-inpi.ts+ route/api/admin/enrichirfeat(sprint-12)Alerte si procédure collective BODACC
[1.1.0] — 2026-04-19
Ajouté — Sprint 11 : SEO programmatique
feat(sprint-11)Référentiels métiers (9) et villes IDF (46 principales)feat(sprint-11)Génération Claude de contenu SEO par couple métier × villefeat(sprint-11)Tablesseo_pages+prestations_types(9 prestations seed)feat(sprint-11)Composants Schema.org : LocalBusiness, Breadcrumb, FAQPage, ItemListfeat(sprint-11)Route/api/og/[slug]OG image dynamique (edge runtime)feat(sprint-11)Page/[metier]/[ville]/[prestation]longue traînefeat(sprint-11)Sitemap étendu : statiques + métier × ville + prestations + artisans
[1.0.0] — 2026-04-17
Première version publique de Batimio. MVP complet V1 livré (Sprints 1 à 10 + Sprint 11 bonus).
Ajouté — Branding & identité
feat(branding)Aligne les icônes sur le bleu primaire du site (7065c71)feat(artisan)Affiche le SIRET dans la fiche publique (52c37a4)feat(favicon)Force le B orange partout, suppression de l'ancien A bleu (e4df40a, 422844a)feat(logo)Remplace le wordmark ArtisanIA par Batimio + nouveau B stylisé (c9f735e)choreRenommage ArtisanIA → Batimio (brand, URLs, config Supabase cloud) (bc10d0a)
Ajouté — Sprint 11 : catégories manquantes + IDF complet
feat(sprint-11)Catégories manquantes + IDF complet + vues pro (9483af3)feat(sirene)Import complet IDF 257k artisans + fix pagination curseur (5d55b9d)feat(sirene)INSEE API V3.11 en production via API Key header (7ee80bd)docsStratégie Sirène hybride API V3 + data.gouv.fr phase 2 (b9210a4)
Ajouté — Sprint 10 : RGPD + SEO + mise en production
feat(sprint-10)RGPD + SEO + pages légales — V1 complète (0952ff4)
Ajouté — Sprint 9 : facturation + export comptable
feat(pro)Page factures + export CSV (4f60747)
Ajouté — Sprint 8 : abonnement + parrainage + onboarding
feat(sprint-8)Stripe Subscriptions + parrainage artisan + onboarding (f04e2e0)feat(pro)Tarifs + abonnement + souscription mock (27b6cd8)feat(sms)Twilio Alpha Sender BATIMIO + fix slug dans import Sirène (0c545b8)docsAjoute programme de parrainage artisan au Sprint 8 (c0b04ef)
Ajouté — Sprint 7 : messagerie + historique + annulation
feat(sprint-7)Messagerie + historique + annulation (79cca0a)
Ajouté — Sprint 6 : tournée géolocalisée
feat(pro)Tournée géolocalisée + check-in GPS (fcfd632)
Ajouté — Sprint 5 + 5.5 : profil public, recherche, design system
feat(public)Profil artisan + recherche + SEO + réponse avis (9ec2894)feat(design)3 moods comparatifs pour choisir la charte graphique (cd46de3)feat(design)Sprint 5.5 — design system moderne (mood bleu Doctolib-like) (433df6a)feat(map)Carte Leaflet des artisans dans/rechercheet/[metier]/[ville](66fc173)feat(map)Marker générique + scroll liste au clic (b8b1237)
Ajouté — Sprint 4 : devis + facturation + paiement + avis
feat(devis)Éditeur de devis pro avec calcul TVA temps réel (12b47ed)feat(cloture)Clôture chantier + facture HTML auto (f0eb2e0)feat(paiement)Escrow Stripe avec mode mock + libération client (7eb6627)feat(avis)Vue client mission + validation paiement + avis (8e997a9)
Ajouté — Sprint 3 : réservation + agenda + notifications
feat(reservation)Parcours réservation client complet (f2b2603)feat(pro)Agenda des disponibilités éditable (c9fe4b1)feat(notifications)SMS Twilio avec fallback mock + PWA SW (9f3e744)
Ajouté — Sprint 2 : diagnostic IA avancé + brief artisan
feat(ia)Claude Vision + DVF réel dans/api/agent(ffc057b)feat(pro)Liste demandes + acceptation avec brief IA (ac7e53a)
Ajouté — Sprint 1 : fondations MVP
feat(client)Géocodage + géoloc + upload photos (d7d97dd)feat(pro)Dashboard KPIs + profil + revendication Sirène (6951713)feat(admin)Auth password + dashboard + sync Sirène + RLS (fc955f1)featPage liste artisans + nettoyage lint/typographie/E2E (e9e4425)
Ajouté — Bootstrap MVP
featMVP ArtisanIA — stack Next.js/Supabase/Claude + Docker local (e5d8224)featInitial commit (ce7a2d2)- Initial commit from Create Next App (ecb99df)
Corrigé
fix(recherche)Matching accent-insensitive + tokenise + multi-champs (9d072cd)fixWrapuseSearchParams()dansSuspense(requis Next.js 15) (6b57dbe)fix(types)Alias métier + correction nullabilité pour build Vercel (0706689)fix(upload)URLs Storage publiques au lieu d'internes Docker (f6579ed)fix(calendrier)Dynamic importssr:falsevia Client Component wrapper (5aaa624)fix(calendrier)suppressHydrationWarningsur le highlight "aujourd'hui" (245c73c)fix(header)Remplace<Button render={<Link/>}>par un<Link>direct (03292ab)fixAuth magic link PKCE dans Docker local (6ebae1a)
Tests & qualité
test(smoke)Test navigateur anti-régression runtime + fix themeColor (9d52e85)
Documentation
docsSection 18 CLAUDE.md — roadmap V1 (Sprints 5 à 10) (0113824)docsAjoute section 17 FAQ ArtisanIA dans CLAUDE.md (4041592)docsMise à jour documents vivants à chaque Sprint livré (afadea3, 38bde5b, a04939f, ed82d8e, 3673dbd, cafb17f, 5a6c48a)
Infrastructure
choreTrigger Vercel build (ajustement email auteur) (29bf735)
Ce changelog est maintenu manuellement à chaque commit de livraison Sprint. Le format des messages de commit suit la spécification Conventional Commits (voir CLAUDE.md §0.1).