# Suivi des chantiers imports — follow-ups & registre SERP

- **Créé** : 2026-07-21, phase 5 (SERP UX)
- **Rôle** : ce fichier est **le point d'entrée du suivi** des chantiers imports. À mettre à jour à chaque avancée (cocher, ajouter une ligne, ouvrir une nouvelle section) — ne pas laisser l'information vivre uniquement dans des PR ou des commentaires de code.
- **Documents liés** :
  - `docs/project/2026-07-21-etat-des-lieux-imports.md` — inventaire complet (matrice capacité × surface, duplications, cible de rationalisation, plan de phases).
  - `docs/superpowers/specs/2026-07-21-import-routing-adr.md` — décision de routage `import-core` (Rust/WASM) vs `services/analytics` (DuckDB), et les points laissés ouverts (§4, §5, GSC).

---

## Règle d'or — formats de noms de fichiers SERP

> **On ne code JAMAIS un pattern de nom de fichier sans un exemple RÉEL d'export du provider.**
> Un nom de fichier « logique » ou déduit de la doc du provider n'est pas suffisant — les providers changent leurs conventions sans préavis (locale, version, plan). Un pattern non vérifié casse silencieusement la détection au lieu d'échouer proprement.

Le registre des patterns vit dans `src/lib/keywords/import/serp-filename.ts` (`SERP_FILENAME_PATTERNS`). Chaque entrée doit être accompagnée, en commentaire, d'un exemple réel de nom de fichier observé (avec sa date d'observation). Tant qu'aucun export authentique n'a été collecté pour un provider, ne pas ajouter de pattern — le fallback (pas de hint filename → picker de mot-clé) est le comportement correct et voulu.

---

## Registre des patterns SERP — état

| Provider | Statut | Pattern connu | Comment collecter |
|---|---|---|---|
| Mangools SERPChecker | ✅ codé | `serpchecker_<terme>_export.csv` — exemple réel observé le 2026-07-21 (ex. `serpchecker_jewelry_manufacturers_export.csv`) | — |
| SE Ranking | ⬜ à collecter | Inconnu — **le contenu** est déjà parsé (`parse_serp_urls_csv` gère les 2 layouts SE Ranking), mais le format du **nom de fichier** exporté par l'outil n'a jamais été observé | Demander à Bastien un export réel, ou le récupérer au prochain usage de l'outil |
| Semrush | ⬜ à collecter | Inconnu | Demander à Bastien un export réel, ou le récupérer si/quand l'outil est utilisé pour du SERP |
| Ahrefs | ⬜ à collecter | Inconnu | Demander à Bastien un export réel, ou le récupérer si/quand l'outil est utilisé pour du SERP |
| Autres déclinaisons Mangools / SERPChecker (locale, plan) | ⬜ à collecter si observé différent du pattern actuel | — | Comparer au pattern existant dès qu'un nouvel export Mangools est disponible |

**Règle d'ajout** : dès qu'un export réel arrive (Bastien ou usage terrain), l'ajouter à `SERP_FILENAME_PATTERNS` avec le nom de fichier en commentaire, puis cocher/mettre à jour la ligne correspondante ci-dessus.

---

## Follow-ups techniques ouverts (issus de l'ADR routage)

- [ ] **Migration brief YTG + catalogue backlinks vers route handler (cap 25 Mo)** — les deux passent encore par des Server Actions plafonnées à 1 Mo (artefact de plateforme, pas une limite fonctionnelle). Cible : tout import fichier via route handler, cap 25 Mo minimum. *(ADR §Caps upload — follow-up non fait, à planifier.)*
- [ ] **Décision produit — quota billing imports keywords** — `lockOrgForQuota` couvre les 2 imports backlinks mais pas le CSV keywords lourd ni GSC. Trois options posées par l'ADR (§4, non tranchées) :
  - (a) étendre `lockOrgForQuota` au chemin `services/analytics` (nécessite un hook async, le job tourne hors requête HTTP) ;
  - (b) accepter l'asymétrie et la documenter comme choix produit assumé ;
  - (c) quota au niveau du job (rejet à l'enqueue plutôt qu'à l'upload).
- [ ] **Double client GSC (TS vs Rust hors Drizzle)** — `src/lib/gsc/client.ts` (TS, user-driven, écrit `keywords.gsc*` jsonb sous Drizzle) et `crates/gsc-worker` (Rust, planifié, SQL brut `sqlx` hors Drizzle vers `gsc_query_snapshots`/`gsc_page_snapshots`) ne partagent aucun code. **Vigilance** : toute migration Drizzle touchant ces tables ne protège pas le chemin Rust (noms de colonnes en dur) — vérifier manuellement à chaque migration.
- [ ] **Trend Semrush perdu** — le module keywords du crate Rust (supprimé, code mort) calculait un `growthPct` que `processor.ts` fixe désormais à `null` en dur. Si le besoin produit revient, le porter dans `processor.ts` — pas de résurrection du module Rust.
- [ ] **Proxy résidentiel** : à brancher quand le taux de `blocked` (`crawlStatus`, `services/analytics/src/firecrawl.ts`) le justifie — `PROXY_SERVER`, conf Nouvelle-conf. Pas de plomberie de flags/UI à ajouter avant ça : le runner sémantique (`src/lib/semantic/analysis-runner.ts`) ne distingue aujourd'hui pas *blocked* de *timeout* dans `Coverage.missing` (un fetch en échec devient simplement `fetchOk:false`, sans raison) — donc pas de libellé « page protégée » côté `semantic-analysis-section.tsx` tant que ce flag n'existe pas réellement dans les données (cf. plan `2026-07-22-firecrawl-onpage-crawl.md`, Task 6).

---

## Livré (2026-07-21/22, phase 5 — SERP UX)

- [x] Détection du mot-clé cible par nom de fichier SERP (`extractSerpFilenameHint`, registre ci-dessus).
- [x] Fix propagation des updates vers la liste Planning après import.
- [x] Filtres par source (SERP / on-page / brief) sur la liste Planning.
- [x] Bouton « Importer » Planning en bleu (KeywordsToolbar complète montée dans Planning).
- [x] Ligne Search Console de la complétude : distinction « connecté sans positions » (En cours) vs « non connecté » (Manquant), bouton « Connecter » retiré (fix/gsc-readiness-overview).

## Livré (2026-07-22, crawl on-page via Firecrawl self-hosted)

- [x] **Crawl on-page → Firecrawl self-hosted** (`crawl.elevaseo.app`, plan `docs/superpowers/plans/2026-07-22-firecrawl-onpage-crawl.md`, spec `docs/superpowers/specs/2026-07-22-firecrawl-onpage-crawl-design.md`) : le fetch direct des pages concurrentes (crawl on-page + corpus sémantique) passe par `/v2/batch-scrape`/`/v2/scrape` Firecrawl, asynchrone via la queue `analytics_jobs` (job `crawl-onpage`, pattern `serp-semantics`), avec taxonomie d'échec par page (`crawlStatus`: `ok` / `blocked` / `timeout` / `empty` / `fetch_error`). Le fetch legacy en dur (`crawlOnpage`/`onpage-fetch` côté action) a été retiré une fois le worker en place (Task 6) ; `onpageFieldsFromDocument` (`src/lib/keywords/onpage.ts`) reste l'unique mapping pur consommé par le worker. `src/lib/keywords/onpage-fetch.ts` reste en place : il sert toujours le crawl de site (`src/lib/pages/crawl-runner.ts` / `services/analytics/src/site-crawl.ts`), une fonctionnalité distincte du crawl on-page SERP.

## À venir

- [x] **Script/automation du setup dev quotidien** LIVRÉ 2026-07-23 : `scripts/dev-firecrawl.sh` (tunnel + dev-all + helper `--enqueue <keywordId>`) + abréviation fish `sidev` — testé live (job enfilé, 7 pages crawlées).

- [x] **Heuristique js_rendered_suspected — faux positifs** LIVRE dev 2026-07-23 : cause racine = le parseur tl de semantic-core cassait sur un < de comparaison JS dans un <script>. Fix : sanitizeRawTextElements vide script/style avant parse (wordCount pos9 3->660) + split honnete js_rendered_suspected (pas de title) vs thin_content_suspected (title present) + libelle UI. Regex durcie post-review.
- [x] **Comptage blocked->failed** LIVRE dev 2026-07-23 : cause = matching URL exacte fragile aux redirects/slash. Fix : normalizeUrlForMatch + matchedPageKeys anti-double-comptage + ecrasement crawlHttpStatus. NB : restart du service analytics prod requis pour charger le code a jour.

- [x] **Worker analytics : pool de claim** LIVRÉ 2026-07-22 (hardening pré-merge prod) : `services/analytics/src/worker-pool.ts` (`startWorkerPool`, DI pur), câblé dans `index.ts` — `WORKER_CONCURRENCY` env, défaut 4, clamp [1,16].
- [x] **Résilience du poll batch + annulation** LIVRÉ 2026-07-22 (hardening pré-merge prod) : 3 polls `unreachable` consécutifs tolérés avant abandon, et `cancelBatchScrape` (DELETE /v2/batch/scrape/:id) appelé sur deadline/échec — plus de batch orphelin qui continue de frapper les domaines.
- [ ] **Distinguo 401/429 API Firecrawl vs page bloquée** dans scrapeOne (pollue le signal « taux de blocked » qui pilote la décision proxy).
- [ ] **Flags sémantiques blocked/timeout** : le runner ne distingue pas la cause d'un fetch null — nécessaire pour que les chips affichent « page protégée » (design §taxonomie, différé honnêtement).
- [ ] **Fenêtre RMW `keywords.serp`** : un import SERP de la même locale pendant un crawl de 5 min est écrasé par le snapshot stale au write final (rare, s'auto-répare au ré-import).
- [ ] **Hygiène suite keywords** : fuite mock.module inter-fichiers + inserts non idempotents (3 échecs préexistants), pattern de poll partagé à factoriser (ticks chevauchants).

- [x] **Page GSC dédiée** LIVRÉ dev 2026-07-23 (branche feature/gsc-page, 8 commits `c3c7aeb..d9bfbc0`, spec `docs/superpowers/specs/2026-07-23-gsc-dedicated-page-design.md`, review finale READY_TO_MERGE) : route top-level `/gsc` (sidebar « Search Console », i18n 4 locales) — card Connexion (états scope-aware, callbackURL /gsc), card Propriétés par site (mapping + sync manuelle avec dernière sync, avertissements échecs partiels pays/pages), card Santé des snapshots (runs du worker Rust, **réservée super-admin** — décision Bastien : `gsc_snapshot_runs` est globale cross-tenant). Réglages → Intégrations = simple renvoi (SE-Ranking y reste), tous les liens GSC re-routés (sources-modal, import-gsc-dialog, onboarding activation), `gsc-integrations.tsx` supprimé. Note OAuth : app passée en Production le 2026-07-23 → refresh tokens durables.
- [ ] **Follow-ups page /gsc** (review finale, non bloquants) : aria-labels distincts par site sur les Selects/boutons « Synchroniser » (a11y) ; pending par ligne au lieu du useTransition partagé (UX) ; tz d'affichage de la health card (RSC = heure serveur) ; si `/gsc` devient très fréquentée, pré-filtrer `isCurrentUserSuperAdmin` sur la session du layout avant le read DB.

- [x] **Connexion GSC scope-aware** LIVRÉ dev 2026-07-23 (`965cc49`+`1813791`, review Fable APPROVED) : cause racine prouvée en prod = compte Google lié sans cocher la case Search Console du consentement granulaire → `scope` en base sans `webmasters.readonly`, UI « Connecté ✓ » mensongère, `sites.list` avalé en silence (dropdown propriétés vide, aucun moyen de re-consentir). Fix : filtre scope dans `getOrgOwnerGscToken` + `resolveWorkerGscToken` (helper pur `src/lib/gsc/scope.ts`), statut `missingScope`/`propertiesError`, bouton « Reconnecter Google Search Console ». Vérifié dans la source Better Auth 1.6.23 : `linkSocial` sur un provider déjà lié **met à jour** la ligne `account` (tokens + scope) — le re-consentement suffit, pas d'unlink.
- [x] **Minors review GSC scope-aware** — CADUC depuis la bascule de l'autorité GSC au niveau du site (2026-07-26) : `owner.ts`/`token.ts`/`worker-token.ts` sont supprimés. (1) et (3) tombent avec eux — `deriveSiteGscState` (`src/lib/gsc/site-state.ts`) parcourt tous les comptes Google de l'autorité dans un ordre total et est couvert par `site-state.test.ts` ; (2) est traité par les raisons typées de `gscFailureMessage` ; (4) reste ouvert, toujours dans `src/lib/gsc/scope.ts`. Texte d'origine conservé ci-dessous pour l'historique.
  - ~~(1) `getOrgOwnerGscToken` ne regarde que le premier compte Google du candidat (`[0]`) alors que `orgHasGoogleAccountMissingScope` les regarde tous — asymétrie si un user a 2 comptes Google liés ; (2) copie member quand `missingScope` (« pas encore connecté » inexact) ; (3) `owner.test.ts` DB-gated à créer (fallthrough owner→admin + table de vérité `orgHasGoogleAccountMissingScope` non testés en réel) ; (4) exact-match `webmasters.readonly` — à élargir si le scope demandé évolue.~~

## Connecteur Mangools (chantier « Connecteur Mangools », 2026-07)

- **Mangools = source d'import API automatique** : enrichissement des métriques
  (volume/KD/CPC), récupération de la **SERP**, et mots-clés **associés**
  (related) passent par l'API Mangools quand le connecteur de l'org est branché
  (BYOK par org, token chiffré AES-GCM — voir `docs/project/ops/serveur-prod.md`).
  Déclencheurs auto à l'ajout et à l'analyse, throttle séquentiel 2 req/s.
- **CSV = fallback permanent** : aucun chemin d'import manuel n'a été retiré. Une
  org sans connecteur (ou un fetch auto qui échoue) continue d'importer
  métriques et SERP par CSV comme avant. Le connecteur **s'ajoute**, il ne
  remplace rien.

### Follow-ups ouverts (chantier Mangools)

- [ ] **Migration `organization.serankingApiKey` → rail `org_connectors`** : la
  clé SE-Ranking vit encore sur une colonne dédiée de `organization`, alors que
  Mangools inaugure le rail générique `org_connectors`. À unifier (SE-Ranking
  devient un connecteur parmi d'autres) — assumé hors périmètre du chantier
  Mangools, tracké ici.
- [ ] **Convention `growthPct` à figer** : recommandation **YoY moins 13 points**
  (`YoY-13-points`) — à acter comme convention unique cross-provider pour éviter
  les définitions divergentes de « croissance » entre sources.
- [ ] **Extension `NON_COMPETITOR_DOMAINS`** : décision **produit** cross-provider
  (la liste des domaines exclus du calcul concurrentiel doit valoir pour toutes
  les sources, pas seulement une). À trancher côté produit.
- [ ] **Kws ajoutés via le dialog `related` invisibles du sync bulk** : un mot-clé
  ajouté depuis le dialog « Mots-clés associés » arrive déjà enrichi
  (`metrics.mangools` non-null) et n'est donc **pas** repris par le sync bulk
  (qui ne cible que les non-enrichis) — il est bien **rattrapé par le sync
  par-ligne**. Comportement voulu, documenté pour lever l'ambiguïté ; à
  surveiller si la logique de sélection du sync bulk évolue.

---

*Convention : ce fichier est LE point d'entrée du suivi imports — le mettre à jour à chaque chantier (cocher les items terminés, ajouter les nouveaux). Voir l'état des lieux (`2026-07-21-etat-des-lieux-imports.md`) pour le diagnostic complet et l'ADR (`2026-07-21-import-routing-adr.md`) pour les décisions de routage actées.*
