Version actuelle v1.17.5

Changelog

Historique des livraisons Batimio. Versionnage majeur.mineur.patch conforme SemVer.

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) metadataBase dans app/layout.tsx — fixe https://batimio.com comme 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 de www., pas de http://, pas de paramètres).
  • feat(seo) Schema.org JSON-LD Organization + WebSite sur 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 de public/icons/icon.svg (doublon 512×512 incohérent avec app/icon.svg 32×32). Manifest PWA mis à jour pour pointer sur /icon.svg unique. É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 dangerouslySetInnerHTML pour 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) Renommage artisans-N.xmlentreprises-N.xml pour 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_SIZE passe 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.xml au 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).
  • construireSegmentEntreprises simplifié : plus de boucle de pagination interne, juste une range() + un order('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.tsx devient 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_active sur artisans reste 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'approche generateSitemaps de Next.js par des route handlers manuels. La fonction generateSitemaps cassait au build ou au routing en prod (v1.17.1 a livré un /sitemap.xml qui 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 = 86400 Next.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 segment validé 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é via generateSitemaps (Next 14.2+) — la prod Batimio contient 257 429 fiches artisan (confirmé après migration 022), alors que l'ancien app/sitemap.ts limitait 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 chunks PG_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_active déjà en place.
  • ✅ Utilisation du client admin (service_role) justifiée — la page est publique, on contourne la RLS pour servir les fiches non_revendiquee de 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.frGoogle 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 Postgres public.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 dans artisans.slug_legacy. La page /artisan/[slug] fait un permanentRedirect 301 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 de slug_legacy et de la fonction slug_seo dans 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_legacy hérite automatiquement des policies existantes sur artisans (lecture publique conditionnée à statut IN ('non_revendiquee', 'actif') via public_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_legacy utilise .eq('slug_legacy', slug) du client Supabase (paramètres échappés).
  • Dépendances — Aucune nouvelle dépendance ; permanentRedirect importé de next/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 de Wizard6Etapes pleine taille). Gère les états hors-pipeline (annulée avec hachures, en-sommeil avec badge ⏸)
  • feat(ui) /client/profil affiche désormais pour chaque demande une barre de progression visuelle des 6 phases (cadrage → solde) à la place du simple badge de phase
  • feat(ui) Titre de demande éditable en ligne via le composant EditableDemandeTitre (bouton ✏️ → champ texte → ✓/✕). Mise à jour immédiate après router.refresh(). Désactivé si la demande est annulée
  • feat(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 en bg-gray-50/60 avec numéro de consultation + date, section « Intervention » en 2 colonnes (libellé + adresse), encart bleu multi-consultation, liste des artisans en rows séparés par divide-y (plus de cartes imbriquées)
  • fix(ui) Wizard forcé sur phase consultation dè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 DB
  • feat(ui) ArtisanCard accepte désormais un prop variant: 'card' | 'row'. En mode row, 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) — ArtisanCard avait son propre wrapper rounded-xl border + un bouton « Contacter » stub, et je l'avais enveloppé dans un second wrapper bordé + un vrai ContacterArtisanButton. Résultat : double bordure + deux boutons. Fix : ArtisanCard accepte désormais un prop action: 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 021 fn_calculer_phase_demande). L'ancien seuil 90 était trop strict pour un particulier qui ne connaît pas toutes les dimensions exactes. Constante exportée SEUIL_CONFIANCE_CONSULTATION = 80
  • feat(ui) Nouveau composant components/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
  • ≥ 80 sans artisan contacté : « ✓ Score atteint ! Tu peux contacter des artisans » + CTA fort vers /client/demande/artisans
  • ≥ 80 avec 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 RLS n'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 script migrate-cloud.ts

#### Migration 020

  • fix(security) db/migrations/020_security_rls_patch.sql — ré-applique défensivement ENABLE ROW LEVEL SECURITY + policies pour les 5 tables manquantes. Utilise DROP POLICY IF EXISTS + CREATE POLICY pour être idempotente
  • notifications : destinataire = auth.uid() (propre utilisateur)
  • mission_etapes : participants de la mission en lecture, artisan en écriture
  • devis_ecarts : artisan voit ses propres écarts
  • codes_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éneaux type='disponible' pour réservation
  • notificationsdestinataire = auth.uid()
  • mission_etapes — client OU artisan de la mission, écriture artisan seulement
  • devis_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 via createClient() (cookies auth) sont couverts par les policies auth.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&nbsp;:

  • 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 sur missions&nbsp;:
  • acompte_payment_intent_id, acompte_montant_ttc, acompte_paiement_statut, acompte_paye_at
  • solde_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_statut et solde_paiement_statut
  • Migration idempotente (IF NOT EXISTS)

#### Librairie Stripe

  • feat(stripe) lib/stripe/escrow.ts refondue&nbsp;:
  • 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/libererEscrow conservé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&nbsp;:
  • 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_id pointe désormais sur le PI solde (compat descendante car c'est le PI libéré à la clôture)
  • feat(api) POST /api/paiement/[id]/validerlibererSolde remplace libererEscrow. Lit solde_payment_intent_id en priorité, fallback legacy sur stripe_payment_intent_id. MAJ solde_paiement_statut='libere' + dates

#### Interface client

  • feat(ui) Nouveau composant components/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 TTC
  • feat(ui) /client/demande/[id] (hub) — encart <PaiementSplit> sous l'artisan retenu dès que les PI sont créés
  • feat(ui) /client/mission/[id] — même encart au-dessus des actions de mission

#### Tests

  • test(unit) tests/unit/escrow-split.test.ts — 7 tests&nbsp;: pourcentages par défaut, calculs acompte/solde, tolérance d'arrondi, pourcentage custom, commission 4 % sur libération
  • Total&nbsp;: 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 en statut='devis_envoye', statut_consultation='en_cours', sans créneau. Copie diagnostic_detail dans devis_ia_initial (référence intangible pour comparaison future). Passe la demande en phase consultation. 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 client
  • feat(api) POST /api/mission/[id]/visite/confirmer — le client confirme un des créneaux proposés. statut_visite='confirmee', email artisan avec date + adresse
  • feat(api) POST /api/mission/[id]/visite/realiser — l'artisan clôture la visite, soumet notes + devis ajusté (devis_artisan_final avec montant_total_ttc, motif_modification). statut_visite='realisee', statut_consultation='devis_envoye'
  • feat(pro) /pro/demandes/[id] — composant VisiteActions affiché 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 passe statut_consultation='retenu' + statut='confirme'. Les autres missions de la demande passent en non_retenu + non_retenu_notif_at. Demande → phase validation. Email automatique envoyé aux artisans non retenus (template consultation_non_retenue)
  • feat(client) Nouveau composant RetenirArtisanButton avec 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 de lib/stripe/escrow.ts et 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 historique diagnostic_detail.qa_history append-only (liste [{q, r}]), migré automatiquement depuis le legacy demandes.mesures. Indexation par question normalisée (lowercase + unaccent + ponctuation dépouillée) pour dédoublonner même si l'IA reformule
  • fix(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 changement
  • fix(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 route app/api/auth/magic-link/route.ts — génère le token via supabase.auth.admin.generateLink puis construit elle-même l'URL sur https://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 lisible
  • feat(auth) Nouveau template email magic_link avec 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 (via demande_id ou mission_id) et rend <Wizard6Etapes>. Accepte aussi une phaseForcee quand la demande n'existe pas encore
  • feat(ui) /client/demande — Wizard figé sur « Cadrage » avant la soumission initiale
  • feat(ui) /client/demande/artisans — Wizard rendu depuis demande_id
  • feat(ui) /client/demande/reservation — Wizard rendu depuis demande_id
  • feat(ui) /client/demande/confirmation — Wizard rendu depuis mission_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/diagnostic sous 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é si diagnostic_detail.erreur est 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/profil liste 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 devis
  • feat(ui) Informations personnelles : email + date d'inscription + ID utilisateur
  • feat(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.maxTokens passe de 1500 → 4000. Cause racine identifiée sur la demande a55e0381-… (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.erreur en base). Un devis maçonnerie complexe dépasse régulièrement 1500 tokens à cause du volume d'actions, matériaux et questions dimensionnelles
  • fix(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_reason Claude + extrait tronqué (500 chars début / 500 fin) pour diagnostiquer rapidement les cas atypiques. Le stop_reason est aussi renvoyé dans le message d'erreur côté client

[1.15.1] — 2026-04-22

Ajouté

  • feat(client) Page /client/profil affiche 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 ligne demandes est 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 dans diagnostic_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 via crypto.randomUUID, propagé à l'API pour servir de clé d'insertion)
  • fix(api) En cas d'échec d'analyse, la réponse inclut le demande_id et (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 trace
  • fix(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) Colonnes demandes.phase, phase_updated_at, mission_choisie_id, en_sommeil_since, en_sommeil_motif, annulee_at, annulee_motif
  • feat(db) Colonnes missions pour workflow visite + devis IA/modifié&nbsp;:

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 Postgres fn_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 progresse
  • feat(ui) components/shared/BadgeIA.tsx — Badge explicite qui signale clairement au client qui a produit le devis&nbsp;: «&nbsp;Devis généré par l'IA&nbsp;», «&nbsp;Validé par l'artisan&nbsp;», «&nbsp;Ajusté par l'artisan&nbsp;»
  • feat(ui) components/client/GererDemande.tsx — Panneau d'actions client&nbsp;: mettre en sommeil, réveiller, annuler. Avec confirmations natives et capture de motif optionnel
  • feat(lib) lib/phases.ts — Source unique de vérité&nbsp;: 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 Auth
  • feat(api) POST /api/demande/:id/sommeil — Avec action: 'pause' | 'reveil'. Le réveil restaure la phase précédente en analysant les données (cadrage / consultation / validation)

#### Intégration

  • feat /client/demande/diagnostic affiche désormais en tête le wizard 6 étapes + le badge IA, et propose en bas le panneau de gestion de la demande
  • feat Si la demande est annulée ou en sommeil, les boutons «&nbsp;Voir les artisans&nbsp;» sont masqués (évite actions incohérentes)
  • Backward-compatible&nbsp;: si demande.phase n'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éma ActionArtisan structuré remplace le tableau de strings plat. Champs&nbsp;: ordre, etape, duree_min, icone (emoji), description optionnelle
  • feat(prompt) Le prompt Claude oblige désormais des étapes strictement chronologiques avec durées réalistes et icône emoji (🧱 🔧 ⚡ 🎨 🏗️ 🪜 📏 …)
  • feat(ui) /client/demande/diagnostic et /pro/demandes/[id] affichent une timeline visuelle&nbsp;:
  • Badge «&nbsp;Étape N&nbsp;»
  • 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
  • compat Backward-compatible via normaliserActions() — les anciens diagnostics en string[] s'affichent en timeline avec duree_min: 0
  • feat(validation) Zod schema dans /api/agent accepte l&apos;union des 2 formats

Corrigé — Ceinture de sécurité sur RepondreQuestions

  • fix(ui) Ajout d&apos;une key prop calculée sur le contenu des questions dans la page parent → force le remount complet du composant quand les questions changent, en complément du useEffect de 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&nbsp;: le state local (reponses, enCours, erreur) n'était pas réinitialisé quand le serveur retournait de nouvelles questions via router.refresh(). Conséquences&nbsp;:
  • Les anciennes réponses s'affichaient dans les nouveaux textarea (placeholder incorrect)
  • Les champs restaient désactivés (disabled={enCours} à true)
  • Le bouton «&nbsp;Mise à jour du diagnostic&nbsp;» restait grisé
  • Solution&nbsp;: useEffect qui observe une signature stable du tableau questions (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 «&nbsp;révise à la hausse&nbsp;» mais Claude pouvait renvoyer une confiance stagnante ou en baisse quand il identifiait encore des ambiguïtés. Double correctif&nbsp;:

1. Prompt renforcé&nbsp;: instruction stricte «&nbsp;confiance DOIT augmenter de MINIMUM 10 points&nbsp;» + contexte envoyé (confiance précédente + nombre de réponses fournies)

2. Plancher post-traitement&nbsp;: 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) Composant RepondreQuestions.tsx&nbsp;: formulaire inline avec une zone de texte par question posée par l&apos;IA
  • feat(api) Route POST /api/demande/:id/preciser&nbsp;:
  • Reçoit les réponses textuelles du client
  • Fusionne dans demandes.mesures (clés q1, 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/diagnostic intégrée avec le formulaire
  • Rafraîchit automatiquement (router.refresh()) après envoi pour afficher le nouveau diagnostic
  • Bouton «&nbsp;Affiner le diagnostic avec mes réponses&nbsp;»
  • Si l&apos;IA estime qu&apos;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é&nbsp;: un tiers ne peut pas préciser la demande d&apos;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 devis
  • lib/claude/devis-adaptatif.ts : prompt Claude + fonction evaluerEtGenererDevis()
  • Deux statuts de sortie : questions (3–8 questions typées) ou pret (DevisNormeResult)
  • Questions catégorisées (dimensions, materiaux, acces, support, finitions) et priorisées (bloquant / important / nice_to_have)
  • Types d'input&nbsp;: mesure (avec unité), choix, oui_non, text, photo
  • feat(api) Route POST /api/devis/adaptatif
  • Fusionne les réponses dans demandes.mesures (persistance multi-passe)
  • Persiste le devis dans missions.devis_normé dès que status=pret
  • Peut être appelée N fois jusqu'à convergence
  • feat(pdf) lib/pdf/devis-html.ts deux modes de rendu HTML A4
  • simplified (envoi client)&nbsp;: langage clair, pas de références matériau, durées en heures, DTU en codes seulement, signatures prestataire/client
  • detailed (usage interne pro)&nbsp;: 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|detailed accepte le paramètre de rendu
  • feat(ui) Composant React components/pro/DevisAdaptatif.tsx
  • Bouton «&nbsp;Évaluer et générer le devis&nbsp;»
  • Formulaire dynamique avec inputs typés selon DevisQuestion.type
  • Badge «&nbsp;Requis&nbsp;» sur questions bloquantes + désactivation submit tant que bloquantes non remplies
  • 4 boutons sortie&nbsp;: client HTML / client PDF / pro HTML / pro PDF
  • feat(ui) /pro/devis/[id] refondue autour du composant adaptatif, DevisForm legacy conservé en fallback

Séparation stricte matériaux / main d'œuvre

Les deux formats séparent clairement les deux postes&nbsp;:

  • 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&nbsp;: Fournitures HT + MO HT + TVA + TTC

Basé sur les DTU

  • Chaque devis cite exclusivement les DTU stockés en DB (normes table, 103 entrées)
  • Validation côté serveur&nbsp;: 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 «&nbsp;ROADMAP V1.1+&nbsp;» étendue avec Sprint 17 détaillé
  • Timeline synthétique mise à jour jusqu'à v1.14

[1.13.2] — 2026-04-20

Corrigé

  • fix(agent) Enum corps_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é&nbsp;: 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 sur site_url racine 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/devis avec ?print=1 auto-trigger dialogue d'impression
  • Mentions légales complètes&nbsp;: 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 + «&nbsp;Bon pour accord&nbsp;» 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 «&nbsp;Imprimer / PDF&nbsp;»
  • 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
  • feat lib/pdf/branding.ts&nbsp;: source unique pour infos éditeur SAS RHEXO
  • feat lib/pdf/css-print.ts&nbsp;: CSS commun A4 + @page + @media print
  • feat UI /pro/devis/[id]&nbsp;: 2 boutons «&nbsp;Prévisualiser&nbsp;» + «&nbsp;Télécharger PDF&nbsp;»
  • feat UI /pro/chantier/[id]&nbsp;: 4 boutons (Lettre HTML + PDF, Devis HTML + PDF)
  • Choix technique&nbsp;: pas de Playwright/Puppeteer → «&nbsp;Print → Save as PDF&nbsp;» natif navigateur, compatible Alpine

[1.12.0] — 2026-04-20

Ajouté — Catalogue DTU complet (95 normes supplémentaires)

  • feat(dtu) Migration 016&nbsp;: 95 nouvelles normes DTU/NF ajoutées au catalogue (e361a91)
  • Couvreur&nbsp;: 19 DTU (40.x complet + 43.x étanchéité)
  • Plombier&nbsp;: 18 DTU (60.x, 61.1, 64.1, 65.x, 75.1)
  • Menuisier&nbsp;: 14 DTU (33, 34, 35, 36, 37, 39, 51.x, 55)
  • Maçon&nbsp;: 14 DTU (13.x, 14.1, 20.x, 21, 23.1, 26.x, 42.1)
  • Plaquiste&nbsp;: 11 DTU (25.x, 27.1, 45.x, 58.x)
  • Peintre&nbsp;: 10 DTU (42.1, 44.1, 53.x, 59.x)
  • Électricien&nbsp;: 8 normes (NF C 15-100, DTU 68.3, 70.1, NF C 14-100, 16-600)
  • Charpentier&nbsp;: 7 DTU (31.x, 32.x, 41.2)
  • Carreleur&nbsp;: 2 DTU (52.1, 52.2)
  • Total&nbsp;: 103 normes en DB (8 Sprint 15 + 95 Sprint 19)
  • feat(dtu) Lettre de mission branchée sur la table normes
  • 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é «&nbsp;DTU XX.Y — Titre (§article)&nbsp;»

[1.11.0] — 2026-04-19

Ajouté — Script de migration Supabase Cloud

  • feat(migrate) scripts/migrate-cloud.ts avec postgres.js (dep postgres ^3.4.5)
  • feat Table _migrations_applied pour idempotence
  • feat npm scripts migrate:cloud et migrate:cloud:dry
  • feat Lecture SUPABASE_DB_URL depuis .env.local
  • feat Gestion des migrations déjà appliquées (pré-marquage manuel possible)

[1.10.0] — 2026-04-19

Ajouté — Mentions légales SAS RHEXO

  • feat CGU section 1 «&nbsp;Éditeur du site&nbsp;» avec SAS RHEXO complet + hébergeurs
  • feat Renumérotation des sections 2-8 CGU
  • feat Confidentialité&nbsp;: SAS RHEXO comme responsable du traitement
  • feat Confidentialité section cookies mise à jour (3 catégories avec consentement par catégorie)

Corrigé

  • fix(ui) Sauts de ligne CGU + confidentialité (classes prose inertes sans plugin typography → remplacées par styles Tailwind explicites via attribute selectors)

[1.9.0] — 2026-04-19

Ajouté — Recherche par ville

  • feat Champ «&nbsp;Ville&nbsp;» remplace «&nbsp;Code postal&nbsp;» sur /recherche
  • feat Parse automatique&nbsp;: accepte «&nbsp;Versailles&nbsp;», «&nbsp;78000&nbsp;» ou «&nbsp;Versailles (78000)&nbsp;»
  • feat Affichage format «&nbsp;Ville (CP)&nbsp;» partout&nbsp;: /recherche + /[metier]/[ville]
  • compat Paramètre URL cp= conservé pour rétro-compatibilité

[1.8.0] — 2026-04-19

Ajouté — Bannière de consentement cookies (RGPD)

  • feat(consent) components/shared/ConsentBanner.tsx&nbsp;: bannière fixe + bouton flottant de réouverture (1325292)
  • feat(consent) lib/consent.ts&nbsp;: state localStorage + events réactifs
  • feat(consent) lib/consent-hook.ts&nbsp;: hook useConsent() via useSyncExternalStore
  • feat(consent) Mode «&nbsp;Personnaliser&nbsp;» avec toggles par catégorie (Essentiel / Analytics / Marketing)
  • feat(consent) Refus aussi visible que l'acceptation (conformité CNIL)
  • feat(consent) GoogleAnalytics gated&nbsp;: GA4/GTM/Ads ne chargent QUE si consent donné
  • feat GA4 configuré avec anonymize_ip: true + cookie_flags: SameSite=None;Secure

Fichiers clés

  • components/shared/ConsentBanner.tsx
  • components/shared/GoogleAnalytics.tsx
  • lib/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
  • feat Metadata verification Google Search Console + Bing Webmaster
  • feat lib/analytics/google.ts&nbsp;: helpers sendGaEvent, sendAdsConversion, GaEvents
  • feat Événements métier pré-câblés&nbsp;: demandeCreee, rdvReserve, paiementConfirme, abonnementSouscrit
  • env Ajout des 5 variables Google dans .env.example&nbsp;: 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&nbsp;: SPF racine + DMARC relaxed
  • Site URL Supabase corrigé&nbsp;: http://localhost:3000https://batimio.com
  • Redirect URL allowlist&nbsp;: prod + dev local + batimio.fr
  • Migrations 010 à 016 appliquées sur Supabase Cloud
  • choix technique Pas 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 table normes
  • 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éactifs
  • lib/consent-hook.ts : hook useConsent() via useSyncExternalStore
  • components/shared/ConsentBanner.tsx : bannière + bouton flottant réouverture
  • Mode "Personnaliser" avec toggles par catégorie
  • Refus aussi visible que l'acceptation (conformité CNIL)
  • feat GoogleAnalytics gated : GA4/GTM/Ads ne chargent QUE si consent donné
  • feat GA4 anonymize_ip: true + cookie_flags: SameSite=None;Secure

Ajouté — Recherche par ville (Sprint 5 refonte)

  • feat Champ "Ville" remplace "Code postal" sur /recherche
  • feat Parse automatique : accepte "Versailles", "78000" ou "Versailles (78000)"
  • feat Affichage format "Ville (CP)" partout : /recherche + /[metier]/[ville]
  • compat Paramètre URL cp= conservé pour rétro-compatibilité

Ajouté — Mentions légales SAS RHEXO

  • feat CGU section 1 "Éditeur du site" avec SAS RHEXO complet + hébergeurs
  • feat Renumérotation sections 2-8 CGU
  • feat Confidentialité : SAS RHEXO comme responsable du traitement
  • feat Confidentialité section cookies mise à jour (3 catégories)

Ajouté — Script de migration Supabase Cloud

  • feat(migrate) scripts/migrate-cloud.ts avec postgres.js (dep postgres ^3.4.5)
  • feat Table _migrations_applied pour idempotence
  • feat npm scripts migrate:cloud et migrate:cloud:dry
  • feat Lecture SUPABASE_DB_URL depuis .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
  • feat Metadata verification Google Search Console + Bing Webmaster
  • feat lib/analytics/google.ts helpers : sendGaEvent, sendAdsConversion, GaEvents
  • feat Événements métier pré-câblés : demandeCreee, rdvReserve, paiementConfirme, abonnementSouscrit
  • env Ajout 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 emailRedirectTo pointe sur site_url racine
  • Défense en profondeur : le middleware préserve le path d'origine comme redirect=
  • fix(ui) Sauts de ligne CGU + confidentialité (classes prose inertes → styles Tailwind explicites)
  • fix(lint) Échappement apostrophes (react/no-unescaped-entities)
  • fix(lint) Date.now() remplacé par new 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 + table mission_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:3000https://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/analytics avec 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
  • feat Redirection 301 batimio.frbatimio.com via next.config.ts redirects()
  • feat Page publique /changelog convertit CHANGELOG.md en HTML
  • feat Badge version du footer cliquable vers /changelog

Corrigé

  • fix(lint) Échappement apostrophes (react/no-unescaped-entities) sur 3 fichiers
  • fix(lint) Date.now() remplacé par new Date().getTime() dans composant async
  • fix(lint) Variable artisan inutilisée remplacée par void 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 × niveau
  • feat(sprint-15) Génération IA devis normé (lib/claude/devis-norme.ts) avec validation refs
  • feat(sprint-15) Route POST /api/devis/norme + persistance missions.devis_normé
  • feat(sprint-15) Table devis_ecarts pour 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 XSS
  • feat(sprint-14) Route POST /api/mission/[id]/lettre avec versioning + historique
  • feat(sprint-14) Table mission_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) Table demande_messages + RLS client
  • feat(sprint-13) Seuil confiance 85% + limite 5 tours avant escalade
  • feat(sprint-13) Route POST /api/dialogue avec tracking tokens + coût EUR
  • feat(sprint-13) Composant React client DialogueIA.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égales
  • feat(sprint-12) Tables artisan_dirigeants, artisan_certifications, artisan_annonces_legales
  • feat(sprint-12) Fonction SQL calc_score_completude + trigger auto
  • feat(sprint-12) Worker scripts/enrich-inpi.ts + route /api/admin/enrichir
  • feat(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 × ville
  • feat(sprint-11) Tables seo_pages + prestations_types (9 prestations seed)
  • feat(sprint-11) Composants Schema.org : LocalBusiness, Breadcrumb, FAQPage, ItemList
  • feat(sprint-11) Route /api/og/[slug] OG image dynamique (edge runtime)
  • feat(sprint-11) Page /[metier]/[ville]/[prestation] longue traîne
  • feat(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)
  • chore Renommage 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)
  • docs Straté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)
  • docs Ajoute 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 /recherche et /[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)
  • feat Page liste artisans + nettoyage lint/typographie/E2E (e9e4425)

Ajouté — Bootstrap MVP

  • feat MVP ArtisanIA — stack Next.js/Supabase/Claude + Docker local (e5d8224)
  • feat Initial commit (ce7a2d2)
  • Initial commit from Create Next App (ecb99df)

Corrigé

  • fix(recherche) Matching accent-insensitive + tokenise + multi-champs (9d072cd)
  • fix Wrap useSearchParams() dans Suspense (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 import ssr:false via Client Component wrapper (5aaa624)
  • fix(calendrier) suppressHydrationWarning sur le highlight "aujourd'hui" (245c73c)
  • fix(header) Remplace <Button render={<Link/>}> par un <Link> direct (03292ab)
  • fix Auth magic link PKCE dans Docker local (6ebae1a)

Tests & qualité

  • test(smoke) Test navigateur anti-régression runtime + fix themeColor (9d52e85)

Documentation

  • docs Section 18 CLAUDE.md — roadmap V1 (Sprints 5 à 10) (0113824)
  • docs Ajoute section 17 FAQ ArtisanIA dans CLAUDE.md (4041592)
  • docs Mise à jour documents vivants à chaque Sprint livré (afadea3, 38bde5b, a04939f, ed82d8e, 3673dbd, cafb17f, 5a6c48a)

Infrastructure

  • chore Trigger 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).