# SEO Insight — Roadmap / Backlog

> Statut vivant (màj 2026-06-10). Source de vérité du **fait / à faire**. Les specs détaillées par feature : `docs/superpowers/specs/`.
> Légende : ✅ livré · 🟡 partiel · ⛔ différé · ⬜ à faire.
> **Miroir Linear** : [projet SEO Insight](https://linear.app/elevaseo/project/seo-insight-f8d655d21939) (team ELEVA) — milestones par section, le backlog ⬜ = issues ELEVA-40→66 ; le CdC produit reste `~/Documents/seo insight/docs/specs/`.

## Socle / infra (transverse)

- ✅ **Better Auth** — orgs / users / members / rôles · vérification email (Resend) · invitations équipe · garde email (disposable/gmail-normalize)
- ✅ **RBAC deux plans** — rôles org corrigés (`member`/`billing`/GSC : billing owner-only, GSC ouvert à `admin`) · Staff SYS à permissions atomiques (`staff_permission`, grille `/admin/staff`) · population SYS dissociée des orgs (orgless par construction, plus de collision de nommage avec l'ancien rôle org `staff`). Détails : `docs/superpowers/specs/2026-07-09-rbac-roles-staff-permissions-design.md` (implémenté, branche `feature/rbac-two-planes`), plan `docs/superpowers/plans/2026-07-09-rbac-two-planes.md` (13 tâches).
- ✅ **Persistance** — PostgreSQL + Drizzle (DB dev + test), migrations `generate`+`migrate` (jamais `db:push`)
- ✅ **rust `import-core` → wasm** — parsers CSV (keywords mangools/semrush, SERP URLs) + parseur HTML on-page (`tl`)
- ✅ **GSC** — connexion + mapping propriété (Settings→Intégrations) · sync positions · discovery · positionnement actionnable
- ✅ **Firecrawl self-hosted** — client `/v1/scrape` (env `FIRECRAWL_API_URL/KEY`) pour le crawl on-page concurrents
- ✅ **Worker Rust planifié — snapshots GSC datés** ([ELEVA-40](https://linear.app/elevaseo/issue/ELEVA-40)) : binaire `crates/gsc-worker` (tokio/reqwest/sqlx) exécuté par cron — fenêtre glissante J-5→J-1, dimensions `date×query` + `date×page`, upserts idempotents dans `gsc_query_snapshots`/`gsc_page_snapshots` + observabilité `gsc_snapshot_runs`. Auth via **token broker** `/api/worker/gsc-token` (Bearer `WORKER_API_KEY`, timing-safe ; Better Auth déchiffre/rafraîchit — `encryptOAuthTokens`). `/api/worker` exclu du proxy cookie-gate (garde propre). Dev : `bun run worker:gsc` (cf. `crates/gsc-worker/README.md`). *Livré 2026-06-10, vérifié live : 2 sites, 572 lignes, re-run idempotent.* → **débloque les pages Stats + tendances temporelles**. ⚠️ Prod : cron Dokploy + `WORKER_API_KEY` env (runbook ELEVA-41)
- ⬜ Cible long terme (cahier) : API Hono · file d'attente · SurrealDB · MCP intégré · i18n

## Keywords — ✅ section complète

reservoir ✅ · recherche ✅ · planning ✅ · import/export ✅ · overview ✅
- Reservoir/recherche : DB read, drapeaux ISO, badge GSC, statut auto si dans GSC, copy/delete inline, shift-select, catégories
- Overview : hub (best of Semrush/Mangools/Ahrefs) sur données réelles
- ✅ **Système de promotion** (sélection → destination) : Réservoir → Recherche/Planning/Contenu · Recherche → Planning (« → Planning ») + Contenu · Chambre de Briefing → Contenu (sélection bulk ajoutée). Dialog partagé « Vers Contenu » : **1 page (groupe)** (1 principal au choix + secondaires) ou **N pages** (dédup des déjà-couverts, warning inline). Table **`content_keywords`** (role primary/secondary, fallback lecture sur `primaryKeywordId` pour le legacy — pas de backfill requis) + section **« Mots-clés »** sur la carte Contenu (définir principal, retirer — principal protégé —, ajouter par recherche). `primaryKeywordId` maintenu en sync. *Livré 2026-06-10, vérifié live (briefing→contenu, groupe réservoir→1 page primary+secondary, warning déjà-couvert, cascade delete).* Backfill prod optionnel : `INSERT INTO content_keywords (content_item_id, keyword_id, role) SELECT id, primary_keyword_id, 'primary' FROM content_items WHERE primary_keyword_id IS NOT NULL ON CONFLICT DO NOTHING;`
- ✅ **GSC dimensions pays + page** ([ELEVA-57](https://linear.app/elevaseo/issue/ELEVA-57)) — la sync interactive ajoute : **overlay par pays cible** (`keywords.gsc_by_country` jsonb, migration **0021** ; 1 appel searchAnalytics filtré `country` par pays, mapping alpha2→alpha3 pur) et **page qui ranke** (`gsc.page` via `["query","page"]`, top par clics). Best-effort autour du sync global inchangé (compteurs `· N pays`). UI : positions par pays dans la **matrice** Recherche + sheet « GSC par pays » / « Page qui ranke » + badge « ⚠️ autre page ranke » sur **Online** (mismatch URL normalisée). `gsc.page` préservé si le fetch pages échoue. *Livré 2026-06-10, vérifié live sur Acieb (494 requêtes, 281 overlays FR, 310 pages ; pos FR 1,36 vs globale 2,04 sur « acieb »).* Différé : historique par pays (worker), pagination >25k, tri matrice par position
- ✅ **Sync SE-Ranking (enrichissement API)** — sélection → « Sync SE-Ranking » sur **réservoir (menu Importer ▾) + recherche + planning** : métriques volume/KD/CPC/competition/intents + trend 12 mois via l'API bulk (`POST /v1/keywords/export`, clé plateforme `SERANKING_API_KEY`, 100 crédits/requête ≤ 5000 kw). Normalisation **Rust wasm** (`parse_seranking_api`, import-core), groupage par pays cible → base régionale (**GB→uk**), limiteur process-wide **5 RPS** (clé partagée, 10 RPS max), merge **enrichment-safe** (UPDATE only, identité intacte, `primarySource` ne bascule que si aucune métrique), quota kw×source existant (pré-check avant crédits + tx autoritaire). Spec : `docs/superpowers/specs/2026-07-14-seranking-enrichment-design.md`. *Livré 2026-07-14.* 💰 Monétisation décidée : **hors forfaits, top-up marge ×5** (coût réel $540/an = 3M crédits). Différé : découverte (similar/related/questions/longtail), auto-enrich post-GSC, limiteur côté broker si multi-instance.
- ✅ **Monétisation SE-Ranking** — 1 sync = 1 requête bulk ; **pack top-up 100 syncs / $9.99** (`seranking_pack_100`, fulfillment idempotent UNIQUE session + clamp, carte solde en Facturation) ; **dialog de confirmation du coût** avant chaque sync (hook `useSerankingSync` partagé 3 surfaces : N syncs · M kw · X pays, CTA achat si solde insuffisant) ; **org interne illimitée** (`organization.seranking_unlimited`, seed eleva-seo) ; **add-on BYO key $9.99/mois** (`addon_seranking_byok` → `org_addons`, allow-list statuts fail-closed, clé trimée validée par ping `/account/subscription`, jamais renvoyée au client, DI confinée server-only — fix critical). Migration **0063**. Spec : `docs/superpowers/specs/2026-07-14-seranking-monetization-design.md`. *Livré 2026-07-14.* ⚠️ Ops prod : souscrire le plan SE-Ranking $45/mois annuel AVANT le 2026-07-28 (trial 100k expire), rejouer `scripts/stripe-bootstrap.ts` sur les prices live, seed unlimited prod. Backlog : métering per-record (slice découverte), auto-recharge, reporting conso par site.
- ✅ **Module « Pages »** (façon Screaming Frog scopé) — section sidebar **Pages** : crawl du **sitemap** (robots.txt + sitemap-index récursif, cap 5 000 URLs) via job `site-crawl` du service analytics (registre HANDLERS, pool 6, reprise reaper), métas par page en **Rust wasm** (`parse_page_meta` : title/description/H1/canonical/robots/lang/wordCount) → table `site_pages` (migration **0064**) + **issues on-page** (title/desc manquants-longs-courts, H1 multiple, noindex, canonical-mismatch). **Mapping keywords↔pages** en 4 signaux (gsc > content > textuel scoré seuil 50 avec frontières de mot > manuel — `manual` jamais écrasé, gardé jusque sous course SQL) ; colonne « **Page ciblée** » au réservoir (badge source, picker manuel, facette « Sans page »). Spec : `docs/superpowers/specs/2026-07-15-pages-crawl-design.md`. *Livré 2026-07-15.* Backlog : vrai status HTTP (200/0 aujourd'hui), index unique partiel anti-double-crawl, spider de liens internes, cron re-crawl, filtre statut UI, chip facette. ⚠️ Deploy : migration 0064 + build:wasm + restart service analytics.
- ✅ **Rattachement keywords→pages (anti-cannibalisation)** — à la promotion « Vers Contenu », décision **par keyword** : « 🔄 Optimiser [page existante] » (fusion — kw ajouté à la fiche de l'URL, statut published→writing, **zéro doublon**) vs « ➕ Créer ». Dimension **`kind` extensible** (`new`/`optimize`, orthogonale au statut) ; page cible visible/éditable en **Recherche + Planning** (colonne « Page ciblée » + picker) ; **suggestion live** (matching textuel) pour les gaps ; **trace de cannibalisation** (badge kanban) si on force la création malgré une page existante ; facette `kind` au Contenu. URL server-trusted, tenancy croisée fermée, rétrocompat totale. Migration **0065**. Spec : `docs/superpowers/specs/2026-07-15-keyword-page-attach-design.md`. *Livré 2026-07-15.* Backlog : i18n du module Contenu (FR-hardcodé), merge de deux content_items (2 create→2 optimize laisse une fiche url=NULL orpheline), déterminisme cible sur doublons d'URL préexistants.
- ✅ **Refonte toolbar Finale (B)** — Recherche + réservoir + planning alignés sur le design Claude Design « Toolbar Keywords - Finale (B) » : **panneau Filtres unique** (pill à badge, 8 facettes en popover non-modal, application instantanée), **menu Sync** (GSC + Enrichir SE-Ranking, pastille fraîcheur 7 j, horodatages via `getKeywordsSyncStatus` max() jsonb, maj optimiste), **barre de sélection flottante** bas-centré (inert quand masquée, menus side-top) remplaçant sticky-top/inline, menu « ⋯ » dissous (exports → menu Exporter — CSV complet = filtrés, CSV sélection dans la barre —, opportunité → toggle, Shortlist/Exclus → **onglets de vue** avec `filter.shortlisted` server-side + panneau Exclus inline), **bandes de pilotage sticky**, **undo 10 s** sur statut/thème/étapes/shortlist (inverse groupé par valeur d'origine, limité aux appartenances réellement ajoutées — fix review), export enrichissement par **portées** (sélection / >1 semaine / >1 mois / jamais synchro). Gotcha documenté : `DropdownMenuContent` force `w-(--trigger-width)` → largeurs explicites obligatoires. Spec : `docs/superpowers/specs/2026-07-16-toolbar-keywords-finale-b.md`. *Livré 2026-07-16, QA live 3 pages.* Backlog : nav clavier ViewTabs, onglet « Quick-wins GSC » (préset à définir), badge « Tous » transitoire en quittant Shortlist, footer Sync GSC affiché sur planning (cosmétique).
- ⬜ Reste possible : affiner planning

## Contenu — 🟡

- ✅ **overview** (KPIs, pipeline, assignee, deadlines)
- ✅ **planning** — kanban **+ vue tableau (défaut)** + 3 ronds d'analyse (SERP · on-page · brief YTG) ; carte = modale 80vw
  - ✅ slice 1 : panneau SERP concurrents (top 10, backlinks/URL, features) depuis export « SERP URLs »
  - ✅ slice 2 : crawl on-page Firecrawl (meta · images · **alt par image** · JSON-LD) + bouton « Crawler on-page »
- ✅ **online** — perf GSC des articles publiés
- ✅ **stats** ([ELEVA-60](https://linear.app/elevaseo/issue/ELEVA-60)) — `/content/stats` : perf **par page** depuis `gsc_page_snapshots` (D1 tranché = GSC). Périodes 7/28/90 j (URL `?p=`, nuqs shallow:false) avec **deltas vs période précédente**, position pondérée par impressions, KPIs site, sparklines clics, **décrochage F4** (badge + filtre ; garde anti-faux-signal : jamais sans impressions des 2 côtés), match articles par URL normalisée (badge), sheet détail (courbe clics + métriques). *Livré 2026-06-10, vérifié live sur Acieb (3 j de données réelles : KPIs ok, deltas « — », 0 faux décrochage).* Reste ⬜ : action « envoyer en refonte » + intégration `online` (« à mettre à jour »)
- ✅ **import-export CSV** ([ELEVA-59](https://linear.app/elevaseo/issue/ELEVA-59)) — round-trip CSV des content_items sur `/content/planning` : export toutes colonnes (BOM UTF-8, ré-importable tel quel) + import **preview→commit** (R2), matching par **titre** (jamais destructif), mots-clés liés **par terme** (principal sync `primaryKeywordId`+`content_keywords`, secondaires `|`), parser RFC-4180 pur TDD. *Livré 2026-06-10, vérifié live (création+liens, re-import → mise à jour, erreur statut signalée).* Scindé en issue dédiée : import inventaire sitemap/Firecrawl → online (F1/F5), CMS (F2), briefs/ICS (F3)

## Backlink — 🟡

- ✅ **reservoir** GLOBAL (directory org) + « Ajouter au site actif » + provisioning par site
- ✅ **DR Ahrefs (API free, sans clé)** — métrique « DA » renommée **DR** (1 score 0-100, badge `dr ?? da`). Cache partagé par domaine (`domain_ratings`, domainNorm→dr) alimenté par un **bouton god-mode super-admin** « Rafraîchir DR (Ahrefs) » (fetch chunké + throttlé, distinct domains manquants/périmés). Gate super-admin via `SUPER_ADMIN_EMAILS` (env) + `assertSuperAdmin()` (client masqué ET action serveur). Endpoint validé live (`{domain_rating:{domain_rating}}`, DR 91 producthunt.com). *Livré 2026-06-10.* ⚠️ **Prérequis** : définir `SUPER_ADMIN_EMAILS=<emails>` dans `.env.local` (dev) + l'env de déploiement (prod), sinon personne n'est super-admin (le bouton ne s'affiche pas — défaut sûr). Hors scope : refresh per-row, auto-fetch à l'ajout, cron, autres métriques Ahrefs.
- ✅ **recherche** ([ELEVA-62](https://linear.app/elevaseo/issue/ELEVA-62)) — `/backlink/recherche` : pool global **scoré pour le site actif** (langue×DR — terme thématique structurellement prêt, données absentes), **ciblage multi-sites** (N sources × M sites en une action, dédup par la nouvelle contrainte unique `backlinks(source,site)`), **alerte footprint** ⚠️ ≥ 3 sites, push **→ Planning** (prospect→planned, live intacts), sheet détail (score décomposé). *Livré 2026-06-10, vérifié live (2×2=4 prospects, planned, footprint 3, sheet).* Différé : F6 shortlist, F8 matrice source×site, DnD R2, seuil configurable
- ✅ **planning** ([ELEVA-63](https://linear.app/elevaseo/issue/ELEVA-63)) — `/backlink/planning` : **board kanban DnD** (@dnd-kit, miroir contenu) Prospect · Planifié · Outreach (`in_progress`) · Live · Perdu, cartes triées par score (DR/footprint/toxic badges) ; **outreach léger** (table `backlink_outreach` 1/backlink : contact, canal, relances, notes) ; passage **live → `firstSeen`** (sans écrasement) → **cadence** (liens live ce mois vs précédent) ; flags **toxic/disavow** + **export disavow Google** (`domain:`) ; select de statut clavier (a11y) ; isolation stricte par site (testée). *Livré 2026-06-10, vérifié live (move→live+firstSeen+cadence, export 1 domaine dédupliqué).* Différé : re-check auto live/lost (worker crawler), campagnes (F8), vue calendrier, priorité manuelle persistée
- ✅ **overview** ([ELEVA-64](https://linear.app/elevaseo/issue/ELEVA-64)) — `/backlink/overview` : dashboard **lecture seule** (zéro migration, zéro écriture). KPIs pool (sources, kinds, langues, **histogramme DR** 5 buckets + N/A via `dr ?? da`), **risques** (footprint ≥3 sites, toxiques/perdus site vs org, à disavow), **campagne du site actif** (compteurs par statut + cadence), **top 5 opportunités** non prospectées (score), **activité récente** (prospects créés + passages live), **actions suggérées** dérivées (perdus → toxiques → prospects → opportunités) avec drill-down vers recherche/planning. Entrée sidebar « Overview » en tête de section. *Livré 2026-06-10, vérifié live (Acieb 0 backlink vs ADVMFR 380 prospects, switch de site, drill-down).* Différé : drill-down filtré par query-params, répartition thématique (pas de données), TF/CF, cache d'agrégats
- ✅ **Catalogue global curé + vue god-mode** (reliquats bug-fix) — `link_sources.global` (migration **0022**) : seul le super-admin flag les sources « 🌐 » de son org ; le **provisioning des nouvelles orgs copie le catalogue global** (fallback fixtures si vide, copies clients non-globales) ; vue god-mode « **Toutes les orgs** » (colonne Org, lecture seule) sur `/backlink/reservoir`. Backfill dev fait (380 sources eleva seo → global) ; ⚠️ prod : même backfill (runbook ELEVA-41). + fixes : tri **effort** par difficulté, lien **gabarit CSV** visible. *Livré 2026-06-10, vérifié live (toggle 🌐, vue 3 orgs × 380).* 
- ⬜ **import-export** — re-scopé en issue Linear dédiée (CdC `export-import.md` : jobs async, profils de mapping, preview/diff, round-trip GSC — l'import CSV réservoir et l'export disavow existent déjà)
- ⬜ **Profil backlink du domaine** — ingérer les exports Semrush Backlink Analytics (`Page ascore, Source url, Target url, Anchor, …`) déjà en main (`~/Downloads/export-kw/aciebenergie.fr-backlinks*.csv`) → analyse des liens entrants de NOS sites

## Products — ⬜ section non démarrée

planning · online · stats · import-export · overview

## Checklist — ✅ amorcé

flight-checker (clone specification.website) + audit + MCP privé/public-limité

## Backlog ciblé (dette / améliorations)

### Keywords / Recherche / GSC
- ✅ **Design toolbar partout** ([ELEVA-87](https://linear.app/elevaseo/issue/ELEVA-87) / [88](https://linear.app/elevaseo/issue/ELEVA-88) / [89](https://linear.app/elevaseo/issue/ELEVA-89)) : kit générique `src/components/ui/design-toolbar.tsx` extrait du réservoir (zéro régression), puis appliqué à **Keywords/Recherche** (facets + bande GSC, toggle Liste⇄Matrice, menu ⋯, Sync GSC primaire, `filters-bar` legacy supprimée), **Backlink** (réservoir avec god-mode intact, profil sur pagination serveur, recherche ; planning kanban non forcé, `backlink-filters-bar` supprimée) et **Contenu** (board kanban⇄liste avec persistance, online ; recherche client pure ajoutée). *Livré 2026-06-11 (52bc045 / d724504 / 80fd301).*
- ✅ **Véracité data design** ([ELEVA-81](https://linear.app/elevaseo/issue/ELEVA-81) / [82](https://linear.app/elevaseo/issue/ELEVA-82) / [83](https://linear.app/elevaseo/issue/ELEVA-83)) : timestamps d'import persistés (« Synchronisé/MAJ/Maj » réels), croissance avec fallback `trendDeltaPct` + sparklines colorées par pente + delta modale dérivé du graphe, trend Semrush mis à l'échelle du volume à l'import (Rust + wasm rebuild ; garde UI pour les données legacy). Reliquats trackés : ELEVA-84/85/86. *Livré 2026-06-11 (0d10a87 / 80dbfb6 / ae26ba6).*
- ✅ **Design réservoir — toolbar + tableau** ([ELEVA-79](https://linear.app/elevaseo/issue/ELEVA-79)) : handoff Claude Design implémenté — recherche large permanente + ⌘K, dropdowns de filtres avec chips actifs effaçables (« Tout effacer »), « Importer ▾ » unique (manuel/lourd/GSC) + « + Ajouter » primaire, méta « N résultats · M filtres » + pagination compacte, table dense (donut KD coloré, sparklines avec aire, croissance ↗/↘, pills statut + badges R/P fusionnés, volume « 1,3 k », actions au survol), barre d'actions groupées sticky. `data-table.tsx` étendu en additif (pagination contrôlée, stickyHeader) sans toucher les autres consommateurs. *Livré 2026-06-11 (6435589).*
- ✅ **Design Paramètres — Compte & Équipe** ([ELEVA-92](https://linear.app/elevaseo/issue/ELEVA-92)) : `SettingsShell` (nav latérale Compte·Équipe·Intégrations + colonne lisible 680px) ; cartes restylées ; sessions lisibles (`formatUserAgent`, session courante surlignée) ; email gardé fonctionnel (pas downgradé en locked) ; navbar alignée Paramètres/Compte/Équipe. Tout le câblage Better Auth conservé. *Livré 2026-06-11 (81b2936).*
- ✅ **Design modal « Détail mot-clé »** ([ELEVA-80](https://linear.app/elevaseo/issue/ELEVA-80)) : le drawer devient un modal centré ~940px (même API `KeywordSheet`) — header (fil d'ariane, h1, chip locale, pill statut, étoile Promu, menu ⋯), toolbar d'édition (statut/thème/intent segmenté/switch promu), KPI strip 6 cases (Volume·KD donut·CPC·Position·Clics·Impressions), tendance 12 mois (area chart + delta), « Page qui ranke » (badge pos + minis + URL + GSC par pays), sources connectées/vides regroupées, footer (Planifier du contenu, Marquer prioritaire = shortlist, Voir la recherche). Briefs conservés. *Livré 2026-06-11 (49b2af4).*
- ✅ **Retrait de la page legacy « Recherche (staged) »** : route + entrée sidebar + prop `stageFilter` supprimés — *livré 2026-06-11 (433b694)*
- ✅ **Sync GSC → Recherche** : overlay + promotion stage research + auto-import positionnés ≤20 + filtre opérateurs (`"`/`()`/OR/AND) — *livré 2026-06-09*
- ✅ **N/A métriques** : Volume/KD/CPC affichent N/A (pas 0) quand pas de source SEO — *livré*
- ✅ **Pagination** (shadcn) sur Recherche + Réservoir keywords + Réservoir backlink — *livré*
- ✅ **Export enrichissement** : listes de termes chunkées par outil (Mangools 700 / Semrush 2000 / Ahrefs 10000), groupées par pays, txt/zip — *livré*
- ✅ **Exclure des mots-clés GSC indésirables** : delete + table `keyword_exclusions`, le sync skip les slugs exclus (pas de recréation) — *livré*
- ✅ **Métrique cross-source** : colonnes consensus Vol/KD/CPC ✦ (médiane semrush/mangools/ahrefs), opt-in — *livré*
- ✅ **Un-exclude UI** : bouton « Exclus (N) » sur Recherche → dialog listant les exclus (slug humanisé) + « Réintégrer » (lève le blocage ; le mot-clé revient au prochain sync s'il rank) — *livré 2026-06-09*
- ✅ **Recherche — pays du site + Shortlist (F8) + Saisonnier (F7)** : la matrice/ciblage utilisent `site.targetCountries` (FR pour Acieb, plus le set proto US/GB/CA/AU) ; table `keyword_shortlist` (panier serveur par site) + bouton « + Shortlist » / « Shortlist (N) » / `Sheet` (retrait + Vider) ; colonne « Saisonnier » opt-in (🌟 pic dans N mois via `seasonalSuggestion`). **F6 (gaps/fan-out inter-pays) différé** (sans valeur tant qu'un site est mono-pays) — *livré 2026-06-09*
- ⬜ **Recherche — F6 gaps/fan-out inter-pays** : différenciation par pays (couverture croisée + fan-out d'un mot-clé sur plusieurs pays) — pertinent quand un site cible >1 pays
- ✅ **Nettoyage `keyword_target` legacy** ([ELEVA-56](https://linear.app/elevaseo/issue/ELEVA-56)) : dev vérifié déjà propre (23 ciblages, tous dans `target_countries`). One-off prod ajouté au runbook ELEVA-41 (`DELETE … NOT (kt.country = ANY (s.target_countries))`, idempotent). *Clos 2026-06-10.*
- ✅ **Export enrichissement — filtres-colonnes** ([ELEVA-52](https://linear.app/elevaseo/issue/ELEVA-52)) : prop `onFilteredRowsChange` sur le DataTable (filtered-row-model TanStack) ; sans sélection, l'export porte sur les lignes visibles (filtres + recherche). *Livré 2026-06-10 (fb0c3a5), vérifié live (filtre « boulangerie » → 10).*
- ✅ **Export enrichissement — statut de suivi** ([ELEVA-49](https://linear.app/elevaseo/issue/ELEVA-49)) : colonne `keywords.enrichment_exports` (outil→date, **migration 0020**), marquage au téléchargement, mode « enrichir » exclut les en-attente (retour détecté à l'import des métriques de l'outil), ligne « {n} déjà exporté(s)… en attente de retour ». *Livré 2026-06-10 (1d22fbe+b711add), vérifié live.* ⚠️ Prod : migration 0020 (couverte par `db:migrate`)
- ✅ **Export données complètes** ([ELEVA-53](https://linear.app/elevaseo/issue/ELEVA-53)) : bouton « CSV complet » (15 colonnes, consensus N/A, positions GSC, BOM UTF-8, quoting RFC-4180), même scope que l'export enrichissement. *Livré 2026-06-10 (5641da8).*
- ✅ **Reliquats bug-fix keywords** — colonne « **Promu** » (badges R/P, visible par défaut, réservoir + recherche) ; **retrait du planning** (icône ✕ inline sur la Chambre de Briefing, action `removeKeywordFromStageAction` testée DB). Vérifié : pas de double colonne KD (consensus « ✦ » opt-in). *Livré 2026-06-10.*
- ✅ **Recherche — import direct** ([ELEVA-48](https://linear.app/elevaseo/issue/ELEVA-48)) : destination « Envoyer vers : Réservoir / Recherche » sur l'import CSV **et** l'ajout manuel (miroir GSC, convention R1) ; `upsertKeywords` merge les stages par union SQL (jamais de perte de stage). *Livré 2026-06-10 (6cb45cd), vérifié live.*
- ✅ **Consensus — source `manual`** ([ELEVA-54](https://linear.app/elevaseo/issue/ELEVA-54)) : décision — `manual` **compte** dans la médiane (acte délibéré, souvent là où les outils n'ont rien) ; `gsc` reste exclu. Aucun chemin n'écrit `metrics.manual` à date → comportement inchangé, consensus prêt. *Livré 2026-06-10 (4cfaf3a).*
- ✅ **SERP import — filtre par URL** ([ELEVA-50](https://linear.app/elevaseo/issue/ELEVA-50)) : `isNonCompetitorDomain` (13 domaines vidéo/sociaux + sous-domaines + suffixe `.gouv.fr`) appliqué dans `parseSerpExport` avant stockage, positions originales préservées. *Livré 2026-06-10 (d10569f), vérifié live + DB.*
- ✅ **SERP import — > 1 Mo** ([ELEVA-51](https://linear.app/elevaseo/issue/ELEVA-51)) : upload route `POST /keywords/serp/import` (FormData, session + tenancy keyword→site→org), panel branché dessus (cap 25 Mo), action déléguée au core `serp-store.ts` ; tenancy ajoutée au passage sur `/backlink/profile/import`. *Livré 2026-06-10 (68be7d9), vérifié live (1,3 Mo sans 413).*
- ✅ **Persistance du choix de vue** Kanban/Tableau ([ELEVA-61](https://linear.app/elevaseo/issue/ELEVA-61)) — localStorage `seo-insight:content-view`, hydratation post-mount (pas de mismatch SSR). *Livré 2026-06-10 (856c6d3).*
- ✅ **Planning keywords — re-sync après import** ([ELEVA-67](https://linear.app/elevaseo/issue/ELEVA-67)) : les pastilles SERP/On-page/Brief ne se mettaient pas à jour après un import dans la sheet — `useState(initialRows)` figeait les données au mount (`key={siteId}` inchangé → pas de remount). Fix : rows lit la prop, la sheet dérive sa ligne par id. *Livré 2026-06-10 (b349213).*

### Analytics (`services/analytics/`)
- ✅ **Normalizer Ahrefs** : 3ᵉ source d'import (export Keywords Explorer / SERP, **CSV UTF-8 requis**, dédup des lignes SERP par mot-clé) ; alimente le consensus cross-source — *livré 2026-06-09*
- ⬜ **R2 staging/prod** : buckets R2 séparés par env (`seo-exports-staging`/`-prod`) + `ANALYTICS_URL` déployé (cf. `services/analytics/README.md`)
- ⬜ **Dashboards analytiques** (au-delà de l'ETL : visualisations sur les données curées)

### Backlink
- ✅ **Profil backlink — contrôles pagination shadcn** ([ELEVA-65](https://linear.app/elevaseo/issue/ELEVA-65)) — footer harmonisé avec le `Pagination` shadcn (Précédent/Suivant, miroir du DataTable). *Livré 2026-06-10 (43c4a66).*

### Support / Staff
- ✅ **Ticketing support MVP** ([EPIC ELEVA-103](https://linear.app/elevaseo/issue/ELEVA-103) · T1 [104](https://linear.app/elevaseo/issue/ELEVA-104) · T2 [105](https://linear.app/elevaseo/issue/ELEVA-105)) : système de tickets in-app. *client* = utilisateur d'org, *staff* = super-admin. Tables `support_tickets`/`support_messages` (migration 0028). Client `/support` (création + liste + fil de discussion type chat, reply bloqué si resolved). **Staff `/support/staff`** (super-admin) = inbox triée priorité>date, filtres mine/non-assignés/ouverts, assign/self-assign (verrou concurrence), changement de statut, **réponse** dans le fil. Emails Resend = T3 (à faire). *Livré 2026-06-11 (1d435cd / 1701e0b).*

### The Checklist (audit + MCP)
- ✅ **Prompt de correction agent + `run_site_audit`** (v1.1 ELEVA-93) : bouton « Copier le prompt agent » sur `/flight-checker` (prompt prêt-à-coller : connexion MCP placeholder — jamais de clé réelle —, fail/warn détaillés, consigne MCP pour les ~117 manual, N/A à part, boucle fix → re-audit, garde-fous — builder pur `src/lib/spec/audit/fix-prompt.ts`) ; tool MCP **`run_site_audit(siteId)`** (première mutation : fetch du site + upsert une-ligne-par-site, **cooldown 60 s** anti-boucle qui renvoie l'existant sans re-fetch, 9 tools au total) ; helper partagé `persistSiteAudit`. Vérifié live (UI + endpoint : run réel puis note cooldown). Spec : `docs/superpowers/specs/2026-07-07-audit-fix-prompt-mcp-design.md`. *Livré 2026-07-07.*
- ✅ **Resync spec upstream + `search_spec_topics` + 4 checks headers** (suite ELEVA-93/98) : pipeline **`bun run spec:sync`** rejouable (fetch `checklist.md` upstream → diff rapporté → fetch des `.md` en mémoire, zéro écriture si un fetch échoue → contenu + `spec-source` + `checklist-data.ts` régénérés, orphelins supprimés, `--dry-run`) ; **clone resynchronisé 135 → 156 topics** (statuts du 2026-07-06 : `skip-links` → required, `view-transitions`/`visibility-aware-rendering`/`invoker-commands` → optional ; HSTS réécrit sans `preload`) — les nouveaux topics arrivent « manual » dans l'audit ; tool MCP **`search_spec_topics`** (full-text déterministe titre/slug/résumé/corps, extraits, limit 1-25 — 8 tools au total) ; **+4 checks headers** (COOP/COEP/CORP, Reporting-Endpoints, Trusted Types via CSP, Server-Timing → registry 39 auto-checks). ⚠️ Les scores des sites déjà audités peuvent baisser au prochain audit (nouveaux warns). Reste-à-faire : cadence de resync (manuel pour l'instant, mensuel suggéré par l'upstream — cron/CI hors périmètre). Spec : `docs/superpowers/specs/2026-07-07-spec-resync-mcp-search-design.md`. *Livré 2026-07-07.*
- ✅ **MCP add-on + clés API** ([EPIC ELEVA-93](https://linear.app/elevaseo/issue/ELEVA-93) · D [101](https://linear.app/elevaseo/issue/ELEVA-101) · E [100](https://linear.app/elevaseo/issue/ELEVA-100) · clés par user [102](https://linear.app/elevaseo/issue/ELEVA-102)) : serveur MCP remote `/api/mcp` (Streamable HTTP, auth clé API hashée → org, gating entitlement `checklist_mcp`, 5 outils lecture scopés org) ; entitlement `org_addons` + clés `mcp_api_keys` **par user** (256 bits, sha256, one-time, compteur d'usage) ; page `/settings/api-keys`. Registry d'audit élargi à ~37 checks ([99](https://linear.app/elevaseo/issue/ELEVA-99)). **Score honnête** : l'anneau = conformité réelle (satisfait/total), plus le taux d'auto-checks trompeur. *Livré 2026-06-11.* ⚠️ Prod : `ANALYTICS_API_KEY` déjà noté ; le MCP add-on s'active par super-admin.
- ✅ **Design Checklist + clone du contenu specification.website** ([ELEVA-96](https://linear.app/elevaseo/issue/ELEVA-96) · [ELEVA-97](https://linear.app/elevaseo/issue/ELEVA-97)) : `/flight-checker` restylé (hero score + anneau, rail de catégories à barres de progression, filtres, lignes lisibles) ; **128 pages de specification.website clonées en local** (`src/content/spec/`, route interne `/flight-checker/spec/[category]/[slug]`, rendu markdown, liens « guide » internes — plus de redirection externe ; attribution retirée). Bug fix : suppression de site sans setState-in-render + état vide « Crée ton premier site ». *Livré 2026-06-11 (a6d706c / 8234615 / 3095a65).* Suivi refresh checklist ([ELEVA-98](https://linear.app/elevaseo/issue/ELEVA-98)) : ✅ soldé par le pipeline `spec:sync` (128→135→156, cf. entrée du 2026-07-07).
- ✅ **Audit auto par site + vue d'ensemble** ([EPIC ELEVA-93](https://linear.app/elevaseo/issue/ELEVA-93) · B [ELEVA-94](https://linear.app/elevaseo/issue/ELEVA-94) · C [ELEVA-95](https://linear.app/elevaseo/issue/ELEVA-95)) : « The Checklist » (clone de specification.website, 135 items) devient un **audit automatisé par site**. Moteur (`src/lib/spec/audit/`) : `fetchSiteSnapshot` (home HTML+headers, robots/sitemap/security.txt/llms.txt) + registry de 20 checks (foundations/seo/security/agent-readiness/i18n) + score, stockés en `site_audits` (migration 0025). UI `/flight-checker` site-aware : « Lancer l'audit » + anneau de score + badges pass/fail/warn/na, items de jugement en checklist manuelle **par site** ; onglet **Vue d'ensemble** cross-sites. Vérifié live (vercel.com → 100, 19/20 pass). Spec : `docs/superpowers/specs/2026-06-11-checklist-audit-overview-design.md`. *Livré 2026-06-11 (e8d3f21 / c55ebab).* **Suite** : serveur MCP add-on (D), gating entitlement (E), sync du contenu spec (A), extension du registry (~90 checks).

### Infra / dette
- ✅ **Audit DB — Batch A : tenancy par id brut + auth analytics** ([ELEVA-72](https://linear.app/elevaseo/issue/ELEVA-72)) : gardes `assertKeywordInActiveOrg`/`assertContentItemInActiveOrg` (complément d'ELEVA-45) sur 15 actions par id brut + filtrage serveur des tableaux d'ids bulk + transactions sur écritures couplées + bearer `ANALYTICS_API_KEY` sur le service analytics (fail-fast prod). ⚠️ **Prod** : définir `ANALYTICS_API_KEY` (même valeur) sur l'app Next et le service. *Livré 2026-06-11 (243766f).*
- ✅ **Audit DB — Batch B : chunking + namespacing** ([ELEVA-73](https://linear.app/elevaseo/issue/ELEVA-73)) : 6 bulk inserts chunkés (`src/lib/db/chunk.ts`, plafond ~65 535 params hors d'atteinte) ; ids `link_sources` namespacés `${orgId}__` à l'import (collision cross-org silencieuse corrigée) ; one-off dev appliqué (380 ids legacy renommés). *Livré 2026-06-11 (e056063).*
- ✅ **Audit DB — Batch C : index + reads + batch GSC** ([ELEVA-74](https://linear.app/elevaseo/issue/ELEVA-74)) : migration 0023 (8 index : `keywords_site_idx` + FK chaudes + partiel global) ; `getKeywordsForSite` sans `serp` (TOAST jamais mappé) ; sync GSC en `UPDATE…FROM (VALUES)` chunké. Différé : pagination serveur réservoir (conflit avec filtrage client de la toolbar), `hasBriefs` lazy. *Livré 2026-06-11 (3a07653/6df7210).*
- ✅ **Audit DB — Batch E : compression & cache** ([ELEVA-78](https://linear.app/elevaseo/issue/ELEVA-78)) : migration 0024 TOAST lz4 (5 jsonb keywords, bloc gardé `feature_not_supported`) ; hygiène DuckDB (`memory_limit`/`threads`/`preserve_insertion_order`) ; gzip CSV→R2 (`.csv.gz`, round-trip vérifié) ; cache overview keywords (`unstable_cache` + tag + TTL 30s, `updateTag` sur 8 mutations). Plan : `docs/superpowers/specs/2026-06-11-compression-cache-plan.md`. ⚠️ Reste à faire : ALTER SYSTEM Dokploy (default_toast_compression/wal_compression/shared_buffers…). *Livré 2026-06-11 (feb6eda/630c2cc/3e4960d).*
- ✅ **Audit DB — Batch D : robustesse analytics** ([ELEVA-76](https://linear.app/elevaseo/issue/ELEVA-76)) : DuckDB fermé en `finally` (`withConnection`) ; reaper jobs `running` orphelins (10min, boot+60s) ; `deleteObject` best-effort post-complete ; Zod env au boot ; Semrush country dérivé du site ; `totals.created/matched` (xmax) ; merge SQL du jsonb `serp` (anti lost-update). Différé : dédup SQL DuckDB (slugify NFD non répliquable). *Livré 2026-06-11 (ea28ec7…).*
- ✅ **Tests — suite combinée** ([ELEVA-44](https://linear.app/elevaseo/issue/ELEVA-44)) : `bun run test:all` — runner scripté (`scripts/test-all.ts`) qui découvre les répertoires de tests et lance **un process bun par répertoire** (isolation des fuites `mock.module` + de la pollution DB inter-fichiers, cf. [[bun-mock-module-global-leak]] / [[db-test-isolation]]) ; séquentiel (DB partagée), tableau récap, exit 1 au moindre échec. Le combiné single-process `bun run test` reste non-gate (41 échecs structurels). *Livré 2026-06-10.*
- ✅ **Tenancy-by-id** ([ELEVA-45](https://linear.app/elevaseo/issue/ELEVA-45)) : garde fail-closed `assertSiteInActiveOrg(siteId)` (`src/lib/sites/assert-site.ts`, React cache/requête) en tête de 38 actions serveur (keywords/content/backlink) ; gsc/sites/analytics déjà org-scopés (vérifié). Surface résiduelle : actions par `keywordId`/`contentItemId` seul (garde keyword→site→org en suivi). *Livré 2026-06-10 (3010772).*
- ✅ **Fuite de connexions Postgres (dev server)** ([ELEVA-42](https://linear.app/elevaseo/issue/ELEVA-42)) : HMR ré-évaluait `db/client.ts` → un nouveau pool postgres.js (max≈10) par recompilation, sans `idle_timeout` → ~100 connexions/h. Fix : `idle_timeout: 20` + `max_lifetime: 30 min` (app + service analytics) + singleton `globalThis.__pgClient` en dev. Vérifié live : 21→3 connexions sous charge. *Livré 2026-06-10 (dc91b0d).*
- ✅ **Durcir contre Google Translate** : le root layout servait `lang="en"` sur une UI FR → Chrome auto-traduisait, injectant des `<font>` qui cassaient le commit DOM de React (`NotFoundError: removeChild` → root gelé). Corrigé : `<html lang>` reflète désormais le `primaryLanguage` du **site actif** (résolu serveur via `getActiveSiteLanguage`, fallback `fr` pour les pages auth) + `translate="no"` (désactive l'auto-traduction). `getActiveOrgId`/`getSitesForOrg` wrappés en React `cache()` (root + (app) layouts partagent une lecture/requête). Vérifié live : `lang=fr`, `translate=no`, **0 font tag** (vs 1458 avant), page non traduite. *Livré 2026-06-10.*
- ✅ **Langue des imports analytics** : le worker passe désormais `site.primaryLanguage` à `processCsv` (plus de hardcode `lang="en"`) — *livré 2026-06-09, vérifié live (Ahrefs→fr)*. Données dev migrées (310 keywords Acieb en→fr, réservoir FR 120→430). ⚠️ **Prod** : après déploiement, rejouer la migration one-off `UPDATE keywords k SET lang=s.primary_language FROM sites s WHERE k.site_id=s.id AND k.primary_source IN ('semrush','mangools','ahrefs') AND k.lang='en' AND s.primary_language<>'en'`. Raffinement possible : surcharge par colonne source (`Country`/`Languages`) au lieu du défaut site (CdC export-import F1)
- ✅ **Cohérence des slugs** : unifié sur la **translittération** (é→e). Diagnostic corrigé — `wasm.slugify` (Rust) ET le slugify analytics **dropaient** les accents (« éclairage »→« clairage ») ; l'outlier était le slugify local de `actions.ts` (translittérait). Drop-accent ne fusionnait pas accentué/non-accentué (« électricité » vs « electricite »). Désormais : **Rust `slugify` translittère** (NFD via `unicode-normalization`, golden-fixture testée) → rebuild wasm ; l'app converge sur `wasm.slugify` (doublon `actions.ts` supprimé) ; **analytics** garde un miroir TS translittérant épinglé au Rust par un test golden partagé. **Migration dev rejouée** : 467→407 keywords (60 collisions fusionnées, p.ex. les 3 variantes « comparateur electricit(é) » → 1 keyword multi-sources mangools+gsc), 0 id drop-accent restant, 0 FK orphelin, exclusion `lectricit-entreprise-devis`→`electricite-entreprise-devis`. Idempotent. *Livré 2026-06-09.* ⚠️ **Dev server** : redémarrer pour charger le nouveau wasm (externe, pas HMR). ⚠️ **Prod runbook** : au déploiement, le build doit exécuter `bun run build:wasm` (le wasm est gitignored, pas dans git ; **aucun hook in-repo ne le lance** — vérifier la commande de build Dokploy), puis rejouer la migration une fois : `MIGRATE_SLUGS=1 bun --env-file=.env.local run src/lib/keywords/migrate-slugs.ts` contre la prod.
- ✅ **`db:migrate:test`** : corrigé (chargeait pas `.env.local` → `BETTER_AUTH_SECRET`) — *livré*
