Préréglage carte par défaut et serve-webapp.sh compatible Traefik

This commit is contained in:
Antoine Jacquin
2026-09-19 11:05:16 +02:00
parent e8a9b0187c
commit 9546173c07
7 changed files with 144 additions and 145 deletions

View File

@ -5,7 +5,7 @@
- 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/` 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). - 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]`, config dans `webapp.env` (modèle `webapp.env.example`, ignoré par git) ; mise à jour = `git pull` puis rebuild image + `restart` (cf. `docs/DEPLOY_WEBAPP.md`). **ATTENTION Pi de prod (192.168.3.10) : la webapp y tourne via `docker compose -f docker-compose.webapp.yml -f docker-compose.webapp.override.yml up -d --build` (labels Traefik `lidar.example.fr` + volume réel `/srv/lidar/output`) — NE PAS utiliser `serve-webapp.sh` là-bas : il recrée le conteneur sans labels (route publique 404) et sur le mauvais volume `./output`.** - stack webapp (machine légère): `./serve-webapp.sh [start|stop|restart|status|sync|logs]`, config dans `webapp.env` (modèle `webapp.env.example`, ignoré par git) ; mise à jour = `git pull` puis `restart` (rebuild inclus). Le script pilote `docker compose` et charge automatiquement `docker-compose.webapp.override.yml` s'il existe — utilisable sur le Pi de prod (192.168.3.10) : labels Traefik `lidar.example.fr` + volume réel `/srv/lidar/output` de l'override préservés.
- 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>`
- test case: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.<module>::<TestClass>::<test_method>` - test case: `docker run --rm lidar-lidar python3 -m pytest -v --pyargs lidar_pipeline.tests.<module>::<TestClass>::<test_method>`

View File

@ -103,20 +103,22 @@ avec `docker compose`, le montage équivalent est à décommenter dans
Trois façons, au choix : Trois façons, au choix :
**a. `serve-webapp.sh` (recommandé)** — script de gestion de la stack **a. `serve-webapp.sh` (recommandé)** — script de gestion de la stack. Il
(arrière-plan, redémarrage automatique au reboot, sous-commandes). La pilote `docker compose` et charge automatiquement
configuration vit dans `webapp.env` (copie du modèle, non versionné) : `docker-compose.webapp.override.yml` s'il existe : les labels Traefik et le
volume réel de l'override sont préservés. La 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, jeton... nano webapp.env # LIDAR_WEBAPP_HOST (labels), jeton...
./serve-webapp.sh # démarre (build au premier lancement) et attend le serveur ./serve-webapp.sh # démarre (build + up -d) et attend le serveur
``` ```
Sous-commandes : `stop`, `restart` (relit `webapp.env`), `status` (conteneur Sous-commandes : `stop` (compose down), `restart` (rebuild + recréation),
+ état du rebuild), `sync` (rebuild + vignettes), `logs`. Port hôte via `status` (conteneur + état du rebuild), `sync` (rebuild + vignettes),
`WEBAPP_PORT` dans `webapp.env` ou l'environnement (`WEBAPP_PORT=9000 `logs`. Le port hôte vient du mapping compose ; `WEBAPP_PORT` ne sert plus
./serve-webapp.sh`). qu'aux sondes locales quand le conteneur est arrêté.
**b. `run.sh`** — conteneur au premier plan (Ctrl-C arrête tout) : pratique **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` pour tester, les variables se passent depuis l'environnement hôte, `~/.ssh`
@ -244,12 +246,11 @@ Sur le Pi, après un `git pull` :
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
``` ```
Avec `serve-webapp.sh`, l'image est à reconstruire explicitement avant de Avec `serve-webapp.sh` (l'override Traefik est chargé automatiquement s'il
redémarrer (le script ne reconstruit qu'au premier lancement) : existe) :
```bash ```bash
docker build -f Dockerfile.webapp -t lidar-webapp . ./serve-webapp.sh restart # rebuild de l'image + recréation du conteneur
./serve-webapp.sh restart
``` ```
Avec `run.sh --serve-webapp`, l'image est reconstruite automatiquement à Avec `run.sh --serve-webapp`, l'image est reconstruite automatiquement à

View File

@ -205,19 +205,23 @@ VIZ_LEGENDS = {
# Couches activées par défaut à l'ouverture de la carte : seules celles-ci # Couches activées par défaut à l'ouverture de la carte : seules celles-ci
# sont allumées, toutes les autres sont désactivées (mais restent accessibles # sont allumées, toutes les autres sont désactivées (mais restent accessibles
# via le panneau). Ordre = pile basse → haute. # via le panneau). Ordre = pile basse → haute : une couche opaque (opacité 1)
DEFAULT_LAYERS = ('slope', 'aspect', 'positive_openness') # doit rester en bas pour ne pas masquer les autres.
DEFAULT_LAYERS = ('positive_openness', 'aspect')
# Opacité par défaut des couches allumées (0–1) ; les absentes restent à 1.
DEFAULT_OPACITY = {'aspect': 0.6}
# Couche « principale » (mise en avant ; index_tiles.json + affichage en # Couche « principale » (mise en avant ; index_tiles.json + affichage en
# direct de la webapp). # direct de la webapp).
DEFAULT_VIZ = DEFAULT_LAYERS[0] DEFAULT_VIZ = DEFAULT_LAYERS[0]
# Restriction du panneau de couches : seule la sélection DEFAULT_LAYERS est # Restriction du panneau de couches : seule la sélection ci-dessous est
# proposée (couches conservées ; les autres ne sont plus ni affichées ni # proposée (couches conservées ; les autres ne sont plus ni affichées ni
# référencées). None = toutes les visualisations présentes sur disque. # référencées). None = toutes les visualisations présentes sur disque.
# Le sélecteur de génération/régénération est piloté par le même registre # Le sélecteur de génération/régénération est piloté par le même registre
# (voir _render_html). # (voir _render_html). La pente y reste proposée mais éteinte par défaut.
PANEL_VIZ = DEFAULT_LAYERS PANEL_VIZ = ('slope', 'aspect', 'positive_openness')
# Correspondance mot-clé de fichier de sortie → nom d'étape --only du pipeline # Correspondance mot-clé de fichier de sortie → nom d'étape --only du pipeline
# (les trois visualisations dont le nom de sortie diffère du nom d'étape, # (les trois visualisations dont le nom de sortie diffère du nom d'étape,
@ -1118,6 +1122,7 @@ def build_index(output_dir, output_format='avif'):
'viz_meta': {k: {'label': VIZ_LABELS.get(k, k)} for k in ordered_viz}, 'viz_meta': {k: {'label': VIZ_LABELS.get(k, k)} for k in ordered_viz},
'stats': {'default_viz': default_viz, 'stats': {'default_viz': default_viz,
'default_layers': default_layers_present(all_viz_keys), 'default_layers': default_layers_present(all_viz_keys),
'default_opacity': dict(DEFAULT_OPACITY),
'n_tiles': len(display_tiles)}, 'n_tiles': len(display_tiles)},
}, ensure_ascii=False), encoding='utf-8') }, ensure_ascii=False), encoding='utf-8')
@ -1233,6 +1238,7 @@ def _render_html(tiles, global_bbox, all_viz_keys, output_format):
stats_json = _json_for_script( stats_json = _json_for_script(
{'default_viz': default_viz, {'default_viz': default_viz,
'default_layers': default_layers_present(all_viz_keys), 'default_layers': default_layers_present(all_viz_keys),
'default_opacity': dict(DEFAULT_OPACITY),
'n_tiles': len(tiles)}, 'n_tiles': len(tiles)},
) )
@ -1930,7 +1936,7 @@ _APP_JS = r"""'use strict';
// --- Données embarquées par index.py --- // --- Données embarquées par index.py ---
// TILES : tuiles affichables (corners WGS84, viz {thumb, full}, meta) // TILES : tuiles affichables (corners WGS84, viz {thumb, full}, meta)
// VIZ_META : {clé: {label}} des visualisations disponibles // VIZ_META : {clé: {label}} des visualisations disponibles
// STATS : {default_viz, n_tiles} // STATS : {default_viz, default_layers, default_opacity, n_tiles}
const viewport = document.getElementById('viewport'); const viewport = document.getElementById('viewport');
@ -1998,9 +2004,26 @@ function defaultLayers() {
? STATS.default_layers ? STATS.default_layers
: ((STATS.default_viz && VIZ_META[STATS.default_viz]) : ((STATS.default_viz && VIZ_META[STATS.default_viz])
? [STATS.default_viz] : [order[0]]); ? [STATS.default_viz] : [order[0]]);
const onSet = new Set(list.filter(k => VIZ_META[k])); const defOrder = list.filter(k => VIZ_META[k]);
// La pile respecte l'ordre de default_layers (bas → haut) : une couche
// opaque (opacité 1) doit rester sous les semi-transparentes.
for (let i = 1; i < defOrder.length; i++) {
const bi = order.indexOf(defOrder[i - 1]), ai = order.indexOf(defOrder[i]);
if (bi >= 0 && ai >= 0 && bi > ai) {
order.splice(bi, 1);
order.splice(order.indexOf(defOrder[i]), 0, defOrder[i - 1]);
}
}
// Opacités par défaut (STATS.default_opacity) ; absentes → 1.
const defOp = (STATS.default_opacity && typeof STATS.default_opacity === 'object')
? STATS.default_opacity : {};
const onSet = new Set(defOrder);
const on = {}, opacity = {}; const on = {}, opacity = {};
for (const k of order) { on[k] = onSet.has(k); opacity[k] = 1; } for (const k of order) {
on[k] = onSet.has(k);
const op = Number(defOp[k]);
opacity[k] = Number.isNaN(op) ? 1 : Math.min(1, Math.max(0, op));
}
return { order, on, opacity, osm: { on: true, opacity: 0.85, dark: true } }; return { order, on, opacity, osm: { on: true, opacity: 0.85, dark: true } };
} }

View File

@ -375,10 +375,17 @@ def test_subtiles_cover_all_layers(tmp_path):
def test_panel_restricted_to_kept_layers(): def test_panel_restricted_to_kept_layers():
"""Le panneau est restreint aux couches conservées (DEFAULT_LAYERS).""" """Le panneau est restreint aux couches conservées (PANEL_VIZ).
from lidar_pipeline.index import PANEL_VIZ, DEFAULT_LAYERS, KEYWORD_TO_STEP
Les couches allumées par défaut (DEFAULT_LAYERS) y figurent toutes — le
panneau peut en proposer davantage (ex. pente éteinte par défaut), et
chaque opacité par défaut vise une couche allumée.
"""
from lidar_pipeline.index import (PANEL_VIZ, DEFAULT_LAYERS, DEFAULT_OPACITY,
KEYWORD_TO_STEP)
from lidar_pipeline.pipeline import VIZ_STEPS from lidar_pipeline.pipeline import VIZ_STEPS
assert PANEL_VIZ == DEFAULT_LAYERS assert set(DEFAULT_LAYERS) <= set(PANEL_VIZ)
assert set(DEFAULT_OPACITY) <= set(DEFAULT_LAYERS)
# Chaque couche conservée correspond à une étape --only valide # Chaque couche conservée correspond à une étape --only valide
steps = {name for name, _ in VIZ_STEPS} steps = {name for name, _ in VIZ_STEPS}
for key in PANEL_VIZ: for key in PANEL_VIZ:

View File

@ -1054,7 +1054,12 @@ def _merge_remote_index():
path.write_text(json.dumps({ path.write_text(json.dumps({
"tiles": merged, "tiles": merged,
"viz_meta": viz_meta, "viz_meta": viz_meta,
"stats": remote.get("stats") or data.get("stats") or {}, # Les stats décrivent les réglages de l'INTERFACE (couches par
# défaut) : elles viennent du rebuild local, fait avec le code de
# cette webapp — celles du worker ne servent que de repli tant
# qu'aucun index local n'existe (sinon une machine mise à jour avant
# l'autre afficherait les vieux réglages du worker).
"stats": data.get("stats") or remote.get("stats") or {},
}, ensure_ascii=False), encoding="utf-8") }, ensure_ascii=False), encoding="utf-8")
# Coquille HTML : la liste embarquée doit être la même (fusionnée). Le # Coquille HTML : la liste embarquée doit être la même (fusionnée). Le
# JSON ne contient jamais de « ; » : la première séquence « ]; » après # JSON ne contient jamais de « ; » : la première séquence « ]; » après

View File

@ -1,35 +1,39 @@
#!/bin/bash #!/bin/bash
# Stack webapp LiDAR — gestion du conteneur carte interactive sur la machine # Stack webapp LiDAR — gestion de la stack compose sur la machine légère
# légère (Raspberry Pi). Cf. docs/DEPLOY_WEBAPP.md. # (Raspberry Pi). Cf. docs/DEPLOY_WEBAPP.md.
#
# Pilote docker compose (jamais docker run direct, qui ignorerait labels et
# volumes de l'override) :
# - docker-compose.webapp.yml (image légère Dockerfile.webapp, port 8973) ;
# - docker-compose.webapp.override.yml s'il existe (non versionné : labels
# Traefik, volume réel, environnement local) — chargé automatiquement :
# la route publique HTTPS et le cache de tuiles réels sont préservés.
# #
# Configuration : variables dans webapp.env (modèle : webapp.env.example, # Configuration : variables dans webapp.env (modèle : webapp.env.example,
# non versionné) ou exportées dans l'environnement : # non versionné) ou exportées — elles alimentent l'interpolation compose
# WEBAPP_PORT port hôte (défaut 8973) # (${LIDAR_WEBAPP_HOST} des labels Traefik, etc.) ; l'environnement du
# LIDAR_GENERATION_URL machine de traitement (ex. http://192.168.1.50:8973) # CONTENEUR vit dans les fichiers compose / override.
# LIDAR_REMOTE_TOKEN jeton si la machine exige LIDAR_API_TOKEN
# LIDAR_REGEN_CIDR réseau autorisé à lancer les générations
# (défaut 192.168.1.0/24, vide = restriction levée)
# #
# GPS sur téléphone (optionnel) : l'API Geolocation exige une connexion # HTTPS : terminé par Traefik (override). Sans Traefik, monter ./tls via un
# sécurisée. ./make-tls-cert.sh crée un certificat auto-signé dans ./tls ; # override local avec LIDAR_SSL_CERTFILE / LIDAR_SSL_KEYFILE (cf.
# dès qu'il est présent, ce script sert la carte en HTTPS (https://<hôte>:port) # make-tls-cert.sh) — ce script ne gère plus le certificat auto-signé.
# 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 (build + arrière-plan, redémarrage auto)
# ./serve-webapp.sh stop arrêter et supprimer le conteneur # ./serve-webapp.sh stop arrêter et supprimer la stack (cache conservé)
# ./serve-webapp.sh restart redémarrer (relit webapp.env) # ./serve-webapp.sh restart reconstruire l'image et recréer le conteneur
# ./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 un rebuild + 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
set -e set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
IMAGE="lidar-webapp"
CONTAINER="lidar-webapp" CONTAINER="lidar-webapp"
COMPOSE_BASE="docker-compose.webapp.yml"
COMPOSE_OVERRIDE="docker-compose.webapp.override.yml"
# Configuration locale (non versionnée) : webapp.env surcharge l'environnement # Configuration locale (non versionnée) : webapp.env surcharge l'environnement
# (sert à l'interpolation ${...} des fichiers compose).
if [ -f "${SCRIPT_DIR}/webapp.env" ]; then if [ -f "${SCRIPT_DIR}/webapp.env" ]; then
set -a set -a
# shellcheck disable=SC1091 # shellcheck disable=SC1091
@ -38,151 +42,109 @@ if [ -f "${SCRIPT_DIR}/webapp.env" ]; then
fi fi
WEBAPP_PORT="${WEBAPP_PORT:-8973}" WEBAPP_PORT="${WEBAPP_PORT:-8973}"
# HTTPS (requis pour la géolocalisation GPS depuis un téléphone) : l'API COMPOSE_FILES=(-f "${SCRIPT_DIR}/${COMPOSE_BASE}")
# Geolocation n'existe qu'en contexte sécurisé. Si le certificat auto-signé if [ -f "${SCRIPT_DIR}/${COMPOSE_OVERRIDE}" ]; then
# ./tls (cf. ./make-tls-cert.sh) est présent, la carte est servie en COMPOSE_FILES+=(-f "${SCRIPT_DIR}/${COMPOSE_OVERRIDE}")
# 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 fi
BASE_URL="${SCHEME}://127.0.0.1:${WEBAPP_PORT}"
CMD="${1:-start}" compose() {
docker compose "${COMPOSE_FILES[@]}" "$@"
}
build_image() { # Port hôte réellement publié par le conteneur (mapping compose) ; repli sur
echo "Build de l'image webapp (cache Docker)..." # WEBAPP_PORT tant que le conteneur est arrêté.
if ! docker build -f "${SCRIPT_DIR}/Dockerfile.webapp" -t "$IMAGE" "$SCRIPT_DIR" > /tmp/lidar_webapp_build.log 2>&1; then published_port() {
cat /tmp/lidar_webapp_build.log >&2 local p
echo "Échec du build (journal : /tmp/lidar_webapp_build.log)" >&2 p="$(docker port "$CONTAINER" 8973/tcp 2>/dev/null | head -1 | sed 's/.*://')"
exit 1 echo "${p:-$WEBAPP_PORT}"
fi
} }
lan_ip() { lan_ip() {
hostname -I 2> /dev/null | awk '{print $1}' hostname -I 2> /dev/null | awk '{print $1}'
} }
cmd_start() { wait_up() {
mkdir -p "${SCRIPT_DIR}/output" echo -n "Attente du serveur sur le port $1..."
if curl -fsS${KFLAG} "${BASE_URL}/api/status" > /dev/null 2>&1; then local up=0 i
echo "Le port ${WEBAPP_PORT} répond déjà (webapp déjà lancée ?)." >&2 for i in $(seq 1 30); do
echo "Arrêter l'existant, ou relancer avec un autre port : WEBAPP_PORT=9000 $0" >&2 if curl -fsS "http://127.0.0.1:$1/api/status" > /dev/null 2>&1; then
exit 1 up=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
# 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
# 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. LIDAR_MOUNT_SSH=0 pour ne PAS l'exposer au conteneur
# (durcissement : la webapp sert des requêtes réseau, inutile d'y donner
# une clé privée si le git pull distant n'est pas utilisé).
SSH_ARGS=()
if [ "${LIDAR_MOUNT_SSH:-1}" = "1" ] && [ -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" \
"${TLS_ARGS[@]}" \
"${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${KFLAG} "${BASE_URL}/api/status" > /dev/null 2>&1; then
UP=1
break break
fi fi
sleep 1 sleep 1
done done
if [ "$UP" != "1" ]; then if [ "$up" != "1" ]; then
echo " KO (voir ./serve-webapp.sh logs)" >&2 echo " KO (voir ./serve-webapp.sh logs)" >&2
exit 1 exit 1
fi fi
echo " OK" echo " OK"
}
cmd_start() {
compose up -d --build
wait_up "$(published_port)"
local port
port="$(published_port)"
echo "============================================" echo "============================================"
echo " Carte LiDAR — webapp (${CONTAINER})" echo " Carte LiDAR — webapp (${CONTAINER})"
echo "============================================" echo "============================================"
echo " URL : ${BASE_URL}/" echo " URL : http://127.0.0.1:${port}/"
if [ -n "$(lan_ip)" ]; then if [ -n "$(lan_ip)" ]; then
echo " URL (LAN) : ${SCHEME}://$(lan_ip):${WEBAPP_PORT}/" echo " URL (LAN) : http://$(lan_ip):${port}/"
fi fi
if [ "$SCHEME" = "https" ]; then if [ -f "${SCRIPT_DIR}/${COMPOSE_OVERRIDE}" ]; then
echo " HTTPS (GPS tél.) : actif — sur le téléphone, acceptez l'avertissement « certificat non fiable »" echo " Override compose : ${COMPOSE_OVERRIDE} chargé (Traefik/volume local)"
else
echo " Override compose : absent (HTTP direct, sans Traefik)"
fi fi
echo " Génération distante: ${LIDAR_GENERATION_URL:-non configurée}" echo " Génération distante: ${LIDAR_GENERATION_URL:-non configurée (override / webapp.env)}"
echo " Cache local tuiles : à la demande (LIDAR_GENERATION_URL)"
echo "============================================" echo "============================================"
} }
cmd_stop() { cmd_stop() {
docker rm -f "$CONTAINER" > /dev/null 2>&1 || true compose down
echo "Conteneur ${CONTAINER} arrêté." echo "Stack webapp arrêtée (conteneur ${CONTAINER} supprimé, cache conservé)."
}
cmd_restart() {
compose up -d --build --force-recreate
wait_up "$(published_port)"
echo "Stack webapp recréée (image reconstruite, relit webapp.env)."
} }
cmd_status() { cmd_status() {
if docker ps --format '{{.Names}}' 2> /dev/null | grep -qx "$CONTAINER"; then compose ps
echo "Conteneur : ${CONTAINER} en cours d'exécution ($(docker ps --filter name="^${CONTAINER}$" --format '{{.Status}}'))" local port
else port="$(published_port)"
echo "Conteneur : ${CONTAINER} arrêté" if curl -fsS "http://127.0.0.1:${port}/api/sync" 2> /dev/null; then
fi
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 http://127.0.0.1:${port}" >&2
fi fi
} }
cmd_sync() { cmd_sync() {
CODE="$(curl -s${KFLAG} -o /dev/null -w '%{http_code}' -X POST "${BASE_URL}/api/sync" || true)" local port code
case "$CODE" in port="$(published_port)"
code="$(curl -s -o /dev/null -w '%{http_code}' -X POST "http://127.0.0.1:${port}/api/sync" || true)"
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)" ;;
*) *)
echo "Échec : HTTP ${CODE:-pas de réponse} (webapp injoignable sur ${BASE_URL} ?)" >&2 echo "Échec : HTTP ${code:-pas de réponse} (webapp injoignable sur http://127.0.0.1:${port} ?)" >&2
exit 1 exit 1
;; ;;
esac esac
} }
CMD="${1:-start}"
case "$CMD" in case "$CMD" in
start) cmd_start ;; start) cmd_start ;;
stop) cmd_stop ;; stop) cmd_stop ;;
restart) cmd_stop; cmd_start ;; restart) cmd_restart ;;
status) cmd_status ;; status) cmd_status ;;
sync) cmd_sync ;; sync) cmd_sync ;;
logs) exec docker logs -f "$CONTAINER" ;; logs) compose logs -f webapp ;;
*) *)
echo "Usage : $0 [start|stop|restart|status|sync|logs]" >&2 echo "Usage : $0 [start|stop|restart|status|sync|logs]" >&2
exit 1 exit 1

View File

@ -2,7 +2,8 @@
# webapp.env est ignoré par git (jetons, chemins locaux). # webapp.env est ignoré par git (jetons, chemins locaux).
# Syntaxe shell simple : KEY=valeur (les valeurs avec espaces entre guillemets). # Syntaxe shell simple : KEY=valeur (les valeurs avec espaces entre guillemets).
# Port hôte de la carte (défaut 8973) # Port des sondes locales de serve-webapp.sh quand le conteneur est arrêté
# (le port publié vient du mapping docker-compose.webapp.yml ; défaut 8973)
#WEBAPP_PORT=8973 #WEBAPP_PORT=8973
# Machine de traitement (service worker de docker-compose.worker.yml). # Machine de traitement (service worker de docker-compose.worker.yml).
@ -31,12 +32,12 @@
# versionné) — cf. docker-compose.webapp.override.yml.example et # versionné) — cf. docker-compose.webapp.override.yml.example et
# docs/DEPLOY_WEBAPP.md. Rien à définir dans ce fichier. # docs/DEPLOY_WEBAPP.md. Rien à définir dans ce fichier.
# #
# b. Sans Traefik (chemin serve-webapp.sh) : certificat auto-signé. # b. Sans Traefik : certificat auto-signé monté via l'override compose
# local (docker-compose.webapp.override.yml) :
# 1. ./make-tls-cert.sh (crée tls/webapp.crt + tls/webapp.key) # 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 # 2. ajouter à l'override le montage ./tls:/app/tls:ro et les deux
# et passe ces variables automatiquement). Accepter l'avertissement # variables ci-dessous, puis ./serve-webapp.sh restart.
# « certificat non fiable » sur le téléphone. Ces lignes n'ont d'effet # Accepter l'avertissement « certificat non fiable » sur le téléphone :
# que si le certificat est stocké ailleurs que dans ./tls :
#LIDAR_SSL_CERTFILE=/app/tls/webapp.crt #LIDAR_SSL_CERTFILE=/app/tls/webapp.crt
#LIDAR_SSL_KEYFILE=/app/tls/webapp.key #LIDAR_SSL_KEYFILE=/app/tls/webapp.key