L'interface webapp ne peut plus rester silencieusement périmée

La webapp légère bâchait app.js/app.css dans l'image au build : après une
évolution de l'interface, le Pi servait l'ancienne carte sans aucun signal
(tuiles invisibles, vieux fichiers). Désormais l'interface embarque sa
version (hash CSS+JS) que le serveur de données publie via /api/tiles : en
cas de divergence, bandeau « interface périmée » dans la carte + log d'alerte
côté webapp. Les assets sont servis en revalidation (jamais immutable) et
une page ouverte depuis plus de 15 min se recharge seule au retour au premier
plan — l'ancienne interface n'est plus jamais affichée après un déploiement.
This commit is contained in:
Antoine Jacquin
2026-09-21 23:43:19 +02:00
parent 7c10ae3e18
commit 64eaad751c
5 changed files with 237 additions and 19 deletions

View File

@ -629,8 +629,11 @@ def test_tiles_endpoint_stamp_diffing(tmp_path, monkeypatch):
import lidar_pipeline.webapp as webapp
monkeypatch.setattr(webapp, "OUTPUT_DIR", tmp_path)
# Fichier absent (aucune tuile encore indexée) → réponse vide
assert webapp.tiles_data() == {"stamp": None, "tiles": None}
# Fichier absent (aucune tuile encore indexée) → réponse vide, avec la
# version de l'interface (le JS embarqué détecte une webapp périmée)
r = webapp.tiles_data()
assert r["stamp"] is None and r["tiles"] is None
assert r["ui_version"] == webapp._ui_version_local()
# Fichier présent → données complètes puis réponse allégée au stamp
(tmp_path / "index_tiles.json").write_text(
@ -691,6 +694,74 @@ def test_tiles_endpoint_merges_remote_index(tmp_path, monkeypatch):
assert d["stamp"] == remote_stamp # max des deux stamps
def test_tiles_endpoint_advertises_ui_version(tmp_path, monkeypatch):
"""/api/tiles publie ui_version : la version du serveur de données prime.
Le JS embarqué (checkUiVersion) la compare à sa propre version et
affiche un bandeau quand la webapp est périmée (assets bâchés dans
l'image au build, image non reconstruite après une évolution).
"""
import lidar_pipeline.webapp as webapp
from lidar_pipeline.index import ui_version
monkeypatch.setattr(webapp, "OUTPUT_DIR", tmp_path)
# Pas de distant (mode mono-machine) : la version de cette webapp
d = webapp.tiles_data()
assert d["ui_version"] == ui_version()
# Distant (mode deux machines) : sa version prime, réponse allégée comprise
monkeypatch.setattr(webapp, "GENERATION_URL", "http://distant:8973")
monkeypatch.setattr(webapp, "_remote_tiles_data", lambda: {
"stamp": 1.0, "tiles": [],
"stats": {"ui_version": "remote0000"},
})
d = webapp.tiles_data()
assert d["ui_version"] == "remote0000"
light = webapp.tiles_data(stamp=d["stamp"])
assert light["tiles"] is None and light["ui_version"] == "remote0000"
def test_warn_ui_drift_once_per_version_pair(tmp_path, monkeypatch, caplog):
"""Avertissement de drift : une fois par paire de versions, sans faux positif.
Le navigateur affiche lui aussi un bandeau ; ce log aide l'opérateur
(cf. Dockerfile.webapp : l'interface est bâchée au build de l'image).
"""
import logging
import lidar_pipeline.webapp as webapp
monkeypatch.setattr(webapp, "_UI_DRIFT_WARNED", set())
local = webapp._ui_version_local()
with caplog.at_level(logging.WARNING):
webapp._warn_ui_drift({"stats": {"ui_version": "autrev0000"}})
webapp._warn_ui_drift({"stats": {"ui_version": "autrev0000"}})
# Versions alignées (mono-machine) : pas d'avertissement
webapp._warn_ui_drift({"stats": {"ui_version": local}})
# Index antérieur à l'ajout de la version : pas d'avertissement
webapp._warn_ui_drift({"stats": {}})
warned = [r for r in caplog.records
if r.getMessage().startswith("Interface webapp périmée")]
assert len(warned) == 1
def test_assets_never_immutable():
"""/assets revalidé systématiquement (no-cache), même avec ?v=.
?v= ne garantit plus l'immuabilité : les assets suivent l'IMAGE, pas le
cache de tuiles — un rebuild change le contenu SOUS le même ?v= de
l'index.html en attente de régénération. Un cache immutable bloquerait
alors l'ancien JS chez les navigateurs qui détiennent encore l'index
antérieur (premier affichage après déploiement) ; la revalidation
ETag/mtime les fait se mettre à jour.
"""
import lidar_pipeline.webapp as webapp
assert (webapp._assets_dir / "app.js").is_file()
for v in (None, "c1d46e3abc"):
resp = webapp.assets("app.js", v=v)
assert resp.headers["Cache-Control"] == "no-cache, must-revalidate"
import pytest
from fastapi import HTTPException
with pytest.raises(HTTPException):
webapp.assets("introuvable.js", v=None)
def test_tiles_endpoint_worker_offline_serves_local(tmp_path, monkeypatch):
"""Worker injoignable : index local servi seul, sans erreur."""
import json