Aller au contenu

Exploitation

Toutes s’exécutent avec docker compose run --rm app <commande>.

CommandeCe qu’elle fait
servedémarre le serveur (c’est la commande par défaut du conteneur)
provisionner-brasseriecrée une brasserie, son premier administrateur et ses emplacements de base
creer-editeurcrée un compte du back-office éditeur
semer-demonstrationinjecte un parc de démonstration avec un an d’historique
reconstruire-projectionsrejoue le flux d’événements et recalcule l’état courant
exporter-locataireexporte l’intégralité des données d’une brasserie en JSON
supprimer-locatairesupprime définitivement une brasserie

Le service sauvegarde produit une archive pg_dump --format=custom au démarrage puis chaque nuit à 03h15, avec rotation : 7 quotidiennes, 4 hebdomadaires, 6 mensuelles (configurable par RETENTION_*).

Deux garde-fous, parce que les deux pannes silencieuses classiques sont d’archiver une base pas encore migrée et d’archiver une base vide sans s’en apercevoir :

  1. la première sauvegarde attend que le schéma existe ;
  2. chaque archive est relue (pg_restore --list) et rejetée si elle ne contient pas les données de evenement_fut.
Fenêtre de terminal
docker compose exec sauvegarde ls -lh /sauvegardes/quotidien
docker compose logs sauvegarde --tail 20

Une sauvegarde jamais restaurée n’est pas une sauvegarde. Celle-ci est rejouée par l’intégration continue à chaque modification, et voici la version manuelle.

Répétition sans risque, dans une base jetable :

Fenêtre de terminal
DUMP=$(docker compose exec -T sauvegarde sh -c 'ls -1t /sauvegardes/quotidien/*.dump | head -1' | tr -d '\r')
docker compose exec -T postgres psql -U postgres -d postgres \
-c "drop database if exists webqr_restaure with (force);" \
-c "create database webqr_restaure;"
docker compose exec -T sauvegarde \
pg_restore -h postgres -U postgres --dbname=webqr_restaure \
--no-owner --no-privileges "$DUMP"
docker compose exec -T postgres psql -U postgres -d webqr_restaure -tAc \
"select (select count(*) from fut) || ' fûts, ' ||
(select count(*) from evenement_fut) || ' événements'"

Comparez avec la base vivante. Puis drop database webqr_restaure.

Restauration réelle, après incident :

Fenêtre de terminal
docker compose stop app caddy
docker compose exec -T postgres psql -U postgres -d postgres \
-c "drop database webqr with (force);" -c "create database webqr owner webqr_owner;"
docker compose exec -T sauvegarde pg_restore -h postgres -U postgres --dbname=webqr "$DUMP"
docker compose start app caddy
docker compose run --rm app reconstruire-projections --verifier

La dernière ligne n’est pas décorative : elle prouve que la base restaurée est cohérente, en rejouant tout le flux d’événements et en le comparant à l’état courant.

L’état d’un fût affiché dans les listes est un cache. La vérité est la suite de ses événements, en ajout seul.

Fenêtre de terminal
# Vérifie sans rien écrire ; sort en erreur s'il existe un écart.
docker compose run --rm app reconstruire-projections --verifier
# Recalcule et corrige.
docker compose run --rm app reconstruire-projections --locataire phare

À lancer après une restauration, après une correction de bug de projection, ou simplement par acquit de conscience. Le rejeu est idempotent.

Fenêtre de terminal
docker compose run --rm app exporter-locataire --locataire phare --sortie phare.json
docker compose run --rm app supprimer-locataire --locataire phare --confirmer

L’export contient toutes les tables du locataire au format JSON ouvert. La suppression est définitive et emporte tout par cascade ; elle est journalisée dans le journal éditeur.

SECRET_APP poivre Argon2id. Le changer invalide tous les mots de passe et tous les codes PIN. Si vous devez le faire :

  1. prévenez les utilisateurs ;
  2. changez la variable et redémarrez ;
  3. réinitialisez chaque mot de passe depuis Administration → Utilisateurs → Réinitialiser.

Il n’y a pas de migration en douceur : c’est le prix du poivre, et il est volontaire.

Journalisation structurée sur la sortie standard, niveau réglé par RUST_LOG (par exemple info,web=debug,sqlx=warn).

PointUsage
GET /santevivacité : le processus répond
GET /pretdisponibilité : la base répond aussi (utilisé par le healthcheck)

Deux journaux d’audit en base :

  • journal_audit — par brasserie : connexions, changements de rôle, archivages, imports, validations de documents, modifications de paramètres. Consultable dans Administration → Journal d'audit.
  • journal_audit_editeur — hors brasserie : créations, suspensions, et chaque ouverture et fermeture d’assistance.

Depuis /editeur, le bouton Assistance ouvre une session dans la brasserie choisie, sur son premier administrateur actif. Cette session :

  • dure une heure au maximum ;
  • porte une marque en base (session.usurpee_par) ;
  • affiche un bandeau rouge permanent dans toute l’application ;
  • est journalisée à l’ouverture et à la fermeture.

C’est un pouvoir réel : ne l’utilisez que sur demande du client, et fermez la session dès que le diagnostic est fait.

Deux niveaux, complémentaires :

  • par adresse IP, en mémoire : 12 tentatives de connexion par minute, 8 pour le code PIN et le back-office éditeur, 240 scans par minute et par utilisateur. Remis à zéro au redémarrage, ce qui est acceptable pour ce qu’il protège.
  • par compte, en base : au-delà de 5 échecs, verrouillage exponentiel (1, 2, 4, 8… minutes, plafonné à 30). Celui-là survit à un redémarrage — c’est lui qui protège réellement contre le bourrage.

Cinq codes PIN erronés révoquent la session : on repasse obligatoirement par le mot de passe.

Le déploiement v1 est mono-instance. Pour passer à plusieurs instances d’application derrière le même proxy, deux points à traiter :

  • la limitation de débit par IP est en mémoire : la déporter en base ou dans un cache partagé ;
  • rien d’autre. Les sessions, les brouillons de capture et les compteurs de référence vivent déjà en base.

PostgreSQL reste l’unique point d’état. Les index et la pagination côté serveur sont dimensionnés pour un parc de plusieurs milliers de fûts et plusieurs centaines de milliers d’événements.