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

@ -12,21 +12,32 @@ Navigateur ──HTTP──▶ Raspberry Pi (image légère, Dockerfile.webapp)
│ └─ transmis à ──▶ machine de traitement
│ /api/sync : rsync output/ ──◀── machine puissante
▼
Machine puissante (image complète, docker-compose.yml
service `serve`) : téléchargement IGN + pipeline GPU
Machine de traitement (image complète,
docker-compose.worker.yml service `worker`) :
téléchargement IGN + pipeline GPU (générateur de tuiles)
```
## Machine de traitement (puissante)
## Machine de traitement (générateur de tuiles)
Le service `serve` existant joue le rôle de worker : il accepte les demandes
de génération envoyées par la webapp du Pi.
Le service `worker` joue le rôle de générateur : il accepte les demandes
de génération envoyées par les webapp distantes (téléchargement IGN +
traitement PDAL/GPU) et expose les tuiles produites pour le rsync.
```bash
docker compose up -d --build serve # carte + API sur http://<ip-machine>:8973
docker compose -f docker-compose.worker.yml up -d --build # API sur http://<ip-machine>:8973
docker compose -f docker-compose.worker.yml logs -f worker
```
Optionnel mais recommandé si le réseau n'est pas de confiance : protéger les
routes mutantes avec un jeton partagé — décommenter dans `docker-compose.yml` :
Traitement batch ponctuel des dalles déjà présentes dans `input/` :
```bash
docker compose -f docker-compose.worker.yml run --rm --build process [-r 0.5,0.2 | --force]
```
Sur une machine sans GPU : retirer les lignes `gpus: all` (traitement CPU,
plus lent). Optionnel mais recommandé si le réseau n'est pas de confiance :
protéger l'API avec un jeton partagé — décommenter dans
`docker-compose.worker.yml` :
```yaml
environment:
@ -35,26 +46,85 @@ environment:
## Raspberry Pi (webapp légère)
### 1. Copier le code sur le Pi
### 0. Prérequis sur le Pi
- **Architecture** : image construite nativement sur la machine (ARM64 ou
x86_64, vérifier avec `uname -m`). Le build se fait sur le Pi lui-même
(`Dockerfile.webapp` : base `python:3.12-slim`, ~200 Mo, sans PDAL/GDAL).
- **Docker + plugin compose** : installation officielle
[docs.docker.com/engine/install](https://docs.docker.com/engine/install/)
(tester avec `docker compose version`).
- **rsync/ssh côté client** : inutile sur l'hôte — ils sont dans l'image —
mais la **clé SSH** de l'hôte est montée dans le conteneur.
- **Espace disque** : prévoir la taille du cache de tuiles (compter la
taille de `output/` sur la machine de traitement, ~quelques dizaines de
Mo par dalle et par résolution).
### 1. Copier le code sur le Pi (git clone)
```bash
git clone <ce-dépôt> lidar && cd lidar # ou rsync du poste de dev
git clone ssh://git@git.example.fr:2222/code_public/lidar_rendu.git lidar
cd lidar
```
Le clone SSH utilise la même clé que le rsync (`~/.ssh`, voir ci-dessous) ;
si le serveur git n'est pas encore connu, faire une première connexion pour
accepter son empreinte. `output/` et `input/` sont ignorés par git : le
dépôt ne contient que le code, le cache de tuiles se remplit ensuite par
rsync (premier chargement, section 4).
### 2. Accès SSH pour le rsync (la Pi tire les tuiles traitées)
```bash
ssh-keygen -t ed25519 # si pas encore de clé
ssh-copy-id lidar@<ip-machine> # compte lecture sur output/
ssh lidar@<ip-machine> exit # 1re connexion : enregistre known_hosts
```
La dernière commande évite le prompt « authenticity of host » pendant le
rsync (le conteneur ne peut pas répondre interactivement).
Sur la machine puissante, le dossier `output/` doit être lisible par ce
compte (ex. `/srv/lidar/output` si vous préférez un chemin dédié — adaptez
LIDAR_SYNC_CMD).
Avec `run.sh`, `~/.ssh` (clé + known_hosts) est monté automatiquement en
lecture seule dans le conteneur ; avec `docker compose`, le montage
équivalent est à décommenter dans `docker-compose.webapp.yml` (voir
l'option b ci-dessous).
### 3. Configurer et lancer
Éditer `docker-compose.webapp.yml` :
Deux façons, au choix :
**a. `run.sh` (le plus simple)** — conteneur webapp seul, cache local des
tuiles ; les variables se passent depuis l'environnement hôte, `~/.ssh`
est monté automatiquement :
```bash
LIDAR_GENERATION_URL=http://192.168.1.50:8973 \
LIDAR_REMOTE_TOKEN=un-secret-à-partager \
LIDAR_SYNC_CMD="rsync -a --delete --exclude=*.tif --exclude=.generation* --exclude=index_thumbs --exclude=index_subtiles lidar@192.168.1.50:/srv/lidar/output/ /data/output/" \
LIDAR_AUTO_SYNC_SECONDS=600 \
./run.sh --serve-webapp # port 8973, ou --serve-webapp 9000
```
`LIDAR_AUTO_SYNC_SECONDS` entretient le cache tout seul : toutes les N
secondes, rsync ramène les nouvelles tuiles et les vignettes manquantes
sont régénérées (les mtimes évitent tout recalcul inutile). Sans cette
variable, le cache se rafraîchit à la demande : bouton ↻, fin d'un run,
ou `POST /api/sync`.
`LIDAR_REGEN_CIDR` restreint le lancement des générations (`POST
/api/generate` : zones, complétion, régénération) aux clients dont l'IP
est dans le réseau indiqué — défaut `192.168.1.0/24`, chaîne vide pour
lever la restriction. L'IP est celle de la connexion (conservée par le
DNAT Docker pour les clients du LAN) ; derrière un reverse proxy local
(dans le réseau autorisé), `X-Forwarded-For` désigne le client réel.
Les autres clients consultent la carte normalement : les boutons de
génération sont masqués et l'API répond 403.
**b. `docker compose`** — éditer `docker-compose.webapp.yml` :
- `LIDAR_GENERATION_URL` : `http://<ip-machine>:8973`
- `LIDAR_REMOTE_TOKEN` : la valeur de `LIDAR_API_TOKEN` de la machine
@ -66,6 +136,14 @@ LIDAR_SYNC_CMD).
- LIDAR_SYNC_CMD=rsync -a --delete --exclude=*.tif --exclude=.generation* --exclude=index_thumbs --exclude=index_subtiles lidar@192.168.1.50:/srv/lidar/output/ /data/output/
```
Décommenter aussi le montage de la clé SSH (rsync) :
```yaml
volumes:
- ./output:/data/output
- ~/.ssh:/home/lidar/.ssh:ro
```
Les vignettes (`index_thumbs/`, `index_subtiles/`) ne se synchronisent pas :
elles sont régénérées sur place par le Pi (`/api/sync` → `build_index`),
c'est le seul travail lourd qu'il fait. Exclure `*.tif` évite de copier des
@ -79,6 +157,10 @@ docker compose -f docker-compose.webapp.yml up -d --build
La carte est sur `http://<ip-pi>:8973/`.
`run.sh` lance le conteneur au premier plan (Ctrl-C arrête tout) : pratique
pour tester. Pour une installation permanente, préférer `docker compose`
(`restart: unless-stopped` : la carte revient après un reboot du Pi).
### 4. Premier chargement
Si aucune tuile n'a encore été synchronisée, déclencher une première fois :
@ -90,6 +172,42 @@ curl -X POST http://<ip-pi>:8973/api/sync
(puis attendre la fin : `curl http://<ip-pi>:8973/api/sync` → `"running":
false`.) Le bouton ↻ de la carte fait la même chose (sync + vignettes).
## Mise à jour
Sur le Pi, après un `git pull` (ou rsync du code) :
```bash
docker compose -f docker-compose.webapp.yml up -d --build # rebuild + redémarrage
```
Avec `run.sh --serve-webapp`, l'image est reconstruite automatiquement à
chaque lancement ; il suffit de relancer la commande. Le cache de tuiles
(`output/`) n'est pas affecté par les mises à jour d'image.
## Dépannage
- **État d'une sync** : `curl http://<ip-pi>:8973/api/sync` → `running`,
`phase` (`sync` puis `index`) et `error` (dernière erreur, ex. fin de
journal rsync en cas d'échec). Les logs du conteneur :
`docker logs lidar-webapp` (ou `docker compose -f docker-compose.webapp.yml
logs -f webapp`).
- **`Permission denied (publickey)` pendant la sync** : refaire
`ssh-copy-id lidar@<ip-machine>` et une connexion manuelle pour
renseigner `known_hosts` ; vérifier que `~/.ssh` est monté dans le
conteneur (automatique avec `run.sh`, à décommenter en compose).
- **Boutons de génération masqués / `403` sur `/api/generate`** : l'IP du
client n'est pas dans `LIDAR_REGEN_CIDR` (défaut `192.168.1.0/24`).
Adapter la variable ou la vider pour lever la restriction.
- **« Machine de traitement injoignable »** : vérifier le service `worker`
(`docker compose -f docker-compose.worker.yml logs -f worker`) et
`LIDAR_GENERATION_URL` (IP + port 8973). La consultation de la carte
reste possible avec le cache local.
- **Carte vide après sync** : attendre la fin du rebuild (`GET /api/sync` →
`"running": false`) puis recharger la page ; le bouton ↻ de la carte
enchaîne sync + vignettes automatiquement.
- **Port 8973 déjà pris** : `--serve-webapp 9000` avec `run.sh`, ou changer
le mapping `ports:` en compose (le conteneur écoute toujours sur 8973).
## Fonctionnement
- Dessiner une zone (bouton « + Zone ») sur la carte du Pi envoie la demande
@ -98,9 +216,19 @@ false`.) Le bouton ↻ de la carte fait la même chose (sync + vignettes).
machine distante en direct.
- À la fin du run, le navigateur appelle `/api/sync` : rsync ramène les
images, le Pi régénère vignettes/sous-tuiles/index.html, puis recharge la
carte. Le bouton ↻ relance le même cycle à tout moment.
carte. Le bouton ↻ relance le même cycle à tout moment. Avec
`LIDAR_AUTO_SYNC_SECONDS`, le conteneur webapp seul maintient aussi son
cache périodiquement (utile quand personne ne consulte la carte).
- Sans machine de traitement configurée, `./run.sh --serve-webapp` sert la
carte en lecture seule depuis `output/` (cache figé, aucun traitement
possible depuis l'interface).
- **Autonomie** : une fois les tuiles chargées dans le cache local, la
webapp fonctionne seule — Leaflet est vendorisé dans `assets/vendor/`
(aucun CDN), tuiles/vignettes/infos sont lues dans `output/`, et un
backend injoignable masque simplement les boutons de génération sans
affecter la consultation.
- Machine de traitement seule (sans Pi) : rien ne change, `--serve` /
`docker compose up serve` fonctionne comme avant en local.
`docker compose up -d --build serve` fonctionne comme avant en local (tout-en-un), et `docker-compose.worker.yml` installe le générateur seul.
- Si la machine de traitement est éteinte, la carte du Pi reste consultable
(données synchronisées) ; seuls les nouveaux traitements sont indisponibles
(message « machine de traitement injoignable »).
@ -108,8 +236,11 @@ false`.) Le bouton ↻ de la carte fait la même chose (sync + vignettes).
## Références
- `lidar_pipeline/webapp.py` : proxy distant (`LIDAR_GENERATION_URL`),
`/api/sync`, jeton `LIDAR_API_TOKEN`/`LIDAR_REMOTE_TOKEN`.
`/api/sync`, cache local périodique (`LIDAR_AUTO_SYNC_SECONDS`), jeton
`LIDAR_API_TOKEN`/`LIDAR_REMOTE_TOKEN`, restriction des générations au
réseau local (`LIDAR_REGEN_CIDR`).
- `lidar_pipeline/index.py` : vignettes/index sans GDAL (pyproj ou repli
affine), recharge après sync.
- `Dockerfile.webapp`, `docker-compose.webapp.yml` : image légère ARM64
- `docker-compose.worker.yml` : générateur de tuiles (machine puissante).
- `Dockerfile.webapp`, `docker-compose.webapp.yml` : webapp légère ARM64
(FastAPI + Pillow AVIF natif + pyproj, ~200 Mo).