# `/backlink/overview` — dashboard netlinking (lecture seule)

**Date:** 2026-06-10 · **Linear:** [ELEVA-64](https://linear.app/elevaseo/issue/ELEVA-64) · **CdC:** `docs/specs/backlink/overview.md`
**Décision de scope (validée):** ELEVA-64 est **scindé** — l'overview est livré maintenant ; **import-export** (CdC F1-F9 : jobs async, profils de mapping, preview/diff, round-trip GSC) devient une issue Linear dédiée, différée (l'import CSV réservoir et l'export disavow existent déjà).

**Périmètre v1:** F1 KPIs pool global · F2 ⭐ risques · F3 campagne du site actif · F4 top opportunités · F5 activité récente · F6 ⭐ actions suggérées · F7 drill-down. **Hors : toute écriture** (CdC) — zéro action serveur, zéro mutation.

**Réponses v1 aux questions ouvertes du CdC:** seuil footprint = `FOOTPRINT_THRESHOLD` existant (≥3) · seuil toxic = tout flag `toxic` · pas de cache dédié (agrégats SQL rapides à cette volumétrie ; cache différé) · répartition par **thème** différée (les 1140 sources seed ont `themes` vide — structurellement pas de données) · TF/CF différés (pas de colonne) → distribution **DR** (fallback DA).

---

## A. Données — `src/lib/backlink/overview.ts` (server-only)

`getBacklinkOverview(orgId, siteId): Promise<BacklinkOverview>` — **aucune migration** (CdC : « Aucune table propre »). Agrégats sur `link_sources`, `domain_ratings`, `backlinks`, réutilisant `relevanceScore`/`footprintAlert`/`FOOTPRINT_THRESHOLD` (relevance.ts) et `monthBounds` (planning-logic.ts).

```ts
export type DrBucket = { label: string; count: number }; // "0–20" | "21–40" | "41–60" | "61–80" | "81–100" | "N/A"

export type BacklinkOverview = {
  pool: {                       // F1 — global org
    sources: number;
    byKind: { kind: SourceKind; count: number }[];      // top 5, desc
    byLanguage: { language: string; count: number }[];  // desc
    drBuckets: DrBucket[];      // dr ?? da, bucket "N/A" si les deux absents
  };
  risks: {                      // F2 ⭐
    footprintDomains: { domainNorm: string; siteCount: number }[]; // ≥ FOOTPRINT_THRESHOLD, desc
    toxicSite: number; toxicOrg: number;     // backlinks toxic (site actif / org)
    lostSite: number;  lostOrg: number;      // status = "lost"
    disavowSite: number;                     // flag disavow non encore "traité" = juste le count
  };
  campaign: {                   // F3 — site actif uniquement (le switch de site global remonte la page)
    counters: Record<BacklinkStatus, number>;
    cadence: { thisMonth: number; lastMonth: number }; // réutilise getCadence
  };
  opportunities: {              // F4 — top 5 sources non prospectées par le site, triées score desc
    id: string; platform: string; domainNorm: string;
    kind: SourceKind; dr: number | null; da: number | null;
    language: string; score: number;
  }[];
  activity: {                   // F5 — 5 événements les plus récents du site, desc
    type: "prospect" | "live";  // prospect = backlinks.createdAt · live = firstSeen
    platform: string; domainNorm: string; at: string;  // ISO
  }[];
};
```

### Logique pure — `src/lib/backlink/overview-logic.ts` (TDD)

- `drBucketLabel(value: number | null): string` — bornes 0–20 / 21–40 / 41–60 / 61–80 / 81–100, `null` → `"N/A"`.
- `buildDrBuckets(values: (number | null)[]): DrBucket[]` — toujours les 6 buckets dans l'ordre (counts à 0 inclus).
- `suggestedActions(o: BacklinkOverview): SuggestedAction[]` (F6 ⭐) — dérivation déterministe, max 4, ordre de priorité :
  1. `lostSite > 0` → « {n} lien(s) perdu(s) à reconquérir » → `/backlink/planning`
  2. `toxicSite > disavowSite… ` — simplification v1 : `toxicSite > 0` → « {n} toxique(s) à examiner (disavow) » → `/backlink/planning`
  3. `counters.prospect > 0` → « {n} prospect(s) à planifier » → `/backlink/planning`
  4. `opportunities.length > 0` → « {n} opportunité(s) à prospecter » → `/backlink/recherche`
  - type `SuggestedAction = { label: string; href: string }` ; liste vide → carte « Rien à faire ✅ ».

## B. UI — `src/components/backlink/overview/`

- **RSC** `src/app/(app)/backlink/overview/page.tsx` — même squelette que planning/page.tsx (org → sites → site actif → `getBacklinkOverview` → vue, `key={siteId}`). Breadcrumb Backlink → Overview. **Sidebar** : « Overview » en tête de la section BACKLINK.
- **Lecture seule** : composants **serveur** purs (aucun `"use client"` nécessaire — pas d'état, pas d'action), grille de cartes mirroir du pattern `keywords/overview` (`StatCard` réutilisé tel quel via import croisé).
- Cartes :
  1. **Pool** (F1) — StatCards : sources, top kinds (badges), langues, mini-histogramme DR (barres div, pattern `kd-histogram`).
  2. **Risques** (F2) — domaines footprint (badge ⚠️ {siteCount} sites), toxiques site/org, perdus site/org ; chaque ligne **lien** vers la sous-page (F7).
  3. **Campagne** (F3) — compteurs par statut (Prospect · Planifié · Outreach · Live · Perdu) + cadence « {thisMonth} ce mois · {lastMonth} le mois dernier ».
  4. **Opportunités** (F4) — top 5 : platform, domainNorm, DrBadge, score → lien `/backlink/recherche`.
  5. **Activité** (F5) — 5 lignes « {platform} — prospect ajouté / passé live · {date} ».
  6. **Actions suggérées** (F6 ⭐) — liste de liens dérivés (`suggestedActions`).
- **F7 drill-down** : liens `next/link` simples vers `/backlink/recherche` et `/backlink/planning` (les sous-pages n'ont pas encore de query-params de filtre — drill-down « filtré » différé avec elles).
- Zéro state : pas de site actif → carte vide « Sélectionnez un site ».

## C. Tests

- **Purs (TDD)** : `drBucketLabel` (bornes exactes 20/21, 100, null) · `buildDrBuckets` (6 buckets ordonnés, counts 0) · `suggestedActions` (priorité, max 4, vide).
- **DB** : `getBacklinkOverview` — pool counts/byKind/byLanguage/drBuckets (avec DR partagé + fallback DA) · risks (footprint ≥3 cross-site, toxic/lost site vs org) · campaign counters · opportunités excluant les sources déjà prospectées par le site · activity ordonnée desc.
- **Live (d3k, Acieb)** : page rendue avec les 380 sources réelles, compteurs cohérents avec /backlink/planning, liens drill-down fonctionnels, switch de site.

## Hors scope

Toute écriture · import-export (issue Linear dédiée) · répartition thématique (pas de données) · TF/CF (pas de colonnes) · drill-down filtré par query-params · cache d'agrégats · sélecteur de site local à la page (le switcher global suffit, pattern des autres pages) · API Hono / MCP tools (CdC §8/§10 — différés avec la plateforme API).
