Vue dézoomée en mosaïque d'overview et cache immutable des images versionnées
This commit is contained in:
@ -172,7 +172,8 @@ if not (_assets_dir / "app.js").is_file():
|
||||
from fastapi.staticfiles import StaticFiles as _StaticFiles
|
||||
from starlette.concurrency import run_in_threadpool
|
||||
|
||||
_ONDEMAND_PREFIXES = ("visualisations", "index_thumbs", "index_subtiles")
|
||||
_ONDEMAND_PREFIXES = ("visualisations", "index_thumbs", "index_subtiles",
|
||||
"index_overview")
|
||||
_FETCH_SEMAPHORE = threading.Semaphore(4)
|
||||
_FETCH_LOCKS = {}
|
||||
_FETCH_LOCKS_GUARD = threading.Lock()
|
||||
@ -259,20 +260,21 @@ def _fetch_remote_to_cache(rel_path):
|
||||
|
||||
|
||||
class _NoCacheStaticFiles(_StaticFiles):
|
||||
"""Fichiers statiques servis sans cache navigateur (revalidation 304).
|
||||
"""Fichiers statiques : cache immutable si l'URL est versionnée (?v=).
|
||||
|
||||
Les images (visualisations, vignettes, sous-tuiles, DTM) n'ont pas
|
||||
toujours une URL invalidée après régénération : le ?v= suit la mtime de
|
||||
la SOURCE, pas celle du fichier servi (vignette recalculée après coup,
|
||||
cache webapp rapatrié…). Sans Cache-Control, le cache heuristique du
|
||||
navigateur peut continuer d'afficher l'ancien rendu sur la même URL.
|
||||
no-cache force la revalidation à chaque affichage — ETag/Last-Modified
|
||||
la rend quasi gratuite quand le fichier n'a pas changé (réponse 304).
|
||||
Les URLs des images portent ?v= (mtime du fichier servi, posée par
|
||||
build_index) : le contenu qu'elles désignent ne change jamais → cache
|
||||
immutable (1 an, jamais revalidé). Sans ?v= (index.html vu directement,
|
||||
rejeu manuel d'URL), on garde no-cache : revalidation 304 quasi gratuite
|
||||
via ETag/Last-Modified, et jamais de rendu périmé après régénération.
|
||||
"""
|
||||
|
||||
async def get_response(self, path, scope):
|
||||
resp = await super().get_response(path, scope)
|
||||
resp.headers["Cache-Control"] = "no-cache, must-revalidate"
|
||||
resp.headers["Cache-Control"] = (
|
||||
"public, max-age=31536000, immutable"
|
||||
if _version_from_query(scope) is not None
|
||||
else "no-cache, must-revalidate")
|
||||
return resp
|
||||
|
||||
|
||||
@ -317,7 +319,8 @@ class _OnDemandStaticFiles(_NoCacheStaticFiles):
|
||||
return _fetch_remote_to_cache(rel)
|
||||
|
||||
|
||||
for name in ("index_thumbs", "index_subtiles", "visualisations", "DTM"):
|
||||
for name in ("index_thumbs", "index_subtiles", "index_overview",
|
||||
"visualisations", "DTM"):
|
||||
_dir = OUTPUT_DIR / name
|
||||
_dir.mkdir(parents=True, exist_ok=True)
|
||||
if name in _ONDEMAND_PREFIXES and GENERATION_URL:
|
||||
@ -329,13 +332,21 @@ for name in ("index_thumbs", "index_subtiles", "visualisations", "DTM"):
|
||||
|
||||
|
||||
@app.get("/assets/{file_path:path}")
|
||||
def assets(file_path: str):
|
||||
"""Sert les fichiers de l'interface sans cache (régénérés à chaque rebuild)."""
|
||||
def assets(file_path: str, v: str = None):
|
||||
"""Sert les fichiers de l'interface sans cache (régénérés à chaque rebuild).
|
||||
|
||||
Exception : ?v= (hash de contenu posé par build_index) → cache immutable.
|
||||
"""
|
||||
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}")
|
||||
return FileResponse(str(p), headers={"Cache-Control": "no-cache, must-revalidate"})
|
||||
# ?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})
|
||||
|
||||
|
||||
# Méthodes de classification du sol acceptées (mêmes valeurs que --ground-classification).
|
||||
@ -1033,8 +1044,13 @@ def tiles_data(stamp: Optional[float] = None):
|
||||
if PANEL_VIZ is not None:
|
||||
allowed = set(PANEL_VIZ)
|
||||
viz_meta = {k: v for k, v in viz_meta.items() if k in allowed}
|
||||
merged_overview = dict((data or {}).get("overview") or {})
|
||||
merged_overview.update((remote or {}).get("overview") or {})
|
||||
return {
|
||||
"tiles": merged,
|
||||
# Mosaïques d'overview : le distant prime (grille recalculée),
|
||||
# le local complète les couches que le worker n'a plus.
|
||||
"overview": merged_overview,
|
||||
"viz_meta": viz_meta,
|
||||
"stats": (remote or data or {}).get("stats") or {},
|
||||
"stamp": current,
|
||||
@ -1073,6 +1089,8 @@ def _merge_remote_index():
|
||||
if k not in keys:
|
||||
merged.append(t)
|
||||
keys.add(k)
|
||||
overview = dict(data.get("overview") or {})
|
||||
overview.update(remote.get("overview") or {})
|
||||
viz_meta = dict(data.get("viz_meta") or {})
|
||||
viz_meta.update(remote.get("viz_meta") or {})
|
||||
from .index import PANEL_VIZ
|
||||
@ -1081,6 +1099,7 @@ def _merge_remote_index():
|
||||
viz_meta = {k: v for k, v in viz_meta.items() if k in allowed}
|
||||
path.write_text(json.dumps({
|
||||
"tiles": merged,
|
||||
"overview": overview,
|
||||
"viz_meta": viz_meta,
|
||||
# Les stats décrivent les réglages de l'INTERFACE (couches par
|
||||
# défaut) : elles viennent du rebuild local, fait avec le code de
|
||||
@ -1091,7 +1110,9 @@ def _merge_remote_index():
|
||||
}, ensure_ascii=False), encoding="utf-8")
|
||||
# Coquille HTML : la liste embarquée doit être la même (fusionnée). Le
|
||||
# JSON ne contient jamais de « ; » : la première séquence « ]; » après
|
||||
# « const TILES = [ » termine forcément l'instruction.
|
||||
# « const TILES = [ » termine forcément l'instruction (idem « }; » pour
|
||||
# const OVERVIEW : à l'intérieur d'un objet JSON, « } » n'est suivi que
|
||||
# de « , » « ] » ou « } »).
|
||||
html_path = OUTPUT_DIR / "index.html"
|
||||
try:
|
||||
html = html_path.read_text(encoding="utf-8")
|
||||
@ -1099,6 +1120,10 @@ def _merge_remote_index():
|
||||
"const TILES = " + json.dumps(merged, ensure_ascii=False)
|
||||
.replace("</", "<\\/") + ";",
|
||||
html, count=1, flags=re.S)
|
||||
new_html = re.sub(r"const OVERVIEW = \{.*?\};",
|
||||
"const OVERVIEW = " + json.dumps(overview, ensure_ascii=False)
|
||||
.replace("</", "<\\/") + ";",
|
||||
new_html, count=1, flags=re.S)
|
||||
if new_html != html:
|
||||
html_path.write_text(new_html, encoding="utf-8")
|
||||
except OSError:
|
||||
|
||||
Reference in New Issue
Block a user