# Sauvegarde et restauration

## Stratégie

Une base physique par établissement (voir `DEPLOYMENT.md`) signifie qu'une
sauvegarde globale ne suffit pas : `bin/backup-tenants.php` dumpe **la base
admin** (catalogue établissements/modules) **et la base de chaque
établissement actif**, séparément, un fichier `.sql.gz` par base.

Pas de compte « superuser de backup » à provisionner séparément : chaque
base tenant est dumpée avec son propre compte applicatif restreint, déjà
stocké chiffré dans `etablissements.db_password_chiffre` — c'est le même
compte que l'application utilise au quotidien, donc forcément déjà capable de
lire sa propre base.

Testé réellement (2026-06-25) : provisioning d'un établissement, exécution du
script, vérification que le dump généré contient bien les 27 tables du
schéma tenant, puis nettoyage.

## Exécution manuelle

```bash
php bin/backup-tenants.php --output-dir=/var/backups/newfarah --retention-days=14
```

- Un fichier par base : `admin_AAAA-MM-JJ_HHMMSS.sql.gz`,
  `tenant_{slug}_AAAA-MM-JJ_HHMMSS.sql.gz`.
- `mysqldump --single-transaction --quick --no-tablespaces` (cohérent même
  sur une base avec écritures concurrentes, comme documenté dans le plan de
  migration original).
- Le mot de passe MySQL n'apparaît jamais dans la liste des processus
  (`ps`/`Get-Process`) : il est passé via un fichier `--defaults-extra-file`
  temporaire, supprimé immédiatement après.
- Rotation automatique : tout fichier `.sql.gz` plus vieux que
  `--retention-days` dans le dossier de sortie est supprimé.
- Le script continue sur les établissements suivants si l'un d'eux échoue
  (déchiffrement impossible, base inaccessible, etc.) — code de sortie non
  nul à la fin s'il y a eu au moins un échec, pour que le cron alerte.

## Cron quotidien recommandé

```cron
0 2 * * * www-data php /var/www/newfarahgroupe/bin/backup-tenants.php --output-dir=/var/backups/newfarah --retention-days=14 >> /var/log/newfarah-backup.log 2>&1
```

Stocker `/var/backups/newfarah` sur un volume distinct du serveur applicatif
(ou synchroniser vers un stockage externe après coup) — une sauvegarde sur le
même disque que la base qu'elle protège ne couvre pas une panne disque.

## Restauration

### Base admin

```bash
gunzip -c admin_2026-06-25_140000.sql.gz | mysql -h HOST -u USER -p newfarah_admin
```

### Une base établissement

```bash
# 1. Recréer la base si nécessaire (CREATE DATABASE newfarah_tenant_xxx ...)
# 2. Restaurer le dump
gunzip -c tenant_cliniquefarah_2026-06-25_140000.sql.gz | mysql -h HOST -u USER -p newfarah_tenant_cliniquefarah
```

Si l'établissement lui-même a aussi été perdu (sa ligne dans
`etablissements`), restaurer d'abord la base admin (ou la ligne concernée),
puisque c'est elle qui contient l'hôte/le nom de base/les identifiants
(chiffrés) à utiliser.

### Fichiers uploadés

`storage/uploads/{nom_de_base_tenant}/...` n'est **pas couvert** par
`bin/backup-tenants.php` (qui ne sauvegarde que les bases de données) :
inclure ce dossier dans la sauvegarde système habituelle du serveur (rsync,
snapshot de volume, etc.) au même rythme que les dumps SQL.

## Vérifier qu'une sauvegarde est restaurable

Ne jamais supposer qu'un dump est bon parce que le script a renvoyé un code
0 — restaurer périodiquement un échantillon dans une base de test et
comparer un comptage de lignes sur quelques tables clés (`patients`,
`users`, `factures`) avec la base source, comme déjà pratiqué pour valider
les migrateurs (voir `MIGRATION.md`).
