Pārlūkot izejas kodu

docs: mise à jour CDC v5.0 — Phase 5 complète

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cedric Hansen 2 nedēļas atpakaļ
vecāks
revīzija
a2b623d9f9
1 mainītis faili ar 182 papildinājumiem un 147 dzēšanām
  1. 182 147
      doc/CDC_backupmanager_ynh.md

+ 182 - 147
doc/CDC_backupmanager_ynh.md

@@ -1,5 +1,5 @@
 # Cahier des charges — `backupmanager_ynh`
-**Version 4.1 — 2026-05-09**
+**Version 5.0 — 2026-07-17**
 
 ## 1. Vision générale
 
@@ -30,8 +30,9 @@ Application YunoHost centralisant toutes les sauvegardes d'un serveur, s'appuyan
 | Nommage | `jerry_nocodb_20260508.tar` |
 | Auth dashboard | SSO YunoHost — admins uniquement |
 | Auth API | `/api` ouvert SSOwat + token `X-BackupManager-Key` |
-| Transfert | HTTP chunked (défaut) + SSH/rsync (optionnel) |
+| Transfert | HTTP chunked (fédération) + SSH/rsync (destinations SSH) |
 | Restauration | Complète : fichiers + user système + systemd + DB |
+| Destinations | Many-to-many : un job peut cibler plusieurs destinations |
 
 ---
 
@@ -46,28 +47,46 @@ backupmanager_ynh/
 ├── conf/
 │   ├── nginx.conf
 │   ├── systemd.service
-│   └── app.conf.j2
+│   ├── app.conf.j2
+│   └── sudoers
 ├── sources/
-│   ├── app.py                  # Flask routes + API REST
+│   ├── app.py                  # Bootstrap Flask + enregistrement blueprints
 │   ├── scheduler.py            # APScheduler
-│   ├── db.py                   # SQLAlchemy (Job, Run, Destination, Setting)
-│   ├── retention.py            # Moteur count/daily/gfs
+│   ├── db.py                   # SQLAlchemy (Job, Run, Destination, Setting, …)
+│   ├── retention.py            # Moteur count/daily/gfs + apply_ssh_retention
 │   ├── notifications.py        # Email SMTP (succès/erreur)
+│   ├── helpers.py              # read_archive_info, get_ynh_apps
+│   ├── blueprints/
+│   │   ├── jobs.py             # Dashboard local, CRUD jobs, archives, restauration
+│   │   ├── destinations.py     # CRUD destinations SSH
+│   │   ├── network.py          # Fédération, push/pull inter-instances
+│   │   ├── settings.py         # Paramètres SMTP, export/import config JSON
+│   │   ├── api.py              # API REST v1 (auth par token)
+│   │   └── overview.py         # Vue globale des archives (local + SSH + instances)
 │   ├── jobs/
-│   │   ├── ynh_backup.py       # yunohost backup create/restore
+│   │   ├── ynh_backup.py       # Point d'entrée execute_job, ynh_app, ynh_system
 │   │   ├── custom_dir.py       # tar + rsync chemins libres
 │   │   ├── db_dump.py          # mysqldump / pg_dump
-│   │   ├── transfer.py         # rsync SSH
-│   │   └── utils.py            # sudo_exists/getsize/listdir/read_backup_info
-│   ├── federation/             # Phase 3 (non commencé)
+│   │   ├── transfer.py         # rsync SSH + push HTTP chunked
+│   │   └── utils.py            # sudo_exists/getsize/listdir/rm/rm_archive
+│   ├── federation/
+│   │   └── client.py           # FederationClient + sync_instance
+│   ├── tests/
+│   │   └── test_retention.py   # Tests pytest (count / daily / GFS)
 │   └── templates/
 │       ├── base.html
 │       ├── dashboard_local.html
+│       ├── dashboard_network.html
 │       ├── job_form.html
 │       ├── job_history.html
+│       ├── archives.html
+│       ├── archives_overview.html   # Vue globale des archives
 │       ├── restore_confirm.html
 │       ├── settings.html
-│       └── destinations.html
+│       ├── destinations.html
+│       ├── remote_instances.html
+│       ├── remote_instance_form.html
+│       └── federation.html
 └── doc/
 ```
 
@@ -78,18 +97,27 @@ backupmanager_ynh/
 ```sql
 -- Jobs de sauvegarde
 CREATE TABLE jobs (
-  id              INTEGER PRIMARY KEY,
-  name            TEXT NOT NULL,
-  type            TEXT NOT NULL,     -- ynh_app|ynh_system|custom_dir|mysql|postgresql
-  config_json     TEXT,
-  cron_expr       TEXT NOT NULL,     -- ex: "0 3 * * 1"
-  retention_mode  TEXT NOT NULL,     -- count|daily|gfs
-  retention_value INTEGER NOT NULL,
-  enabled         BOOLEAN DEFAULT 1,
-  core_only       BOOLEAN DEFAULT 0,
-  destination_id  INTEGER REFERENCES destinations(id),
-  created_at      DATETIME,
-  updated_at      DATETIME
+  id                   INTEGER PRIMARY KEY,
+  name                 TEXT NOT NULL,
+  type                 TEXT NOT NULL,        -- ynh_app|ynh_system|custom_dir|mysql|postgresql
+  config_json          TEXT,
+  cron_expr            TEXT NOT NULL,        -- ex: "0 3 * * 1" ou "" (manuel uniquement)
+  retention_mode       TEXT NOT NULL,        -- count|daily|gfs
+  retention_value      INTEGER NOT NULL,
+  retention_gfs_config TEXT,                 -- JSON {"daily":N,"weekly":M,"monthly":P}
+  enabled              BOOLEAN DEFAULT 1,
+  core_only            BOOLEAN DEFAULT 0,
+  created_at           DATETIME,
+  updated_at           DATETIME
+);
+
+-- Relation many-to-many Job ↔ Destination
+CREATE TABLE job_destinations (
+  id          INTEGER PRIMARY KEY,
+  job_id      INTEGER REFERENCES jobs(id) ON DELETE CASCADE,
+  dest_type   TEXT NOT NULL,    -- ssh | instance
+  dest_id     INTEGER,          -- FK → destinations.id (si ssh)
+  instance_id INTEGER           -- FK → remote_instances.id (si instance)
 );
 
 -- Destinations de transfert SSH/rsync
@@ -100,7 +128,7 @@ CREATE TABLE destinations (
   port        INTEGER DEFAULT 22,
   user        TEXT NOT NULL DEFAULT 'root',
   remote_path TEXT NOT NULL,
-  key_name    TEXT,                  -- fichier clé dans data_dir/keys/
+  key_name    TEXT,             -- fichier clé dans data_dir/keys/
   enabled     BOOLEAN DEFAULT 1,
   created_at  DATETIME
 );
@@ -111,26 +139,26 @@ CREATE TABLE runs (
   job_id        INTEGER REFERENCES jobs(id),
   started_at    DATETIME,
   finished_at   DATETIME,
-  status        TEXT,               -- running|success|error
-  log_text      TEXT,               -- préfixé [RESTAURATION] si restauration
+  status        TEXT,           -- running|success|warning|error
+  log_text      TEXT,           -- préfixé [RESTAURATION] si restauration
   archive_name  TEXT,
   size_bytes    INTEGER
 );
 
 -- Paramètres SMTP et notifications
 CREATE TABLE settings (
-  key   TEXT PRIMARY KEY,           -- smtp_host, smtp_port, smtp_user, ...
+  key   TEXT PRIMARY KEY,       -- smtp_host, smtp_port, smtp_user, …
   value TEXT NOT NULL DEFAULT ''
 );
 
--- Instances distantes enregistrées (Phase 3)
+-- Instances distantes enregistrées (fédération)
 CREATE TABLE remote_instances (
   id          INTEGER PRIMARY KEY,
-  name        TEXT NOT NULL,        -- ex: "tom"
-  url         TEXT NOT NULL,        -- https://tom.domaine.fr
+  name        TEXT NOT NULL,    -- ex: "tom"
+  url         TEXT NOT NULL,    -- https://tom.domaine.fr
   api_key     TEXT NOT NULL,
   last_seen   DATETIME,
-  status      TEXT,                 -- online|offline|error
+  status      TEXT,             -- online|offline|error
   created_at  DATETIME
 );
 
@@ -138,23 +166,25 @@ CREATE TABLE remote_instances (
 CREATE TABLE remote_runs (
   id              INTEGER PRIMARY KEY,
   instance_id     INTEGER REFERENCES remote_instances(id),
+  job_id          INTEGER,
   job_name        TEXT,
+  job_type        TEXT,
   last_run_at     DATETIME,
   last_status     TEXT,
-  archive_count   INTEGER,
+  last_archive_name TEXT,
   last_size_bytes INTEGER
 );
 
 -- Transferts chunked en cours
 CREATE TABLE uploads (
-  upload_id        TEXT PRIMARY KEY, -- uuid4
+  upload_id        TEXT PRIMARY KEY,  -- uuid4
   filename         TEXT,
   total_size       INTEGER,
   chunk_size       INTEGER,
   chunks_received  INTEGER DEFAULT 0,
-  checksum         TEXT,             -- SHA256
+  checksum         TEXT,              -- SHA256
   started_at       DATETIME,
-  status           TEXT              -- pending|in_progress|complete|error
+  status           TEXT               -- pending|in_progress|complete|error
 );
 ```
 
@@ -170,14 +200,16 @@ BACKUP_CORE_ONLY=1 yunohost backup create --apps nextcloud --name <archive_name>
 ```json
 { "app_id": "nocodb", "core_only": false }
 ```
+Le nom du job est auto-rempli depuis le label de l'application YunoHost sélectionnée.
 
 ### `ynh_system`
 ```bash
-yunohost backup create --system --name <archive_name>
+yunohost backup create --system [hook1 hook2 …] --name <archive_name>
 ```
 ```json
-{ "parts": [] }
+{ "hooks": ["conf_nginx", "conf_ssowat"] }
 ```
+Les hooks système disponibles sont listés dynamiquement. La liste vide déclenche une sauvegarde système complète.
 
 ### `custom_dir`
 ```json
@@ -217,30 +249,42 @@ jerry_hermes-agent_20260508.info.json   # SÉPARÉ hors tar, requis YNH
 
 ## 6. Rétention
 
+La rétention est appliquée **après chaque backup réussi**, localement et sur chaque destination. Les échecs de suppression sont loggés explicitement dans le run (plus de silence silencieux).
+
 ### Mode `count`
 ```
-→ Garde les N dernières archives
-→ Supprime les plus anciennes
+→ Garde les N dernières archives par job
+→ Supprime les plus anciennes au-delà de N
 ```
 
 ### Mode `daily` — fenêtre glissante
 ```
-retention_value: 7
-→ 1 archive par jour sur les 7 derniers jours
-→ Fenêtre glissante J-1 à J-7
-→ Toute archive > J-7 supprimée
+retention_value: 30
+→ 1 archive par jour sur les 30 derniers jours
+→ Fenêtre glissante : toute archive > J-30 supprimée
+→ Doublons du même jour éliminés (garde le plus récent)
 ```
 
-### Mode `gfs` *(Phase 4)*
-```
-daily: 7 / weekly: 4 / monthly: 3
+### Mode `gfs` — Grandfather-Father-Son
+```json
+{ "daily": 7, "weekly": 4, "monthly": 12 }
 ```
+- **Fils** : conserve les N archives les plus récentes
+- **Père** : conserve 1 archive par semaine (la plus récente) sur M semaines
+- **Grand-Père** : conserve 1 archive par mois (la plus récente) sur P mois
+- Une archive peut satisfaire plusieurs niveaux simultanément
+
+### Rétention distante SSH
+Après chaque transfert rsync SSH réussi, la même politique est appliquée sur le serveur distant via SSH (`find` pour lister, `rm -f` pour supprimer). Les échecs de connexion ou de suppression sont loggés sans bloquer le job.
+
+### Rétention distante instance fédérée
+Après chaque push HTTP chunked, la rétention est appliquée via l'API REST de l'instance distante (`DELETE /api/v1/archives/<name>`).
 
 ---
 
 ## 7. Nommage des archives
 
-**Format :** `{instance}_{app}_{YYYYMMDD}.tar`
+**Format :** `{instance}_{label}_{YYYYMMDD}.tar`
 
 ```
 jerry_nocodb_20260508.tar
@@ -250,22 +294,35 @@ jerry_hermes-agent_20260508.tar
 jerry_mysql_mabase_20260508.tar
 ```
 
+Si le nom du jour existe déjà (backup manuel + automatique le même jour), un suffixe est ajouté : `jerry_nextcloud_20260508_2.tar`.
+
 **`backup_info.json` embarqué dans le tar :**
 ```json
 {
   "instance_name": "jerry",
   "instance_url": "https://jerry.mondomaine.fr",
-  "app": "nocodb",
   "type": "ynh_app",
   "created_at": "2026-05-08T03:00:00",
-  "backupmanager_version": "1.0.0",
-  "yunohost_version": "12.1.17"
+  "backupmanager_version": "1.0.0"
 }
 ```
 
 ---
 
-## 8. Intégration YunoHost
+## 8. Destinations (many-to-many)
+
+Un job peut avoir **plusieurs destinations** : aucune (local uniquement), une ou plusieurs SSH, une ou plusieurs instances fédérées, ou toute combinaison.
+
+Après chaque backup réussi :
+1. Rétention locale appliquée
+2. Pour chaque destination dans l'ordre :
+   - Transfert (rsync SSH ou HTTP chunked)
+   - Rétention distante appliquée sur cette destination
+   - Log intermédiaire persisté en base après chaque étape (checkpoint)
+
+---
+
+## 9. Intégration YunoHost
 
 | Outil YNH | Usage |
 |---|---|
@@ -288,55 +345,52 @@ auth_header = false
 protected = true
 ```
 
+**Sudoers :** les commandes autorisées couvrent `yunohost backup`, `mysqldump`, `pg_dump`, `stat`, `find`, `rsync`, `tar`, `rm`, `mkdir`, `chown`, `chmod`, `useradd`, `systemctl`.
+
 ---
 
-## 9. API REST
+## 10. API REST
 
 Tous les endpoints protégés par `X-BackupManager-Key`.
 
 ```
-GET  /api/v1/health
-GET  /api/v1/jobs
-GET  /api/v1/jobs/<id>/runs
-POST /api/v1/jobs/<id>/run
-GET  /api/v1/archives
-GET  /api/v1/archives/<name>/info
+GET    /api/v1/health
+GET    /api/v1/summary
+GET    /api/v1/jobs
+GET    /api/v1/jobs/<id>/runs
+POST   /api/v1/jobs/<id>/run
+GET    /api/v1/running
+GET    /api/v1/archives
+GET    /api/v1/archives/<name>/info
 DELETE /api/v1/archives/<name>
-POST /api/v1/archives/<name>/transfer
-POST /api/v1/archives/upload/start
-POST /api/v1/archives/upload/<id>/chunk/<n>
-POST /api/v1/archives/upload/<id>/finish
+GET    /api/v1/archives/<name>/download
+GET    /api/v1/archives/<name>/info-json-download
+POST   /api/v1/archives/<name>/restore
+GET    /api/v1/archives/<name>/restore/status
+POST   /api/v1/archives/upload/start
+POST   /api/v1/archives/upload/<id>/chunk/<n>
+POST   /api/v1/archives/upload/<id>/finish
 DELETE /api/v1/archives/upload/<id>
-POST /api/v1/archives/<name>/restore          # Phase 3
-GET  /api/v1/archives/<name>/restore/status   # Phase 3
 ```
 
 ---
 
-## 10. Transfert inter-instances
+## 11. Transfert inter-instances
 
-### HTTP Chunked (défaut)
+### HTTP Chunked (fédération)
 ```
 Chunks : 50 MB | Reprise : upload_id SQLite | Vérif : SHA256 | Transport : HTTPS
 ```
 
-### SSH/rsync (optionnel, activable par instance)
+### SSH/rsync (destinations)
 ```bash
-ssh-keygen -t ed25519 -f $data_dir/keys/backupmanager_rsa -N "" -C "backupmanager@jerry"
-rsync -az -e "ssh -i $key -p $port" archive.tar archive.info.json user@host:/home/yunohost.backup/archives/
+ssh-keygen -t ed25519 -f $data_dir/keys/dest_<name>_ed25519 -N "" -C "backupmanager@<dest>"
+rsync -az -e "ssh -i $key -p $port" archive.tar archive.info.json user@host:/remote/path/
 ```
 
-### Sélection automatique
-| Situation | Mode |
-|---|---|
-| SSH non configuré | HTTP chunked |
-| SSH + archive < 2 GB | HTTP chunked |
-| SSH + archive > 2 GB | rsync auto |
-| Override manuel | Au choix |
-
 ---
 
-## 11. Restauration assistée
+## 12. Restauration assistée
 
 | Action | Dashboard BM | Webadmin YNH |
 |---|---|---|
@@ -348,27 +402,27 @@ rsync -az -e "ssh -i $key -p $port" archive.tar archive.info.json user@host:/hom
 | Permissions | ✅ `chown/chmod` | ❌ |
 | Post-restore commands | ✅ | ❌ |
 | MySQL / PostgreSQL | ✅ | ❌ |
-| Port firewall | ✅ `yunohost firewall` | ❌ |
-| Instance distante | ✅ Phase 3 | ❌ |
+| Instance distante | ✅ | ❌ |
 
 ---
 
-## 12. Sécurité
+## 13. Sécurité
 
 | Élément | Mesure |
 |---|---|
 | Dashboard | SSO YunoHost admins |
 | Token API | `secrets.token_hex(32)`, hashé bcrypt |
 | Clé SSH | ed25519, `$data_dir/keys/`, permissions `600` |
-| Credentials DB | Jamais en clair, config protégée |
+| Credentials DB | Jamais en clair, config protégée (`chmod 600`) |
 | Logs | Sans credentials |
 | Inter-instances | HTTPS obligatoire |
+| sudo_rm | Vérifie le code retour, logue les échecs explicitement |
 
 ---
 
-## 13. Phases de développement
+## 14. Phases de développement
 
-### Phase 1 — MVP local
+### Phase 1 — MVP local
 - [x] manifest.toml, install, remove, nginx, systemd
 - [x] Flask + SQLite + APScheduler
 - [x] Jobs ynh_app et ynh_system
@@ -388,79 +442,60 @@ rsync -az -e "ssh -i $key -p $port" archive.tar archive.info.json user@host:/hom
 - [x] Accès archives root-owned via sudo (stat/find/tar/rsync)
 
 ### Phase 3 — Fédération ✅ (testé VPS 2026-05-10)
-**Sous-phases :**
-- **3A** — Fondations : DB (RemoteInstance, RemoteRun, Upload) + API REST complète
-- **3B** — Instances distantes : UI enregistrement + test connexion + sync état
-- **3C** — Dashboard réseau : vue agrégée multi-instances
-- **3D** — Transfert HTTP chunked : push/pull archives entre instances
-- **3E** — Contrôle distant : déclencher backup/restauration sur instance distante
-
-**Avancement :**
-- [x] 3A — Modèles DB RemoteInstance / RemoteRun / Upload
-- [x] 3A — API : /summary, /archives/<name>/info, /archives/<name>/restore (+status), upload chunked
-- [x] 3A — API : /archives/<name>/download, /archives/<name>/info-json-download
-- [x] 3B — UI instances distantes (liste, ajout, édition, suppression, test, sync)
-- [x] 3B — federation/client.py (FederationClient + sync_instance)
-- [x] 3C — Dashboard réseau (vue agrégée locale + distante avec statuts)
-- [x] 3D — Push archive HTTP chunked (sha256 + reprise upload_id)
-- [x] 3D — Pull dernière archive d'un job distant + .info.json
-- [x] 3E — Lancer un job sur instance distante depuis le dashboard réseau
-- [x] 3E — Token API affiché dans les Paramètres + URL instance
-
-**Notes techniques Phase 3 :**
-- sudo rsync crée des fichiers /tmp owned root → cleanup via sudo rm -rf (sudoers)
-- Pull : récupère archive_name via get_job_runs() pour toujours avoir la dernière version
-- Runs bloqués "en cours" nettoyés toutes les heures par APScheduler (> 6h → error)
+- [x] Modèles DB RemoteInstance / RemoteRun / Upload
+- [x] API REST complète (/summary, /archives, /restore, upload chunked, /running)
+- [x] UI instances distantes (liste, ajout, édition, suppression, test, sync)
+- [x] FederationClient + sync_instance
+- [x] Dashboard réseau (vue agrégée locale + distante avec statuts)
+- [x] Push archive HTTP chunked (sha256 + reprise upload_id)
+- [x] Pull dernière archive d'un job distant + .info.json
+- [x] Lancer un job sur instance distante depuis le dashboard réseau
+- [x] Token API affiché dans les Paramètres + URL instance
+- [x] Rétention distante après push HTTP chunked
 
 ### Phase 3 bis — Refactoring & corrections ✅ (2026-05-10)
-- [x] Découpage app.py (1142 lignes) → 5 blueprints Flask + helpers.py
-  - `blueprints/jobs.py` — Blueprint("jobs") : dashboard, CRUD, restauration, archives
-  - `blueprints/destinations.py` — Blueprint("dest")
-  - `blueprints/network.py` — Blueprint("network") : fédération, push/pull
-  - `blueprints/settings.py` — Blueprint("cfg")
-  - `blueprints/api.py` — Blueprint("api", url_prefix="/api/v1") + auth before_request
-- [x] Fix : `sudo cat` non autorisé dans sudoers → remplacé par `sudo rsync → open → sudo rm`
-- [x] Fix : .info.json non rapatrié (500 sur `os.unlink` root-owned) → `finally` avec `sudo rm -rf`
+- [x] Découpage app.py → 5 blueprints Flask + helpers.py
+- [x] Fix : `sudo cat` non autorisé → remplacé par `sudo rsync → open → sudo rm`
+- [x] Fix : .info.json non rapatrié → `finally` avec `sudo rm -rf`
 - [x] Tous les templates mis à jour (`url_for('blueprint.fonction')`)
 
-**Notes techniques :**
-- Threads background : `app = current_app._get_current_object()` avant le thread, `with app.app_context()` dans le thread
-- API auth scoped au blueprint via `@bp.before_request` (pas de filtre global)
-
 ### Phase 4 — Refonte UI ✅ (2026-05-10)
-- [x] Navigateur d'archives `/archives` — tableau filtrable (nom/type/statut), actions Restaurer · Pousser · Télécharger · Supprimer ; listing via `sudo_listdir`
-- [x] Barre d'activité sticky — bandeau sous le header, polling `/api/v1/running` toutes les 5 s, disparaît si idle ; rechargement auto au terme d'un job
-- [x] Endpoint `GET /api/v1/running` — retourne les runs locaux en status=running
-- [x] Boutons unifiés — 4 classes CSS (`btn-primary/secondary/ghost/danger`) en cartouche `rounded-full` avec couleurs distinctes ; balise `<style type="text/tailwindcss">` (CDN)
-- [x] Navigation simplifiée — 3 liens : Dashboard · Archives · Paramètres
-- [x] Dashboard home — colonne "Transfert" (destination rsync ou Local), "Prochaine exéc." supprimée ; section "Serveurs fédérés" en dessous avec jobs distants + Lancer + Rapatrier
-- [x] Paramètres multi-onglets — Destinations (1er) · Instances · Configuration ; bouton contextuel selon onglet actif
-- [x] Icône application — favicon + logo navbar + `doc/LOGO.png` pour le webadmin YunoHost
-
-### Phase 5 — Finitions
-- [ ] Rétention GFS (daily N / weekly N / monthly N)
-- [ ] Export/import config JSON (jobs, destinations, instances)
+- [x] Navigateur d'archives `/archives` — tableau filtrable, actions Restaurer · Pousser · Télécharger · Supprimer
+- [x] Barre d'activité sticky — polling `/api/v1/running` toutes les 5 s, rechargement auto
+- [x] Boutons unifiés — 4 classes CSS (`btn-primary/secondary/ghost/danger`)
+- [x] Navigation simplifiée — Dashboard · Archives · Paramètres
+- [x] Dashboard home — colonne Transfert, section Serveurs fédérés
+- [x] Paramètres multi-onglets — Destinations · Instances · Configuration
+- [x] Icône application — favicon + logo navbar
+
+### Phase 5 — Consolidation ✅ (2026-07-17)
+- [x] Rétention GFS (Grandfather-Father-Son : daily/weekly/monthly)
+- [x] Export/import configuration JSON (jobs, destinations, instances, SMTP)
+- [x] Tests automatisés pytest — rétention count/daily/GFS (17 tests)
+- [x] Destinations multiples par job (many-to-many : SSH + instances combinables)
+- [x] Sélection des hooks système pour les jobs ynh_system
+- [x] Auto-remplissage du nom depuis le label de l'app YunoHost sélectionnée
+- [x] Logs intermédiaires persistés après chaque étape (checkpoint en base)
+- [x] Fix rétention silencieuse : `sudo_rm` vérifie le code retour, échecs loggés
+- [x] Rétention distante SSH : après chaque transfert rsync, nettoyage via SSH
+- [x] Vue globale des archives `/overview` : local + SSH + instances, suppression manuelle, rétention à la demande
+
+### En cours / À venir
 - [ ] Script backup/restore de l'app pour YNH (SQLite + clés SSH)
-- [ ] Tests automatisés (pytest)
+- [ ] Pagination de l'historique des runs
+- [ ] Notifications Gotify / Matrix (en plus de SMTP)
 
 ---
 
-## 14. Feuille de route UI
-
-*Réalisé le 2026-05-10 — toutes les propositions initiales implémentées (avec adaptations).*
-
-### A — Navigateur d'archives ✅
-Page `/archives` avec tableau filtrable (nom, type, statut), listing via `sudo_listdir`, actions par ligne : Restaurer · Pousser (dropdown instances) · Télécharger · Supprimer.
-
-### B — Barre d'activité globale ✅
-Bandeau sticky sous le header, toujours visible pendant la navigation. Polling `/api/v1/running` toutes les 5 s. Affiche chaque activité : type · nom · durée. Rechargement automatique de la page au terme d'un job pour mettre à jour les statuts.
-
-### C — Navigation revue ✅
-Navbar réduite à 3 liens : **Dashboard** · **Archives** · **Paramètres**. Les instances distantes sont dans `Paramètres > Instances`. Le dashboard home intègre directement les serveurs fédérés.
+## 15. Notes techniques
 
-### D — Serveurs fédérés sur la page d'accueil ✅
-Section "Serveurs fédérés" sous le tableau des jobs locaux : statut, liste des jobs distants avec statut/taille, boutons Lancer et Rapatrier. Bouton "Synchroniser tout" en en-tête.
+- **Threads APScheduler** : `app = current_app._get_current_object()` avant le thread, `with app.app_context()` dans le thread
+- **API auth** : scoped au blueprint via `@bp.before_request` (pas de filtre global)
+- **sudo rsync** : crée des fichiers `/tmp` owned root → cleanup via `sudo rm -rf` (sudoers)
+- **Runs bloqués** : nettoyés toutes les heures par APScheduler (> 6h → status error)
+- **Migration SQLite** : colonnes ajoutées à chaud via `ALTER TABLE` dans `app.py` au démarrage
+- **Rétention SSH** : utilise `find -printf '%f\t%s\n'` (GNU find, disponible sur Debian/Ubuntu)
 
 ---
 
-*backupmanager_ynh — CDC v4.5 — Avancement mis à jour le 2026-05-10 | Phase 1 ✅ | Phase 2 ✅ | Phase 3 ✅ | Phase 3bis ✅ | Phase 4 ✅ | Phase 5 en cours*
+*backupmanager_ynh — CDC v5.0 — 2026-07-17 | Phase 1 ✅ | Phase 2 ✅ | Phase 3 ✅ | Phase 3bis ✅ | Phase 4 ✅ | Phase 5 ✅*