Fusionner l'index du worker dans la coquille au rebuild (1er chargement complet)
- 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.
This commit is contained in:
@ -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 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)
|
- 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).
|
- 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 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.<module>`
|
- test file: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.<module>`
|
||||||
- test case: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.<module>::<TestClass>::<test_method>`
|
- test case: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.<module>::<TestClass>::<test_method>`
|
||||||
|
|||||||
@ -332,6 +332,51 @@ def test_rebuild_flag_synchronous(monkeypatch):
|
|||||||
webapp._rebuild.update(saved)
|
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(
|
||||||
|
"<html><script>const TILES = " + _json.dumps([locale, perimee])
|
||||||
|
+ ";</script></html>", 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):
|
def test_static_mounts_served_no_cache(tmp_path):
|
||||||
"""Les montages d'images servent Cache-Control: no-cache.
|
"""Les montages d'images servent Cache-Control: no-cache.
|
||||||
|
|
||||||
|
|||||||
@ -999,6 +999,59 @@ def tiles_data(stamp: Optional[float] = None):
|
|||||||
_rebuild = {"running": False, "error": None, "done": None, "phase": 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():
|
def _start_rebuild():
|
||||||
"""Lance en arrière-plan le rebuild de l'index (vignettes + carte).
|
"""Lance en arrière-plan le rebuild de l'index (vignettes + carte).
|
||||||
|
|
||||||
@ -1021,6 +1074,14 @@ def _start_rebuild():
|
|||||||
try:
|
try:
|
||||||
from .index import build_index
|
from .index import build_index
|
||||||
build_index(OUTPUT_DIR)
|
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
|
except Exception as e: # noqa: BLE001 — remonté à l'UI via l'état
|
||||||
_rebuild["error"] = str(e)
|
_rebuild["error"] = str(e)
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
Reference in New Issue
Block a user