diff --git a/.gitignore b/.gitignore index 8dafe34..cb49126 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ build/ .coverage htmlcov/ +# Interface bâchée dans l'image (régénérée au build, cf. Dockerfile.webapp) +lidar_pipeline/webapp_assets/ + # Docker .docker/ diff --git a/Dockerfile.webapp b/Dockerfile.webapp index 5cd2422..daa9fc5 100644 --- a/Dockerfile.webapp +++ b/Dockerfile.webapp @@ -34,6 +34,11 @@ RUN pip3 install --no-cache-dir \ COPY lidar_pipeline/ ./lidar_pipeline/ COPY webapp-entrypoint.sh /webapp-entrypoint.sh +# CSS/JS de l'interface (app.js/app.css + Leaflet) bâchés dans l'image : la +# webapp les sert depuis /app/lidar_pipeline/webapp_assets, plus depuis le +# volume — le code de l'interface suit l'image, régénéré à chaque build. +RUN python3 -c "import sys; sys.path.insert(0, '/app'); from lidar_pipeline.index import _write_ui_assets; _write_ui_assets('/app/lidar_pipeline/webapp_assets')" + RUN groupadd -g 1000 lidar && \ useradd -u 1000 -g lidar -m -s /bin/bash lidar && \ mkdir -p /data/output /data/input && \ diff --git a/lidar_pipeline/index.py b/lidar_pipeline/index.py index ab7cddd..eb39c19 100644 --- a/lidar_pipeline/index.py +++ b/lidar_pipeline/index.py @@ -1138,14 +1138,16 @@ def build_index(output_dir, output_format='avif'): return html_path -def _write_assets(output_dir): - """Écrit les fichiers statiques de l'interface (CSS + JS) dans output/assets/. +def _write_ui_assets(assets_dir): + """Écrit les fichiers statiques de l'interface (CSS + JS) dans `assets_dir`. - Inclut Leaflet vendorisé (assets/vendor/leaflet) : la carte reste - fonctionnelle hors ligne, sans dépendance au CDN unpkg. + Inclut Leaflet vendorisé (vendor/leaflet) : la carte reste fonctionnelle + hors ligne, sans dépendance au CDN unpkg. La webapp bâche ces fichiers + dans l'image (cf. Dockerfile.webapp) ; le mode serveur statique les écrit + dans output/assets/ (cf. _write_assets). """ import shutil - assets_dir = Path(output_dir) / 'assets' + 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') @@ -1156,6 +1158,10 @@ def _write_assets(output_dir): shutil.copytree(src_vendor, dst_vendor) +def _write_assets(output_dir): + """Écrit les fichiers statiques de l'interface dans output/assets/.""" + _write_ui_assets(Path(output_dir) / 'assets') + def _panel_selection(all_viz_keys): """Couches proposées dans le panneau + couche activée par défaut. diff --git a/lidar_pipeline/tests/test_index.py b/lidar_pipeline/tests/test_index.py index 5acf71b..5d66809 100644 --- a/lidar_pipeline/tests/test_index.py +++ b/lidar_pipeline/tests/test_index.py @@ -319,6 +319,17 @@ def test_build_index_generates_html(tmp_path): assert len(thumbs) >= 2 # au moins hillshade pour chaque tuile +def test_write_ui_assets(tmp_path): + """_write_ui_assets écrit app.js/app.css dans le dossier cible (bâchés image).""" + from lidar_pipeline.index import _write_ui_assets, _APP_JS + + dst = tmp_path / "ui" + _write_ui_assets(dst) + js = (dst / "app.js").read_text(encoding="utf-8") + assert js == _APP_JS + assert (dst / "app.css").read_text(encoding="utf-8").startswith("/*") + + def test_mid_thumbnails_generated(tmp_path): """Vignette intermédiaire 640 px générée et référencée dans l'index.""" from lidar_pipeline.index import build_index diff --git a/lidar_pipeline/webapp.py b/lidar_pipeline/webapp.py index f0cbe4f..634ba76 100644 --- a/lidar_pipeline/webapp.py +++ b/lidar_pipeline/webapp.py @@ -146,10 +146,15 @@ async def _log_client_ip(request: Request, call_next): logger.info(f"{request.method} {path} — client : {ip or '?'}") return await call_next(request) -# assets/ (CSS/JS de l'interface) est créé dès le démarrage pour que le monteur -# statique soit actif même avant la première génération de l'index. -_assets_dir = OUTPUT_DIR / "assets" -_assets_dir.mkdir(parents=True, exist_ok=True) +# CSS/JS de l'interface : servis depuis l'IMAGE (bâchés au build, cf. +# Dockerfile.webapp), plus depuis le volume — le code de l'interface suit +# l'image, pas le cache de tuiles. En repli (dev, source non bâchée), on les +# régénère depuis les constantes de l'index pour que /assets soit actif même +# avant la première génération. +_assets_dir = Path(__file__).resolve().parent / "webapp_assets" +if not (_assets_dir / "app.js").is_file(): + from .index import _write_ui_assets + _write_ui_assets(_assets_dir) # --- Cache à la demande : la visualisation peuple le cache local ------------- # Les images viennent de la machine de traitement par HTTP : une image absente