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

@ -333,20 +333,22 @@ for name in ("index_thumbs", "index_subtiles", "index_overview",
@app.get("/assets/{file_path:path}")
def assets(file_path: str, v: str = None):
"""Sert les fichiers de l'interface sans cache (régénérés à chaque rebuild).
"""Sert les fichiers de l'interface en revalidation (jamais immutable).
Exception : ?v= (hash de contenu posé par build_index) → cache immutable.
?v= est le hash de contenu posé par build_index — mais les assets
suivent l'IMAGE, pas le cache de tuiles : un rebuild de l'image change
le contenu SOUS le même ?v= de l'index.html en attente de régénération.
Un cache immutable les bloquerait alors chez les navigateurs qui
détiennent encore l'index.html antérieur (premier affichage après
déploiement = ancien JS). no-cache → revalidation ETag/mtime quasi
gratuite (304) ; si le fichier a changé (mtime neuve après rebuild),
le navigateur récupère le contenu frais même depuis un index.html périmé.
"""
base = _assets_dir.resolve()
p = (_assets_dir / file_path).resolve()
if base not in p.parents or not p.is_file():
raise HTTPException(404, f"asset introuvable : {file_path}")
# ?v= : hash de contenu hexadécimal posé par build_index (l'URL change
# à chaque évolution du CSS/JS) → cache immutable.
cache = ("public, max-age=31536000, immutable"
if v
else "no-cache, must-revalidate")
return FileResponse(str(p), headers={"Cache-Control": cache})
return FileResponse(str(p), headers={"Cache-Control": "no-cache, must-revalidate"})
# Méthodes de classification du sol acceptées (mêmes valeurs que --ground-classification).
@ -966,6 +968,39 @@ def available_layers():
_REMOTE_INDEX = {"data": None, "fetched": 0.0}
_LOCAL_UI_VERSION = None
_UI_DRIFT_WARNED = set()
def _ui_version_local():
"""Version du JS/CSS bâti dans cette webapp (index.ui_version), en cache."""
global _LOCAL_UI_VERSION
if _LOCAL_UI_VERSION is None:
from .index import ui_version
_LOCAL_UI_VERSION = ui_version()
return _LOCAL_UI_VERSION
def _warn_ui_drift(remote_data):
"""Dérive de version webapp/serveur de données : avertit une fois par paire.
L'interface de la webapp est bâchée au build de l'image (cf.
Dockerfile.webapp) : sans rebuild, les tuiles produites par un worker
plus récent ne s'affichent pas. Le navigateur signale le décalage lui
aussi (bandeau « interface périmée ») ; ce log aide l'opérateur.
"""
remote_ver = ((remote_data or {}).get("stats") or {}).get("ui_version")
local_ver = _ui_version_local()
if not remote_ver or remote_ver == local_ver:
return
key = (local_ver, remote_ver)
if key in _UI_DRIFT_WARNED:
return
_UI_DRIFT_WARNED.add(key)
logger.warning(
f"Interface webapp périmée (webapp {local_ver}, "
f"serveur {remote_ver}) — reconstruire l'image : "
f"git pull && docker compose up -d --build")
_REMOTE_INDEX_TTL = 60.0
@ -991,6 +1026,7 @@ def _remote_tiles_data():
data = json.loads(r.read().decode("utf-8"))
_worker_mark(True)
_REMOTE_INDEX.update({"data": data, "fetched": now})
_warn_ui_drift(data)
except Exception as e:
_worker_mark(False)
_REMOTE_INDEX["fetched"] = now # ne pas marteler à chaque requête
@ -1022,10 +1058,17 @@ def tiles_data(stamp: Optional[float] = None):
remote_stamp = (remote or {}).get("stamp")
stamps = [s for s in (local_stamp, remote_stamp) if s is not None]
current = max(stamps) if stamps else None
# Version de l'interface du SERVEUR DE DONNÉES (le worker en mode deux
# machines, soi-même sinon) : le JS embarqué compare sa propre version
# (index.ui_version, __UI_VERSION__) et affiche un bandeau si la webapp
# est périmée (assets bâchés au build, image non reconstruite).
ui_ver = (((remote or {}).get("stats")
or (data or {}).get("stats") or {}).get("ui_version")
or _ui_version_local())
if stamp is not None and current is not None and abs(current - stamp) < 1e-4:
return {"stamp": current, "tiles": None}
return {"stamp": current, "tiles": None, "ui_version": ui_ver}
if data is None and remote is None:
return {"stamp": None, "tiles": None}
return {"stamp": None, "tiles": None, "ui_version": ui_ver}
# L'index distant prime (tuiles régénérées : ?v= neuf → re-téléchargement) ;
# le local complète les positions que le worker n'a plus (historique).
merged = list((remote or {}).get("tiles") or [])
@ -1053,6 +1096,8 @@ def tiles_data(stamp: Optional[float] = None):
"overview": merged_overview,
"viz_meta": viz_meta,
"stats": (remote or data or {}).get("stats") or {},
# Version de l'interface du serveur de données (cf. plus haut).
"ui_version": ui_ver,
"stamp": current,
}