# Récap — Keyword Reservoir → parité Backlinks (per-site)

**Date :** 2026-06-05
**Branche :** `feature/keywords-reservoir-parity` (depuis `dev`)
**Spec :** `docs/superpowers/specs/2026-06-05-keywords-reservoir-backlinks-parity-design.md`
**Plan :** `docs/superpowers/plans/2026-06-05-keywords-reservoir-backlinks-parity.md`
**Statut :** ✅ Terminé (+ itération de retours UI/DB). `tsc --noEmit` clean ; tests DB verts par dossier (keywords 29 / backlink 15 / db 1) ; `bun test` normal (DB skip) 0 fail. Mergé ff-only sur `dev`.

> ⚠️ Les tests DB se lancent **par dossier** (`bun test src/lib/keywords`). Les lancer tous en parallèle (`DB_TESTS=1 bun test`) provoque des races de truncate inter-fichiers (limitation pré-existante du harness, pas une régression).

## Objectif

Aligner `/keywords/reservoir` sur le réservoir backlinks (DB **et** design), **en gardant l'archi per-site** (conforme à `conventions.md` — seuls `backlink/reservoir` et `checklist` sont globaux). Exécuté en **sub-agents** (1 implémenteur + 2 reviewers — conformité spec puis qualité — par tâche).

## Ce qui a été fait

### 1. Primitive `Pill` partagée
- Extraction du style « dark-substrate » (`PILL` + `ACCENT`) des badges backlinks vers `src/components/ui/pill.tsx` (export `Pill`, `PillAccent`, `PILL`, `ACCENT`).
- `backlink/source-badges.tsx` ré-importe depuis ce module (zéro changement visuel côté backlinks).

### 2. Polish des badges keywords
- `status-badge.tsx` / `intent-badge.tsx` → pills dark-substrate (mapping accent par statut / intent).
- Nouveau `source-badge.tsx` (pill neutre) utilisé dans la colonne **Source** (remplace le texte brut).
- `KD`/`CPC` **inchangés** (dégradé HSL continu conservé — plus informatif que les buckets DA).
- Ajout de `"manual"` au **type** `KeywordSource` (colonne `text`, aucune migration). `KEYWORD_SOURCES` reste à 4 providers (la sheet n'affiche pas de carte « manual » vide).

### 3. Persistance des actions groupées
- Nouvelles server actions **site-scoped** dans `src/lib/keywords/actions.ts` :
  - `setKeywordsStatusAction(siteId, ids, status)`
  - `setKeywordsThemeAction(siteId, ids, theme)`
- `reservoir.tsx` : les handlers bulk écrivent maintenant en DB puis `refresh()` (fini la mutation d'état local éphémère), avec garde try/catch (pas de rejection non gérée).

### 4. Suppression groupée
- `deleteKeywordsAction(siteId, ids)` (DELETE site-scoped).
- Bouton **Supprimer** destructif + confirmation `AlertDialog` dans `bulk-actions-bar.tsx`, câblé dans `reservoir.tsx`.

### 5. Detail sheet éditable
- `updateKeywordAction(siteId, id, patch{status?,theme?,intent?})` — patch partiel site-scoped (les champs absents ne sont pas touchés ; `theme: null` appliqué).
- `keyword-sheet.tsx` : **statut / thème / intent** éditables (Select + toggles `<Pill>` dans un `<fieldset><legend>` a11y), persistance immédiate → `refresh()`. Métriques / tendance / SERP restent en lecture seule (import-owned).

### 6. Ajout manuel d'un mot-clé
- `addKeywordAction(siteId, {term, lang, country, theme?, intent})` → slug déterministe, rejet doublon / terme vide, insert `status:"idea"`, `primarySource:"manual"`, `sources:["manual"]`, `metrics:{}`, `trend:[]`. Slugify via `\p{Diacritic}` (robuste).
- Nouveau `add-keyword-dialog.tsx` (calqué sur l'`AddSourceDialog` backlinks) + bouton **Ajouter** dans `keywords-toolbar.tsx`.

### Correctif d'intégration
- `research/research-container.tsx` (2ᵉ consommateur de `KeywordSheet`, repéré par la review finale) mis à jour avec les props désormais requises `siteId`/`onSaved` → `tsc` clean.

## Tests
- `src/lib/keywords/actions.test.ts` (gated `DB_TESTS=1`, DB `seo_insight_test`) : 7 tests couvrant les 5 write actions, dont scoping par site (ids d'un autre site ignorés), patch partiel, id déterministe (`Éthique`→`ethique`), rejets doublon/vide.
- Suite keywords complète : **24 pass / 0 fail**. `tsc --noEmit` : **0 erreur**.

## Itération retours (post-parité)

Suite aux retours sur captures d'écran :
- **Sheet élargie** à `sm:max-w-[50vw]` (était trop étroite).
- **Création de catégories** depuis le dropdown Thème (dialog **et** sheet) via un `<ThemeSelect>` partagé (item « ➕ Créer une catégorie… » → form inline).
- **Thèmes persistés en DB** : nouvelle table per-site `keyword_themes` (`id=${siteId}-${slug}`, FK cascade, index siteId) + migration `drizzle/0002_wonderful_shotgun.sql` (appliquée dev + test, dev reseed = 15 thèmes). `theme-context` chargé par site via `fetchThemesForSiteAction` ; `create`/`rename` persistent (`createThemeAction` idempotent / `renameThemeAction`). `getThemesForSite` site-scoped. 4 tests DB (`themes.test.ts`).
- **Merge multi-source des métriques** : `upsertKeywords` fusionne le jsonb `metrics` (`||`) et union+dédup `sources` (`array_agg(distinct)`), au lieu de remplacer. Réimport même source = écrase cette source (latest wins) ; réimport autre source = coexistence. +1 test (`import.test.ts`).
- Correctif tri colonne Thème (thèmes créés triés par slug au lieu de se regrouper sous "").

## Commits (depuis le plan)
```
493b279 refactor(ui): extract shared Pill primitive from backlink badges
3d80fa0 feat(keywords): dark-substrate pills for status/intent/source + manual source type
b16ab85 refactor(keywords): drop dead fallback in SourceBadge
29950e4 feat(keywords): persist bulk status/theme changes to DB
0ddc183 fix(keywords): guard bulk handlers against unhandled rejection
4a26349 feat(keywords): bulk delete with confirm
6a5d5aa feat(keywords): editable detail sheet (status/theme/intent)
2f2c4e8 refactor(keywords): tidy update patch type + sheet a11y/deps
89aa4bd feat(keywords): manual single-keyword add (manual source)
18a9e1e test(keywords): assert intent persisted + fix add-dialog locale defaults
d3f31ff fix(keywords): pass siteId/onSaved to KeywordSheet in research view
7d71cf1 feat(keywords): creatable theme categories + wider detail sheet
4c23330 feat(keywords): merge per-source metrics on re-import
67b5251 feat(keywords): persist themes per-site in DB (create/rename)
0137736 fix(keywords): sort created themes by slug + refresh ThemeSelect doc
```
**Total branche : 28 fichiers, +3734 / −81.**

## Décisions notables
- **Per-site** conservé (pas de pool global type linkSources+backlinks).
- KD/CPC : dégradé continu gardé (parité = langage de pills cohérent, pas buckets identiques).
- `"manual"` ajouté au type mais **hors** `KEYWORD_SOURCES`.
- Convention « fire-and-forget » du codebase respectée (pas d'infra toast → erreurs swallow, comme `refresh()` existant).

## Hors scope (volontaire)
- Pool global / ciblage per-site.
- Persistance des préférences colonnes/filtres.
- Vue `research/` : édition de la sheet persiste en DB mais ses lignes restent mockées (proto) — limitation connue.
- Suppression de thèmes (seulement create/rename pour l'instant) ; pas de contrainte `UNIQUE(siteId,slug)` explicite (la PK `${siteId}-${slug}` la garantit déjà).
- `KeywordTheme` reste une union de littéraux côté type alors que les thèmes sont désormais dynamiques en DB (cast `as KeywordTheme` ; à élargir en `string` un jour).

## Fait dans cette itération
- ✅ Thèmes persistés en DB (create/rename per-site).
- ✅ Merge multi-source des métriques.
- ✅ Mergé ff-only sur `dev`, branche supprimée.
