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:
@ -307,11 +307,19 @@ def test_build_index_generates_html(tmp_path):
|
||||
# les tuiles terminées sans recharger (mode incrémental du pipeline).
|
||||
assert "/api/tiles" in app_js and "pollLiveTiles" in app_js
|
||||
assert "registerTile" in app_js and "mergeTilesData" in app_js
|
||||
# Garde anti-page périmée : un onglet ouvert depuis plus de 15 min
|
||||
# (JS ancien après un déploiement) se recharge au retour au premier
|
||||
# plan — l'ancienne interface n'est jamais affichée.
|
||||
assert "reloadStalePage" in app_js and "visibilitychange" in app_js
|
||||
# Export des données de carte (servi par /api/tiles)
|
||||
data = json.loads((output_dir / "index_tiles.json").read_text(encoding='utf-8'))
|
||||
assert data['tiles'] and data['tiles'][0]['corners']
|
||||
assert 'aspect' in data['viz_meta']
|
||||
assert data['stats']['n_tiles'] == len(data['tiles'])
|
||||
# Version de l'interface exportée : la webapp légère détecte une image
|
||||
# périmée (assets bâchés au build, non reconstruite après évolution)
|
||||
from lidar_pipeline.index import ui_version
|
||||
assert data['stats']['ui_version'] == ui_version()
|
||||
# Vérifie les vignettes générées
|
||||
thumb_dir = output_dir / "index_thumbs"
|
||||
assert thumb_dir.is_dir()
|
||||
@ -320,13 +328,23 @@ def test_build_index_generates_html(tmp_path):
|
||||
|
||||
|
||||
def test_write_ui_assets(tmp_path):
|
||||
"""_write_ui_assets écrit app.js/app.css dans le dossier cible (bâchés image)."""
|
||||
from lidar_pipeline.index import _write_ui_assets, _APP_JS
|
||||
"""_write_ui_assets écrit app.js/app.css dans le dossier cible (bâchés image).
|
||||
|
||||
app.js a __UI_VERSION__ substitué par la version réelle (ui_version) :
|
||||
le JS compare cette version à celle du serveur de données et signale
|
||||
une webapp périmée (bandeau, cf. checkUiVersion).
|
||||
"""
|
||||
from lidar_pipeline.index import _write_ui_assets, _APP_JS, ui_version
|
||||
|
||||
dst = tmp_path / "ui"
|
||||
_write_ui_assets(dst)
|
||||
v = ui_version()
|
||||
assert v == ui_version() and len(v) == 10
|
||||
js = (dst / "app.js").read_text(encoding="utf-8")
|
||||
assert js == _APP_JS
|
||||
assert js == _APP_JS.replace("__UI_VERSION__", v)
|
||||
assert v in js and "__UI_VERSION__" not in js
|
||||
# Le placeholder et le check sont embarqués dans la source
|
||||
assert '"__UI_VERSION__"' in _APP_JS and "checkUiVersion" in _APP_JS
|
||||
assert (dst / "app.css").read_text(encoding="utf-8").startswith("/*")
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Reference in New Issue
Block a user