Centrer la carte sur le GPS du téléphone (HTTPS terminé par le proxy)
Le bouton ⌖ recentre la carte sur la position GPS du téléphone. L'API Geolocation exige un contexte sécurisé : la webapp sert désormais du HTTP et le TLS est porté par le reverse proxy (Traefik, labels dans l'override local) ; le webapp est transparent au proxy (URLs relatives, X-Forwarded-For pour la restriction LIDAR_REGEN_CIDR). Ajoute le modèle d'override docker-compose.webapp.override.yml.example, un rebuild d'index à la demande et met à jour doc/déploiement ; les tuiles sont servies à la demande.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@ -45,6 +45,9 @@ htmlcov/
|
|||||||
.env.local
|
.env.local
|
||||||
webapp.env
|
webapp.env
|
||||||
|
|
||||||
|
# Certificat TLS auto-signé (webapp HTTPS / GPS téléphone, cf. make-tls-cert.sh)
|
||||||
|
tls/
|
||||||
|
|
||||||
# Données et artefacts internes (jamais dans le dépôt)
|
# Données et artefacts internes (jamais dans le dépôt)
|
||||||
data/
|
data/
|
||||||
.swival/
|
.swival/
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
- build: `docker build -t lidar-lidar .`
|
- build: `docker build -t lidar-lidar .`
|
||||||
- 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/` (rsync local depuis `./output` monté `/data/remote:ro`, auto-sync 120 s). 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|update]`, config dans `webapp.env` (modèle `webapp.env.example`, ignoré par git)
|
- stack webapp (machine légère): `./serve-webapp.sh [start|stop|restart|status|sync|logs|update]`, config dans `webapp.env` (modèle `webapp.env.example`, ignoré par git)
|
||||||
- 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>`
|
||||||
|
|||||||
@ -11,10 +11,10 @@ FROM python:3.12-slim
|
|||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
ENV TZ=Europe/Paris
|
ENV TZ=Europe/Paris
|
||||||
|
|
||||||
# rsync + client SSH : synchronisation des tuiles depuis la machine de
|
# Client SSH : inspection/accès à la machine de traitement si besoin. La
|
||||||
# traitement (LIDAR_SYNC_CMD). La clé se monte dans /home/lidar/.ssh.
|
# clé se monte dans /home/lidar/.ssh. Les tuiles étant servies à la demande
|
||||||
|
# (plus de rsync dans l'image), seul le client SSH est conservé.
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
rsync \
|
|
||||||
openssh-client \
|
openssh-client \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
@ -32,11 +32,13 @@ RUN pip3 install --no-cache-dir \
|
|||||||
# Code : webapp.py + index.py seulement utilisés — les modules de traitement
|
# Code : webapp.py + index.py seulement utilisés — les modules de traitement
|
||||||
# (dtm, visualizations...) sont copiés mais jamais importés par la webapp.
|
# (dtm, visualizations...) sont copiés mais jamais importés par la webapp.
|
||||||
COPY lidar_pipeline/ ./lidar_pipeline/
|
COPY lidar_pipeline/ ./lidar_pipeline/
|
||||||
|
COPY webapp-entrypoint.sh /webapp-entrypoint.sh
|
||||||
|
|
||||||
RUN groupadd -g 1000 lidar && \
|
RUN groupadd -g 1000 lidar && \
|
||||||
useradd -u 1000 -g lidar -m -s /bin/bash lidar && \
|
useradd -u 1000 -g lidar -m -s /bin/bash lidar && \
|
||||||
mkdir -p /data/output /data/input && \
|
mkdir -p /data/output /data/input && \
|
||||||
chown -R lidar:lidar /data
|
chown -R lidar:lidar /data && \
|
||||||
|
chmod +x /webapp-entrypoint.sh
|
||||||
|
|
||||||
ENV LIDAR_INPUT_DIR=/data/input \
|
ENV LIDAR_INPUT_DIR=/data/input \
|
||||||
LIDAR_OUTPUT_DIR=/data/output \
|
LIDAR_OUTPUT_DIR=/data/output \
|
||||||
@ -46,4 +48,6 @@ USER lidar
|
|||||||
|
|
||||||
EXPOSE 8973
|
EXPOSE 8973
|
||||||
|
|
||||||
CMD ["python3", "-m", "uvicorn", "lidar_pipeline.webapp:app", "--host", "0.0.0.0", "--port", "8973"]
|
# Sert en HTTP, ou en HTTPS si LIDAR_SSL_CERTFILE + LIDAR_SSL_KEYFILE sont
|
||||||
|
# définis (requis pour la géolocalisation GPS depuis un téléphone).
|
||||||
|
CMD ["/webapp-entrypoint.sh"]
|
||||||
|
|||||||
@ -6,9 +6,8 @@
|
|||||||
# génération sur le port hôte 8974 ;
|
# génération sur le port hôte 8974 ;
|
||||||
# - webapp : interface légère (image Dockerfile.webapp, sans PDAL/GPU) —
|
# - webapp : interface légère (image Dockerfile.webapp, sans PDAL/GPU) —
|
||||||
# carte sur le port hôte 8973, avec son PROPRE cache de tuiles
|
# carte sur le port hôte 8973, avec son PROPRE cache de tuiles
|
||||||
# (./output-webapp) alimenté par rsync depuis ./output du
|
# (./output-webapp) peuplé à la demande depuis le worker,
|
||||||
# worker (monté en lecture seule dans la webapp), comme le
|
# comme le fait le Raspberry Pi.
|
||||||
# Raspberry Pi le fait par SSH vers la machine de traitement.
|
|
||||||
#
|
#
|
||||||
# Intérêt en local : mettre à jour le code de l'interface (rebuild webapp)
|
# Intérêt en local : mettre à jour le code de l'interface (rebuild webapp)
|
||||||
# sans toucher au worker ni aux générations en cours, et réciproquement.
|
# sans toucher au worker ni aux générations en cours, et réciproquement.
|
||||||
@ -52,21 +51,14 @@ services:
|
|||||||
- "8973:8973"
|
- "8973:8973"
|
||||||
volumes:
|
volumes:
|
||||||
# Cache local propre à la webapp (équivalent du disque du Raspberry
|
# Cache local propre à la webapp (équivalent du disque du Raspberry
|
||||||
# Pi) — PAS le ./output du worker : la séparation rendu/interface
|
# Pi) — PAS le ./output du worker : la séparation rendu/interface doit
|
||||||
# doit être réelle, les tuiles arrivent par le rsync ci-dessous.
|
# être réelle, les tuiles arrivent à la demande depuis le worker.
|
||||||
- ./output-webapp:/data/output
|
- ./output-webapp:/data/output
|
||||||
# Sortie du worker, en lecture seule : source du rsync local qui
|
|
||||||
# remplace le "rsync par SSH vers la machine de traitement".
|
|
||||||
- ./output:/data/remote:ro
|
|
||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
- LIDAR_OUTPUT_DIR=/data/output
|
- LIDAR_OUTPUT_DIR=/data/output
|
||||||
# Backend de génération = worker du même réseau compose (DNS docker)
|
# Backend de génération = worker du même réseau compose (DNS docker) ;
|
||||||
|
# les tuiles sont servies à la demande depuis le worker (cache peuplé
|
||||||
|
# au fil des consultations, vignettes régénérées localement par /api/sync)
|
||||||
- LIDAR_GENERATION_URL=http://lidar-worker:8973
|
- LIDAR_GENERATION_URL=http://lidar-worker:8973
|
||||||
# Rapatriement des tuiles : mêmes exclusions que la prod (les
|
|
||||||
# vignettes/sous-tuiles sont régénérées localement par /api/sync)
|
|
||||||
- LIDAR_SYNC_CMD=rsync -a --delete --exclude=*.tif --exclude=.generation* --exclude=index_thumbs --exclude=index_subtiles /data/remote/ /data/output/
|
|
||||||
# Cache auto-entretenu : resynchronisation + vignettes toutes les
|
|
||||||
# 2 min (plus réactif que la prod pour tester en local)
|
|
||||||
- LIDAR_AUTO_SYNC_SECONDS=120
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
37
docker-compose.webapp.override.yml.example
Normal file
37
docker-compose.webapp.override.yml.example
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# docker-compose.webapp.override.yml — EXEMPLE (copier sans le suffixe .example)
|
||||||
|
#
|
||||||
|
# NON VERSIONNÉ (ignoré par git) : il vit sur le Pi. Ce fichier démontre la
|
||||||
|
# config Traefik qui termine le TLS (certificat porté par le proxy) et expose
|
||||||
|
# la carte en https://<domaine> — contexte sécurisé requis par le bouton ⌖
|
||||||
|
# (« centrer sur la position GPS » du téléphone).
|
||||||
|
#
|
||||||
|
# cp docker-compose.webapp.override.yml.example docker-compose.webapp.override.yml
|
||||||
|
# # adapter LIDAR_WEBAPP_HOST (et si besoin l'entrypoint / le certresolver)
|
||||||
|
#
|
||||||
|
# docker compose ne merge PAS automatiquement un override au nom custom
|
||||||
|
# (il n'auto-charge que docker-compose.override.yml) : passer -f explicitement.
|
||||||
|
# docker compose -f docker-compose.webapp.yml \
|
||||||
|
# -f docker-compose.webapp.override.yml up -d --build
|
||||||
|
#
|
||||||
|
# docker compose applique ce fichier par-dessus docker-compose.webapp.yml ;
|
||||||
|
# il suffit de la clé `labels`, le reste hérite du compose principal. Le
|
||||||
|
# webapp sert du HTTP ; Traefik y accède via le port 8973 du conteneur.
|
||||||
|
#
|
||||||
|
# Le domaine se définit par LIDAR_WEBAPP_HOST (dans un .env à côté du compose
|
||||||
|
# ou l'environnement) ; défaut lidar.local. Remplacer par le domaine réel, ou
|
||||||
|
# simplement écrire le domaine en dur dans la règle Host() ci-dessous.
|
||||||
|
services:
|
||||||
|
webapp:
|
||||||
|
labels:
|
||||||
|
# Activer le provider Docker pour ce conteneur.
|
||||||
|
- traefik.enable=true
|
||||||
|
# Router sur le domaine (adapter) vers le service webapp:8973.
|
||||||
|
- "traefik.http.routers.lidar-webapp.rule=Host(`${LIDAR_WEBAPP_HOST:-lidar.local}`)"
|
||||||
|
# Entrée HTTPS (443) + TLS avec le certificat porté par Traefik.
|
||||||
|
- traefik.http.routers.lidar-webapp.entrypoints=websecure
|
||||||
|
- traefik.http.routers.lidar-webapp.tls=true
|
||||||
|
# Si un certresolver spécifique (ACME / Let's Encrypt…) plutôt que le
|
||||||
|
# certificat par défaut du proxy :
|
||||||
|
# - "traefik.http.routers.lidar-webapp.tls.certresolver=letsencrypt"
|
||||||
|
# Service explicite vers le conteneur (nom_docker:port) :
|
||||||
|
# - "traefik.http.services.lidar-webapp.loadbalancer.server.port=8973"
|
||||||
@ -18,9 +18,9 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "8973:8973"
|
- "8973:8973"
|
||||||
volumes:
|
volumes:
|
||||||
# Copie locale des tuiles traitées, synchronisée par LIDAR_SYNC_CMD
|
# Cache local des tuiles, peuplé à la demande depuis LIDAR_GENERATION_URL
|
||||||
- ./output:/data/output
|
- ./output:/data/output
|
||||||
# Clé SSH du rsync vers la machine de traitement (LIDAR_SYNC_CMD)
|
# Clé SSH du hôte (déploiement, git pull distant) si nécessaire
|
||||||
# - ~/.ssh:/home/lidar/.ssh:ro
|
# - ~/.ssh:/home/lidar/.ssh:ro
|
||||||
# Configuration SSH (known_hosts) si nécessaire
|
# Configuration SSH (known_hosts) si nécessaire
|
||||||
# - ~/.ssh/config:/home/lidar/.ssh/config:ro
|
# - ~/.ssh/config:/home/lidar/.ssh/config:ro
|
||||||
@ -35,14 +35,16 @@ services:
|
|||||||
# - LIDAR_GENERATION_URL=http://192.168.1.50:8973
|
# - LIDAR_GENERATION_URL=http://192.168.1.50:8973
|
||||||
# Jeton partagé si LIDAR_API_TOKEN est défini sur la machine de traitement
|
# Jeton partagé si LIDAR_API_TOKEN est défini sur la machine de traitement
|
||||||
# - LIDAR_REMOTE_TOKEN=change-moi
|
# - LIDAR_REMOTE_TOKEN=change-moi
|
||||||
# Ramène les tuiles traitées + sidecars DTM (les vignettes sont
|
# Les tuiles sont servies à la demande depuis LIDAR_GENERATION_URL
|
||||||
# régénérées localement par /api/sync, ne pas les synchroniser)
|
# (cache peuplé au fil des consultations) : plus de rsync ni de sync
|
||||||
# - 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/
|
# périodique ; le bouton ↻ / POST /api/sync déclenchent le rebuild.
|
||||||
# Cache local auto-entretenu : resynchronisation + vignettes toutes les
|
|
||||||
# N secondes (0/défaut = seulement à la demande, bouton ↻ ou /api/sync)
|
|
||||||
# - LIDAR_AUTO_SYNC_SECONDS=600
|
|
||||||
# Réseau autorisé à lancer les générations depuis la carte (défaut
|
# Réseau autorisé à lancer les générations depuis la carte (défaut
|
||||||
# 192.168.1.0/24 ; chaîne vide = restriction levée). Les autres clients
|
# 192.168.1.0/24 ; chaîne vide = restriction levée). Les autres clients
|
||||||
# consultent la carte en lecture seule (boutons masqués, API 403).
|
# consultent la carte en lecture seule (boutons masqués, API 403).
|
||||||
# - LIDAR_REGEN_CIDR=192.168.1.0/24
|
# - LIDAR_REGEN_CIDR=192.168.1.0/24
|
||||||
|
# Le webapp sert du HTTP. Le TLS est terminé par le reverse proxy (Traefik,
|
||||||
|
# certificat porté par le proxy) : la configuration Traefik (routage HTTPS)
|
||||||
|
# vit dans le fichier override local docker-compose.webapp.override.yml
|
||||||
|
# (non versionné, sur le Pi) — cf. docs/DEPLOY_WEBAPP.md « Centrer la
|
||||||
|
# carte sur la position GPS » et docker-compose.webapp.override.yml.example.
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@ -3,7 +3,8 @@
|
|||||||
# Tourne sur la machine puissante (GPU + PDAL) et expose l'API que les
|
# Tourne sur la machine puissante (GPU + PDAL) et expose l'API que les
|
||||||
# webapp distantes (Raspberry Pi, docker-compose.webapp.yml) appellent :
|
# webapp distantes (Raspberry Pi, docker-compose.webapp.yml) appellent :
|
||||||
# dessin d'une zone → téléchargement IGN + traitement GPU ici, les tuiles
|
# dessin d'une zone → téléchargement IGN + traitement GPU ici, les tuiles
|
||||||
# sont relues par la webapp via son LIDAR_SYNC_CMD (rsync).
|
# sont servies à la demande par la webapp (cache peuplé au fil des
|
||||||
|
# consultations).
|
||||||
#
|
#
|
||||||
# docker compose -f docker-compose.worker.yml up -d --build
|
# docker compose -f docker-compose.worker.yml up -d --build
|
||||||
# docker compose -f docker-compose.worker.yml logs -f worker
|
# docker compose -f docker-compose.worker.yml logs -f worker
|
||||||
|
|||||||
@ -10,7 +10,7 @@ Navigateur ──HTTP──▶ Raspberry Pi (image légère, Dockerfile.webapp)
|
|||||||
│ sert carte + vignettes (régénérées localement)
|
│ sert carte + vignettes (régénérées localement)
|
||||||
│ /api/generate, /api/preview, /api/status
|
│ /api/generate, /api/preview, /api/status
|
||||||
│ └─ transmis à ──▶ machine de traitement
|
│ └─ transmis à ──▶ machine de traitement
|
||||||
│ /api/sync : rsync output/ ──◀── machine puissante
|
│ /api/sync : rebuild local (tuiles servies à la demande)
|
||||||
▼
|
▼
|
||||||
Machine de traitement (image complète,
|
Machine de traitement (image complète,
|
||||||
docker-compose.worker.yml service `worker`) :
|
docker-compose.worker.yml service `worker`) :
|
||||||
@ -21,7 +21,7 @@ Navigateur ──HTTP──▶ Raspberry Pi (image légère, Dockerfile.webapp)
|
|||||||
|
|
||||||
Le service `worker` joue le rôle de générateur : il accepte les demandes
|
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 +
|
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.
|
traitement PDAL/GPU) et expose les tuiles produites, servies à la demande par les webapp.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.worker.yml up -d --build # API sur http://<ip-machine>:8973
|
docker compose -f docker-compose.worker.yml up -d --build # API sur http://<ip-machine>:8973
|
||||||
@ -54,8 +54,8 @@ environment:
|
|||||||
- **Docker + plugin compose** : installation officielle
|
- **Docker + plugin compose** : installation officielle
|
||||||
[docs.docker.com/engine/install](https://docs.docker.com/engine/install/)
|
[docs.docker.com/engine/install](https://docs.docker.com/engine/install/)
|
||||||
(tester avec `docker compose version`).
|
(tester avec `docker compose version`).
|
||||||
- **rsync/ssh côté client** : inutile sur l'hôte — ils sont dans l'image —
|
- **Clé SSH côté hôte** : montée dans le conteneur pour le déploiement et le
|
||||||
mais la **clé SSH** de l'hôte est montée dans le conteneur.
|
`git pull` distant (le rsync de tuiles, supprimé, n'en avait pas besoin).
|
||||||
- **Espace disque** : prévoir la taille du cache de tuiles (compter la
|
- **Espace disque** : prévoir la taille du cache de tuiles (compter la
|
||||||
taille de `output/` sur la machine de traitement, ~quelques dizaines de
|
taille de `output/` sur la machine de traitement, ~quelques dizaines de
|
||||||
Mo par dalle et par résolution).
|
Mo par dalle et par résolution).
|
||||||
@ -67,31 +67,37 @@ git clone ssh://git@git.example.fr:2222/code_public/lidar_rendu.git lidar
|
|||||||
cd lidar
|
cd lidar
|
||||||
```
|
```
|
||||||
|
|
||||||
Le clone SSH utilise la même clé que le rsync (`~/.ssh`, voir ci-dessous) ;
|
Le clone SSH utilise la clé de l'hôte (`~/.ssh`, section 2). Si le serveur
|
||||||
si le serveur git n'est pas encore connu, faire une première connexion pour
|
git n'est pas encore connu, faire une première connexion pour accepter son
|
||||||
accepter son empreinte. `output/` et `input/` sont ignorés par git : le
|
empreinte. `output/` et `input/` sont ignorés par git : le dépôt ne contient
|
||||||
dépôt ne contient que le code, le cache de tuiles se remplit ensuite par
|
que le code, le cache de tuiles se remplit à la demande depuis la machine de
|
||||||
rsync (premier chargement, section 4).
|
traitement (section 4).
|
||||||
|
|
||||||
### 2. Accès SSH pour le rsync (la Pi tire les tuiles traitées)
|
### 2. Accès SSH (déploiement)
|
||||||
|
|
||||||
|
Le rsync ayant été retiré, le SSH ne sert plus à ramener les tuiles (elles
|
||||||
|
sont servies à la demande, section 4). Il sert au déploiement et au support
|
||||||
|
: se connecter à la machine de traitement pour y inspecter `output/` ou y
|
||||||
|
déposer des tuiles manuellement.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ssh-keygen -t ed25519 # si pas encore de clé
|
ssh-keygen -t ed25519 # si pas encore de clé
|
||||||
ssh-copy-id lidar@<ip-machine> # compte lecture sur output/
|
ssh-copy-id lidar@<ip-machine> # compte sur la machine de traitement
|
||||||
ssh lidar@<ip-machine> exit # 1re connexion : enregistre known_hosts
|
ssh lidar@<ip-machine> exit # 1re connexion : enregistre known_hosts
|
||||||
```
|
```
|
||||||
|
|
||||||
La dernière commande évite le prompt « authenticity of host » pendant le
|
La dernière commande évite le prompt « authenticity of host » en connexion
|
||||||
rsync (le conteneur ne peut pas répondre interactivement).
|
non interactive (le conteneur ne peut pas répondre).
|
||||||
|
|
||||||
Sur la machine puissante, le dossier `output/` doit être lisible par ce
|
Le cache de tuiles du Pi se remplit à la demande depuis la machine de
|
||||||
compte (ex. `/srv/lidar/output` si vous préférez un chemin dédié — adaptez
|
traitement ; ce compte SSH sert à l'inspecter ou à y déposer des tuiles
|
||||||
LIDAR_SYNC_CMD).
|
manuellement si besoin.
|
||||||
|
|
||||||
Avec `serve-webapp.sh` et `run.sh`, `~/.ssh` (clé + known_hosts) est monté
|
Avec `serve-webapp.sh` et `run.sh`, la clé SSH de l'hôte (`~/.ssh`, clé +
|
||||||
automatiquement en lecture seule dans le conteneur ; avec `docker compose`,
|
known_hosts) est montée automatiquement en lecture seule dans le conteneur ;
|
||||||
le montage équivalent est à décommenter dans `docker-compose.webapp.yml`
|
avec `docker compose`, le montage équivalent est à décommenter dans
|
||||||
(voir l'option c ci-dessous).
|
`docker-compose.webapp.yml` (voir l'option c ci-dessous). Le wrapper local
|
||||||
|
`ssh` (agent forwarding) simplifie ces connexions.
|
||||||
|
|
||||||
### 3. Configurer et lancer
|
### 3. Configurer et lancer
|
||||||
|
|
||||||
@ -103,12 +109,12 @@ configuration vit dans `webapp.env` (copie du modèle, non versionné) :
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp webapp.env.example webapp.env
|
cp webapp.env.example webapp.env
|
||||||
nano webapp.env # LIDAR_GENERATION_URL, LIDAR_SYNC_CMD, jeton...
|
nano webapp.env # LIDAR_GENERATION_URL, jeton...
|
||||||
./serve-webapp.sh # démarre (build au premier lancement) et attend le serveur
|
./serve-webapp.sh # démarre (build au premier lancement) et attend le serveur
|
||||||
```
|
```
|
||||||
|
|
||||||
Sous-commandes : `stop`, `restart` (relit `webapp.env`), `status` (conteneur
|
Sous-commandes : `stop`, `restart` (relit `webapp.env`), `status` (conteneur
|
||||||
+ état de la sync), `sync` (force rsync + vignettes), `logs`, `update`
|
+ état du rebuild), `sync` (rebuild + vignettes), `logs`, `update`
|
||||||
(`git pull` + rebuild + redémarrage). Port hôte via `WEBAPP_PORT` dans
|
(`git pull` + rebuild + redémarrage). Port hôte via `WEBAPP_PORT` dans
|
||||||
`webapp.env` ou l'environnement (`WEBAPP_PORT=9000 ./serve-webapp.sh`).
|
`webapp.env` ou l'environnement (`WEBAPP_PORT=9000 ./serve-webapp.sh`).
|
||||||
|
|
||||||
@ -119,16 +125,13 @@ est monté automatiquement :
|
|||||||
```bash
|
```bash
|
||||||
LIDAR_GENERATION_URL=http://192.168.1.50:8973 \
|
LIDAR_GENERATION_URL=http://192.168.1.50:8973 \
|
||||||
LIDAR_REMOTE_TOKEN=un-secret-à-partager \
|
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
|
./run.sh --serve-webapp # port 8973, ou --serve-webapp 9000
|
||||||
```
|
```
|
||||||
|
|
||||||
`LIDAR_AUTO_SYNC_SECONDS` entretient le cache tout seul : toutes les N
|
Les tuiles sont servies à la demande : une image absente du cache local est
|
||||||
secondes, rsync ramène les nouvelles tuiles et les vignettes manquantes
|
téléchargée depuis la machine de traitement au premier affichage, le cache
|
||||||
sont régénérées (les mtimes évitent tout recalcul inutile). Sans cette
|
se remplit ainsi progressivement. Le bouton ↻, la fin d'un run ou `POST
|
||||||
variable, le cache se rafraîchit à la demande : bouton ↻, fin d'un run,
|
/api/sync` déclenchent un rebuild de l'index et régénèrent les vignettes.
|
||||||
ou `POST /api/sync`.
|
|
||||||
|
|
||||||
`LIDAR_REGEN_CIDR` restreint le lancement des générations (`POST
|
`LIDAR_REGEN_CIDR` restreint le lancement des générations (`POST
|
||||||
/api/generate` : zones, sélection, complétion, régénération) aux clients
|
/api/generate` : zones, sélection, complétion, régénération) aux clients
|
||||||
@ -147,14 +150,10 @@ génération sont masqués et l'API répond 403.
|
|||||||
- `LIDAR_GENERATION_URL` : `http://<ip-machine>:8973`
|
- `LIDAR_GENERATION_URL` : `http://<ip-machine>:8973`
|
||||||
- `LIDAR_REMOTE_TOKEN` : la valeur de `LIDAR_API_TOKEN` de la machine
|
- `LIDAR_REMOTE_TOKEN` : la valeur de `LIDAR_API_TOKEN` de la machine
|
||||||
(inutile si aucun token là-bas)
|
(inutile si aucun token là-bas)
|
||||||
- `LIDAR_SYNC_CMD` : la commande rsync qui copie `output/` distant vers
|
- le cache local se remplit à la demande depuis `LIDAR_GENERATION_URL` (pas
|
||||||
`/data/output/` local, ex :
|
de `LIDAR_SYNC_CMD`)
|
||||||
|
|
||||||
```yaml
|
Décommenter aussi le montage de la clé SSH (déploiement) :
|
||||||
- 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
|
```yaml
|
||||||
volumes:
|
volumes:
|
||||||
@ -191,6 +190,52 @@ curl -X POST http://<ip-pi>:8973/api/sync
|
|||||||
(puis attendre la fin : `curl http://<ip-pi>:8973/api/sync` → `"running":
|
(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).
|
false`.) Le bouton ↻ de la carte fait la même chose (sync + vignettes).
|
||||||
|
|
||||||
|
### 5. Centrer la carte sur la position GPS (téléphone)
|
||||||
|
|
||||||
|
Le bouton **⌖** (barre d'outils de la carte, à côté du recadrage) recentre la
|
||||||
|
carte sur la position GPS du téléphone et y dessine un marqueur vert.
|
||||||
|
|
||||||
|
Les navigateurs n'exposent l'API Geolocation qu'en **contexte sécurisé**
|
||||||
|
(HTTPS). Servie en `http://<ip-pi>:8973`, la carte ne peut donc pas obtenir la
|
||||||
|
position sur téléphone : le bouton affiche alors « connexion sécurisée (HTTPS)
|
||||||
|
requise ».
|
||||||
|
|
||||||
|
**TLS terminé par le proxy (cas d'usage ici)** — le certificat est porté par
|
||||||
|
**Traefik** : le webapp sert du HTTP, Traefik expose `https://<domaine>` et y
|
||||||
|
applique son certificat (valable, sans avertissement). Le téléphone se connecte
|
||||||
|
sur l'URL Traefik → contexte sécurisé → le bouton ⌖ fonctionne.
|
||||||
|
|
||||||
|
La config Traefik (routage HTTPS) vit dans le fichier **override** local
|
||||||
|
`docker-compose.webapp.override.yml` (non versionné, sur le Pi) — Compose le
|
||||||
|
merge par-dessus `docker-compose.webapp.yml`. Le modèle versionné
|
||||||
|
`docker-compose.webapp.override.yml.example` fournit les labels (provider
|
||||||
|
Docker) qui routent `Host(\`${LIDAR_WEBAPP_HOST}\`)` vers le service, en
|
||||||
|
`websecure` (443) + TLS avec le certificat porté par Traefik. Sur le Pi :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp docker-compose.webapp.override.yml.example docker-compose.webapp.override.yml
|
||||||
|
# adapter le domaine : LIDAR_WEBAPP_HOST=carte.example.fr (dans un .env à
|
||||||
|
# côté du compose, l'environnement, ou en dur dans la règle Host())
|
||||||
|
# docker compose n'auto-charge PAS un override au nom custom (seulement
|
||||||
|
# docker-compose.override.yml) → passer -f explicitement :
|
||||||
|
docker compose -f docker-compose.webapp.yml \
|
||||||
|
-f docker-compose.webapp.override.yml up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Adapter si le proxy n'est pas en `websecure` ou que le provider n'est pas
|
||||||
|
Docker : changer `entrypoints` / ajouter
|
||||||
|
`traefik.http.routers.lidar-webapp.tls.certresolver=…`, ou déclarer le
|
||||||
|
service directement dans la config Traefik. Le webapp est transparent au
|
||||||
|
proxy (URLs relatives) et remonte l'IP réelle via `X-Forwarded-For` pour la
|
||||||
|
restriction `LIDAR_REGEN_CIDR` : l'IP du téléphone doit y figurer pour lancer
|
||||||
|
des générations.
|
||||||
|
|
||||||
|
> **Sans Traefik** — servir la carte en HTTPS avec un certificat auto-signé :
|
||||||
|
> `./make-tls-cert.sh` crée `tls/webapp.{crt,key}`, à monter dans le
|
||||||
|
> conteneur avec `LIDAR_SSL_CERTFILE` / `LIDAR_SSL_KEYFILE` (le chemin
|
||||||
|
> `serve-webapp.sh` le fait automatiquement). Le téléphone accepte alors
|
||||||
|
> l'avertissement « certificat non fiable » une fois.
|
||||||
|
|
||||||
## Mise à jour
|
## Mise à jour
|
||||||
|
|
||||||
Avec `serve-webapp.sh`, une seule commande (`git pull`, rebuild de l'image,
|
Avec `serve-webapp.sh`, une seule commande (`git pull`, rebuild de l'image,
|
||||||
@ -200,7 +245,7 @@ redémarrage) :
|
|||||||
./serve-webapp.sh update
|
./serve-webapp.sh update
|
||||||
```
|
```
|
||||||
|
|
||||||
Sinon, sur le Pi, après un `git pull` (ou rsync du code) :
|
Sinon, sur le Pi, après un `git pull` :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f docker-compose.webapp.yml up -d --build # rebuild + redémarrage
|
docker compose -f docker-compose.webapp.yml up -d --build # rebuild + redémarrage
|
||||||
@ -212,15 +257,14 @@ chaque lancement ; il suffit de relancer la commande. Le cache de tuiles
|
|||||||
|
|
||||||
## Dépannage
|
## Dépannage
|
||||||
|
|
||||||
- **État d'une sync** : `curl http://<ip-pi>:8973/api/sync` → `running`,
|
- **État d'un rebuild** : `curl http://<ip-pi>:8973/api/sync` → `running`,
|
||||||
`phase` (`sync` puis `index`) et `error` (dernière erreur, ex. fin de
|
`phase` (`index`) et `error` (dernière erreur). Les logs du conteneur :
|
||||||
journal rsync en cas d'échec). Les logs du conteneur :
|
|
||||||
`docker logs lidar-webapp` (ou `docker compose -f docker-compose.webapp.yml
|
`docker logs lidar-webapp` (ou `docker compose -f docker-compose.webapp.yml
|
||||||
logs -f webapp`).
|
logs -f webapp`).
|
||||||
- **`Permission denied (publickey)` pendant la sync** : refaire
|
- **`Permission denied (publickey)` en SSH** : refaire
|
||||||
`ssh-copy-id lidar@<ip-machine>` et une connexion manuelle pour
|
`ssh-copy-id lidar@<ip-machine>` et une connexion manuelle pour renseigner
|
||||||
renseigner `known_hosts` ; vérifier que `~/.ssh` est monté dans le
|
`known_hosts` ; vérifier que `~/.ssh` est monté dans le conteneur
|
||||||
conteneur (automatique avec `run.sh`, à décommenter en compose).
|
(automatique avec `run.sh`, à décommenter en compose).
|
||||||
- **Boutons de génération masqués / `403` sur `/api/generate`** : l'IP du
|
- **Boutons de génération masqués / `403` sur `/api/generate`** : l'IP du
|
||||||
client n'est dans aucun des réseaux de `LIDAR_REGEN_CIDR` (défaut :
|
client n'est dans aucun des réseaux de `LIDAR_REGEN_CIDR` (défaut :
|
||||||
localhost + plages privées). Adapter la variable ou la vider pour lever
|
localhost + plages privées). Adapter la variable ou la vider pour lever
|
||||||
@ -244,11 +288,10 @@ chaque lancement ; il suffit de relancer la commande. Le cache de tuiles
|
|||||||
à la machine de traitement, qui télécharge les dalles IGN puis les traite.
|
à la machine de traitement, qui télécharge les dalles IGN puis les traite.
|
||||||
La file de génération (progression tuile par tuile) est lue depuis la
|
La file de génération (progression tuile par tuile) est lue depuis la
|
||||||
machine distante en direct.
|
machine distante en direct.
|
||||||
- À la fin du run, le navigateur appelle `/api/sync` : rsync ramène les
|
- À la fin du run, le navigateur appelle `/api/sync` : les images produites
|
||||||
images, le Pi régénère vignettes/sous-tuiles/index.html, puis recharge la
|
sont servies à la demande depuis la machine de traitement, le Pi régénère
|
||||||
carte. Le bouton ↻ relance le même cycle à tout moment. Avec
|
vignettes/sous-tuiles/index.html, puis recharge la carte. Le bouton ↻
|
||||||
`LIDAR_AUTO_SYNC_SECONDS`, le conteneur webapp seul maintient aussi son
|
relance le même cycle à tout moment.
|
||||||
cache périodiquement (utile quand personne ne consulte la carte).
|
|
||||||
- Sans machine de traitement configurée, `./run.sh --serve-webapp` sert la
|
- Sans machine de traitement configurée, `./run.sh --serve-webapp` sert la
|
||||||
carte en lecture seule depuis `output/` (cache figé, aucun traitement
|
carte en lecture seule depuis `output/` (cache figé, aucun traitement
|
||||||
possible depuis l'interface).
|
possible depuis l'interface).
|
||||||
@ -266,7 +309,7 @@ chaque lancement ; il suffit de relancer la commande. Le cache de tuiles
|
|||||||
## Références
|
## Références
|
||||||
|
|
||||||
- `lidar_pipeline/webapp.py` : proxy distant (`LIDAR_GENERATION_URL`),
|
- `lidar_pipeline/webapp.py` : proxy distant (`LIDAR_GENERATION_URL`),
|
||||||
`/api/sync`, cache local périodique (`LIDAR_AUTO_SYNC_SECONDS`), jeton
|
`/api/sync` (rebuild de l'index), cache local à la demande, jeton
|
||||||
`LIDAR_API_TOKEN`/`LIDAR_REMOTE_TOKEN`, restriction des générations au
|
`LIDAR_API_TOKEN`/`LIDAR_REMOTE_TOKEN`, restriction des générations au
|
||||||
réseau local (`LIDAR_REGEN_CIDR`).
|
réseau local (`LIDAR_REGEN_CIDR`).
|
||||||
- `lidar_pipeline/index.py` : vignettes/index sans GDAL (pyproj ou repli
|
- `lidar_pipeline/index.py` : vignettes/index sans GDAL (pyproj ou repli
|
||||||
|
|||||||
@ -1332,9 +1332,11 @@ _HTML_TEMPLATE = """<!DOCTYPE html>
|
|||||||
<button id="zoomIn" title="Zoomer">+</button>
|
<button id="zoomIn" title="Zoomer">+</button>
|
||||||
<button id="zoomOut" title="Dézoomer">−</button>
|
<button id="zoomOut" title="Dézoomer">−</button>
|
||||||
<button id="zoomReset" title="Recadrer sur l'ensemble des tuiles">⤢</button>
|
<button id="zoomReset" title="Recadrer sur l'ensemble des tuiles">⤢</button>
|
||||||
|
<button id="btnGps" title="Centrer la carte sur la position GPS du téléphone">⌖</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div id="zoomIndicator" class="chip">Zoom</div>
|
<div id="zoomIndicator" class="chip">Zoom</div>
|
||||||
|
<div id="gpsToast" class="chip" hidden></div>
|
||||||
<div id="hint" class="chip">
|
<div id="hint" class="chip">
|
||||||
Molette : zoom · Clic-glisser : déplacer<br>
|
Molette : zoom · Clic-glisser : déplacer<br>
|
||||||
Clic sur tuile : infos de génération
|
Clic sur tuile : infos de génération
|
||||||
@ -1618,6 +1620,7 @@ body.zooming .ltile { will-change: transform; }
|
|||||||
#mapTools { top: 12px; right: 12px; display: flex; gap: 6px; padding: 6px; }
|
#mapTools { top: 12px; right: 12px; display: flex; gap: 6px; padding: 6px; }
|
||||||
#mapTools button { width: 32px; height: 32px; padding: 0; font-size: 16px; }
|
#mapTools button { width: 32px; height: 32px; padding: 0; font-size: 16px; }
|
||||||
#btnMenu { display: none; }
|
#btnMenu { display: none; }
|
||||||
|
#btnGps.busy { color: var(--accent); border-color: var(--accent); }
|
||||||
|
|
||||||
/* --- Puces d'état (bas d'écran) --- */
|
/* --- Puces d'état (bas d'écran) --- */
|
||||||
.chip {
|
.chip {
|
||||||
@ -1627,6 +1630,10 @@ body.zooming .ltile { will-change: transform; }
|
|||||||
font-size: 11px; color: var(--muted);
|
font-size: 11px; color: var(--muted);
|
||||||
}
|
}
|
||||||
#zoomIndicator { bottom: 12px; left: var(--left-ui); font-variant-numeric: tabular-nums; }
|
#zoomIndicator { bottom: 12px; left: var(--left-ui); font-variant-numeric: tabular-nums; }
|
||||||
|
#gpsToast {
|
||||||
|
top: 54px; left: 50%; transform: translateX(-50%);
|
||||||
|
max-width: 80vw; text-align: center; line-height: 1.5;
|
||||||
|
}
|
||||||
#hint { bottom: 12px; right: 12px; text-align: right; line-height: 1.5; }
|
#hint { bottom: 12px; right: 12px; text-align: right; line-height: 1.5; }
|
||||||
|
|
||||||
/* --- Onglet Génération --- */
|
/* --- Onglet Génération --- */
|
||||||
@ -2351,6 +2358,53 @@ document.getElementById('zoomReset').addEventListener('click', () => {
|
|||||||
history.replaceState(null, '', location.pathname + location.search);
|
history.replaceState(null, '', location.pathname + location.search);
|
||||||
if (gridBounds) map.fitBounds(gridBounds, { padding: [24, 24] });
|
if (gridBounds) map.fitBounds(gridBounds, { padding: [24, 24] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Position GPS du téléphone : centrer la carte dessus
|
||||||
|
// ============================================================
|
||||||
|
const gpsBtn = document.getElementById('btnGps');
|
||||||
|
const gpsToast = document.getElementById('gpsToast');
|
||||||
|
let gpsToastTimer = null;
|
||||||
|
let gpsMarker = null;
|
||||||
|
function gpsMsg(text, ms) {
|
||||||
|
gpsToast.textContent = text;
|
||||||
|
gpsToast.hidden = false;
|
||||||
|
clearTimeout(gpsToastTimer);
|
||||||
|
gpsToastTimer = setTimeout(() => { gpsToast.hidden = true; }, ms || 4000);
|
||||||
|
}
|
||||||
|
gpsBtn.addEventListener('click', () => {
|
||||||
|
if (!('geolocation' in navigator)) {
|
||||||
|
gpsMsg('Géolocalisation non prise en charge par ce navigateur ' +
|
||||||
|
'(connexion sécurisée HTTPS requise).', 6000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
gpsBtn.classList.add('busy');
|
||||||
|
try {
|
||||||
|
navigator.geolocation.getCurrentPosition(pos => {
|
||||||
|
gpsBtn.classList.remove('busy');
|
||||||
|
const ll = [pos.coords.latitude, pos.coords.longitude];
|
||||||
|
map.setView(ll, Math.max(map.getZoom(), 16));
|
||||||
|
if (gpsMarker) gpsMarker.remove();
|
||||||
|
gpsMarker = L.circleMarker(ll, {
|
||||||
|
radius: 5, color: '#39d98a', fillColor: '#39d98a', fillOpacity: 0.9
|
||||||
|
}).addTo(map);
|
||||||
|
if (pos.coords.accuracy) {
|
||||||
|
gpsMarker.bindTooltip('Précision GPS ~' + Math.round(pos.coords.accuracy) + ' m');
|
||||||
|
}
|
||||||
|
gpsMsg('Carte centrée sur votre position GPS.');
|
||||||
|
}, err => {
|
||||||
|
gpsBtn.classList.remove('busy');
|
||||||
|
let why = 'Aucune position reçue';
|
||||||
|
if (err && err.code === 1) why = 'Autorisation de localisation refusée';
|
||||||
|
else if (err && err.code === 2) why = 'Position momentanément indisponible';
|
||||||
|
else if (err && err.code === 3) why = 'Délai de localisation dépassé';
|
||||||
|
gpsMsg(why + '.', 6000);
|
||||||
|
}, { enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 });
|
||||||
|
} catch (e) {
|
||||||
|
gpsBtn.classList.remove('busy');
|
||||||
|
gpsMsg('La géolocalisation exige une connexion sécurisée (HTTPS).', 6000);
|
||||||
|
}
|
||||||
|
});
|
||||||
// will-change pendant l'animation de zoom uniquement : le scaling passe sur
|
// will-change pendant l'animation de zoom uniquement : le scaling passe sur
|
||||||
// le GPU, et les couches sont libérées juste après (mémoire vidéo).
|
// le GPU, et les couches sont libérées juste après (mémoire vidéo).
|
||||||
let zoomGpuTimer = null;
|
let zoomGpuTimer = null;
|
||||||
|
|||||||
@ -686,63 +686,25 @@ def test_viz_step_labels_without_pipeline():
|
|||||||
assert labels["pos_open"] == "Openness positive"
|
assert labels["pos_open"] == "Openness positive"
|
||||||
|
|
||||||
|
|
||||||
def test_sync_runs_command_then_rebuild(tmp_path, monkeypatch):
|
def test_sync_is_rebuild_only(tmp_path, monkeypatch):
|
||||||
"""POST /api/sync exécute LIDAR_SYNC_CMD puis build_index en arrière-plan."""
|
"""/api/sync régénère l'index en arrière-plan (plus de rsync)."""
|
||||||
import time as _time
|
import time as _time
|
||||||
import lidar_pipeline.webapp as webapp
|
import lidar_pipeline.webapp as webapp
|
||||||
import lidar_pipeline.index as index_mod
|
import lidar_pipeline.index as index_mod
|
||||||
calls = []
|
calls = []
|
||||||
monkeypatch.setattr(index_mod, "build_index", lambda out: calls.append(out))
|
monkeypatch.setattr(index_mod, "build_index", lambda out: calls.append(out))
|
||||||
monkeypatch.setattr(webapp, "SYNC_CMD", "exit 0")
|
|
||||||
webapp._rebuild["done"] = None # ignore l'état d'un test précédent
|
webapp._rebuild["done"] = None # ignore l'état d'un test précédent
|
||||||
assert webapp.sync_and_rebuild()["sync"] is True
|
assert webapp.sync_and_rebuild()["demarré"] is True
|
||||||
for _ in range(200):
|
for _ in range(200):
|
||||||
s = webapp.rebuild_status()
|
s = webapp.rebuild_status()
|
||||||
if s["done"] is not None and not s["running"]:
|
if s["done"] is not None and not s["running"]:
|
||||||
break
|
break
|
||||||
_time.sleep(0.05)
|
_time.sleep(0.05)
|
||||||
assert calls # l'index a été reconstruit après la synchronisation
|
assert calls # l'index a été reconstruit
|
||||||
assert webapp.rebuild_status()["error"] is None
|
assert webapp.rebuild_status()["error"] is None
|
||||||
assert webapp.rebuild_status()["done"] is not None
|
assert webapp.rebuild_status()["done"] is not None
|
||||||
|
|
||||||
|
|
||||||
def test_sync_failure_reported_but_index_rebuilt(tmp_path, monkeypatch):
|
|
||||||
"""Un rsync en échec est remonté dans l'état sans bloquer le rebuild."""
|
|
||||||
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 3")
|
|
||||||
webapp._rebuild["done"] = None
|
|
||||||
webapp.sync_and_rebuild()
|
|
||||||
for _ in range(200):
|
|
||||||
s = webapp.rebuild_status()
|
|
||||||
if s["done"] is not None and not s["running"]:
|
|
||||||
break
|
|
||||||
_time.sleep(0.05)
|
|
||||||
assert calls # rebuild quand même (données locales éventuelles)
|
|
||||||
assert "rc=3" in webapp.rebuild_status()["error"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_sync_without_cmd_is_rebuild_only(monkeypatch):
|
|
||||||
"""Sans LIDAR_SYNC_CMD (machine locale), /api/sync = simple rebuild."""
|
|
||||||
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", None)
|
|
||||||
webapp._rebuild["done"] = None
|
|
||||||
assert webapp.sync_and_rebuild()["sync"] is False
|
|
||||||
for _ in range(200):
|
|
||||||
s = webapp.rebuild_status()
|
|
||||||
if s["done"] is not None and not s["running"]:
|
|
||||||
break
|
|
||||||
_time.sleep(0.05)
|
|
||||||
assert calls and webapp.rebuild_status()["error"] is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_require_token():
|
def test_require_token():
|
||||||
"""Le jeton (si défini) bloque les appels sans ou avec mauvais mauvais header."""
|
"""Le jeton (si défini) bloque les appels sans ou avec mauvais mauvais header."""
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
@ -851,25 +813,4 @@ def test_regen_cidr_accepts_comma_separated_list():
|
|||||||
webapp.REGEN_CIDR = old
|
webapp.REGEN_CIDR = old
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|||||||
@ -7,9 +7,8 @@ le téléchargement IGN). Endpoints :
|
|||||||
GET /api/status → état de la génération en cours (ou dernière terminée)
|
GET /api/status → état de la génération en cours (ou dernière terminée)
|
||||||
GET /api/tiles → données de carte (index_tiles.json) pour l'affichage
|
GET /api/tiles → données de carte (index_tiles.json) pour l'affichage
|
||||||
en direct ; ?stamp=X → réponse allégée si inchangé
|
en direct ; ?stamp=X → réponse allégée si inchangé
|
||||||
POST /api/sync → synchroniser les tuiles (LIDAR_SYNC_CMD, ex. rsync
|
POST /api/sync → régénérer l'index local (vignettes + carte) en
|
||||||
depuis la machine de traitement) puis régénérer la
|
arrière-plan (les tuiles sont servies à la demande)
|
||||||
carte en arrière-plan
|
|
||||||
POST /api/preview → cellules 1 km intersectant une bbox WGS84, OU (option
|
POST /api/preview → cellules 1 km intersectant une bbox WGS84, OU (option
|
||||||
all_missing=true) toutes les dalles LHD présentes dans
|
all_missing=true) toutes les dalles LHD présentes dans
|
||||||
input/ qui manquent au moins une visualisation demandée.
|
input/ qui manquent au moins une visualisation demandée.
|
||||||
@ -48,10 +47,11 @@ Architecture deux machines (cf. docs/DEPLOY_WEBAPP.md) : la webapp légère
|
|||||||
(sans PDAL/GPU, ex. Raspberry Pi) délègue la génération à la machine de
|
(sans PDAL/GPU, ex. Raspberry Pi) délègue la génération à la machine de
|
||||||
traitement via LIDAR_GENERATION_URL — /api/generate, /api/preview et
|
traitement via LIDAR_GENERATION_URL — /api/generate, /api/preview et
|
||||||
/api/status sont transmis tels quels au service distant, qui exécute la même
|
/api/status sont transmis tels quels au service distant, qui exécute la même
|
||||||
webapp avec le pipeline complet. Les résultats reviennent par la commande de
|
webapp avec le pipeline complet. Les images produites sont servies à la
|
||||||
synchronisation LIDAR_SYNC_CMD (rsync), puis /api/sync régénère vignettes et
|
demande depuis GENERATION_URL (cache local peuplé au fil des consultations)
|
||||||
carte localement. LIDAR_API_TOKEN (machine de traitement) + LIDAR_REMOTE_TOKEN
|
puis /api/sync régénère vignettes et carte localement. LIDAR_API_TOKEN
|
||||||
(webapp légère) protègent optionnellement les appels distants.
|
(machine de traitement) + LIDAR_REMOTE_TOKEN (webapp légère) protègent
|
||||||
|
optionnellement les appels distants.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -90,9 +90,6 @@ GENERATION_URL = (os.environ.get("LIDAR_GENERATION_URL") or "").rstrip("/")
|
|||||||
# (LIDAR_REMOTE_TOKEN ici, LIDAR_API_TOKEN là-bas). Optionnel.
|
# (LIDAR_REMOTE_TOKEN ici, LIDAR_API_TOKEN là-bas). Optionnel.
|
||||||
REMOTE_TOKEN = os.environ.get("LIDAR_REMOTE_TOKEN") or None
|
REMOTE_TOKEN = os.environ.get("LIDAR_REMOTE_TOKEN") or None
|
||||||
API_TOKEN = os.environ.get("LIDAR_API_TOKEN") or None
|
API_TOKEN = os.environ.get("LIDAR_API_TOKEN") or None
|
||||||
# Commande shell qui ramène les tuiles traitées dans output/ local (rsync
|
|
||||||
# depuis la machine de traitement). Exécutée par /api/sync avant le rebuild.
|
|
||||||
SYNC_CMD = os.environ.get("LIDAR_SYNC_CMD") or None
|
|
||||||
# Réseau autorisé à lancer la (re)génération des tuiles (/api/generate) :
|
# Réseau autorisé à lancer la (re)génération des tuiles (/api/generate) :
|
||||||
# liste de CIDR séparés par virgules, vide = restriction levée. Par défaut :
|
# liste de CIDR séparés par virgules, vide = restriction levée. Par défaut :
|
||||||
# boucle locale + plages privées RFC1918 — couvre le LAN, la machine hôte
|
# boucle locale + plages privées RFC1918 — couvre le LAN, la machine hôte
|
||||||
@ -105,10 +102,6 @@ SYNC_CMD = os.environ.get("LIDAR_SYNC_CMD") or None
|
|||||||
REGEN_CIDR = (os.environ.get(
|
REGEN_CIDR = (os.environ.get(
|
||||||
"LIDAR_REGEN_CIDR",
|
"LIDAR_REGEN_CIDR",
|
||||||
"127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16") or "").strip()
|
"127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16") or "").strip()
|
||||||
# Cache local périodique : relance la synchronisation + le rebuild toutes
|
|
||||||
# les N secondes (0 = désactivé). Sert au conteneur webapp seul, qui maintient
|
|
||||||
# sa copie des tuiles de la machine de traitement.
|
|
||||||
AUTO_SYNC_SECONDS = float(os.environ.get("LIDAR_AUTO_SYNC_SECONDS", "0") or 0)
|
|
||||||
|
|
||||||
# Résolution générée par /api/generate (cf. _build_command) : 0,2 m seule,
|
# Résolution générée par /api/generate (cf. _build_command) : 0,2 m seule,
|
||||||
# la 0,5 m n'est plus produite. La détection des tuiles à compléter exige
|
# la 0,5 m n'est plus produite. La détection des tuiles à compléter exige
|
||||||
@ -134,10 +127,6 @@ if GENERATION_URL:
|
|||||||
else:
|
else:
|
||||||
logger.info("Backend de génération des tuiles : local — consultation "
|
logger.info("Backend de génération des tuiles : local — consultation "
|
||||||
"autonome du cache, sans LIDAR_GENERATION_URL")
|
"autonome du cache, sans LIDAR_GENERATION_URL")
|
||||||
if SYNC_CMD:
|
|
||||||
logger.info(f"Cache local des tuiles : LIDAR_SYNC_CMD"
|
|
||||||
+ (f" (rafraîchi toutes les {AUTO_SYNC_SECONDS:g}s)"
|
|
||||||
if AUTO_SYNC_SECONDS > 0 else " (à la demande)"))
|
|
||||||
|
|
||||||
# assets/ (CSS/JS de l'interface) est créé dès le démarrage pour que le monteur
|
# 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.
|
# statique soit actif même avant la première génération de l'index.
|
||||||
@ -145,11 +134,11 @@ _assets_dir = OUTPUT_DIR / "assets"
|
|||||||
_assets_dir.mkdir(parents=True, exist_ok=True)
|
_assets_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
# --- Cache à la demande : la visualisation peuple le cache local -------------
|
# --- Cache à la demande : la visualisation peuple le cache local -------------
|
||||||
# Sans LIDAR_SYNC_CMD (pas de rsync), les images viennent de la machine de
|
# Les images viennent de la machine de traitement par HTTP : une image absente
|
||||||
# traitement par HTTP : une image absente du disque — ou plus ancienne que la
|
# du disque — ou plus ancienne que la version ?v= référencée (mtime ms de la
|
||||||
# version ?v= référencée (mtime ms de la source sur le worker) — est téléchar-
|
# source sur le worker) — est téléchargée depuis GENERATION_URL, écrite sur
|
||||||
# gée depuis GENERATION_URL, écrite sur disque puis servie. Requêtes concur-
|
# disque puis servie. Requêtes concurrentes dédupliquées (verrou par fichier),
|
||||||
# rentes dédupliquées (verrou par fichier), flux réseau plafonné (sémaphore).
|
# flux réseau plafonné (sémaphore).
|
||||||
from fastapi.staticfiles import StaticFiles as _StaticFiles
|
from fastapi.staticfiles import StaticFiles as _StaticFiles
|
||||||
from starlette.concurrency import run_in_threadpool
|
from starlette.concurrency import run_in_threadpool
|
||||||
|
|
||||||
@ -782,91 +771,46 @@ def tiles_data(stamp: Optional[float] = None):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# --- Synchronisation + rebuild de l'index en arrière-plan -------------------
|
# --- Rebuild de l'index en arrière-plan --------------------------------------
|
||||||
|
# Les tuiles sont servies à la demande (cache peuplé depuis la machine de
|
||||||
|
# traitement) : /api/sync ne fait plus qu'en régénérer l'index local
|
||||||
|
# (vignettes + carte).
|
||||||
_rebuild = {"running": False, "error": None, "done": None, "phase": None}
|
_rebuild = {"running": False, "error": None, "done": None, "phase": None}
|
||||||
|
|
||||||
|
|
||||||
def _start_rebuild(with_sync):
|
def _start_rebuild():
|
||||||
"""Lance en arrière-plan : LIDAR_SYNC_CMD (si demandé) puis build_index.
|
"""Lance en arrière-plan le rebuild de l'index (vignettes + carte)."""
|
||||||
|
|
||||||
Sur la webapp légère, with_sync=True ramène d'abord les tuiles traitées
|
|
||||||
par la machine distante (rsync) ; l'échec éventuel de la synchronisation
|
|
||||||
est remonté dans l'état sans empêcher le rebuild des données locales.
|
|
||||||
"""
|
|
||||||
if _rebuild["running"]:
|
if _rebuild["running"]:
|
||||||
raise HTTPException(409, "une synchronisation/rebuild est déjà en cours")
|
raise HTTPException(409, "un rebuild est déjà en cours")
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
_rebuild["running"] = True
|
_rebuild["running"] = True
|
||||||
_rebuild["error"] = None
|
_rebuild["error"] = None
|
||||||
_rebuild["done"] = None
|
_rebuild["done"] = None
|
||||||
sync_error = None
|
|
||||||
if with_sync and SYNC_CMD:
|
|
||||||
_rebuild["phase"] = "sync"
|
|
||||||
try:
|
|
||||||
r = subprocess.run(SYNC_CMD, shell=True, capture_output=True,
|
|
||||||
text=True, timeout=4 * 3600)
|
|
||||||
if r.returncode != 0:
|
|
||||||
tail = (r.stderr or r.stdout or "").strip()[-500:]
|
|
||||||
sync_error = (f"synchronisation échouée (rc={r.returncode})"
|
|
||||||
+ (f" : {tail}" if tail else ""))
|
|
||||||
except Exception as e: # noqa: BLE001 — remonté à l'UI via l'état
|
|
||||||
sync_error = f"synchronisation échouée : {e}"
|
|
||||||
_rebuild["phase"] = "index"
|
_rebuild["phase"] = "index"
|
||||||
try:
|
try:
|
||||||
from .index import build_index
|
from .index import build_index
|
||||||
build_index(OUTPUT_DIR)
|
build_index(OUTPUT_DIR)
|
||||||
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"] = f"{sync_error} ; {e}" if sync_error else str(e)
|
_rebuild["error"] = str(e)
|
||||||
else:
|
|
||||||
_rebuild["error"] = sync_error
|
|
||||||
finally:
|
finally:
|
||||||
_rebuild["running"] = False
|
_rebuild["running"] = False
|
||||||
_rebuild["phase"] = None
|
_rebuild["phase"] = None
|
||||||
_rebuild["done"] = time.time()
|
_rebuild["done"] = time.time()
|
||||||
|
|
||||||
threading.Thread(target=_run, daemon=True).start()
|
threading.Thread(target=_run, daemon=True).start()
|
||||||
return {"demarré": True, "sync": bool(with_sync and SYNC_CMD)}
|
return {"demarré": True}
|
||||||
|
|
||||||
|
|
||||||
def _auto_sync_once():
|
|
||||||
"""Un cycle de cache local : synchronisation + rebuild, si rien ne tourne.
|
|
||||||
|
|
||||||
Retourne True si un cycle a été lancé, False si un rebuild était déjà en
|
|
||||||
cours (le prochain cycle rattrapera).
|
|
||||||
"""
|
|
||||||
if _rebuild["running"]:
|
|
||||||
return False
|
|
||||||
try:
|
|
||||||
_start_rebuild(with_sync=True)
|
|
||||||
return True
|
|
||||||
except HTTPException:
|
|
||||||
return False # course perdue contre un lancement manuel : tant pis
|
|
||||||
|
|
||||||
|
|
||||||
def _auto_sync_loop():
|
|
||||||
"""Boucle du cache local : synchronise les tuiles périodiquement."""
|
|
||||||
while True:
|
|
||||||
time.sleep(AUTO_SYNC_SECONDS)
|
|
||||||
_auto_sync_once()
|
|
||||||
|
|
||||||
|
|
||||||
# Conteneur webapp seul (cache local des tuiles) : lancer la boucle de
|
|
||||||
# synchronisation en arrière-plan si demandé. Sans LIDAR_SYNC_CMD il n'y a
|
|
||||||
# rien à synchroniser — pas de boucle (juste le rebuild à la demande).
|
|
||||||
if AUTO_SYNC_SECONDS > 0 and SYNC_CMD:
|
|
||||||
threading.Thread(target=_auto_sync_loop, daemon=True,
|
|
||||||
name="cache-tuiles").start()
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/sync", dependencies=[Depends(_require_token)])
|
@app.post("/api/sync", dependencies=[Depends(_require_token)])
|
||||||
def sync_and_rebuild():
|
def sync_and_rebuild():
|
||||||
"""Synchronise les tuiles (LIDAR_SYNC_CMD) puis régénère la carte en arrière-plan.
|
"""Régénère l'index local (vignettes + carte) en arrière-plan.
|
||||||
|
|
||||||
Sans LIDAR_SYNC_CMD (machine de traitement locale), se réduit à un simple
|
Les tuiles étant servies à la demande, il n'y a plus de synchronisation à
|
||||||
rebuild de l'index — même comportement que /api/rebuild.
|
exécuter : /api/sync se réduit à un rebuild de l'index (même
|
||||||
|
comportement que /api/rebuild).
|
||||||
"""
|
"""
|
||||||
return _start_rebuild(with_sync=True)
|
return _start_rebuild()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/sync")
|
@app.get("/api/sync")
|
||||||
@ -877,7 +821,7 @@ def sync_status():
|
|||||||
@app.post("/api/rebuild", dependencies=[Depends(_require_token)])
|
@app.post("/api/rebuild", dependencies=[Depends(_require_token)])
|
||||||
def rebuild_index():
|
def rebuild_index():
|
||||||
"""Régénère la carte (index.html, vignettes, sous-tuiles) en arrière-plan."""
|
"""Régénère la carte (index.html, vignettes, sous-tuiles) en arrière-plan."""
|
||||||
return _start_rebuild(with_sync=False)
|
return _start_rebuild()
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/rebuild")
|
@app.get("/api/rebuild")
|
||||||
@ -1090,9 +1034,9 @@ def export_tiles(req: ExportRequest):
|
|||||||
"""Assemble des dalles adjacentes en image ou PDF (export.py).
|
"""Assemble des dalles adjacentes en image ou PDF (export.py).
|
||||||
|
|
||||||
Local par nature : les tuiles assemblées sont celles du cache output/
|
Local par nature : les tuiles assemblées sont celles du cache output/
|
||||||
(la webapp légère possède sa copie via rsync) — aucune délégation à la
|
(la webapp légère les possède via le cache à la demande) — aucune
|
||||||
machine de traitement. Un seul export à la fois (CPU/mémoire limités sur
|
délégation à la machine de traitement. Un seul export à la fois
|
||||||
Raspberry Pi).
|
(CPU/mémoire limités sur Raspberry Pi).
|
||||||
"""
|
"""
|
||||||
import re as _re
|
import re as _re
|
||||||
from .export import EXPORT_FORMATS, build_export
|
from .export import EXPORT_FORMATS, build_export
|
||||||
@ -1153,4 +1097,13 @@ def export_file(name: str):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import uvicorn
|
import uvicorn
|
||||||
uvicorn.run(app, host="0.0.0.0", port=PORT)
|
# HTTPS optionnel : la géolocalisation du navigateur (bouton ⌖, centrage
|
||||||
|
# GPS depuis un téléphone) n'est disponible qu'en contexte sécurisé.
|
||||||
|
# Définir LIDAR_SSL_CERTFILE + LIDAR_SSL_KEYFILE (certificat auto-signé)
|
||||||
|
# pour servir la carte en https://<hôte>:PORT ; le téléphone accepte
|
||||||
|
# l'avertissement « certificat non fiable » puis le GPS fonctionne.
|
||||||
|
_ssl_cert = os.environ.get("LIDAR_SSL_CERTFILE")
|
||||||
|
_ssl_key = os.environ.get("LIDAR_SSL_KEYFILE")
|
||||||
|
_ssl_kw = ({"ssl_certfile": _ssl_cert, "ssl_keyfile": _ssl_key}
|
||||||
|
if _ssl_cert and _ssl_key else {})
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=PORT, **_ssl_kw)
|
||||||
|
|||||||
53
make-tls-cert.sh
Executable file
53
make-tls-cert.sh
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Génère un certificat auto-signé pour servir la webapp en HTTPS.
|
||||||
|
#
|
||||||
|
# Pourquoi : l'API Geolocation des navigateurs (bouton ⌖ « centrer sur la
|
||||||
|
# position GPS du téléphone ») n'est disponible qu'en contexte sécurisé
|
||||||
|
# (HTTPS). Servir la carte en https://<hôte>:PORT débloque le GPS sur
|
||||||
|
# téléphone ; le certificat auto-signé affiche un avertissement « non fiable »
|
||||||
|
# que l'on accepte une fois (l'usage est sur le LAN, pas exposé).
|
||||||
|
#
|
||||||
|
# Usage :
|
||||||
|
# ./make-tls-cert.sh # crée tls/webapp.crt + tls/webapp.key
|
||||||
|
# ./make-tls-cert.sh --force # régénère même si le certificat existe
|
||||||
|
#
|
||||||
|
# Le certificat est monté dans le conteneur (./tls → /app/tls) et activé par
|
||||||
|
# les variables LIDAR_SSL_CERTFILE / LIDAR_SSL_KEYFILE (cf. serve-webapp.sh
|
||||||
|
# et docs/DEPLOY_WEBAPP.md).
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
FORCE=0
|
||||||
|
[ "${1:-}" = "--force" ] && FORCE=1
|
||||||
|
|
||||||
|
mkdir -p tls
|
||||||
|
# Garde-fou : si docker compose a déjà monté ./tls, Docker l'a créé root:root.
|
||||||
|
# En tant qu'utilisateur (non root), on ne peut pas écrire dedans.
|
||||||
|
if [ ! -w tls ]; then
|
||||||
|
echo "Erreur : tls/ n'est pas writable par $(id -un) (souvent créé root par Docker)." >&2
|
||||||
|
echo "Corriger : sudo chown \"$(id -un)\" tls puis relancer $0" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$FORCE" -eq 0 ] && [ -f tls/webapp.crt ] && [ -f tls/webapp.key ]; then
|
||||||
|
echo "Certificat auto-signé déjà présent : tls/webapp.crt + tls/webapp.key"
|
||||||
|
echo "Régénérer : $0 --force"
|
||||||
|
else
|
||||||
|
CN="${LIDAR_CERT_CN:-lidar-webapp}"
|
||||||
|
# SAN : nom local du conteneur + IP générique. Le téléphone se connecte
|
||||||
|
# par l'IP du Pi ; le SAN IP ne couvre pas cette IP dynamique d'où
|
||||||
|
# l'avertissement de nom, mais le certificat reste valable (10 ans).
|
||||||
|
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
||||||
|
-keyout tls/webapp.key -out tls/webapp.crt \
|
||||||
|
-subj "/CN=${CN}" \
|
||||||
|
-addext "subjectAltName=DNS:${CN},DNS:lidar-webapp,DNS:*.local,IP:0.0.0.0" \
|
||||||
|
> /dev/null
|
||||||
|
chmod 600 tls/webapp.key
|
||||||
|
echo "Certificat auto-signé créé : tls/webapp.crt + tls/webapp.key (valable 10 ans, CN=${CN})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Activation (webapp.env) :"
|
||||||
|
echo " LIDAR_SSL_CERTFILE=/app/tls/webapp.crt"
|
||||||
|
echo " LIDAR_SSL_KEYFILE=/app/tls/webapp.key"
|
||||||
|
echo "serve-webapp.sh monte ./tls → /app/tls et passe ces variables automatiquement."
|
||||||
|
echo "Puis : ./serve-webapp.sh restart — la carte répond sur https://<hôte>:PORT."
|
||||||
21
run.sh
21
run.sh
@ -25,7 +25,7 @@
|
|||||||
# --serve-webapp [PORT]
|
# --serve-webapp [PORT]
|
||||||
# Conteneur webapp seul (image légère) : carte servie depuis
|
# Conteneur webapp seul (image légère) : carte servie depuis
|
||||||
# le cache local de tuiles, génération déléguée à la machine
|
# le cache local de tuiles, génération déléguée à la machine
|
||||||
# de traitement (LIDAR_GENERATION_URL, LIDAR_SYNC_CMD)
|
# de traitement (LIDAR_GENERATION_URL), tuiles à la demande
|
||||||
# -h Afficher l'aide complète
|
# -h Afficher l'aide complète
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
@ -186,7 +186,7 @@ while [ $# -gt 0 ]; do
|
|||||||
echo " --serve-webapp [PORT]"
|
echo " --serve-webapp [PORT]"
|
||||||
echo " Conteneur webapp seul (image légère, sans PDAL/GPU) :"
|
echo " Conteneur webapp seul (image légère, sans PDAL/GPU) :"
|
||||||
echo " carte servie depuis le cache local de tuiles, génération"
|
echo " carte servie depuis le cache local de tuiles, génération"
|
||||||
echo " déléguée via LIDAR_GENERATION_URL + LIDAR_SYNC_CMD"
|
echo " déléguée via LIDAR_GENERATION_URL (tuiles à la demande)"
|
||||||
echo " (cf. docs/DEPLOY_WEBAPP.md)"
|
echo " (cf. docs/DEPLOY_WEBAPP.md)"
|
||||||
echo " --test Exécuter les tests unitaires"
|
echo " --test Exécuter les tests unitaires"
|
||||||
echo " -h Afficher cette aide"
|
echo " -h Afficher cette aide"
|
||||||
@ -299,38 +299,29 @@ fi
|
|||||||
|
|
||||||
# Mode --serve-webapp : conteneur webapp SEUL (image légère, sans PDAL/GPU).
|
# Mode --serve-webapp : conteneur webapp SEUL (image légère, sans PDAL/GPU).
|
||||||
# La carte est servie depuis le cache local de tuiles (output/) ; la
|
# La carte est servie depuis le cache local de tuiles (output/) ; la
|
||||||
# génération est déléguée à la machine de traitement et les tuiles reviennent
|
# génération est déléguée à la machine de traitement et les tuiles sont
|
||||||
# par LIDAR_SYNC_CMD (rsync), rafraîchies aussi par LIDAR_AUTO_SYNC_SECONDS.
|
# servies à la demande depuis LIDAR_GENERATION_URL (plus de rsync).
|
||||||
# Variables passées telles quelles depuis l'environnement hôte :
|
# Variables passées telles quelles depuis l'environnement hôte :
|
||||||
# LIDAR_GENERATION_URL machine de traitement (ex: http://192.168.1.50:8973)
|
# LIDAR_GENERATION_URL machine de traitement (ex: http://192.168.1.50:8973)
|
||||||
# LIDAR_REMOTE_TOKEN jeton si la machine exige LIDAR_API_TOKEN
|
# LIDAR_REMOTE_TOKEN jeton si la machine exige LIDAR_API_TOKEN
|
||||||
# LIDAR_SYNC_CMD rsync qui remplit le cache local de tuiles
|
|
||||||
# LIDAR_AUTO_SYNC_SECONDS resynchronisation périodique (0 = off)
|
|
||||||
# LIDAR_REGEN_CIDR réseau autorisé à lancer les générations
|
# LIDAR_REGEN_CIDR réseau autorisé à lancer les générations
|
||||||
# (défaut 192.168.1.0/24, vide = restriction levée)
|
# (défaut 192.168.1.0/24, vide = restriction levée)
|
||||||
if [ "$WEBAPP_FLAG" -eq 1 ]; then
|
if [ "$WEBAPP_FLAG" -eq 1 ]; then
|
||||||
build_webapp_image
|
build_webapp_image
|
||||||
mkdir -p "$OUTPUT_DIR"
|
mkdir -p "$OUTPUT_DIR"
|
||||||
AUTO_SYNC_INFO="${LIDAR_AUTO_SYNC_SECONDS:-0}"
|
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " Carte LiDAR — webapp seule (image légère)"
|
echo " Carte LiDAR — webapp seule (image légère)"
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " http://127.0.0.1:${WEBAPP_SERVE_PORT}/"
|
echo " http://127.0.0.1:${WEBAPP_SERVE_PORT}/"
|
||||||
echo " Génération distante : ${LIDAR_GENERATION_URL:-non configurée (LIDAR_GENERATION_URL)}"
|
echo " Génération distante : ${LIDAR_GENERATION_URL:-non configurée (LIDAR_GENERATION_URL)}"
|
||||||
if [ -n "$LIDAR_SYNC_CMD" ]; then
|
echo " Cache local tuiles : à la demande (LIDAR_GENERATION_URL)"
|
||||||
echo " Cache local tuiles : LIDAR_SYNC_CMD$([ "${LIDAR_AUTO_SYNC_SECONDS:-0}" != "0" ] && echo " (auto toutes les ${AUTO_SYNC_INFO}s)")"
|
|
||||||
else
|
|
||||||
echo " Cache local tuiles : désactivé (définir LIDAR_SYNC_CMD pour rsync)"
|
|
||||||
fi
|
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
WEBAPP_ENV=()
|
WEBAPP_ENV=()
|
||||||
[ -n "$LIDAR_GENERATION_URL" ] && WEBAPP_ENV+=(-e LIDAR_GENERATION_URL="$LIDAR_GENERATION_URL")
|
[ -n "$LIDAR_GENERATION_URL" ] && WEBAPP_ENV+=(-e LIDAR_GENERATION_URL="$LIDAR_GENERATION_URL")
|
||||||
[ -n "$LIDAR_REMOTE_TOKEN" ] && WEBAPP_ENV+=(-e LIDAR_REMOTE_TOKEN="$LIDAR_REMOTE_TOKEN")
|
[ -n "$LIDAR_REMOTE_TOKEN" ] && WEBAPP_ENV+=(-e LIDAR_REMOTE_TOKEN="$LIDAR_REMOTE_TOKEN")
|
||||||
[ -n "$LIDAR_API_TOKEN" ] && WEBAPP_ENV+=(-e LIDAR_API_TOKEN="$LIDAR_API_TOKEN")
|
[ -n "$LIDAR_API_TOKEN" ] && WEBAPP_ENV+=(-e LIDAR_API_TOKEN="$LIDAR_API_TOKEN")
|
||||||
[ -n "$LIDAR_SYNC_CMD" ] && WEBAPP_ENV+=(-e LIDAR_SYNC_CMD="$LIDAR_SYNC_CMD")
|
|
||||||
[ -n "${LIDAR_AUTO_SYNC_SECONDS:-}" ] && WEBAPP_ENV+=(-e LIDAR_AUTO_SYNC_SECONDS="$LIDAR_AUTO_SYNC_SECONDS")
|
|
||||||
[ -n "${LIDAR_REGEN_CIDR:-}" ] && WEBAPP_ENV+=(-e LIDAR_REGEN_CIDR="$LIDAR_REGEN_CIDR")
|
[ -n "${LIDAR_REGEN_CIDR:-}" ] && WEBAPP_ENV+=(-e LIDAR_REGEN_CIDR="$LIDAR_REGEN_CIDR")
|
||||||
# Clé SSH du rsync (LIDAR_SYNC_CMD) : montée en lecture seule si présente
|
# Clé SSH du hôte (déploiement, git pull distant) : montée en lecture seule si présente
|
||||||
WEBAPP_SSH=()
|
WEBAPP_SSH=()
|
||||||
[ -d "${HOME}/.ssh" ] && WEBAPP_SSH+=(-v "${HOME}/.ssh:/home/lidar/.ssh:ro")
|
[ -d "${HOME}/.ssh" ] && WEBAPP_SSH+=(-v "${HOME}/.ssh:/home/lidar/.ssh:ro")
|
||||||
exec docker run --rm --init \
|
exec docker run --rm --init \
|
||||||
|
|||||||
@ -7,17 +7,21 @@
|
|||||||
# WEBAPP_PORT port hôte (défaut 8973)
|
# WEBAPP_PORT port hôte (défaut 8973)
|
||||||
# LIDAR_GENERATION_URL machine de traitement (ex. http://192.168.1.50:8973)
|
# LIDAR_GENERATION_URL machine de traitement (ex. http://192.168.1.50:8973)
|
||||||
# LIDAR_REMOTE_TOKEN jeton si la machine exige LIDAR_API_TOKEN
|
# LIDAR_REMOTE_TOKEN jeton si la machine exige LIDAR_API_TOKEN
|
||||||
# LIDAR_SYNC_CMD rsync qui remplit le cache local de tuiles
|
|
||||||
# LIDAR_AUTO_SYNC_SECONDS resynchronisation périodique (défaut 0 = off)
|
|
||||||
# LIDAR_REGEN_CIDR réseau autorisé à lancer les générations
|
# LIDAR_REGEN_CIDR réseau autorisé à lancer les générations
|
||||||
# (défaut 192.168.1.0/24, vide = restriction levée)
|
# (défaut 192.168.1.0/24, vide = restriction levée)
|
||||||
#
|
#
|
||||||
|
# GPS sur téléphone (optionnel) : l'API Geolocation exige une connexion
|
||||||
|
# sécurisée. ./make-tls-cert.sh crée un certificat auto-signé dans ./tls ;
|
||||||
|
# dès qu'il est présent, ce script sert la carte en HTTPS (https://<hôte>:port)
|
||||||
|
# et le bouton ⌖ « centrer sur la position GPS » fonctionne sur téléphone
|
||||||
|
# (accepter l'avertissement « certificat non fiable » une fois).
|
||||||
|
#
|
||||||
# Usage :
|
# Usage :
|
||||||
# ./serve-webapp.sh [start] démarrer (arrière-plan, redémarrage auto)
|
# ./serve-webapp.sh [start] démarrer (arrière-plan, redémarrage auto)
|
||||||
# ./serve-webapp.sh stop arrêter et supprimer le conteneur
|
# ./serve-webapp.sh stop arrêter et supprimer le conteneur
|
||||||
# ./serve-webapp.sh restart redémarrer (relit webapp.env)
|
# ./serve-webapp.sh restart redémarrer (relit webapp.env)
|
||||||
# ./serve-webapp.sh status état du conteneur et de la dernière sync
|
# ./serve-webapp.sh status état du conteneur et de la dernière sync
|
||||||
# ./serve-webapp.sh sync forcer sync rsync + régénération des vignettes
|
# ./serve-webapp.sh sync forcer un rebuild + régénération des vignettes
|
||||||
# ./serve-webapp.sh logs suivre les logs du conteneur
|
# ./serve-webapp.sh logs suivre les logs du conteneur
|
||||||
# ./serve-webapp.sh update git pull, rebuild de l'image, redémarrer
|
# ./serve-webapp.sh update git pull, rebuild de l'image, redémarrer
|
||||||
set -e
|
set -e
|
||||||
@ -25,8 +29,6 @@ set -e
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
IMAGE="lidar-webapp"
|
IMAGE="lidar-webapp"
|
||||||
CONTAINER="lidar-webapp"
|
CONTAINER="lidar-webapp"
|
||||||
WEBAPP_PORT="${WEBAPP_PORT:-8973}"
|
|
||||||
BASE_URL="http://127.0.0.1:${WEBAPP_PORT}"
|
|
||||||
|
|
||||||
# Configuration locale (non versionnée) : webapp.env surcharge l'environnement
|
# Configuration locale (non versionnée) : webapp.env surcharge l'environnement
|
||||||
if [ -f "${SCRIPT_DIR}/webapp.env" ]; then
|
if [ -f "${SCRIPT_DIR}/webapp.env" ]; then
|
||||||
@ -35,6 +37,22 @@ if [ -f "${SCRIPT_DIR}/webapp.env" ]; then
|
|||||||
. "${SCRIPT_DIR}/webapp.env"
|
. "${SCRIPT_DIR}/webapp.env"
|
||||||
set +a
|
set +a
|
||||||
fi
|
fi
|
||||||
|
WEBAPP_PORT="${WEBAPP_PORT:-8973}"
|
||||||
|
|
||||||
|
# HTTPS (requis pour la géolocalisation GPS depuis un téléphone) : l'API
|
||||||
|
# Geolocation n'existe qu'en contexte sécurisé. Si le certificat auto-signé
|
||||||
|
# ./tls (cf. ./make-tls-cert.sh) est présent, la carte est servie en
|
||||||
|
# https://<hôte>:PORT ; le téléphone accepte l'avertissement « certificat non
|
||||||
|
# fiable » une fois. Sans certificat : HTTP (le bouton GPS est indisponible).
|
||||||
|
TLS_DIR="${SCRIPT_DIR}/tls"
|
||||||
|
if [ -f "${TLS_DIR}/webapp.crt" ] && [ -f "${TLS_DIR}/webapp.key" ]; then
|
||||||
|
SCHEME=https
|
||||||
|
KFLAG=" -k" # -k : les sondes locales tolèrent le certificat auto-signé
|
||||||
|
else
|
||||||
|
SCHEME=http
|
||||||
|
KFLAG=""
|
||||||
|
fi
|
||||||
|
BASE_URL="${SCHEME}://127.0.0.1:${WEBAPP_PORT}"
|
||||||
|
|
||||||
CMD="${1:-start}"
|
CMD="${1:-start}"
|
||||||
|
|
||||||
@ -53,7 +71,7 @@ lan_ip() {
|
|||||||
|
|
||||||
cmd_start() {
|
cmd_start() {
|
||||||
mkdir -p "${SCRIPT_DIR}/output"
|
mkdir -p "${SCRIPT_DIR}/output"
|
||||||
if curl -fsS "${BASE_URL}/api/status" > /dev/null 2>&1; then
|
if curl -fsS${KFLAG} "${BASE_URL}/api/status" > /dev/null 2>&1; then
|
||||||
echo "Le port ${WEBAPP_PORT} répond déjà (webapp déjà lancée ?)." >&2
|
echo "Le port ${WEBAPP_PORT} répond déjà (webapp déjà lancée ?)." >&2
|
||||||
echo "Arrêter l'existant, ou relancer avec un autre port : WEBAPP_PORT=9000 $0" >&2
|
echo "Arrêter l'existant, ou relancer avec un autre port : WEBAPP_PORT=9000 $0" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@ -68,17 +86,19 @@ cmd_start() {
|
|||||||
if [ -n "${LIDAR_REMOTE_TOKEN:-}" ]; then
|
if [ -n "${LIDAR_REMOTE_TOKEN:-}" ]; then
|
||||||
ENV_ARGS+=(-e LIDAR_REMOTE_TOKEN="$LIDAR_REMOTE_TOKEN")
|
ENV_ARGS+=(-e LIDAR_REMOTE_TOKEN="$LIDAR_REMOTE_TOKEN")
|
||||||
fi
|
fi
|
||||||
if [ -n "${LIDAR_SYNC_CMD:-}" ]; then
|
|
||||||
ENV_ARGS+=(-e LIDAR_SYNC_CMD="$LIDAR_SYNC_CMD")
|
|
||||||
fi
|
|
||||||
if [ -n "${LIDAR_AUTO_SYNC_SECONDS:-}" ]; then
|
|
||||||
ENV_ARGS+=(-e LIDAR_AUTO_SYNC_SECONDS="$LIDAR_AUTO_SYNC_SECONDS")
|
|
||||||
fi
|
|
||||||
# Définie même vide (= restriction levée) ; absente = défaut webapp.py
|
# Définie même vide (= restriction levée) ; absente = défaut webapp.py
|
||||||
if [ -n "${LIDAR_REGEN_CIDR+x}" ]; then
|
if [ -n "${LIDAR_REGEN_CIDR+x}" ]; then
|
||||||
ENV_ARGS+=(-e LIDAR_REGEN_CIDR="${LIDAR_REGEN_CIDR:-}")
|
ENV_ARGS+=(-e LIDAR_REGEN_CIDR="${LIDAR_REGEN_CIDR:-}")
|
||||||
fi
|
fi
|
||||||
# Clé SSH du rsync (LIDAR_SYNC_CMD) : montée en lecture seule si présente
|
# Certificat auto-signé (HTTPS → GPS téléphone) : monté en lecture seule
|
||||||
|
# et activé via LIDAR_SSL_CERTFILE / LIDAR_SSL_KEYFILE (cf. make-tls-cert.sh).
|
||||||
|
TLS_ARGS=()
|
||||||
|
if [ "$SCHEME" = "https" ]; then
|
||||||
|
TLS_ARGS+=(-v "${TLS_DIR}:/app/tls:ro")
|
||||||
|
TLS_ARGS+=(-e LIDAR_SSL_CERTFILE=/app/tls/webapp.crt)
|
||||||
|
TLS_ARGS+=(-e LIDAR_SSL_KEYFILE=/app/tls/webapp.key)
|
||||||
|
fi
|
||||||
|
# Clé SSH du hôte (déploiement, git pull distant) : montée en lecture seule si présente
|
||||||
SSH_ARGS=()
|
SSH_ARGS=()
|
||||||
if [ -d "${HOME}/.ssh" ]; then
|
if [ -d "${HOME}/.ssh" ]; then
|
||||||
SSH_ARGS+=(-v "${HOME}/.ssh:/home/lidar/.ssh:ro")
|
SSH_ARGS+=(-v "${HOME}/.ssh:/home/lidar/.ssh:ro")
|
||||||
@ -90,6 +110,7 @@ cmd_start() {
|
|||||||
--user 1000:1000 \
|
--user 1000:1000 \
|
||||||
-p "${WEBAPP_PORT}:8973" \
|
-p "${WEBAPP_PORT}:8973" \
|
||||||
-v "${SCRIPT_DIR}/output:/data/output" \
|
-v "${SCRIPT_DIR}/output:/data/output" \
|
||||||
|
"${TLS_ARGS[@]}" \
|
||||||
"${SSH_ARGS[@]}" \
|
"${SSH_ARGS[@]}" \
|
||||||
-e LIDAR_OUTPUT_DIR=/data/output \
|
-e LIDAR_OUTPUT_DIR=/data/output \
|
||||||
"${ENV_ARGS[@]}" \
|
"${ENV_ARGS[@]}" \
|
||||||
@ -97,7 +118,7 @@ cmd_start() {
|
|||||||
echo -n "Attente du serveur sur le port ${WEBAPP_PORT}..."
|
echo -n "Attente du serveur sur le port ${WEBAPP_PORT}..."
|
||||||
UP=0
|
UP=0
|
||||||
for _ in $(seq 1 30); do
|
for _ in $(seq 1 30); do
|
||||||
if curl -fsS "${BASE_URL}/api/status" > /dev/null 2>&1; then
|
if curl -fsS${KFLAG} "${BASE_URL}/api/status" > /dev/null 2>&1; then
|
||||||
UP=1
|
UP=1
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
@ -113,18 +134,13 @@ cmd_start() {
|
|||||||
echo "============================================"
|
echo "============================================"
|
||||||
echo " URL : ${BASE_URL}/"
|
echo " URL : ${BASE_URL}/"
|
||||||
if [ -n "$(lan_ip)" ]; then
|
if [ -n "$(lan_ip)" ]; then
|
||||||
echo " URL (LAN) : http://$(lan_ip):${WEBAPP_PORT}/"
|
echo " URL (LAN) : ${SCHEME}://$(lan_ip):${WEBAPP_PORT}/"
|
||||||
|
fi
|
||||||
|
if [ "$SCHEME" = "https" ]; then
|
||||||
|
echo " HTTPS (GPS tél.) : actif — sur le téléphone, acceptez l'avertissement « certificat non fiable »"
|
||||||
fi
|
fi
|
||||||
echo " Génération distante: ${LIDAR_GENERATION_URL:-non configurée}"
|
echo " Génération distante: ${LIDAR_GENERATION_URL:-non configurée}"
|
||||||
if [ -n "${LIDAR_SYNC_CMD:-}" ]; then
|
echo " Cache local tuiles : à la demande (LIDAR_GENERATION_URL)"
|
||||||
if [ -n "${LIDAR_AUTO_SYNC_SECONDS:-}" ] && [ "${LIDAR_AUTO_SYNC_SECONDS}" != "0" ]; then
|
|
||||||
echo " Cache local tuiles : rsync (auto toutes les ${LIDAR_AUTO_SYNC_SECONDS}s)"
|
|
||||||
else
|
|
||||||
echo " Cache local tuiles : rsync (à la demande)"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo " Cache local tuiles : désactivé (LIDAR_SYNC_CMD)"
|
|
||||||
fi
|
|
||||||
echo "============================================"
|
echo "============================================"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +155,7 @@ cmd_status() {
|
|||||||
else
|
else
|
||||||
echo "Conteneur : ${CONTAINER} arrêté"
|
echo "Conteneur : ${CONTAINER} arrêté"
|
||||||
fi
|
fi
|
||||||
if curl -fsS "${BASE_URL}/api/sync" 2> /dev/null; then
|
if curl -fsS${KFLAG} "${BASE_URL}/api/sync" 2> /dev/null; then
|
||||||
echo ""
|
echo ""
|
||||||
else
|
else
|
||||||
echo "Webapp : injoignable sur ${BASE_URL}"
|
echo "Webapp : injoignable sur ${BASE_URL}"
|
||||||
@ -147,7 +163,7 @@ cmd_status() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_sync() {
|
cmd_sync() {
|
||||||
CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST "${BASE_URL}/api/sync" || true)"
|
CODE="$(curl -s${KFLAG} -o /dev/null -w '%{http_code}' -X POST "${BASE_URL}/api/sync" || true)"
|
||||||
case "$CODE" in
|
case "$CODE" in
|
||||||
200) echo "Sync lancée (suivi : ./serve-webapp.sh status, logs : ./serve-webapp.sh logs)" ;;
|
200) echo "Sync lancée (suivi : ./serve-webapp.sh status, logs : ./serve-webapp.sh logs)" ;;
|
||||||
409) echo "Une sync est déjà en cours (suivi : ./serve-webapp.sh status)" ;;
|
409) echo "Une sync est déjà en cours (suivi : ./serve-webapp.sh status)" ;;
|
||||||
|
|||||||
23
webapp-entrypoint.sh
Executable file
23
webapp-entrypoint.sh
Executable file
@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Point d'entrée du conteneur webapp : sert la carte via uvicorn.
|
||||||
|
#
|
||||||
|
# HTTPS optionnel (requis pour la géolocalisation GPS depuis un téléphone) :
|
||||||
|
# si LIDAR_SSL_CERTFILE + LIDAR_SSL_KEYFILE sont définis ET que les fichiers
|
||||||
|
# existent (certificat auto-signé, cf. ./make-tls-cert.sh), la carte est servie
|
||||||
|
# en https://<hôte>:PORT et le bouton ⌖ fonctionne sur téléphone. Si les
|
||||||
|
# variables sont définies mais les fichiers absents (cert non encore généré),
|
||||||
|
# repli en HTTP avec un avertissement — la carte reste consultable, seul le
|
||||||
|
# GPS est indisponible.
|
||||||
|
set -euo pipefail
|
||||||
|
PORT="${LIDAR_PORT:-8973}"
|
||||||
|
SSL_ARGS=()
|
||||||
|
if [ -n "${LIDAR_SSL_CERTFILE:-}" ] && [ -n "${LIDAR_SSL_KEYFILE:-}" ]; then
|
||||||
|
if [ -f "${LIDAR_SSL_CERTFILE}" ] && [ -f "${LIDAR_SSL_KEYFILE}" ]; then
|
||||||
|
echo "Webapp servie en HTTPS (certificat ${LIDAR_SSL_CERTFILE}) — GPS actif sur téléphone"
|
||||||
|
SSL_ARGS=(--ssl-certfile "${LIDAR_SSL_CERTFILE}" --ssl-keyfile "${LIDAR_SSL_KEYFILE}")
|
||||||
|
else
|
||||||
|
echo "Avertissement : certificats TLS absents (${LIDAR_SSL_CERTFILE} / ${LIDAR_SSL_KEYFILE}) — service en HTTP." >&2
|
||||||
|
echo "Générer le certificat puis relancer : ./make-tls-cert.sh && docker compose up -d --build" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exec python3 -m uvicorn lidar_pipeline.webapp:app --host 0.0.0.0 --port "${PORT}" ${SSL_ARGS[@]+"${SSL_ARGS[@]}"}
|
||||||
@ -12,16 +12,31 @@
|
|||||||
# Jeton partagé si la machine de traitement définit LIDAR_API_TOKEN
|
# Jeton partagé si la machine de traitement définit LIDAR_API_TOKEN
|
||||||
#LIDAR_REMOTE_TOKEN=un-secret-à-partager
|
#LIDAR_REMOTE_TOKEN=un-secret-à-partager
|
||||||
|
|
||||||
# rsync qui ramène les tuiles traitées vers le cache local.
|
# Les tuiles sont servies à la demande depuis LIDAR_GENERATION_URL (cache
|
||||||
# Les vignettes (index_thumbs/, index_subtiles/) sont régénérées localement,
|
# local peuplé au fil des consultations) : plus de rsync ni de sync périodique.
|
||||||
# ne pas les synchroniser.
|
|
||||||
#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/"
|
|
||||||
|
|
||||||
# Cache auto-entretenu : resync + vignettes toutes les N secondes (0 = off)
|
|
||||||
#LIDAR_AUTO_SYNC_SECONDS=600
|
|
||||||
|
|
||||||
# Réseaux autorisés à lancer les générations depuis la carte
|
# Réseaux autorisés à lancer les générations depuis la carte
|
||||||
# (liste de CIDR séparés par virgules ; défaut : localhost + plages
|
# (liste de CIDR séparés par virgules ; défaut : localhost + plages
|
||||||
# privées 127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 ;
|
# privées 127.0.0.0/8,::1,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 ;
|
||||||
# vide pour lever la restriction)
|
# vide pour lever la restriction)
|
||||||
#LIDAR_REGEN_CIDR=192.168.1.0/24
|
#LIDAR_REGEN_CIDR=192.168.1.0/24
|
||||||
|
|
||||||
|
# --- GPS sur téléphone ---------------------------------------------------
|
||||||
|
# L'API Geolocation (bouton ⌖ « centrer sur la position GPS ») exige un
|
||||||
|
# contexte sécurisé (HTTPS). Deux façons de l'activer :
|
||||||
|
#
|
||||||
|
# a. Traefik (recommandé) : le webapp sert du HTTP, Traefik expose
|
||||||
|
# https://<domaine> avec un certificat valable. La config Traefik est
|
||||||
|
# dans l'override local docker-compose.webapp.override.yml (non
|
||||||
|
# versionné) — cf. docker-compose.webapp.override.yml.example et
|
||||||
|
# docs/DEPLOY_WEBAPP.md. Rien à définir dans ce fichier.
|
||||||
|
#
|
||||||
|
# b. Sans Traefik (chemin serve-webapp.sh) : certificat auto-signé.
|
||||||
|
# 1. ./make-tls-cert.sh (crée tls/webapp.crt + tls/webapp.key)
|
||||||
|
# 2. ./serve-webapp.sh restart (sert en https://<hôte>:PORT, monte ./tls
|
||||||
|
# et passe ces variables automatiquement). Accepter l'avertissement
|
||||||
|
# « certificat non fiable » sur le téléphone. Ces lignes n'ont d'effet
|
||||||
|
# que si le certificat est stocké ailleurs que dans ./tls :
|
||||||
|
#LIDAR_SSL_CERTFILE=/app/tls/webapp.crt
|
||||||
|
#LIDAR_SSL_KEYFILE=/app/tls/webapp.key
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user