# Connecteur Mangools (BYOK par organisation) — Design

- **Date** : 2026-07-25
- **Statut** : **DESIGN validé (brainstorm)** — prêt pour `writing-plans` après relecture Bastien.
- **Objectif produit** : supprimer les imports manuels par défaut. Le client branche **son** token API Mangools ; l'app enrichit les mots-clés (KWFinder) et récupère les SERP (SERPChecker) automatiquement, et offre la découverte de **mots-clés associés** dans Réservoir et Recherche.
- **Feature entièrement gratuite** côté seo-insight (aucun gating billing) ; les seules limites sont celles du plan Mangools du client.
- **Chantier parent** : rationalisation des imports (source unique). Le CSV manuel devient le **fallback** sans token.

## Décisions verrouillées (brainstorm)

| Sujet | Décision |
|---|---|
| Modèle | **BYOK** : le client apporte son token API Mangools (résout ToS/redistribution + quotas). |
| Portée token | **Par organisation**, géré owner/admin. Chiffré au repos. |
| Triggers auto | (a) ajout d'un mot-clé sans métriques → enrichissement auto ; (b) lancement d'analyse sémantique → fetch SERP auto. |
| Onboarding | Étape **optionnelle skippable** « Connecte tes outils » + CTA contextuels là où le token débloquerait la feature. |
| Nouvelle capacité | « **Mots-clés associés** » dans Réservoir ET Recherche (`GET /kwfinder/related-keywords`). |
| Fallback | Sans token : imports manuels actuels, rien ne casse. |
| MCP Mangools | Hors périmètre app (canal agent). À référencer plus tard dans les prompts générés. |

## API Mangools — faits validés par spike (2026-07-25, fixtures réelles)

Base `https://api.mangools.com/v3`, header `x-access-token`. Doc : apidocs.mangools.com.
Fixtures capturées (scratchpad session) : `fixture-locations.json`, `fixture-related.json`, `fixture-serps.json` — **à copier dans les fixtures de test du repo au moment du plan**.

- **`GET /kwfinder/related-keywords?kw=…&location_id=…`** → `{ _id, countKeywordsBeforeLimit, keywords[], language, location, source }`. Chaque keyword : `{ _id, kw, lid, sv (volume moyen 12m), svn, svs, cpc, ppc, msv (historique mensuel [année, moisIdx, volume] depuis 2015), seo (≈ KD, cachée), seo_ts, ts }`. **La difficulté (`seo`) est incluse** — pas de lookup séparé. Jusqu'à 10 000 kw par réponse.
- **`GET /serpchecker/serps?kw=…&location_id=…&platform_id=1|2`** → racine `{ kw, location{_id,code,label}, location_id, platform_id, rank, ctr[] (par position), items[], serpFeaturesImpact, serp_snapshot_id, serp_source_url, … }`. Items typés (`ORGANIC`, `PEOPLE_ALSO_ASK`, … — 41 types) ; par item organique : `{ url, title, desc, serpRank, ctr, domain, m: { moz{…}, majestic{ CitationFlow, TrustFlow, RefDomains, ExtBackLinks, … }, fb, rank } }`. ⚠️ **`location_id=0` retombe sur US (2840)** — toujours passer un id explicite.
- **`GET /mangools/locations?query=France`** → tableau `{ _id, name, canonical_name, country_code, target_type (Country/Region/City), google_domain, tld, code, label }`. **France = 2250**. Sans `query`, réponse vide — l'endpoint est une recherche.
- **`POST /kwfinder/keyword-imports`** (« details for a set of keywords », batch ~700) — pour l'enrichissement en masse. Forme de réponse à valider à l'implémentation (non spiké — 1 tâche du plan commence par la spiker et figer la fixture).
- **Quotas** : chaque requête consomme les « Keyword lookups » du plan client ; **requêtes identiques < 24 h non décomptées** (cache serveur Mangools). 401 = token invalide ; 402/429 = quota/rate limit.

## Architecture

### 1. Connecteurs d'organisation (générique, réutilisable)

- **Table `org_connectors`** (Drizzle, domain-schema) : `id`, `orgId` (FK), `provider` (`'mangools'` — enum extensible, accueillera `'anthropic'` pour le BYOK IA de Frame B), `encryptedToken` (AES-256-GCM keyed off `BETTER_AUTH_SECRET`, même approche que `encryptOAuthTokens`), `status` (`'active' | 'invalid'`), `connectedBy` (userId), `createdAt`, `updatedAt`. Unique (orgId, provider).
- **`src/lib/connectors/`** : `crypto.ts` (chiffre/déchiffre, pur), `store.ts` (CRUD server-only, org-scopé via `assertSiteInActiveOrg`-like), `actions.ts` (wrappers `"use server"` : connecter [valide le token par un appel léger `/mangools/locations?query=France`], déconnecter, statut). Seuls owner/admin écrivent ; tous les membres consomment.
- **Jamais** le token en clair vers le client ; les server actions ne renvoient que `{ connected, status }`.

### 2. Client API Mangools

- **`src/lib/mangools/client.ts`** (server-only) : `relatedKeywords(token, { kw, locationId, languageId? })`, `serps(token, { kw, locationId, platformId? })`, `keywordImports(token, kws[])`, `searchLocations(token, query)`. Fetch typé, erreurs taxonomisées : `unauthorized` (→ marquer le connecteur `invalid`), `quota_exceeded`, `rate_limited`, `upstream_error`. **Aucun retry automatique** sur `quota_exceeded` (c'est le quota du client).
- **Rate limiting (exigence Bastien : « on respecte les reqs/s »)** : la doc Mangools ne publie pas de limite chiffrée → throttle **conservateur côté client** : exécution **séquentielle** des appels d'un même job (jamais de fan-out parallèle sur l'API), token-bucket ~**2 req/s max** par org (constante configurable), et respect strict d'un `429` (backoff + `Retry-After` si présent, une seule reprise). Le throttle vit dans le client (`throttle.ts`, pur, testé) — aucun appelant ne peut le contourner.
- **`src/lib/mangools/locations.ts`** : résolution `country code (site) → location_id` — table de correspondance persistée (`mangools_locations` cache DB ou JSON seedé : les pays de `siteCountryOptions`), remplie via `searchLocations` à la première demande. France=2250 seedé d'office.
- **`src/lib/mangools/normalize.ts`** (pur) : réponse Mangools → nos formes internes (mêmes champs que l'import CSV remplit aujourd'hui : volume, KD, CPC, tendance → `keywords`; SERP items → la forme consommée par `runSerpSemantics`/import SERP existant). **C'est la clé de l'intégration : en aval, rien ne change.**

### 3. Enrichissement automatique (les 2 triggers)

- **Ajout d'un mot-clé sans métriques** (Réservoir/Recherche, dialogs + import) → job `mangools-enrich` (service analytics, file existante) : débounce/regroupe, appelle `keywordImports` en batch, écrit volume/KD/CPC/tendance via les upserts partagés existants. Idempotent ; skip si le connecteur est absent/invalid.
- **Lancement d'analyse sémantique** (`startSerpSemanticsAction`) : si pas de SERP fraîche pour (kw, pays), fetch `serps` → normalisation → même pipeline que l'import SERP actuel (URLs + métriques), puis la sémantique déroule. Fallback : comportement actuel (demande d'import manuel).
- Tout résultat est **persisté** (nos tables) — on ne re-consomme jamais un lookup pour une donnée qu'on a déjà ; le cache 24 h Mangools couvre les doublons résiduels.

### 3bis. Suivi de sync visible par mot-clé (exigence Bastien) — GÉNÉRIQUE par provider

**Exigence Bastien (2026-07-25)** : ce rail de suivi est **provider-agnostique**. Le statut d'enrichissement, le spinner par ligne, le bouton sync par kw et les entrées du menu Sync sont conçus pour N sources d'enrichissement : `mangools` aujourd'hui, **SE-Ranking branché sur le même rail**, et tout provider futur sans refonte (le statut porte la source ; l'UI ne connaît pas les providers, elle rend ce que le rail expose).

- **Statut d'enrichissement par mot-clé** persisté (`queued → syncing → done | error`, + horodatage) — même philosophie que la taxonomie `crawlStatus` du crawl. Le job met à jour le statut **au fil de l'eau** (par mot-clé, pas en fin de batch).
- **UI : spinner par ligne** dans Réservoir/Recherche qui s'actualise pendant la sync (polling léger/refetch de la table tant qu'il reste des `queued|syncing`, arrêt automatique quand tout est stable) — l'utilisateur suit la progression kw par kw ; `error` affiché avec cause courte (quota, introuvable…).
- **Bouton sync par mot-clé** dans les actions de la ligne (à côté de copier/ouvrir/supprimer) : re-enrichit CE mot-clé à la demande.
- **Sync global** : entrée « **Enrichir via Mangools** » (volume, KD, CPC, tendance) dans le menu Sync existant — même pattern que « Synchroniser GSC » / « Enrichir via SE-Ranking » (qui reste en place), avec la mention « Dernière sync Mangools : il y a X » dans le pied du menu. Porte sur la sélection, ou tout ce qui n'a pas de métriques si rien n'est sélectionné.

### 4. Mots-clés associés (Réservoir + Recherche)

- Entrée : bouton « Mots-clés associés » sur une ligne (et action de la toolbar sur la sélection).
- **`related-keywords-dialog.tsx`** (composant partagé monté sur les deux surfaces, comme les dialogs récents) : appel server action org-scopée → tableau trié par pertinence (kw, volume `sv`, KD `seo`, CPC), recherche/filtre local, cases à cocher, actions « **Ajouter au Réservoir** » / « **Envoyer en Recherche** ». Langue/pays = ceux du site actif (mêmes règles que les dialogs keywords : `siteLanguageOptions`/`siteCountryOptions`).
- Les mots-clés ajoutés sont **écrits avec leurs métriques** (déjà dans la réponse) — zéro lookup supplémentaire, pas de passage par le job d'enrichissement.
- Sans connecteur : le bouton affiche le CTA « Connecter Mangools » (lien réglages/onboarding).

### 5. Surfaces réglages & onboarding

- **`/settings/integrations`** : carte Mangools — état (connecté/invalide/absent), champ token (input masqué), **ajout ET remplacement** du token (chaque saisie déclenche le **check de validation** — appel léger `/mangools/locations?query=France` — avant d'écraser l'ancien ; échec du check = ancien token conservé + message), bouton déconnecter, lien « Obtenir un token » → mangools.com/api-token. Réservé owner/admin (lecture pour les autres).
- **Onboarding** : étape « Connecte tes outils » optionnelle et skippable — carte Mangools avec le bénéfice (« fini les imports manuels : volumes, difficulté et SERP en automatique »), même formulaire, bouton « Passer ».
- **CTA contextuels** : là où l'absence de token bloque l'auto (dialog d'analyse sémantique sans SERP, bouton mots-clés associés), un encart discret « Connecter Mangools ».

## Cas limites & erreurs

- Token invalide (401) → connecteur marqué `invalid`, bannière dans réglages + CTA reconnexion ; les surfaces retombent en mode manuel.
- Quota épuisé (402/429) → message explicite (« Quota Mangools de votre plan épuisé — réessayez demain ou importez manuellement ») ; **aucun retry auto** ; fallback manuel toujours disponible.
- Pays du site sans `location_id` résolu → tentative `searchLocations` ; si introuvable, fallback location France ? Non : **erreur claire + choix manuel de localisation dans le dialog** (pas de données silencieusement fausses).
- Réponses partielles/latence : timeouts courts (10-20 s), jobs asynchrones pour l'enrichissement (jamais bloquer l'UI d'ajout).
- Multi-orgs : le token est résolu par org active — jamais de fuite inter-orgs (tests dédiés).

## Tests

- `crypto.ts` : round-trip chiffrement (pur).
- `client.ts` : fixtures réelles du spike (locations/related/serps) + erreurs 401/402/429 simulées.
- `normalize.ts` : Mangools → formes internes (golden tests sur fixtures).
- `locations.ts` : résolution FR/pays présents, cache, introuvable.
- Job `mangools-enrich` : batch, idempotence, skip sans connecteur.
- Actions org-scopées : isolation inter-orgs.

## Hors périmètre (différé)

- SERPWatcher (rank tracking), LinkMiner/SiteProfiler (backlinks/domaine), AI Search Watcher — le connecteur les rend faciles à ajouter ensuite.
- MCP Mangools dans les prompts générés (chantier prompts).
- Connecteur `anthropic` (BYOK IA — chantier IA Frame B) : **utilisera la même table/le même rail** `org_connectors`.
- Suppression des imports CSV : jamais — ils restent le fallback permanent.
