# Déploiement Procédures d'exploitation pour `horaires.ita-ito.com`. Le [README](README.md) couvre l'installation locale et la configuration d'Authentik. ## Première installation sur le VPS Prérequis : Docker, Docker Compose, et un Traefik déjà en route possédant le réseau externe nommé par `TRAEFIK_NETWORK`. ```bash git clone /opt/horaires && cd /opt/horaires cp .env.example .env sed -i "s|^AUTH_SECRET=.*|AUTH_SECRET=$(openssl rand -base64 33)|" .env sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -base64 24)|" .env # Renseignez APP_DOMAIN, AUTH_URL, AUTH_AUTHENTIK_*, TRANSLATION_API_KEY. mkdir -p backups docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait ``` Les migrations s'appliquent au démarrage. Si elles échouent, le conteneur s'arrête **avant** de servir quoi que ce soit, plutôt que d'exposer une base incohérente : `docker compose logs app` dira pourquoi. Ensuite : ```bash docker compose exec app node .migrator/node_modules/prisma/build/index.js migrate status curl -fsS https://horaires.ita-ito.com/api/health ``` Puis, une fois connecté : *Paramètres* pour le canton et les intervalles de réveil, *Fériés* → **Synchroniser maintenant**, *Horaires* pour la vraie semaine, et enfin l'appairage de l'écran (README). ## Mise à jour ```bash cd /opt/horaires ./scripts/backup.sh # ou attendre le dump nocturne git pull docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait docker compose logs -f app # ^C une fois « démarrage de l'application » ``` `--wait` rend la main seulement quand le *healthcheck* passe. S'il ne passe pas, la mise à jour a échoué et l'ancien conteneur a déjà été remplacé : voir le retour arrière ci-dessous. **Avant une mise à jour qui touche au rendu** (polices, gabarit, logo), sachez que le nom de fichier de l'image change et que tous les écrans redessineront une fois. C'est sans gravité, seulement un cycle de batterie. ## Sauvegarde Le service `backup` dépose un dump compressé par jour dans `./backups` et purge au-delà de `BACKUP_KEEP_DAYS` (14 par défaut). Dump immédiat : ```bash docker compose exec -T db pg_dump -U horaires horaires | gzip > backups/horaires-$(date +%Y%m%d-%H%M%S).sql.gz ``` Ces fichiers contiennent les horaires, les messages et le journal d'audit. Ils ne contiennent **aucun secret** : les jetons d'appareil n'y figurent que sous forme d'empreintes, et les identifiants Authentik vivent dans `.env`, qui n'est pas dans les sauvegardes. Sauvegardez `.env` séparément, ailleurs. ## Restauration ```bash docker compose -f docker-compose.yml -f docker-compose.prod.yml stop app gunzip -c backups/horaires-20260920-030000.sql.gz \ | docker compose exec -T db psql -U horaires -d horaires docker compose -f docker-compose.yml -f docker-compose.prod.yml start app ``` Après une restauration, les écrans appairés depuis le dump ne le sont plus du point de vue de la base : ils recevront un 401 et devront être réappairés par le portail captif. Rien d'autre ne se perd. ## Retour arrière L'image porte la version du dépôt, donc revenir en arrière veut dire revenir au commit : ```bash git log --oneline -10 git checkout docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build --wait ``` **Si la mise à jour comportait une migration**, revenir au code ne suffit pas : le schéma est déjà migré. Prisma ne défait pas une migration. Restaurez alors le dump pris avant la mise à jour, puis reprenez le code. C'est la raison d'être de la sauvegarde préalable. ## Ce qu'il faut surveiller | Signe | Où | Que faire | |---|---|---| | L'écran n'a pas donné signe de vie | alerte du tableau de bord | batterie, puis Wi-Fi de la boutique | | Synchro des fériés en échec | alerte, page Fériés | le cache local tient ; relancer à la main | | Messages sans traduction | alerte, page Messages | `npm run translate:test` pour savoir pourquoi | | Conteneur *unhealthy* | `docker compose ps` | `docker compose logs app` | ## Le panneau tombe sur le certificat TLS **C'est arrivé sur cette installation, et c'est réglé — voici pourquoi, pour le jour où ça recommence.** Le firmware de l'écran embarque un magasin d'autorités de certification **figé au moment de sa compilation**. Il ne peut donc pas valider une chaîne qui s'enracine sur une autorité créée après lui. Let's Encrypt a mis en service la racine `ISRG Root YR` le 13 mai 2026 ; le firmware du kit est antérieur, et la poignée de main TLS échoue **avant** qu'une seule requête soit émise — d'où le symptôme déroutant : l'écran affiche « API connection cannot be established » et ni Traefik ni l'application n'ont rien vu passer. Diagnostic, dans l'ordre : 1. **L'écran a-t-il seulement atteint le serveur ?** `docker compose logs app --since 30m | grep /api/setup` et `docker logs traefik --since 30m | grep horaires`. Si les deux sont vides, l'échec est dans la couche TLS, pas dans l'application. 2. **Quelle chaîne est servie, et jusqu'à quelle racine ?** ```bash echo | openssl s_client -connect horaires.ita-ito.com:443 \ -servername horaires.ita-ito.com 2>/dev/null | grep -E "^ *[0-9] s:" ``` Comparez la racine à ce que le système connaît : ```bash openssl crl2pkcs7 -nocrl -certfile /etc/ssl/certs/ca-certificates.crt \ | openssl pkcs7 -print_certs -noout | grep "Root YR" ``` Si une machine à jour ne la connaît pas, un firmware de 2025 encore moins. 3. **Le protocole est-il en cause ?** Souvent soupçonné, rarement coupable : ```bash echo | openssl s_client -connect horaires.ita-ito.com:443 -tls1_2 | grep "Cipher is" ``` Un ESP32 a besoin de TLS 1.2 et d'une suite `ECDHE-RSA-AES*-GCM`. Deux corrections possibles : - **Préférer une racine ancienne.** `preferredChain: "ISRG Root X1"` sur le resolver dans `traefik.yml`, puis renouvellement. `ISRG Root X1` est dans à peu près tous les magasins depuis 2021. Aucun compromis de sécurité, mais cela dépend de ce que Let's Encrypt propose encore comme chaîne alternative, et cela touche le Traefik partagé par tous les sites. - **Servir l'écran en clair** — ce qui est fait ici. Le routeur `horaires-device` dans `docker-compose.prod.yml` expose sur `:80` les seules routes `/api/setup`, `/api/display`, `/api/log` et `/api/device/`, avec une priorité qui passe devant la redirection HTTP→HTTPS générale. L'administration reste en TLS. Le jeton d'appareil circule alors en clair : il ne sert à rien d'autre et se révoque depuis *Paramètres → Appareils*. L'application **refuse** une requête d'appareil non chiffrée tant que `DEVICE_ALLOW_HTTP=true` n'est pas posé : ouvrir cette porte est une décision écrite, pas la conséquence silencieuse d'une configuration de proxy. Le jour où le firmware apprend les racines récentes, retirez le routeur `horaires-device` et remettez `DEVICE_ALLOW_HTTP=false`.