Préréglage carte par défaut et serve-webapp.sh compatible Traefik
This commit is contained in:
@ -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>`
|
||||||
|
|||||||
@ -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 à
|
||||||
|
|||||||
@ -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 } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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:
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
188
serve-webapp.sh
188
serve-webapp.sh
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user