Skip to content

Sauvegardes automatiques de la base de prod (timer VPS + R2 + alerte Grafana) - #293

Merged
Oasis35 merged 3 commits into
mainfrom
claude/project-thread-e83khb
Oct 11, 2026
Merged

Oasis35 merged 3 commits into
mainfrom
claude/project-thread-e83khb

Conversation

@Oasis35

@Oasis35 Oasis35 commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Before: aucune sauvegarde de la base de prod ; une panne ou une erreur sur le VPS faisait tout perdre.

After: chaque nuit à 3 h UTC, un timer systemd du VPS sauvegarde la base inseconds sur le VPS (7 jours) puis l'envoie chiffrée vers Cloudflare R2 (30 quotidiennes + 12 mensuelles). Une alerte Grafana prévient si aucune sauvegarde réussie n'arrive en 26 h. Un script permet aussi de lister, rapatrier et tester la restauration d'une sauvegarde.

La sauvegarde tourne entièrement sur le VPS, hors de l'API : elle continue même si l'appli est en panne, et ne changera pas à la bascule v2 (même base).

How

  • deploy/infra/backup.sh : pg_dump -Fc dans un conteneur postgres:17-alpine (tous schémas, v1 et v2), vérification pg_restore --list, rotation locale, envoi rclone crypt (image rclone/rclone:1.75.2, configuration par variables d'environnement). Sous-commandes list-remote, fetch, restore-test (base jetable inseconds_restore_test, supprimée ensuite). Secrets lus dans deploy/infra/.env, passés aux conteneurs par -e NOM (jamais sur la ligne de commande). Sans les clés R2, sauvegarde locale seulement.
  • Surveillance : à chaque passage, un log OTLP JSON « Sauvegarde OK » / « Sauvegarde en échec » (service inseconds-backup) est envoyé à Grafana Cloud avec la configuration OTEL_EXPORTER_OTLP_* de .env.prod (celle de l'API, en-têtes passés à curl hors ligne de commande). Jamais bloquant. Requête et réglages de l'alerte « aucun Sauvegarde OK en 26 h » dans le README.
  • deploy/infra/systemd/inseconds-backup.service + .timer : 3 h UTC, Persistent=true (rattrape une nuit ratée si le VPS était éteint), lance le script du checkout de prod, remis à jour à chaque déploiement.
  • Doc : deploy/infra/README.md § Sauvegardes (mise en place, timer, alerte, restauration), .env.example, CLAUDE.md, docs/TACHES.md.

Testé en local (pas de Docker dans l'environnement : faux docker qui lance les vrais binaires) avec un vrai PostgreSQL et le vrai rclone 1.75.2 sur un stockage local : sauvegarde, rotation locale et distante, copie mensuelle unique, fichier distant chiffré et identique au dump une fois déchiffré, fetch, restore-test, dump corrompu, mauvais mot de passe (pas de fichier partiel), configuration R2 incomplète. Signal Grafana testé contre un faux serveur OTLP : JSON valide, en-tête Authorization décodé (%20), envoi d'échec, serveur injoignable non bloquant. systemd-analyze calendar OK, shellcheck propre. Non testé : le vrai R2, le vrai Grafana Cloud et le vrai Docker du VPS (première exécution à faire par Clément, cf. README).

À faire sur le VPS après le merge (une fois) :

  1. renseigner les clés R2 et BACKUP_ENCRYPTION_PASSWORD dans deploy/infra/.env (phrase de passe aussi dans KeePass) ;
  2. ./backup.sh puis ./backup.sh restore-test ;
  3. installer le timer (sudo cp … /etc/systemd/system/, systemctl enable --now inseconds-backup.timer) ;
  4. créer l'alerte Grafana.

- deploy/infra/backup.sh : pg_dump de la base inseconds (tous schémas), vérification
  (pg_restore --list), 7 jours gardés sur le VPS, envoi chiffré (rclone crypt) vers
  Cloudflare R2 (daily/ 30 j, monthly/ 365 j) et ping healthchecks.io si configurés ;
  sous-commandes list-remote, fetch et restore-test (base jetable).
- .github/workflows/db-backup.yml : lance le script chaque nuit à 3 h UTC et à la
  demande, par SSH (le dump ne passe jamais par GitHub).
- Doc : deploy/infra/README.md § Sauvegardes, .env.example, CLAUDE.md, README FR/EN,
  docs/TACHES.md.
@Oasis35 Oasis35 self-assigned this Oct 9, 2026
@Oasis35
Oasis35 marked this pull request as draft October 9, 2026 20:43
@Oasis35
Oasis35 marked this pull request as ready for review October 9, 2026 20:43
@Oasis35
Oasis35 marked this pull request as draft October 9, 2026 20:43
@Oasis35

Oasis35 commented Oct 9, 2026

Copy link
Copy Markdown
Owner Author

Stand by sera repris bientôt

@Oasis35
Oasis35 marked this pull request as ready for review October 10, 2026 12:31
Le workflow GitHub Actions est remplacé par un timer systemd (3 h UTC,
Persistent=true) qui lance backup.sh depuis le checkout de prod. Le ping
healthchecks.io est remplacé par un log OTLP envoyé à Grafana Cloud avec la
configuration de l'API ; une alerte Grafana prévient si aucune sauvegarde
réussie n'arrive en 26 h.
@Oasis35 Oasis35 changed the title Sauvegardes de la base de prod : backup.sh + workflow nocturne Sauvegardes automatiques de la base de prod (timer VPS + R2 + alerte Grafana) Oct 11, 2026
@sonarqubecloud

Copy link
Copy Markdown

@Oasis35
Oasis35 merged commit c4a3f08 into main Oct 11, 2026
19 checks passed
@Oasis35
Oasis35 deleted the claude/project-thread-e83khb branch October 11, 2026 16:12
Oasis35 added a commit that referenced this pull request Oct 11, 2026
* Envoi des emails via Brevo au lieu de Resend (#197)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Staging → main : compteur de gels corrigé (#199)

* Envoi des emails via Brevo au lieu de Resend (#196)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Gel de série : retirer du compteur les gels déjà engagés sur une série protégée (#198)

La gélule du header affichait encore le stock en base (ex. 2) alors que l'accueil
annonçait qu'un gel avait sauvé la série : le stock n'est décrémenté qu'à la
complétion du défi. StreakView.Freezes déduit désormais les jours manqués en
statut protected, ce qui correspond au stock qui restera après la partie.

* Déconnexion : ne plus faire tourner le jeton du compte (multi-appareils cassé) (#239)

Tous les appareils d'un compte v1 partagent le même AuthToken : le faire
tourner à la déconnexion (M6, #179) déconnectait tous les autres appareils.
La déconnexion n'efface plus que le cookie de l'appareil courant.

* fix(partage): repli execCommand quand clipboard.writeText échoue (#200) (#241)

Le bouton « Partager mon score » ne copiait parfois rien au premier clic :
navigator.clipboard.writeText rejette de façon intermittente (NotAllowedError,
« Document is not focused »). Sur rejet, ClipboardService retombe sur une copie
textarea + execCommand('copy') et remonte l'échec (sans le texte) à l'API.

* Admin : renommer un morceau du défi du jour depuis le pool (#246)

* feat(admin): renommer un morceau du défi du jour depuis le pool

Retire le verrou RenameLock (409 track_locked) : le bouton ✎ est actif
sur toutes les lignes du pool, défi du jour compris. Les réponses déjà
enregistrées gardent leur verdict, les suivantes sont corrigées avec le
nouveau nom.

* feat(admin): avertissement dans la modale de renommage pour un morceau du défi du jour

* fix(audio): palier coupé avant le son sur iPhone (relecture) (#249)

Safari iOS émet `playing` dès l'appel à play(), avant que le son reparte,
sans second `playing` ensuite : le chrono d'arrêt coupait le palier à
l'aveugle (0,5s → 0,1s de son, voire aucun). Le chrono relit désormais
la position de lecture à son échéance et se reprogramme tant que le
palier n'est pas atteint ; rien ne coupe pendant un seek.

* Bump the nuget-minor-patch group with 5 updates (#245)

Bumps coverlet.collector from 10.0.1 to 10.1.0
Bumps WolverineFx from 6.40.0 to 6.43.0
Bumps WolverineFx.EntityFrameworkCore from 6.40.0 to 6.43.0
Bumps WolverineFx.FluentValidation from 6.40.0 to 6.43.0
Bumps WolverineFx.RuntimeCompilation from 6.40.0 to 6.43.0

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.EntityFrameworkCore
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.FluentValidation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.RuntimeCompilation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...

* Conformité aux conditions de l'API Deezer (#251)

* Conformité aux conditions de l'API Deezer

- Confidentialité : ajoute la mention obligatoire « usage strictement privé,
  dans le cadre familial » (art. IV des conditions Deezer) et corrige le
  passage qui affirmait que Deezer ne reçoit rien : le navigateur charge
  extraits et pochettes depuis les CDN Deezer, qui voient l'adresse IP.
- Stories Instagram : plus de pochette Deezer dans les images (Deezer
  interdit de stocker ses images). L'API du récap hebdo ne renvoie plus de
  coverUrl, une vignette note de musique la remplace.
- Doc : piège 47 (CLAUDE.md) et docs des stories.

* Spec stories : toHaveSize plutôt que length (Sonar S5906)

* docs(v2): lecteur audio Howler.js dans le plan et le plan de développement

AudioPort de la v2 sur Howler.js en Web Audio (décision du 01/10/2026),
CORS du CDN des extraits Deezer vérifié le 02/10/2026, CSP connect-src.
Fichiers repris de env/staging avec ces modifications.

* docs(v2): retire le plan v2 de cette PR (déplacé dans une PR vers env/staging)

* Bump brace-expansion in /src/front/InSeconds.Client (#267)

Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.18 to 1.1.21.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v1.1.18...v1.1.21)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.21
  dependency-type: indirect

* Bump source-map-js from 1.2.1 to 1.2.2 in /src/front/InSeconds.Client (#269)

Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](7rulnik/source-map-js@v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect
..

* Sauvegardes automatiques de la base de prod (timer VPS + R2 + alerte Grafana) (#293)

* Sauvegardes de la base de prod : backup.sh + workflow nocturne

- deploy/infra/backup.sh : pg_dump de la base inseconds (tous schémas), vérification
  (pg_restore --list), 7 jours gardés sur le VPS, envoi chiffré (rclone crypt) vers
  Cloudflare R2 (daily/ 30 j, monthly/ 365 j) et ping healthchecks.io si configurés ;
  sous-commandes list-remote, fetch et restore-test (base jetable).
- .github/workflows/db-backup.yml : lance le script chaque nuit à 3 h UTC et à la
  demande, par SSH (le dump ne passe jamais par GitHub).
- Doc : deploy/infra/README.md § Sauvegardes, .env.example, CLAUDE.md, README FR/EN,
  docs/TACHES.md.

* Sauvegardes : timer systemd du VPS et alerte Grafana au lieu du workflow

Le workflow GitHub Actions est remplacé par un timer systemd (3 h UTC,
Persistent=true) qui lance backup.sh depuis le checkout de prod. Le ping
healthchecks.io est remplacé par un log OTLP envoyé à Grafana Cloud avec la
configuration de l'API ; une alerte Grafana prévient si aucune sauvegarde
réussie n'arrive en 26 h.

* backup.sh : paramètres positionnels nommés (Sonar)

* backup.sh : envoi vers R2 par l'entrée standard (rcat) au lieu d'un dossier monté (#294)

Sur le VPS, le conteneur rclone ne voyait pas le dump dans le dossier monté
(« Source doesn't exist or is a directory »). Le dump passe désormais par
l'entrée standard (rcat) à l'envoi et par la sortie standard (cat) au
téléchargement : plus de montage, plus de dépendance aux droits Docker.

* docs: SSH par clé et par login/mot de passe, tous deux actifs sur le VPS (#296)

* docs: SSH par mot de passe réactivé sur le VPS (accès de secours)

* docs: préciser que clé SSH et login/mot de passe restent tous deux actifs
Oasis35 added a commit that referenced this pull request Oct 12, 2026
* Envoi des emails via Brevo au lieu de Resend (#197)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Staging → main : compteur de gels corrigé (#199)

* Envoi des emails via Brevo au lieu de Resend (#196)

BrevoEmailSender remplace ResendEmailSender (API transactionnelle Brevo,
header api-key). Configuration Brevo:*, variable BREVO_API_KEY, page
confidentialité et doc mises à jour.

* Gel de série : retirer du compteur les gels déjà engagés sur une série protégée (#198)

La gélule du header affichait encore le stock en base (ex. 2) alors que l'accueil
annonçait qu'un gel avait sauvé la série : le stock n'est décrémenté qu'à la
complétion du défi. StreakView.Freezes déduit désormais les jours manqués en
statut protected, ce qui correspond au stock qui restera après la partie.

* Déconnexion : ne plus faire tourner le jeton du compte (multi-appareils cassé) (#239)

Tous les appareils d'un compte v1 partagent le même AuthToken : le faire
tourner à la déconnexion (M6, #179) déconnectait tous les autres appareils.
La déconnexion n'efface plus que le cookie de l'appareil courant.

* fix(partage): repli execCommand quand clipboard.writeText échoue (#200) (#241)

Le bouton « Partager mon score » ne copiait parfois rien au premier clic :
navigator.clipboard.writeText rejette de façon intermittente (NotAllowedError,
« Document is not focused »). Sur rejet, ClipboardService retombe sur une copie
textarea + execCommand('copy') et remonte l'échec (sans le texte) à l'API.

* Admin : renommer un morceau du défi du jour depuis le pool (#246)

* feat(admin): renommer un morceau du défi du jour depuis le pool

Retire le verrou RenameLock (409 track_locked) : le bouton ✎ est actif
sur toutes les lignes du pool, défi du jour compris. Les réponses déjà
enregistrées gardent leur verdict, les suivantes sont corrigées avec le
nouveau nom.

* feat(admin): avertissement dans la modale de renommage pour un morceau du défi du jour

* fix(audio): palier coupé avant le son sur iPhone (relecture) (#249)

Safari iOS émet `playing` dès l'appel à play(), avant que le son reparte,
sans second `playing` ensuite : le chrono d'arrêt coupait le palier à
l'aveugle (0,5s → 0,1s de son, voire aucun). Le chrono relit désormais
la position de lecture à son échéance et se reprogramme tant que le
palier n'est pas atteint ; rien ne coupe pendant un seek.

* Bump the nuget-minor-patch group with 5 updates (#245)

Bumps coverlet.collector from 10.0.1 to 10.1.0
Bumps WolverineFx from 6.40.0 to 6.43.0
Bumps WolverineFx.EntityFrameworkCore from 6.40.0 to 6.43.0
Bumps WolverineFx.FluentValidation from 6.40.0 to 6.43.0
Bumps WolverineFx.RuntimeCompilation from 6.40.0 to 6.43.0

---
updated-dependencies:
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: coverlet.collector
  dependency-version: 10.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.EntityFrameworkCore
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.FluentValidation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
- dependency-name: WolverineFx.RuntimeCompilation
  dependency-version: 6.43.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nuget-minor-patch
...

* Conformité aux conditions de l'API Deezer (#251)

* Conformité aux conditions de l'API Deezer

- Confidentialité : ajoute la mention obligatoire « usage strictement privé,
  dans le cadre familial » (art. IV des conditions Deezer) et corrige le
  passage qui affirmait que Deezer ne reçoit rien : le navigateur charge
  extraits et pochettes depuis les CDN Deezer, qui voient l'adresse IP.
- Stories Instagram : plus de pochette Deezer dans les images (Deezer
  interdit de stocker ses images). L'API du récap hebdo ne renvoie plus de
  coverUrl, une vignette note de musique la remplace.
- Doc : piège 47 (CLAUDE.md) et docs des stories.

* Spec stories : toHaveSize plutôt que length (Sonar S5906)

* docs(v2): lecteur audio Howler.js dans le plan et le plan de développement

AudioPort de la v2 sur Howler.js en Web Audio (décision du 01/10/2026),
CORS du CDN des extraits Deezer vérifié le 02/10/2026, CSP connect-src.
Fichiers repris de env/staging avec ces modifications.

* docs(v2): retire le plan v2 de cette PR (déplacé dans une PR vers env/staging)

* Bump brace-expansion in /src/front/InSeconds.Client (#267)

Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.18 to 1.1.21.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](juliangruber/brace-expansion@v1.1.18...v1.1.21)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.21
  dependency-type: indirect
...

* Bump source-map-js from 1.2.1 to 1.2.2 in /src/front/InSeconds.Client (#269)

Bumps [source-map-js](https://github.com/7rulnik/source-map-js) from 1.2.1 to 1.2.2.
- [Release notes](https://github.com/7rulnik/source-map-js/releases)
- [Changelog](https://github.com/7rulnik/source-map-js/blob/main/CHANGELOG.md)
- [Commits](7rulnik/source-map-js@v1.2.1...v1.2.2)

---
updated-dependencies:
- dependency-name: source-map-js
  dependency-version: 1.2.2
  dependency-type: indirect

* Sauvegardes automatiques de la base de prod (timer VPS + R2 + alerte Grafana) (#293)

* Sauvegardes de la base de prod : backup.sh + workflow nocturne

- deploy/infra/backup.sh : pg_dump de la base inseconds (tous schémas), vérification
  (pg_restore --list), 7 jours gardés sur le VPS, envoi chiffré (rclone crypt) vers
  Cloudflare R2 (daily/ 30 j, monthly/ 365 j) et ping healthchecks.io si configurés ;
  sous-commandes list-remote, fetch et restore-test (base jetable).
- .github/workflows/db-backup.yml : lance le script chaque nuit à 3 h UTC et à la
  demande, par SSH (le dump ne passe jamais par GitHub).
- Doc : deploy/infra/README.md § Sauvegardes, .env.example, CLAUDE.md, README FR/EN,
  docs/TACHES.md.

* Sauvegardes : timer systemd du VPS et alerte Grafana au lieu du workflow

Le workflow GitHub Actions est remplacé par un timer systemd (3 h UTC,
Persistent=true) qui lance backup.sh depuis le checkout de prod. Le ping
healthchecks.io est remplacé par un log OTLP envoyé à Grafana Cloud avec la
configuration de l'API ; une alerte Grafana prévient si aucune sauvegarde
réussie n'arrive en 26 h.

* backup.sh : paramètres positionnels nommés (Sonar)

* backup.sh : envoi vers R2 par l'entrée standard (rcat) au lieu d'un dossier monté (#294)

Sur le VPS, le conteneur rclone ne voyait pas le dump dans le dossier monté
(« Source doesn't exist or is a directory »). Le dump passe désormais par
l'entrée standard (rcat) à l'envoi et par la sortie standard (cat) au
téléchargement : plus de montage, plus de dépendance aux droits Docker.

* docs: SSH par clé et par login/mot de passe, tous deux actifs sur le VPS (#296)

* docs: SSH par mot de passe réactivé sur le VPS (accès de secours)

* docs: préciser que clé SSH et login/mot de passe restent tous deux actifs

* Bascule v2 : workflow remplaçant minimal pour le rendre lançable depuis env/staging (#302)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant