From 6d96da6546786e3b47a20aa7ee0dd01866a6c02a Mon Sep 17 00:00:00 2001 From: Antoine Jacquin Date: Thu, 17 Sep 2026 08:01:01 +0200 Subject: [PATCH] Fusionner l'index du worker dans la coquille au rebuild (1er chargement complet) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sur la webapp légère, le rebuild ne voyait que le cache local : la coquille HTML embarquait un sous-ensemble des tuiles et le premier chargement n'affichait pas les tuiles récentes du worker — elles n'arrivaient qu'au sondage /api/tiles suivant (15 s). - _merge_remote_index() applique au rebuild la même fusion que /api/tiles (l'index distant prime, le local complète) et réécrit index_tiles.json ET la liste embarquée dans index.html. - AGENTS.md : avertissement — le Pi de prod tourne en compose+override (labels Traefik + /srv/lidar/output), jamais via serve-webapp.sh. --- AGENTS.md | 2 +- lidar_pipeline/tests/test_webapp.py | 45 +++++++++++++++++++++ lidar_pipeline/webapp.py | 61 +++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index b3242d7..2cc345f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ - build webapp légère (Raspberry Pi, déploiement 2 machines — cf. `docs/DEPLOY_WEBAPP.md`): `docker compose -f docker-compose.webapp.yml up -d --build` (image `Dockerfile.webapp`, sans PDAL/GPU) - build générateur de tuiles (machine de traitement): `docker compose -f docker-compose.worker.yml up -d --build` (service `worker`, API pour les webapp distantes) - simulation locale du mode deux machines : `docker compose -f docker-compose.local-2m.yml up -d --build` — worker GPU sur :8974 + webapp légère sur :8973 avec son PROPRE cache `output-webapp/` peuplé à la demande depuis le worker. Permet de rebuild l'interface sans toucher au worker, et réciproquement. Résolution 0,2 m uniquement (GENERATE_RESOLUTIONS). -- stack webapp (machine légère): `./serve-webapp.sh [start|stop|restart|status|sync|logs]`, config dans `webapp.env` (modèle `webapp.env.example`, ignoré par git) ; mise à jour = `git pull` puis rebuild image + `restart` (cf. `docs/DEPLOY_WEBAPP.md`) +- stack webapp (machine légère): `./serve-webapp.sh [start|stop|restart|status|sync|logs]`, config dans `webapp.env` (modèle `webapp.env.example`, ignoré par git) ; mise à jour = `git pull` puis rebuild image + `restart` (cf. `docs/DEPLOY_WEBAPP.md`). **ATTENTION Pi de prod (192.168.3.10) : la webapp y tourne via `docker compose -f docker-compose.webapp.yml -f docker-compose.webapp.override.yml up -d --build` (labels Traefik `lidar.example.fr` + volume réel `/srv/lidar/output`) — NE PAS utiliser `serve-webapp.sh` là-bas : il recrée le conteneur sans labels (route publique 404) et sur le mauvais volume `./output`.** - test all: `./run.sh --test` (rebuild automatique de l'image avant les tests ; en `docker run` direct, rebuild manuellement d'abord) - test file: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.` - test case: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.::::` diff --git a/lidar_pipeline/tests/test_webapp.py b/lidar_pipeline/tests/test_webapp.py index be17fe3..1cec64e 100644 --- a/lidar_pipeline/tests/test_webapp.py +++ b/lidar_pipeline/tests/test_webapp.py @@ -332,6 +332,51 @@ def test_rebuild_flag_synchronous(monkeypatch): webapp._rebuild.update(saved) +def test_merge_remote_index_merges_files(tmp_path, monkeypatch): + """Le rebuild fusionne l'index du worker dans les fichiers, pas seulement /api/tiles. + + Sans fusion, la coquille HTML embarque un sous-ensemble des tuiles (le + cache local seul) : au 1er chargement, les tuiles récentes du worker + manquent et n'arrivent qu'au sondage suivant. + """ + import json as _json + import lidar_pipeline.webapp as webapp + + monkeypatch.setattr(webapp, "OUTPUT_DIR", tmp_path) + locale = {"col": 10, "row": 20, "resolution": 0.2, "name": "locale", "viz": {}} + perimee = {"col": 11, "row": 20, "resolution": 0.2, "name": "locale-perimee", "viz": {}} + dist_neuve = {"col": 11, "row": 20, "resolution": 0.2, "name": "distante-neuve", "viz": {}} + dist_seule = {"col": 12, "row": 20, "resolution": 0.2, "name": "distante-seule", "viz": {}} + (tmp_path / "index_tiles.json").write_text(_json.dumps( + {"tiles": [locale, perimee], + "viz_meta": {"aspect": {"label": "Aspect"}}, + "stats": {"n_tiles": 2}}), encoding="utf-8") + (tmp_path / "index.html").write_text( + "", encoding="utf-8") + monkeypatch.setattr(webapp, "_remote_tiles_data", lambda: { + "tiles": [dist_neuve, dist_seule], + "viz_meta": {"aspect": {"label": "Aspect distant"}}, + "stats": {"n_tiles": 2}, + }) + webapp._merge_remote_index() + data = _json.loads((tmp_path / "index_tiles.json").read_text(encoding="utf-8")) + names = [t["name"] for t in data["tiles"]] + # Le distant prime sur la même position, le local complète les autres. + assert "distante-neuve" in names and "locale" in names and "distante-seule" in names + assert "locale-perimee" not in names + assert data["viz_meta"]["aspect"]["label"] == "Aspect distant" + # La coquille embarque la même liste fusionnée. + html = (tmp_path / "index.html").read_text(encoding="utf-8") + assert "distante-neuve" in html and "distante-seule" in html + assert "locale-perimee" not in html + # Worker injoignable : fichiers laissés intacts. + before = (tmp_path / "index_tiles.json").read_text(encoding="utf-8") + monkeypatch.setattr(webapp, "_remote_tiles_data", lambda: None) + webapp._merge_remote_index() + assert (tmp_path / "index_tiles.json").read_text(encoding="utf-8") == before + + def test_static_mounts_served_no_cache(tmp_path): """Les montages d'images servent Cache-Control: no-cache. diff --git a/lidar_pipeline/webapp.py b/lidar_pipeline/webapp.py index 015e922..c9fc753 100644 --- a/lidar_pipeline/webapp.py +++ b/lidar_pipeline/webapp.py @@ -999,6 +999,59 @@ def tiles_data(stamp: Optional[float] = None): _rebuild = {"running": False, "error": None, "done": None, "phase": None} +def _merge_remote_index(): + """Fusionne l'index du worker dans index_tiles.json et la coquille HTML. + + Le rebuild local ne voit que le cache : sans fusion, la coquille embarque + un sous-ensemble des tuiles (celles déjà rapatriées) et le premier + chargement de la carte n'affiche pas les tuiles récentes du worker — + elles n'arrivent qu'au sondage /api/tiles qui suit. Mêmes règles que + tiles_data : l'index distant prime (tuiles régénérées), le local complète + les positions que le worker n'a plus (historique). + """ + remote = _remote_tiles_data() # frais : _start_rebuild a vidé le cache + if not remote or not remote.get("tiles"): + return + path = OUTPUT_DIR / "index_tiles.json" + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (OSError, ValueError): + data = {} + merged = list(remote["tiles"]) + keys = {_tile_entry_key(t) for t in merged} + for t in data.get("tiles") or []: + k = _tile_entry_key(t) + if k not in keys: + merged.append(t) + keys.add(k) + viz_meta = dict(data.get("viz_meta") or {}) + viz_meta.update(remote.get("viz_meta") or {}) + from .index import PANEL_VIZ + 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} + path.write_text(json.dumps({ + "tiles": merged, + "viz_meta": viz_meta, + "stats": remote.get("stats") or data.get("stats") or {}, + }, 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. + html_path = OUTPUT_DIR / "index.html" + try: + html = html_path.read_text(encoding="utf-8") + new_html = re.sub(r"const TILES = \[.*?\];", + "const TILES = " + json.dumps(merged, ensure_ascii=False) + ";", + html, count=1, flags=re.S) + if new_html != html: + html_path.write_text(new_html, encoding="utf-8") + except OSError: + pass + logger.info(f"Index fusionné avec la machine de traitement : " + f"{len(merged)} tuile(s) au total") + + def _start_rebuild(): """Lance en arrière-plan le rebuild de l'index (vignettes + carte). @@ -1021,6 +1074,14 @@ def _start_rebuild(): try: from .index import build_index build_index(OUTPUT_DIR) + # Mode deux machines : le rebuild local ne voit que le cache — + # on y fusionne l'index du worker pour que coquille embarquée et + # /api/tiles servent la même vue complète dès le 1er chargement. + if GENERATION_URL: + try: + _merge_remote_index() + except Exception as e: # noqa: BLE001 — index local valable + logger.warning(f"Fusion de l'index distant ignorée : {e}") except Exception as e: # noqa: BLE001 — remonté à l'UI via l'état _rebuild["error"] = str(e) finally: