# Règles du projet — consolidation de la mémoire

Export au **2026-09-03** des neuf entrées de mémoire projet, regroupées par thème pour être
retravaillées. Chaque règle garde sa date, son déclencheur (ce qui l'a fait naître), sa raison
d'être et sa mise en œuvre — c'est le déclencheur qui permet de juger si elle vaut toujours.

Source : `~/.claude/projects/-Users-humperdoo-Github-seoinsight-v2/memory/`. Ce fichier n'y est
pas relié : le retravailler ne modifie pas la mémoire, il faudra reporter les décisions.

**Statut** en tête de chaque règle : ✅ en vigueur · ⚠️ à revoir · 🗑️ périmée.

---

## 1. Interface et écrans

### 1.1 — Blocks officiels shadcn, jamais d'écran écrit à la main ✅
*2026-08-31*

Tout écran part d'un block officiel ui.shadcn.com ajouté **par la CLI**
(`bunx shadcn@latest add <block>`). Sign-in et sign-up actés : `login-05`, `signup-05`. Shell
acté : `dashboard-01` combiné à `sidebar-07`, sidebar `collapsible="icon"`.

**Avant de construire un écran, demander à l'utilisateur quel block ou quel layout il veut** —
ne pas choisir seul.

**Pourquoi** — cohérence avec la règle « CLIs officiels uniquement » ; l'utilisateur veut
arbitrer lui-même le rendu ; un squelette écrit à la main diverge des versions officielles.

### 1.2 — Composants réutilisables par défaut ✅
*2026-08-31*

Jamais de design refait par écran : toute l'app partage la même base. Un besoin qui revient
devient un composant partagé dans `web/components/`, réutilisé tel quel — pas de variantes
locales. Exemples actés : un composant chart unique, une table **toujours** avec recherche et
filtres.

Avant de créer un composant, vérifier s'il existe déjà ; s'il n'existe pas, le créer directement
comme composant réutilisable (props génériques), pas comme un cas particulier d'écran.

### 1.3 — Global avant le CSS spécifique ✅
*2026-09-02*

Toute décision visuelle se pose d'abord au niveau **global** — tokens de `web/app/globals.css`
(couleurs, polices, rayons, densité) et composants partagés — et seulement ensuite, s'il reste un
vrai besoin local, en classes utilitaires sur un écran.

Si un rendu doit changer partout, c'est un token. S'il revient deux fois, c'est un composant.
**Corollaire** : changer l'identité = changer les tokens, et tous les écrans non touchés en
héritent gratuitement.

### 1.4 — Responsive obligatoire, règle absolue 🔴 ✅
*2026-09-01 — déclenché par l'écran Keywords qui débordait, bouton d'import coupé hors écran*

> « Absolument mettre ça gravé dans la mémoire du projet. Tout doit être responsif. Là je vois
> que le layout ne l'est pas. On est pas bon. »

1. **La page ne scrolle JAMAIS horizontalement.** Précision de l'utilisateur : « on peut
   scroller À L'INTÉRIEUR des tables, mais on ne scrolle pas dans la fenêtre générale ». Une
   table vit dans son conteneur `overflow-x-auto` en restant `w-full` ; le défilement horizontal
   **dans** la table est légitime et attendu.
2. Les actions d'en-tête se replient sous le titre en largeur réduite — jamais coupées.
3. Les barres de filtres wrappent ; les largeurs fixes sont plafonnées sur petits écrans.
4. **Vérification obligatoire avant merge** à ~1280, ~768 et ~375 px. Un débordement horizontal
   est un finding **bloquant**, pas un mineur.

**Pourquoi** — l'app est utilisée en split-screen et sur portable par des agences ; un layout qui
déborde rend les actions inaccessibles. C'est un bug fonctionnel, pas cosmétique.

### 1.5 — Travail de fond toujours visible ✅
*2026-09-01 — déclenché par un écran « Import lancé » resté muet pendant tout le traitement*

Partout où un job tourne (import, synchro, crawl) :

1. **Spinner `Quantum` de ldrs** — `import { Quantum } from 'ldrs/react'`, taille 45, vitesse
   1.75. **Couleur définie une seule fois en token global** (`--color-spinner: #00D4B2`),
   consommée via `color="var(--color-spinner)"` — jamais de hex dans un composant.
2. **Toast sonner persistant en haut à droite** pendant toute la durée : pas d'auto-dismiss tant
   que le job tourne, mise à jour en place, survit à la navigation (provider dans le layout
   `(app)`, réhydraté depuis l'api au rechargement), succès cliquable vers le rapport.

Le toast a été arbitré contre un `AlertDialog` : un modal bloque, un travail de fond doit rester
ignorable.

**Pourquoi** — un écran statique pendant deux minutes d'import ressemble à un plantage.

### 1.6 — Badges et actions colorés, icônes, statuts ✅
*2026-09-02 — déclenché par l'écran Intégrations rendu entièrement en gris*

- **Les badges et les actions portent une couleur** sémantique. Un écran monochrome où
  « Connectée », « Enregistrer » et « Supprimer la clé » ont la même valeur visuelle est un
  défaut, pas de la sobriété. La hiérarchie des boutons doit se voir : action principale pleine,
  secondaire en contour, destructive en rouge.
- **Icône plutôt que texte quand le sens est évident**, pattern shadcn :
  `<Badge variant="secondary"><BadgeCheck data-icon="inline-start" />Verified</Badge>`. Icônes
  lucide, `data-icon="inline-start"` ou `"inline-end"`.
- **Statut = pastille + couleur + mot.** Vert = ok, ambre = en attente, rouge = erreur, neutre =
  non configuré. **Jamais la couleur seule** (accessibilité, daltonisme).
- **L'action est adjacente au statut**, pas reléguée ailleurs sur la carte.

**Mise en œuvre** — couleurs de statut en tokens globaux, jamais un `bg-emerald-500` dans un
écran. Piège identifié : ne pas créer deux verts voisins — l'accent de marque `#00D4B2` est déjà
un vert-cyan et sert de couleur de succès ; on distingue action et statut par le **traitement**
(fond plein = action, voile transparent + texte = statut), pas par deux teintes proches. L'ambre
reste réservé à l'attente.

### 1.7 — Pas de texte explicatif dans l'interface ✅
*2026-09-02*

Exemple rejeté par l'utilisateur, tel quel :

> Source préférée : Mangools — KWFinder — ses valeurs s'affichent en tête, aucune autre n'est
> masquée.

On reste simple, pas de superflu. Si une explication paraît nécessaire, **on la propose et on
demande validation** — on ne l'ajoute jamais de sa propre initiative.

**Pourquoi** — ces phrases sont écrites pour rassurer le concepteur sur une décision
d'architecture, pas pour l'utilisateur, qui n'a pas le problème qu'elles résolvent. Elles
alourdissent l'écran et se périment dès que le comportement bouge.

**Frontière** — un libellé, un badge, un message d'erreur actionnable, un tooltip court sur un
élément vraiment ambigu : oui. Une phrase qui explique une règle métier : non.

### 1.8 — Pas de pixel-perfect ✅
*2026-08-31*

On vise du responsive propre, pas la reproduction au pixel d'une maquette. Une maquette donne une
direction et une hiérarchie, pas des coordonnées.

---

## 2. Intégrations d'API externes

### 2.1 — Mini tests, jamais de batch, 2 éléments maximum ✅
*2026-09-02*

Sur toute API externe à quota, **toujours 2 éléments maximum par appel**. Vaut pour les sondes
manuelles, les scripts de capture et les tests écrits par les workers.

**Pourquoi** — le quota est réel et payé par l'utilisateur. Plan Mangools « combo » :
`related-keywords` 1200 unités/mois, **1 unité par requête et non par mot-clé**, débit plafonné à
3 requêtes par courte période. La forme d'un payload se lit aussi bien sur 2 éléments que sur 50.

**Mise en œuvre** — `--limit 2` sur toute capture, 2 mots-clés maximum par appel, espacement ≥ 3 s,
relire `limits.json` avant et après pour mesurer. Jamais de boucle d'appels réels. Les fixtures
déjà capturées doivent suffire. **À répéter explicitement dans le brief de tout subagent** qui
touche un adaptateur d'API externe.

> ⚠️ Nuance découverte le 2026-09-03 et à intégrer : les endpoints de **listes** KWFinder
> (`GET /kwfinder/lists`, `GET /kwfinder/lists/{id}`) ne consomment **aucun** quota — mesuré. La
> règle des 2 éléments reste bonne pour les tests, mais en production les gros lots sont
> souhaitables puisque le coût est par requête.

### 2.2 — Lien « où obtenir ma clé » pour chaque fournisseur ✅
*2026-09-02*

Chaque fournisseur d'API dans `/settings/integrations` porte un lien disant où trouver sa clé :
où créer un compte, et l'URL directe de la page du token.

**Mise en œuvre** — l'information vit dans la **définition du fournisseur** (libellé, URL
d'inscription, URL du token), pas en dur dans le JSX : un nouveau fournisseur hérite du bloc sans
qu'on y pense. Liens externes en `target="_blank"` + `rel="noreferrer noopener"`.

**Pourquoi** — sans ce lien, l'écran est un cul-de-sac et la fonctionnalité paraît cassée alors
qu'il ne manque qu'une adresse.

### 2.3 — Fixture réelle d'abord ✅
*Règle de spec, appliquée tout au long de T8*

Le parse d'une API non documentée s'écrit **sur des captures réelles**, jamais sur une
supposition. L'adaptateur reste verrouillé (`PARSER_WRITTEN = False`, exception levée avant tout
appel réseau) tant que les captures n'existent pas.

**Éprouvée le 2026-09-02** : la spec OpenAPI scrapée s'est révélée fausse à trois endroits
(`_id` absent du schéma mais présent dans la réponse, `msv` typé plat alors qu'il est en
triplets, timestamps en millisecondes). Un SDK généré hérite de ces erreurs.

---

## 3. Environnement de développement

### 3.1 — Pile unifiée sous d3k ✅
*2026-09-03 — remplace la règle « layout 3 panneaux Cmux »*

`mise run up` lance d3k autour des **trois** processus (api + worker + web) : les logs FastAPI,
worker, Next et navigateur arrivent dans un seul timeline horodaté, lisible par `d3k errors`,
`d3k logs`, `d3k fix`.

Points non évidents, tous appris à la dure :
- `-c` remplace la commande lancée **mais pas la détection de projet** : d3k doit tourner depuis
  `web/`.
- `--data-dir` épinglé sur `~/.d3k/seoinsight`, sinon le chemin des logs suit le répertoire
  d'invocation et tout ce qui les lit devient aveugle en silence.
- Pas de `-k` sur `concurrently` : il tuerait toute la pile quand on relance le worker seul.
- **`env -u DATABASE_URL` sur `api` et `worker`** : d3k est compilé avec Bun, Bun charge le
  `.env` du dossier courant (`web/`) et le transmet à sa descendance. L'api recevait l'URL de la
  base **AUTH**. Elle tiennent leur base de `api/.env`, jamais d'un environnement hérité.
- `fastapi dev` recharge seul ; le **worker** ne recharge pas et doit être relancé après un merge
  api.

### 3.2 — Cmux : lecture des panneaux ⚠️
*2026-09-01, partiellement périmée*

L'utilisateur travaille dans **Cmux**. Claude lit n'importe quel panneau via
`cmux read-screen --surface surface:N [--scrollback]`, liste avec `cmux list-panels`, écrit avec
`send-panel` + `send-key-panel Enter`. Utile aussi : `cmux notify`, et
`new-workspace --focus false` pour un test non intrusif (toujours nettoyer avec `workspace close`).

> 🗑️ **Périmé** : la partie « `mise run up` = layout 3 panneaux » ne vaut plus depuis 3.1 — un
> seul panneau suffit. La lecture des panneaux, elle, reste utile.

### 3.3 — Pièges récurrents à transmettre aux workers ✅

1. **Octet NUL littéral** dans un `.ts`/`.tsx` : le fichier devient **binaire pour git**, donc
   invisible dans tous les diffs. Vu deux fois. Toujours `" "` échappé.
2. **Base UI est strict** : `DropdownMenuLabel` exige un `<Menu.Group>` parent ; un
   `render={<Link/>}` sur un composant-bouton exige `nativeButton={false}` ; un spinner ldrs (des
   `<div>`) ne va **jamais** dans un `<p>`. Vu trois fois.
3. **Écritures atomiques** quand d3k sert l'arbre en direct : une édition en deux passes
   hot-reloade un état cassé chez l'utilisateur. Un seul write cohérent par fichier.
4. `grep -P` **n'existe pas** sur le grep de macOS et échoue en silence — vérifier les octets en
   Python plutôt qu'avec un grep Perl.

---

## 4. Décisions produit actées

| Date | Décision |
|---|---|
| 2026-09-01 | **Multi-sources sans hiérarchie (A5)** — la cellule montre la valeur de la première source de l'ordre avec sa pastille de provenance ; les autres au survol et dans le panneau. Aucune valeur élue, fusionnée ni moyennée. Pas de code couleur KD inter-sources : les échelles diffèrent par fournisseur. |
| 2026-09-01 | **Filtre Pays/Langue gardé à l'affichage (A6)** — le cloisonnement reste la loi à l'import. |
| 2026-09-02 | **Pas de synchro récurrente** — la machinerie (`import_schedules`, sweep) reste en place mais dormante, aucun réglage de cadence exposé. |
| 2026-09-02 | **Import API = listes KWFinder existantes uniquement.** Découverte d'un domaine et enrichissement par collage restent dans le code, retirés de l'écran. « Le reste sera une autre feature. » |
| 2026-09-03 | **CPC affiché en dollars** (`currencyDisplay: "narrowSymbol"` → « 3,05 $ »). L'api ne transporte pas de devise ; le jour où une source rendra des euros, il faudra qu'elle remonte du serveur. |
| 2026-09-03 | **Interface entièrement en français** — « Mots-clés », plus « Keywords ». |

---

## 5. Ce qui reste à trancher

- Les **trois libellés courts** signalés par un worker plutôt que glissés en douce : les deux
  descriptions de tuiles de source, et « Un import, une cible. » — à valider ou retirer.
- Un **import API réel de plus de 1000 lignes** reste à faire : seul moyen de vérifier la
  convention de pagination, jamais observée (trois gardes la protègent en attendant).
- L'**avertissement de clé React** sur le réservoir, non reproduit : se déclenche quand un import
  tourne pendant que le réservoir est ouvert (`JobsToaster` appelle `router.refresh()` à la fin
  d'un job suivi).
- La **passe « spec honnête »** sur l'OpenAPI (SEOINSIGHT-65), préalable à tout SDK client.

---

## Note de méthode

Une entrée de mémoire porte quatre choses : la **règle**, la **date**, le **déclencheur** — ce
qui l'a fait naître, souvent un écran raté ou un bug — et le **pourquoi**. C'est le déclencheur
qui rend la règle jugeable plus tard : sans lui, on ne sait plus si elle répond encore à un
problème réel ou si elle est devenue une superstition qu'on applique par habitude.
