# Backlog de durcissement pré-production

État à la fin de l'implémentation V1 (plugin 12/12 tâches, API 12/12, branches mergées dans `dev`).
Le code passe les reviews de branche : les points listés ici ne sont **pas** des bugs ouverts connus, ce
sont les écarts entre « correct » et « exploitable en production chez un client payant ».

Ordre de traitement recommandé : **Fiabilité restore** → **Exploitation** → **Distribution**.
Le restore est le seul chemin du produit qui peut détruire un site client ; tout le reste est
récupérable.

Légende : **[en cours]** = traité dans le lot de durcissement actuel, ne pas rouvrir en double.

---

## Lot 1 — Fiabilité restore (critique)

Un restore incorrect ne dégrade pas le service : il casse le site du client, souvent
irrécupérablement (le backup ayant justement été fait *après* le problème). Ce lot passe en premier.

### 1.1 Exclure les tables Action Scheduler du dump **[en cours]**

- **Où** : `plugin/src/Backup/DbDumper.php`
- **Risque** : le restore réimporte `actionscheduler_actions` / `actionscheduler_claims` **pendant**
  que la tranche de restore tourne dans Action Scheduler. On écrase l'état du scheduler qui pilote
  le restore en cours : action courante remplacée par une vieille ligne, claim perdu, restore
  interrompu au milieu de l'apply.
- **Fix** : exclure `{prefix}actionscheduler_%` de la liste des tables dumpées (le scheduler se
  reconstruit tout seul). Documenter que les jobs planifiés ne sont pas restaurés — c'est voulu.

### 1.2 `SET sql_mode` à l'import **[en cours]**

- **Où** : `plugin/src/Restore/RestoreRunner.php` (phase `import_db`)
- **Risque** : si le serveur cible tourne avec `NO_BACKSLASH_ESCAPES`, les valeurs échappées par le
  `DbDumper` (échappement backslash newline-safe) sont réinterprétées littéralement. Le contenu est
  corrompu silencieusement — pas d'erreur SQL, juste des données fausses en base.
- **Fix** : poser `SET SESSION sql_mode=''` (ou au minimum retirer `NO_BACKSLASH_ESCAPES`) à
  **chaque tranche**, au même endroit que `SET NAMES utf8mb4` et `FOREIGN_KEY_CHECKS=0` — les
  variables de session ne survivent pas d'une requête PHP à l'autre.

### 1.3 Borne en octets sur les chunks d'INSERT + BLOB en hexadécimal **[en cours]**

- **Où** : `plugin/src/Backup/DbDumper.php` (`CHUNK = 1000`)
- **Risque** : le chunk est borné à 1000 **lignes**, pas en octets. Une table à colonnes longtext /
  BLOB produit un `INSERT` dépassant `max_allowed_packet` : le dump s'écrit sans erreur, et c'est
  l'**import** qui échoue — donc à la restauration, quand le site est déjà en maintenance et
  partiellement écrasé.
- **Fix** : couper aussi sur un budget d'octets (~1 Mo, largement sous le `max_allowed_packet` par
  défaut) et sérialiser les colonnes binaires en littéraux hexadécimaux `0x...` plutôt qu'en octets
  bruts échappés.

### 1.4 `last_error` non vérifié sur `SHOW CREATE TABLE` / `CHECKSUM TABLE` **[en cours]**

- **Où** : `plugin/src/Backup/DbDumper.php`
- **Risque** : si `SHOW CREATE TABLE` échoue (permissions, table crashée), on écrit un dump sans
  `CREATE` mais avec des `INSERT`. Le manifest référence un dump structurellement invalide, et
  personne ne le sait avant le restore.
- **Fix** : contrôler `$wpdb->last_error` après ces deux requêtes comme c'est déjà fait par chunk
  d'INSERT, et faire échouer le dump de la table.

### 1.5 Skip de `wp-config.php` à l'apply **[en cours]**

- **Où** : `plugin/src/Restore/RestoreRunner.php` (phase `apply_files`)
- **Risque** : restaurer un `wp-config.php` d'une autre époque (ou d'un autre environnement)
  réécrit les credentials DB, les salts et le préfixe de tables. Le site ne redémarre plus, et
  l'admin n'a plus d'accès pour corriger.
- **Fix** : exclure `wp-config.php` de l'apply (et de la suppression des surnuméraires). À
  documenter comme comportement produit : la config n'est jamais restaurée.

### 1.6 `realpath` anti-symlink dans `applyFile` / `extraneousFiles` **[en cours]**

- **Où** : `plugin/src/Restore/RestoreRunner.php`, `plugin/src/Support/Paths.php`
- **Risque** : `Paths::absolute()` rejette les traversées `..` mais ne résout pas les liens
  symboliques. Un dossier symlinké à l'intérieur d'`ABSPATH` permet d'écrire — ou de **supprimer**,
  via la passe « fichiers surnuméraires » — en dehors de l'arborescence physique du site.
- **Fix** : `realpath(dirname($abs))` puis post-condition « le chemin réel commence bien par le
  `realpath(ABSPATH)` », sur l'apply **et** sur la suppression.

### 1.7 Watchdog cron du restore **[en cours]**

- **Où** : `plugin/src/Restore/RestoreRunner.php`
- **Risque** : le BackupRunner est relancé par son cron quotidien, qui appelle `start()` et reprend
  ainsi tout job dont le heartbeat a dépassé le TTL. Le restore n'a pas d'équivalent : si le process
  meurt en pleine phase `apply_files`, le site reste **en mode maintenance, à moitié restauré**,
  jusqu'à ce qu'un humain reclique dans l'admin.
- **Fix** : une action récurrente (courte, ~5 min) qui relance un step pour tout job de restore
  `running` dont le heartbeat est périmé, en réutilisant le pattern `recoverStale` gardé du
  BackupRunner.

### 1.8 Mode maintenance vs runner Action Scheduler

- **Où** : `plugin/src/Restore/RestoreRunner.php`
- **Risque** : le fichier `.maintenance` de WordPress court-circuite le chargement normal. Selon la
  façon dont Action Scheduler est déclenché (WP-Cron via requête HTTP), poser `.maintenance` peut
  empêcher les tranches **suivantes** de s'exécuter — le restore se bloque lui-même en maintenance.
- **Fix** : valider le comportement réel sous wp-env, et si nécessaire remplacer `.maintenance` par
  un drop-in de maintenance custom qui laisse passer le déclencheur d'Action Scheduler.

---

## Lot 2 — Exploitation

Ce lot ne protège pas des données, il protège du **support client**. Sans lui, chaque incident se
traduit par un ticket, parce que l'utilisateur n'a aucun moyen de savoir ce qui se passe.

### 2.1 Écran de statut des jobs

- **Où** : `plugin/src/Admin/` (nouvelle page ou section)
- **Risque** : aujourd'hui un backup qui échoue sur quota dépassé (402), token révoqué (401), ou qui
  skippe des fichiers illisibles, échoue **en silence**. L'utilisateur croit être sauvegardé alors
  qu'il ne l'est pas — le pire mode de défaillance d'un produit de backup.
- **Fix** : une vue lisant `wpb_jobs` : phase courante, avancement, `last_error`, nombre de fichiers
  skippés, horodatage du dernier succès. Couvre aussi le feedback manquant sur un `manifest_sha256`
  rejeté au déclenchement du restore.

### 2.2 Exclusion mutuelle backup ↔ restore

- **Où** : `plugin/src/Backup/BackupRunner.php`, `plugin/src/Restore/RestoreRunner.php`
- **Risque** : chaque runner a son propre verrou, mais rien n'empêche un backup planifié de démarrer
  pendant un restore. Le scan indexe alors un système de fichiers à moitié réécrit et le dump lit
  une base à moitié importée : on produit une snapshot cohérente en apparence, incohérente en
  réalité — qui deviendra le point de restauration « le plus récent ».
- **Fix** : un verrou partagé (ou une vérification croisée d'un job `running` de l'autre type) dans
  les deux `start()`. Le restore gagne toujours ; le backup est reporté, pas annulé.

### 2.3 Recovery sur `manifest_blobs_missing` (re-dump DB)

- **Où** : `plugin/src/Backup/BackupRunner.php` (phase `manifest`)
- **Risque** : l'API refuse un commit dont certains blobs référencés ne sont pas `stored` (400
  `manifest_blobs_missing`). Si le blob manquant est un dump DB (purgé par le GC, ou dont le confirm
  a échoué), le backup boucle sur l'échec de commit : les fichiers seront re-vérifiés, mais rien ne
  re-déclenche le dump de la table concernée. Aucun backup ne se termine plus jamais.
- **Fix** : sur `manifest_blobs_missing`, comparer les sha manquants au manifest DB
  (`wpb_db_manifest`) : invalider le checksum stocké des tables concernées et repasser en phase `db`
  pour les re-dumper, puis recommitter.

### 2.4 Cache et timeouts des pages admin

- **Où** : `plugin/src/Admin/SnapshotsPage.php`, `plugin/src/Api/Client.php`
- **Risque** : la liste des snapshots et l'usage appellent l'API en synchrone au rendu de la page. Si
  l'API est lente ou injoignable, l'admin WordPress se fige pour l'utilisateur (retries × timeout),
  jusqu'au 500 sur le tableau de bord.
- **Fix** : mettre en cache les réponses dans un transient court (1–5 min) et réduire le timeout HTTP
  sur les appels déclenchés par un rendu de page (par opposition aux appels des tranches de job, qui
  peuvent être patients).

### 2.5 Préflight espace disque

- **Où** : `plugin/src/Restore/RestoreRunner.php` (phase `plan`), `plugin/src/Backup/DbDumper.php`
- **Risque** : le restore télécharge tous les blobs manquants dans `wpb-tmp` avant d'appliquer. Sur
  un hébergement mutualisé avec quota, le disque se remplit **en pleine phase download** : plus rien
  n'est écrivable, y compris la base — le site tombe pendant l'opération censée le sauver.
- **Fix** : estimer la taille totale à télécharger à la fin du `plan`, comparer à
  `disk_free_space()`, et refuser de démarrer avec un message actionnable plutôt que d'échouer à
  mi-parcours.

---

## Lot 3 — Distribution

Rien de tout ceci n'est nécessaire pour que le code fonctionne ; tout est nécessaire pour le
**livrer**. À faire avant la première mise en vente, pas avant le premier test client.

### 3.1 Build : `composer install --no-dev` + vendor + zip

- **Où** : nouveau script à la racine ou dans `plugin/`
- **Risque** : `vendor/` est gitignoré. Un zip fabriqué à la main depuis le dépôt donne un plugin qui
  fatal-error à l'activation (`vendor/autoload.php` absent). Il n'existe aujourd'hui **aucun** moyen
  reproductible de produire l'artefact distribuable.
- **Fix** : un script de build : `composer install --no-dev --optimize-autoloader`, exclusion de
  `tests/`, `phpunit.xml.dist`, `.wp-env.json`, `composer.lock`, puis `zip` versionné depuis
  `WPB_VERSION`.

### 3.2 En-têtes du plugin : `Update URI`, `Plugin URI`, `Author`

- **Où** : `plugin/wp-back.php`
- **Risque** : sans en-tête `Update URI`, WordPress considère `wp-back` comme un plugin du dépôt
  officiel et peut **écraser le plugin commercial** par un homonyme de wordpress.org lors d'une mise
  à jour automatique. Les en-têtes `Plugin URI` / `Author` manquent aussi.
- **Fix** : ajouter `Update URI:` pointant vers notre domaine de mises à jour, plus les en-têtes
  d'identité. Prérequis de tout mécanisme d'auto-update.

### 3.3 i18n : `load_plugin_textdomain` + unification FR/EN

- **Où** : `plugin/wp-back.php`, `plugin/src/Admin/*`
- **Risque** : le text domain `wp-back` est déclaré dans l'en-tête mais jamais chargé, et les chaînes
  d'interface mélangent français et anglais (dont la confirmation JS du restore, non traduite). Un
  produit commercial multi-langue ne peut pas partir comme ça.
- **Fix** : `load_plugin_textdomain()` sur `init`, passer toutes les chaînes visibles par `__()` /
  `esc_html__()` avec le domaine, générer le `.pot`, choisir l'anglais comme langue source.

### 3.4 CI matricielle PHP 8.1 / 8.2 / 8.3

- **Où** : `.github/workflows/` (à créer)
- **Risque** : la cible déclarée est PHP 8.1 (`Requires PHP: 8.1`) mais la machine de dev tourne en
  8.5. Une syntaxe 8.2+ passe les tests locaux et fatal-error chez le premier client sur 8.1. Rien
  ne l'attrape aujourd'hui.
- **Fix** : workflow lançant `vendor/bin/phpunit` sur 8.1 / 8.2 / 8.3, plus `pnpm -C api test`,
  `lint` et `typecheck`.

### 3.5 `uninstall.php`

- **Où** : `plugin/uninstall.php` (à créer)
- **Risque** : la désinstallation laisse en base les tables `wpb_files` / `wpb_jobs` et les options
  (dont le **token de site**, un JWT en clair). Sur une revente ou une migration du site, le secret
  reste dans le dump SQL.
- **Fix** : `uninstall.php` supprimant les tables, les options (`wpb_site_token`, `wpb_plan`,
  `wpb_db_manifest`, réglages de planification) et les fichiers temporaires. Ne **jamais** toucher
  aux données distantes : la suppression des snapshots reste une opération backend.

### 3.6 Test d'intégration wp-env sur MySQL réel

- **Où** : `plugin/tests/` (nouvelle suite `Integration`)
- **Risque** : toute la suite actuelle (153 tests) tourne sur des doublures en mémoire et un `wpdb`
  factice. Trois choses ne sont donc prouvées nulle part : que le schéma passe `dbDelta`, que
  `WpdbFileIndex` fonctionne contre un vrai MySQL (types, collation, upserts `ON DUPLICATE KEY`), et
  que le couple dump → restore fait un aller-retour fidèle.
- **Fix** : suite d'intégration sous `wp-env` (`.wp-env.json` existe déjà) : activation +
  `dbDelta`, CRUD `WpdbFileIndex`, et un round-trip dump/import sur une base contenant des
  emoji, des BLOB et des valeurs à backslashes.

---

## API — suivi post-merge

Issues relevées à la review finale de la branche API. Aucune n'est bloquante en préproduction ;
**#1 et #2 sont à traiter avant l'ouverture commerciale**.

1. **Race commit ↔ sweep GC** — `api/src/routes/snapshots.ts`. Un blob peut être supprimé par le GC
   dans la fenêtre sub-seconde entre sa validation et l'`INSERT` de la snapshot, laissant une
   snapshot référençant du vide. Fix : ré-armer `created_at` des blobs `needed` pendant la
   validation du commit, avant l'`INSERT` (le sweep, conditionnel sur `created_at`, ne matchera
   plus).
2. **Signer `Content-Length` dans les presigned PUT** — `api/src/lib/wasabi.ts`. Aujourd'hui l'URL
   présignée n'impose pas la taille : un site compromis peut uploader un objet arbitrairement plus
   gros que sa réservation, à notre charge. Le `confirm` détecte l'écart et purge, mais après coup —
   le coût de stockage et de trafic est déjà encouru. Le plugin envoie déjà `Content-Length` : le
   signer côté API ferme l'abus en amont.
3. **Job de réconciliation `storage_bytes_used`** — recalcul périodique par préfixe (spec §6). Le
   compteur peut dériver : un `deleteObject` Wasabi qui échoue après la suppression de la ligne D1
   laisse l'objet orphelin sans décrémenter le compteur (sur-compte), et un crash entre les deux
   `UPDATE` du `confirm` sous-compte. Sans réconciliation, la dérive est monotone et finit par
   fausser la facturation.
4. **`snapshot_id` opaque ou séquencé par site** — `migrations/`. L'`AUTOINCREMENT` est global :
   deux snapshots d'un même site laissent déduire le volume d'activité de toute la plateforme entre
   les deux. Fix : identifiant opaque (nanoid) ou séquence par site.
5. **`resetRateLimits()` dans un setup de test partagé** — `api/test/`. Le rate limiter est un état
   de module ; l'oubli d'un reset rend un test flaky selon l'ordre d'exécution.
6. **Consolidation `SHA256_RE` + `assertValidManifest` en type guard** — export unique depuis
   `lib/wasabi`, et signature `(m: unknown, key: string): asserts m is Manifest` (aujourd'hui le
   paramètre est typé `Manifest`, ce qui rend les gardes de shape logiquement redondantes aux yeux
   du compilateur alors qu'elles valident de la donnée non fiable).
7. **DX** — passe `oxfmt` sur tout le projet ; commenter le placeholder `database_id` dans
   `wrangler.jsonc` au premier déploiement réel. (La subtilité `CI=1` de `wrangler d1 migrations
   apply` est désormais documentée dans `CLAUDE.md`.)
8. **Checklist du premier déploiement** — `d1 create` + `migrations apply --remote` ; les 3 secrets
   via `wrangler secret put` avec une **clé Wasabi scopée IAM au niveau objet** (pas de
   `ListBucket`) ; seed des buckets réels ; format haute entropie des `license_key` ; alerte sur
   échec du cron GC ; règle WAF de rate limiting sur `/v1/sites/activate` en complément du limiteur
   applicatif (en mémoire d'isolate, donc non global).

---

## V1.1 et suivi

Points connus, bornés, sans impact attendu en V1. À reprendre quand le produit sera stabilisé.

### Robustesse des runners

- **Ownership du mode maintenance** — `removeMaintenance()` est inconditionnel dans `failWith()` /
  `finishRestore()` : une tranche zombie pourrait retirer le `.maintenance` d'un restore plus récent.
  Même classe de problème que le verrou, et bornée par lui. Le garder naïvement risque de laisser un
  site verrouillé — d'où le report.
- **Heartbeat monotone** — `updateIfRunning` peut affecter 0 ligne si l'écriture est
  byte-identique dans la même seconde ; utiliser un horodatage strictement croissant (microtime).
- **Heartbeat intra-tranche sur les phases restantes** — il existe sur l'upload et sur
  plan/download ; l'étendre à `db`, `hash`, `manifest` et au parcours des fichiers surnuméraires.
- **Idempotence du commit côté API** — clé d'idempotence par `scan_id` / `manifest_sha256` pour que
  deux commits identiques ne créent jamais deux snapshots.
- **`done_with_warnings`** — un backup ou un restore ayant skippé des fichiers se termine en `done`,
  indistinguable d'un succès total. Introduire un état intermédiaire (dépend de 2.1).
- **Import DB non transactionnel** — un échec en milieu d'import laisse la base partiellement
  restaurée. Une transaction par table (moteurs transactionnels seulement) limiterait la casse.
- **Le restore réécrit le code du plugin en cours d'exécution** — appliquer les fichiers du plugin
  (et d'Action Scheduler) en dernier, ou les exclure de l'apply.
- **Listing persisté par scan** — le re-walk par lot reste O(N) même avec le pruning des dossiers
  exclus ; persister la liste des chemins au début du scan.

### Minors (triage de review, sans risque identifié)

- Plugin : `autoload = false` sur `wpb_site_token` / `wpb_plan` ; garde `is_array()` sur
  `json_decode` dans `Api/Client` ; purge des vieilles lignes de `wpb_jobs` ; `is_numeric()` sur
  l'heure de planification (une valeur non numérique donne 0 h au lieu du défaut 2 h) ;
  `function_exists('as_*')` dans `Schedule::apply()` ; une ligne d'exclusion `/` persistée comme
  chaîne vide ; `wp_die()` sans statut 403 ; confirmation du restore en JS uniquement.
- API : `Retry-After` sur les 429 ; normalisation du `ts` à l'insertion (le tri est
  lexicographique) ; validation syntaxique du `ts` (un 13ᵉ mois passe la regex) ; tiebreaker
  `ORDER BY ts, id` ; messages 401 distincts à unifier ; course `COUNT` → `INSERT` sur `max_sites`
  (dépassement d'un site au maximum, accepté).
- **Risque accepté à documenter dans la spec** : un tiers connaissant le couple (clé de licence, URL
  du site) peut provoquer une rotation du token de site — déni de service sur les backups du site
  jusqu'à réactivation, pas d'accès aux données.
