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

7.1 KiB

Déploiement

Procédures d'exploitation pour horaires.ita-ito.com. Le README 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.

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 :

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

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 :

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

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 :

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

# 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.