Files
vliaudatandClaude Opus 5 13abcf3242 chore: drop the plain-HTTP route now the panel is proven on TLS
The transport log settles it: the device reaches the server over https
and validates the Let's Encrypt chain without trouble. The dedicated
port, the Traefik entrypoint and the plain-HTTP router were all built on
a hypothesis the evidence has since refused.

DEVICE_ALLOW_HTTP goes back to false, so the device routes refuse an
unencrypted request again.

DEPLOY.md is rewritten around the real cause — the firmware does not
follow redirects, and a trailing slash was answered with a 308 — and
records the three hypotheses that were wrong, so nobody spends another
evening on them. It also names the trap that made this slow: Traefik,
a production Next server and tcpdump were each read as saying "no
traffic" when all three were simply silent by default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012cSY9pVhZmJUKNN7wf1Myd
2026-09-21 23:19:45 +02:00

182 lines
7.1 KiB
Markdown

# 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 <dépôt> /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 <commit-précédent>
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.