# Cahier des charges — `backupmanager_ynh`
**Version 4.1 — 2026-05-09**

## 1. Vision générale

Application YunoHost centralisant toutes les sauvegardes d'un serveur, s'appuyant au maximum sur les outils YunoHost natifs, extensible en écosystème fédéré multi-instances.

**Périmètre :** Apps YNH · Système YNH · Répertoires custom · MySQL · PostgreSQL

```
┌──────────────┐  API  ┌──────────────┐  API  ┌──────────────┐
│  Instance A  │◄─────►│  Instance B  │◄─────►│  Instance C  │
│  (maître)    │       │  (nœud)      │       │  (nœud)      │
└──────────────┘       └──────────────┘       └──────────────┘
  N'importe quelle instance peut être "maître"
```

---

## 2. Décisions clés

| Sujet | Décision |
|---|---|
| Langage | Python 3 + Flask |
| Scheduler | APScheduler + SQLite |
| DB | SQLite + SQLAlchemy |
| Frontend | Jinja2 + TailwindCSS CDN |
| Proxy/Service | Nginx + systemd YunoHost natifs |
| Archives | `/home/yunohost.backup/archives/` |
| 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) |
| Restauration | Complète : fichiers + user système + systemd + DB |

---

## 3. Structure du dépôt

```
backupmanager_ynh/
├── manifest.toml
├── scripts/
│   ├── _common.sh
│   ├── install / remove / backup / restore / upgrade
├── conf/
│   ├── nginx.conf
│   ├── systemd.service
│   └── app.conf.j2
├── sources/
│   ├── app.py                  # Flask routes + API REST
│   ├── scheduler.py            # APScheduler
│   ├── db.py                   # SQLAlchemy (Job, Run, Destination, Setting)
│   ├── retention.py            # Moteur count/daily/gfs
│   ├── notifications.py        # Email SMTP (succès/erreur)
│   ├── jobs/
│   │   ├── ynh_backup.py       # yunohost backup create/restore
│   │   ├── 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é)
│   └── templates/
│       ├── base.html
│       ├── dashboard_local.html
│       ├── job_form.html
│       ├── job_history.html
│       ├── restore_confirm.html
│       ├── settings.html
│       └── destinations.html
└── doc/
```

---

## 4. Modèle de données SQLite

```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
);

-- Destinations de transfert SSH/rsync
CREATE TABLE destinations (
  id          INTEGER PRIMARY KEY,
  name        TEXT NOT NULL,
  host        TEXT NOT NULL,
  port        INTEGER DEFAULT 22,
  user        TEXT NOT NULL DEFAULT 'root',
  remote_path TEXT NOT NULL,
  key_name    TEXT,                  -- fichier clé dans data_dir/keys/
  enabled     BOOLEAN DEFAULT 1,
  created_at  DATETIME
);

-- Historique des exécutions (backup ET restauration)
CREATE TABLE runs (
  id            INTEGER PRIMARY KEY,
  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
  archive_name  TEXT,
  size_bytes    INTEGER
);

-- Paramètres SMTP et notifications
CREATE TABLE settings (
  key   TEXT PRIMARY KEY,           -- smtp_host, smtp_port, smtp_user, ...
  value TEXT NOT NULL DEFAULT ''
);

-- Instances distantes enregistrées (Phase 3)
CREATE TABLE remote_instances (
  id          INTEGER PRIMARY KEY,
  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
  created_at  DATETIME
);

-- Cache états distants
CREATE TABLE remote_runs (
  id              INTEGER PRIMARY KEY,
  instance_id     INTEGER REFERENCES remote_instances(id),
  job_name        TEXT,
  last_run_at     DATETIME,
  last_status     TEXT,
  archive_count   INTEGER,
  last_size_bytes INTEGER
);

-- Transferts chunked en cours
CREATE TABLE uploads (
  upload_id        TEXT PRIMARY KEY, -- uuid4
  filename         TEXT,
  total_size       INTEGER,
  chunk_size       INTEGER,
  chunks_received  INTEGER DEFAULT 0,
  checksum         TEXT,             -- SHA256
  started_at       DATETIME,
  status           TEXT              -- pending|in_progress|complete|error
);
```

---

## 5. Types de jobs

### `ynh_app`
```bash
yunohost backup create --apps <app_id> --name <archive_name>
BACKUP_CORE_ONLY=1 yunohost backup create --apps nextcloud --name <archive_name>
```
```json
{ "app_id": "nocodb", "core_only": false }
```

### `ynh_system`
```bash
yunohost backup create --system --name <archive_name>
```
```json
{ "parts": [] }
```

### `custom_dir`
```json
{
  "source_path": "/opt/hermes-agent",
  "excludes": ["cache/", "logs/", "*.tmp"],
  "restore": {
    "system_user": { "name": "hermes-agent", "home": "/opt/hermes-agent", "shell": "/bin/false" },
    "systemd_service": { "name": "hermes-agent", "service_file": "/opt/hermes-agent/hermes-agent.service" },
    "permissions": { "owner": "hermes-agent:hermes-agent", "mode": "750" },
    "post_restore_commands": ["systemctl restart hermes-agent"]
  }
}
```

**Format archive compatible YunoHost :**
```
jerry_hermes-agent_20260508.tar
├── backup.csv              # requis YNH
├── backup_info.json        # métadonnées BackupManager
└── data/custom/opt/hermes-agent/...

jerry_hermes-agent_20260508.info.json   # SÉPARÉ hors tar, requis YNH
```

### `mysql`
```json
{ "database": "mabase", "user": "mabase_user" }
```

### `postgresql`
```json
{ "database": "mabase", "user": "postgres" }
```

---

## 6. Rétention

### Mode `count`
```
→ Garde les N dernières archives
→ Supprime les plus anciennes
```

### 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
```

### Mode `gfs` *(Phase 4)*
```
daily: 7 / weekly: 4 / monthly: 3
```

---

## 7. Nommage des archives

**Format :** `{instance}_{app}_{YYYYMMDD}.tar`

```
jerry_nocodb_20260508.tar
jerry_nextcloud_20260508.tar
jerry_system_20260508.tar
jerry_hermes-agent_20260508.tar
jerry_mysql_mabase_20260508.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"
}
```

---

## 8. Intégration YunoHost

| Outil YNH | Usage |
|---|---|
| `yunohost backup create` | Jobs ynh_app et ynh_system |
| `yunohost backup restore` | Restauration webadmin |
| `yunohost app list --output-as json` | Formulaire job |
| `/home/yunohost.backup/archives/` | Toutes les archives |
| Nginx + systemd + SSOwat + Let's Encrypt | Infra app |

**SSOwat (`manifest.toml`) :**
```toml
[resources.permissions.main]
url = "/"
allowed = "admins"

[resources.permissions.api]
url = "/api"
allowed = "visitors"    # sécurisé par token Flask
auth_header = false
protected = true
```

---

## 9. 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
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
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

### HTTP Chunked (défaut)
```
Chunks : 50 MB | Reprise : upload_id SQLite | Vérif : SHA256 | Transport : HTTPS
```

### SSH/rsync (optionnel, activable par instance)
```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/
```

### 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

| Action | Dashboard BM | Webadmin YNH |
|---|---|---|
| App YNH | ✅ (via YNH) | ✅ natif |
| Système YNH | ✅ (via YNH) | ✅ natif |
| Fichiers custom_dir | ✅ complet | ✅ partiel |
| User système | ✅ `useradd` | ❌ |
| Service systemd | ✅ `systemctl` | ❌ |
| Permissions | ✅ `chown/chmod` | ❌ |
| Post-restore commands | ✅ | ❌ |
| MySQL / PostgreSQL | ✅ | ❌ |
| Port firewall | ✅ `yunohost firewall` | ❌ |
| Instance distante | ✅ Phase 3 | ❌ |

---

## 12. 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 |
| Logs | Sans credentials |
| Inter-instances | HTTPS obligatoire |

---

## 13. Phases de développement

### Phase 1 — MVP local
- [x] manifest.toml, install, remove, nginx, systemd
- [x] Flask + SQLite + APScheduler
- [x] Jobs ynh_app et ynh_system
- [x] Rétention count et daily
- [x] Dashboard local (jobs + historique + Run now)

### Phase 2 — Périmètre complet ✅ (testé VPS 2026-05-09)
- [x] Jobs custom_dir (exclusions + format YNH compatible)
- [x] Jobs mysql et postgresql
- [x] Restauration complète custom_dir, mysql, postgresql, ynh_app, ynh_system
- [x] Restaurations tracées dans l'historique du job
- [x] Destinations rsync SSH
- [x] Transfert automatique post-backup
- [x] Nommage unique (suffixe _2, _3… si doublon du jour)
- [x] Notifications email SMTP (succès/erreur)
- [x] Liste BDD live dans le formulaire (mysql/postgresql)
- [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)

### 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] 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)
- [ ] Script backup/restore de l'app pour YNH (SQLite + clés SSH)
- [ ] Tests automatisés (pytest)

---

## 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.

### 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.

---

*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*