diff --git a/.gitignore b/.gitignore index 6d3150c..be19d1c 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ htmlcov/ # Fichiers de configuration locaux .env .env.local +webapp.env # Données et artefacts internes (jamais dans le dépôt) data/ diff --git a/AGENTS.md b/AGENTS.md index 4278e0a..712b8b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,7 @@ - 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 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) +- 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 file: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.` - test case: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.::::` diff --git a/docs/DEPLOY_WEBAPP.md b/docs/DEPLOY_WEBAPP.md index 7d2640d..be065af 100644 --- a/docs/DEPLOY_WEBAPP.md +++ b/docs/DEPLOY_WEBAPP.md @@ -88,17 +88,32 @@ 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). +Avec `serve-webapp.sh` et `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 c ci-dessous). ### 3. Configurer et lancer -Deux façons, au choix : +Trois 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` +**a. `serve-webapp.sh` (recommandé)** — script de gestion de la stack +(arrière-plan, redémarrage automatique au reboot, sous-commandes). La +configuration vit dans `webapp.env` (copie du modèle, non versionné) : + +```bash +cp webapp.env.example webapp.env +nano webapp.env # LIDAR_GENERATION_URL, LIDAR_SYNC_CMD, jeton... +./serve-webapp.sh # démarre (build au premier lancement) et attend le serveur +``` + +Sous-commandes : `stop`, `restart` (relit `webapp.env`), `status` (conteneur ++ état de la sync), `sync` (force rsync + vignettes), `logs`, `update` +(`git pull` + rebuild + redémarrage). Port hôte via `WEBAPP_PORT` dans +`webapp.env` ou l'environnement (`WEBAPP_PORT=9000 ./serve-webapp.sh`). + +**b. `run.sh`** — conteneur au premier plan (Ctrl-C arrête tout) : pratique +pour tester, les variables se passent depuis l'environnement hôte, `~/.ssh` est monté automatiquement : ```bash @@ -124,7 +139,7 @@ DNAT Docker pour les clients du LAN) ; derrière un reverse proxy local 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` : +**c. `docker compose`** — éditer `docker-compose.webapp.yml` : - `LIDAR_GENERATION_URL` : `http://:8973` - `LIDAR_REMOTE_TOKEN` : la valeur de `LIDAR_API_TOKEN` de la machine @@ -158,8 +173,9 @@ docker compose -f docker-compose.webapp.yml up -d --build La carte est sur `http://: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). +pour tester. Pour une installation permanente, `serve-webapp.sh` (option a) +ou `docker compose` (option c) redémarrent la carte après un reboot du Pi +(`--restart unless-stopped`). ### 4. Premier chargement @@ -174,7 +190,14 @@ 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) : +Avec `serve-webapp.sh`, une seule commande (`git pull`, rebuild de l'image, +redémarrage) : + +```bash +./serve-webapp.sh update +``` + +Sinon, 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 @@ -203,10 +226,13 @@ chaque lancement ; il suffit de relancer la commande. Le cache de tuiles `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). + `"running": false`, ou `./serve-webapp.sh status`) puis recharger la + page ; le bouton ↻ de la carte enchaîne sync + vignettes + automatiquement. +- **Port 8973 déjà pris** : `WEBAPP_PORT=9000 ./serve-webapp.sh` (ou la + variable dans `webapp.env`), `--serve-webapp 9000` avec `run.sh`, ou + changer le mapping `ports:` en compose (le conteneur écoute toujours sur + 8973). ## Fonctionnement @@ -244,3 +270,6 @@ chaque lancement ; il suffit de relancer la commande. Le cache de tuiles - `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). +- `serve-webapp.sh`, `webapp.env.example` : gestion de la stack webapp sur + le Pi (démarrage/arrêt/sync/logs/update), configuration locale dans + `webapp.env` (ignoré par git). diff --git a/serve-webapp.sh b/serve-webapp.sh new file mode 100755 index 0000000..2bd564e --- /dev/null +++ b/serve-webapp.sh @@ -0,0 +1,180 @@ +#!/bin/bash +# Stack webapp LiDAR — gestion du conteneur carte interactive sur la machine +# légère (Raspberry Pi). Cf. docs/DEPLOY_WEBAPP.md. +# +# Configuration : variables dans webapp.env (modèle : webapp.env.example, +# non versionné) ou exportées dans l'environnement : +# WEBAPP_PORT port hôte (défaut 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_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 +# (défaut 192.168.1.0/24, vide = restriction levée) +# +# Usage : +# ./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 restart redémarrer (relit webapp.env) +# ./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 logs suivre les logs du conteneur +# ./serve-webapp.sh update git pull, rebuild de l'image, redémarrer +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +IMAGE="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 +if [ -f "${SCRIPT_DIR}/webapp.env" ]; then + set -a + # shellcheck disable=SC1091 + . "${SCRIPT_DIR}/webapp.env" + set +a +fi + +CMD="${1:-start}" + +build_image() { + echo "Build de l'image webapp (cache Docker)..." + if ! docker build -f "${SCRIPT_DIR}/Dockerfile.webapp" -t "$IMAGE" "$SCRIPT_DIR" > /tmp/lidar_webapp_build.log 2>&1; then + cat /tmp/lidar_webapp_build.log >&2 + echo "Échec du build (journal : /tmp/lidar_webapp_build.log)" >&2 + exit 1 + fi +} + +lan_ip() { + hostname -I 2> /dev/null | awk '{print $1}' +} + +cmd_start() { + mkdir -p "${SCRIPT_DIR}/output" + if curl -fsS "${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 "Arrêter l'existant, ou relancer avec un autre port : WEBAPP_PORT=9000 $0" >&2 + exit 1 + fi + if ! docker image inspect "$IMAGE" > /dev/null 2>&1; then + build_image + fi + ENV_ARGS=() + if [ -n "${LIDAR_GENERATION_URL:-}" ]; then + ENV_ARGS+=(-e LIDAR_GENERATION_URL="$LIDAR_GENERATION_URL") + fi + if [ -n "${LIDAR_REMOTE_TOKEN:-}" ]; then + ENV_ARGS+=(-e LIDAR_REMOTE_TOKEN="$LIDAR_REMOTE_TOKEN") + 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 + if [ -n "${LIDAR_REGEN_CIDR+x}" ]; then + ENV_ARGS+=(-e LIDAR_REGEN_CIDR="${LIDAR_REGEN_CIDR:-}") + fi + # Clé SSH du rsync (LIDAR_SYNC_CMD) : montée en lecture seule si présente + SSH_ARGS=() + if [ -d "${HOME}/.ssh" ]; then + SSH_ARGS+=(-v "${HOME}/.ssh:/home/lidar/.ssh:ro") + fi + docker rm -f "$CONTAINER" > /dev/null 2>&1 || true + docker run -d --init \ + --name "$CONTAINER" \ + --restart unless-stopped \ + --user 1000:1000 \ + -p "${WEBAPP_PORT}:8973" \ + -v "${SCRIPT_DIR}/output:/data/output" \ + "${SSH_ARGS[@]}" \ + -e LIDAR_OUTPUT_DIR=/data/output \ + "${ENV_ARGS[@]}" \ + "$IMAGE" > /dev/null + echo -n "Attente du serveur sur le port ${WEBAPP_PORT}..." + UP=0 + for _ in $(seq 1 30); do + if curl -fsS "${BASE_URL}/api/status" > /dev/null 2>&1; then + UP=1 + break + fi + sleep 1 + done + if [ "$UP" != "1" ]; then + echo " KO (voir ./serve-webapp.sh logs)" >&2 + exit 1 + fi + echo " OK" + echo "============================================" + echo " Carte LiDAR — webapp (${CONTAINER})" + echo "============================================" + echo " URL : ${BASE_URL}/" + if [ -n "$(lan_ip)" ]; then + echo " URL (LAN) : http://$(lan_ip):${WEBAPP_PORT}/" + fi + echo " Génération distante: ${LIDAR_GENERATION_URL:-non configurée}" + if [ -n "${LIDAR_SYNC_CMD:-}" ]; then + 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 "============================================" +} + +cmd_stop() { + docker rm -f "$CONTAINER" > /dev/null 2>&1 || true + echo "Conteneur ${CONTAINER} arrêté." +} + +cmd_status() { + if docker ps --format '{{.Names}}' 2> /dev/null | grep -qx "$CONTAINER"; then + echo "Conteneur : ${CONTAINER} en cours d'exécution ($(docker ps --filter name="^${CONTAINER}$" --format '{{.Status}}'))" + else + echo "Conteneur : ${CONTAINER} arrêté" + fi + if curl -fsS "${BASE_URL}/api/sync" 2> /dev/null; then + echo "" + else + echo "Webapp : injoignable sur ${BASE_URL}" + fi +} + +cmd_sync() { + CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST "${BASE_URL}/api/sync" || true)" + case "$CODE" in + 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)" ;; + *) + echo "Échec : HTTP ${CODE:-pas de réponse} (webapp injoignable sur ${BASE_URL} ?)" >&2 + exit 1 + ;; + esac +} + +cmd_update() { + echo "Mise à jour du code (git pull)..." + git -C "$SCRIPT_DIR" pull --ff-only + build_image + cmd_start +} + +case "$CMD" in + start) cmd_start ;; + stop) cmd_stop ;; + restart) cmd_stop; cmd_start ;; + status) cmd_status ;; + sync) cmd_sync ;; + logs) exec docker logs -f "$CONTAINER" ;; + update) cmd_update ;; + *) + echo "Usage : $0 [start|stop|restart|status|sync|logs|update]" >&2 + exit 1 + ;; +esac diff --git a/webapp.env.example b/webapp.env.example new file mode 100644 index 0000000..f32e57e --- /dev/null +++ b/webapp.env.example @@ -0,0 +1,25 @@ +# Configuration de la stack webapp — copier en webapp.env et adapter. +# webapp.env est ignoré par git (jetons, chemins locaux). +# Syntaxe shell simple : KEY=valeur (les valeurs avec espaces entre guillemets). + +# Port hôte de la carte (défaut 8973) +#WEBAPP_PORT=8973 + +# Machine de traitement (service worker de docker-compose.worker.yml). +# Commenté : consultation autonome du cache local, aucune génération. +#LIDAR_GENERATION_URL=http://192.168.1.50:8973 + +# Jeton partagé si la machine de traitement définit LIDAR_API_TOKEN +#LIDAR_REMOTE_TOKEN=un-secret-à-partager + +# rsync qui ramène les tuiles traitées vers le cache local. +# Les vignettes (index_thumbs/, index_subtiles/) sont régénérées localement, +# 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éseau autorisé à lancer les générations depuis la carte +# (défaut 192.168.1.0/24 ; vide pour lever la restriction) +#LIDAR_REGEN_CIDR=192.168.1.0/24