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:
@ -1319,7 +1319,8 @@ def build_index(output_dir, output_format='avif', generate_overview=True):
|
||||
'default_layers': default_layers_present(all_viz_keys),
|
||||
'default_opacity': dict(DEFAULT_OPACITY),
|
||||
'default_blend': dict(DEFAULT_BLEND),
|
||||
'n_tiles': len(display_tiles)},
|
||||
'n_tiles': len(display_tiles),
|
||||
'ui_version': ui_version()},
|
||||
}, ensure_ascii=False), encoding='utf-8')
|
||||
|
||||
_write_assets(output_dir)
|
||||
@ -1337,6 +1338,19 @@ def build_index(output_dir, output_format='avif', generate_overview=True):
|
||||
return html_path
|
||||
|
||||
|
||||
def ui_version():
|
||||
"""Version de l'interface : hash court du contenu source CSS+JS.
|
||||
|
||||
Embarqué dans le app.js servi (__UI_VERSION__ remplacé par
|
||||
_write_ui_assets) et dans index_tiles.json (stats) : le navigateur
|
||||
compare sa propre version à celle annoncée par le serveur de données
|
||||
(/api/tiles) et signale une webapp périmée (assets bâchés au build de
|
||||
l'image, cf. Dockerfile.webapp — l'image doit être reconstruite après
|
||||
toute évolution de l'interface).
|
||||
"""
|
||||
return hashlib.sha1((_APP_CSS + _APP_JS).encode('utf-8')).hexdigest()[:10]
|
||||
|
||||
|
||||
def _write_ui_assets(assets_dir):
|
||||
"""Écrit les fichiers statiques de l'interface (CSS + JS) dans `assets_dir`.
|
||||
|
||||
@ -1349,7 +1363,11 @@ def _write_ui_assets(assets_dir):
|
||||
assets_dir = Path(assets_dir)
|
||||
assets_dir.mkdir(parents=True, exist_ok=True)
|
||||
(assets_dir / 'app.css').write_text(_APP_CSS, encoding='utf-8')
|
||||
(assets_dir / 'app.js').write_text(_APP_JS, encoding='utf-8')
|
||||
# __UI_VERSION__ : hash du contenu source (ui_version), substitué à
|
||||
# l'écriture — le JS compare cette version à celle du serveur de données
|
||||
# et affiche un bandeau si la webapp est périmée.
|
||||
(assets_dir / 'app.js').write_text(
|
||||
_APP_JS.replace('__UI_VERSION__', ui_version()), encoding='utf-8')
|
||||
src_vendor = Path(__file__).parent / 'assets' / 'vendor'
|
||||
dst_vendor = assets_dir / 'vendor'
|
||||
if src_vendor.is_dir():
|
||||
@ -1411,8 +1429,7 @@ def _render_html(tiles, global_bbox, all_viz_keys, output_format, overview=None)
|
||||
# une URL neuve à chaque évolution du CSS/JS force le rechargement chez
|
||||
# tous les navigateurs, même ceux qui ont une entrée de cache antérieure
|
||||
# aux en-têtes no-cache de la webapp.
|
||||
assets_v = hashlib.sha1(
|
||||
(_APP_CSS + _APP_JS).encode('utf-8')).hexdigest()[:10]
|
||||
assets_v = ui_version()
|
||||
|
||||
# Options du sélecteur de génération/régénération : les couches du panneau
|
||||
# (PANEL_VIZ si restreint, sinon le registre complet — mêmes libellés,
|
||||
@ -2174,6 +2191,28 @@ _APP_JS = r"""'use strict';
|
||||
// VIZ_META : {clé: {label}} des visualisations disponibles
|
||||
// STATS : {default_viz, default_layers, default_opacity, n_tiles}
|
||||
|
||||
// Version de l'interface (hash source CSS+JS, __UI_VERSION__ substitué par
|
||||
// _write_ui_assets) : comparée à la version du serveur de données
|
||||
// (/api/tiles → ui_version). Mismatch = webapp périmée (image bâchée au
|
||||
// build, non reconstruite après une évolution de l'interface) → bandeau.
|
||||
const UI_VERSION = "__UI_VERSION__";
|
||||
|
||||
function checkUiVersion(data) {
|
||||
if (!data || !data.ui_version || data.ui_version === UI_VERSION) return;
|
||||
let banner = document.getElementById('uiStaleBanner');
|
||||
if (banner) return;
|
||||
banner = document.createElement('div');
|
||||
banner.id = 'uiStaleBanner';
|
||||
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:10000;' +
|
||||
'background:#e94560;color:#fff;font:13px/1.5 sans-serif;padding:6px 14px;' +
|
||||
'box-shadow:0 2px 8px rgba(0,0,0,.35)';
|
||||
banner.textContent = '⚠ Interface périmée (webapp ' + UI_VERSION +
|
||||
', serveur ' + data.ui_version + ') : des tuiles peuvent ne pas ' +
|
||||
's\'afficher. Rebuild de la webapp requis : git pull && docker compose ' +
|
||||
'up -d --build';
|
||||
document.body.appendChild(banner);
|
||||
}
|
||||
|
||||
const viewport = document.getElementById('viewport');
|
||||
|
||||
if (typeof L === 'undefined') {
|
||||
@ -4418,6 +4457,7 @@ function pollLiveTiles() {
|
||||
const q = liveTilesStamp != null ? '?stamp=' + liveTilesStamp : '';
|
||||
fetch('/api/tiles' + q).then(r => r.ok ? r.json() : null).then(d => {
|
||||
if (!d) return;
|
||||
checkUiVersion(d);
|
||||
liveTilesStamp = d.stamp;
|
||||
if (d.tiles) mergeTilesData(d);
|
||||
}).catch(() => {});
|
||||
@ -4616,6 +4656,30 @@ setInterval(() => {
|
||||
if (!document.hidden && !genPollTimer) pollLiveTiles();
|
||||
}, 15000);
|
||||
|
||||
// ============================================================
|
||||
// Garde anti-page périmée (garantie : l'ancienne interface n'est
|
||||
// jamais affichée)
|
||||
// ============================================================
|
||||
// Un onglet laissé ouvert pendant un déploiement webapp continuerait
|
||||
// d'exécuter le JS ancien. Dès que la page a plus de 15 min, son retour
|
||||
// au premier plan (onglet mobile, fenêtre) déclenche le rechargement :
|
||||
// le document étant servi no-store, le nouvel index.html amène le
|
||||
// ?v= neuf et donc le JS à jour. Vue et file de génération sont
|
||||
// conservées ; pas de rechargement pendant un run suivi (genPollTimer).
|
||||
const loadedAt = Date.now();
|
||||
function reloadStalePage() {
|
||||
if (genPollTimer || Date.now() - loadedAt < 15 * 60 * 1000) return;
|
||||
try {
|
||||
const c = map.getCenter();
|
||||
sessionStorage.setItem('lidarMap', JSON.stringify({ lat: c.lat, lng: c.lng, zoom: map.getZoom() }));
|
||||
} catch (e) { /* sessionStorage indisponible : pas grave */ }
|
||||
console.info("Page chargée depuis plus de 15 min — rechargement automatique pour l'interface à jour");
|
||||
location.reload();
|
||||
}
|
||||
document.addEventListener('visibilitychange',
|
||||
() => { if (!document.hidden) reloadStalePage(); });
|
||||
window.addEventListener('focus', reloadStalePage);
|
||||
|
||||
// ============================================================
|
||||
// Détection de nouvelles couches (run de génération en cours)
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user