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:
@ -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,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user