Add Leaflet interactive map, tile generator compose, auto-sync cache, deploy docs

index.py rewritten as a continuous Leaflet map: rotated L93 tiles, stackable
visualization layers (per-layer opacity, drag-reorder persisted in
localStorage), tile info panel, live rebuild after each tile during a run.
Leaflet is vendored in assets/vendor/ so the map works fully offline;
georeferencing falls back rasterio -> pyproj -> affine so the lightweight
webapp (no GDAL) is supported.

docker-compose.worker.yml adds the tile generator service (full image + GPU)
that remote webapps call via LIDAR_GENERATION_URL, plus a one-shot process
profile. webapp.py gains LIDAR_AUTO_SYNC_SECONDS periodic cache refresh and
LIDAR_REGEN_CIDR restricting generation to the local network. run.sh
--serve-webapp now mounts ~/.ssh read-only so the rsync sync works.

docs/DEPLOY_WEBAPP.md completed for Raspberry Pi deployment: prerequisites,
git clone install, SSH key setup, first sync, update procedure and
troubleshooting.
This commit is contained in:
Antoine Jacquin
2026-09-04 21:36:14 +02:00
parent 422f58d772
commit 23969c9e14
17 changed files with 1828 additions and 336 deletions

View File

@ -583,3 +583,81 @@ def test_require_token():
# Sans jeton configuré : tout passe (LAN de confiance)
webapp.API_TOKEN = None
assert webapp._require_token(None) is None
class _FakeRequest:
"""Requête minimale pour tester la restriction par IP (peer + XFF)."""
def __init__(self, host=None, xff=None):
self.client = type("C", (), {"host": host})() if host else None
self.headers = {"x-forwarded-for": xff} if xff else {}
def test_generate_restricted_to_lan_cidr():
"""/api/generate n'est autorisé que depuis LIDAR_REGEN_CIDR (défaut 192.168.1.0/24)."""
from fastapi import HTTPException
import lidar_pipeline.webapp as webapp
old = webapp.REGEN_CIDR
webapp.REGEN_CIDR = "192.168.1.0/24"
try:
# IP du réseau autorisé
webapp._require_lan_for_generation(_FakeRequest(host="192.168.1.42"))
# Reverse proxy local (pair dans le réseau) : X-Forwarded-For désigne
# le client réel derrière lui — refusé s'il est hors réseau
webapp._require_lan_for_generation(
_FakeRequest(host="192.168.1.50", xff="192.168.1.10, 192.168.1.50"))
for host, xff in (("10.0.0.5", None), ("8.8.8.8", None),
("192.168.3.1", None), ("2001:db8::1", None),
# XFF forgé par un client externe : ignoré (pair hors réseau)
("8.8.8.8", "192.168.1.5"),
# Client réel externe derrière le proxy local
("192.168.1.50", "8.8.8.8"),
# IP absente
(None, None)):
try:
webapp._require_lan_for_generation(_FakeRequest(host=host, xff=xff))
assert False, f"403 attendu pour host={host!r} xff={xff!r}"
except HTTPException as e:
assert e.status_code == 403
assert "réseau local" in e.detail
finally:
webapp.REGEN_CIDR = old
def test_generate_cidr_disabled(monkeypatch):
"""LIDAR_REGEN_CIDR vide = restriction levée (tout client autorisé)."""
import lidar_pipeline.webapp as webapp
monkeypatch.setattr(webapp, "REGEN_CIDR", "")
webapp._require_lan_for_generation(_FakeRequest(host="8.8.8.8"))
webapp._require_lan_for_generation(_FakeRequest())
def test_status_exposes_regen_allowed():
"""/api/status indique à l'interface si le client peut générer."""
import lidar_pipeline.webapp as webapp
assert webapp.status(_FakeRequest(host="192.168.1.7"))["regen_allowed"] is True
assert webapp.status(_FakeRequest(host="8.8.8.8"))["regen_allowed"] is False
def test_auto_sync_once_skips_when_busy(monkeypatch):
"""Le cycle de cache ne lance rien si un rebuild tourne déjà."""
import time as _time
import lidar_pipeline.webapp as webapp
import lidar_pipeline.index as index_mod
calls = []
monkeypatch.setattr(index_mod, "build_index", lambda out: calls.append(out))
monkeypatch.setattr(webapp, "SYNC_CMD", "exit 0")
# Un cycle démarre bien (sync + rebuild)
assert webapp._auto_sync_once() is True
for _ in range(200):
if not webapp.rebuild_status()["running"]:
break
_time.sleep(0.05)
assert calls
# Rebuild artificiellement occupé : le cycle suivant est sauté sans erreur
webapp._rebuild["running"] = True
try:
assert webapp._auto_sync_once() is False
assert len(calls) == 1
finally:
webapp._rebuild["running"] = False