# 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 ne se connecte pas **C'est arrivé sur cette installation. Le diagnostic a pris une soirée et trois fausses pistes ; voici ce qu'il faut savoir avant de recommencer.** ### La cause, une fois pour toutes Le firmware **ne suit pas les redirections**. Il demande `/api/setup/` avec une barre oblique finale ; Next répondait `308` pour normaliser, l'écran traitait ce code comme un échec, n'obtenait jamais de jeton, puis se faisait refuser `/api/display` en `401` et affichait « API connection cannot be established ». L'application sert désormais les deux écritures (`skipTrailingSlashRedirect` et des réécritures dans `next.config.ts`). Si vous ajoutez une route destinée à l'appareil, **ajoutez-y sa variante avec barre oblique**. ### Ce qui n'était PAS en cause Trois hypothèses ont été poursuivies avant la bonne. Elles sont consignées ici pour que personne ne les reprenne : - **La chaîne TLS.** La racine `ISRG Root YR` date de mai 2026 et n'est pas dans le magasin d'Ubuntu ; j'en ai conclu qu'un firmware antérieur ne pouvait pas la valider. **Faux** : le journal montre l'écran arriver en `https` sans difficulté. - **Le réseau de la boutique.** Un test depuis un téléphone sur le même Wi-Fi a montré que rien n'était filtré. - **Le port.** Un entrypoint Traefik en clair sur `2300` a été mis en place puis retiré : il n'a jamais servi à rien. ### Pourquoi c'était si long : les outils silencieux Trois fois de suite, un outil muet a été lu comme un diagnostic négatif. | Outil | Piège | |---|---| | Traefik | ne journalise pas les accès par défaut | | Next.js en production | ne journalise pas les requêtes | | `tcpdump` | met sa sortie en tampon sans `-l` : fichier vide malgré du trafic | **Et l'écran disait la réponse depuis le début.** Il envoyait son rapport d'erreur sur `/api/log` — sans jeton, puisqu'il n'avait jamais réussi à s'appairer — et la route répondait `401`. Un appareil incapable de s'authentifier est précisément celui qu'il faut écouter : ces journaux sont maintenant acceptés et écrits dans les logs du serveur. ### Le diagnostic, dans l'ordre ```bash # 1. Par quel transport l'écran arrive-t-il, et arrive-t-il ? docker compose logs app --since 3h | grep "\[device\]" # 2. Que dit l'écran lui-même ? docker compose logs app --since 3h | grep "appareil non authentifié" # 3. Les deux écritures répondent-elles sans redirection ? for p in /api/setup /api/setup/ /api/display /api/display/; do curl -s -o /dev/null -w "$p -> %{http_code} %{redirect_url}\n" \ "https://horaires.ita-ito.com$p" done ``` Un `308` à l'étape 3 est la panne d'origine qui revient. ### Si le TLS est réellement en cause un jour Le firmware valide aujourd'hui la chaîne Let's Encrypt. Si une racine future lui échappait vraiment, deux voies : `preferredChain: "ISRG Root X1"` sur le resolver dans `traefik.yml`, ou un entrypoint en clair réservé aux quatre routes de l'appareil. Dans ce second cas, `DEVICE_ALLOW_HTTP=true` est nécessaire : l'application refuse une requête d'appareil non chiffrée sans cette décision écrite.