Ajout du code initial ADS-B
20
.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Secrets - JAMAIS committer
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Sauvegardes et fichiers temporaires
|
||||||
|
*.bak
|
||||||
|
*.bak_*
|
||||||
|
*.bak-*
|
||||||
|
backups/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
tar1090-db/
|
||||||
|
adsb2pg/tar1090-db/
|
||||||
|
/v
|
||||||
|
webapp/v
|
||||||
|
docker-compose*.bak*
|
||||||
|
docker-compose_*_DoNotWork.yml
|
||||||
33
adsb-stack.service
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Stack ADS-B (readsb + tar1090 + adsb2pg)
|
||||||
|
Requires=docker.service
|
||||||
|
After=docker.service network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
RemainAfterExit=yes
|
||||||
|
WorkingDirectory=/data/adsb
|
||||||
|
|
||||||
|
# Laisse le bus USB s'énumérer avant de détecter les dongles
|
||||||
|
ExecStartPre=/bin/sleep 15
|
||||||
|
|
||||||
|
# Recalcule .env.receivers (RX<n>_SERIAL + COMPOSE_PROFILES) depuis le
|
||||||
|
# matériel réellement présent. C'est ce qui rend le stack adaptatif :
|
||||||
|
# 1, 2 ou 3 dongles démarrent les profiles rx1..rx3 correspondants, et un
|
||||||
|
# dongle débranché ne laisse pas tourner un readsb à vide.
|
||||||
|
ExecStartPre=/data/adsb/scripts/gen_env.sh
|
||||||
|
|
||||||
|
# Les deux --env-file sont nécessaires :
|
||||||
|
# .env → PG_PASSWORD (secret, non versionné)
|
||||||
|
# .env.receivers → COMPOSE_PROFILES et RX<n>_SERIAL (généré)
|
||||||
|
# Dès qu'on passe --env-file, Compose cesse de charger .env automatiquement,
|
||||||
|
# d'où l'obligation de le déclarer explicitement lui aussi.
|
||||||
|
ExecStart=/usr/bin/docker compose --env-file /data/adsb/.env --env-file /data/adsb/.env.receivers up -d --remove-orphans
|
||||||
|
|
||||||
|
ExecStop=/usr/bin/docker compose --env-file /data/adsb/.env --env-file /data/adsb/.env.receivers down
|
||||||
|
|
||||||
|
TimeoutStartSec=180
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
7
adsb2pg/Dockerfile
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
COPY adsb2pg.py .
|
||||||
|
COPY tar1090-db /tar1090-db
|
||||||
|
CMD ["python", "-u", "adsb2pg.py"]
|
||||||
BIN
adsb2pg/adsb2pg.py
Normal file
2
adsb2pg/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
requests==2.32.3
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
97
apply_tuning.sh
Executable file
@ -0,0 +1,97 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# Application du tuning postgresql.conf -- Stack ADS-B Linux-25
|
||||||
|
# Hardware : i7-7567U, 4 threads logiques, 16 Go RAM, SSD Samsung
|
||||||
|
# Conteneur : wordpress-postgres (postgis/postgis:16-3.5)
|
||||||
|
# Fichier de config : volume pg-conf-std, monté /etc/postgresql
|
||||||
|
# ============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONTAINER="wordpress-postgres"
|
||||||
|
CONF_PATH_IN_CONTAINER="/etc/postgresql/postgresql.conf"
|
||||||
|
BACKUP_DIR="/backup/postgres_tuning_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
|
echo "==> 1. Sauvegarde du postgresql.conf actuel"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
docker cp "${CONTAINER}:${CONF_PATH_IN_CONTAINER}" "$BACKUP_DIR/postgresql.conf.bak"
|
||||||
|
echo " Sauvegardé : $BACKUP_DIR/postgresql.conf.bak"
|
||||||
|
|
||||||
|
echo "==> 2. Application des nouveaux paramètres"
|
||||||
|
# On utilise sed dans le conteneur pour modifier les lignes existantes,
|
||||||
|
# et on ajoute les nouvelles directives en fin de fichier (section CUSTOMIZED
|
||||||
|
# OPTIONS déjà présente, cohérent avec les ajouts précédents JCA 2026-06-29)
|
||||||
|
|
||||||
|
docker exec "$CONTAINER" bash -c "cat >> ${CONF_PATH_IN_CONTAINER} << 'EOF'
|
||||||
|
|
||||||
|
# ── Tuning mémoire/IO/planner (Claude, $(date +%Y-%m-%d)) ──
|
||||||
|
# Hardware : i7-7567U 4 threads logiques, 16 Go RAM, SSD Samsung
|
||||||
|
# Remplace les valeurs précédentes (shared_buffers=512MB, work_mem=256MB)
|
||||||
|
# par des valeurs adaptées à la RAM disponible et au risque d'OOM.
|
||||||
|
shared_buffers = 3200MB
|
||||||
|
effective_cache_size = 9GB
|
||||||
|
work_mem = 32MB
|
||||||
|
maintenance_work_mem = 512MB
|
||||||
|
max_wal_size = 4GB
|
||||||
|
min_wal_size = 1GB
|
||||||
|
checkpoint_completion_target = 0.9
|
||||||
|
random_page_cost = 1.1
|
||||||
|
effective_io_concurrency = 150
|
||||||
|
max_parallel_workers_per_gather = 2
|
||||||
|
autovacuum_analyze_scale_factor = 0.05
|
||||||
|
autovacuum_vacuum_cost_delay = 2ms
|
||||||
|
EOF"
|
||||||
|
|
||||||
|
echo "==> 3. Vérification de la syntaxe (parse à blanc)"
|
||||||
|
# postgres --check-config n'existe pas nativement ; on vérifie via un restart
|
||||||
|
# contrôlé avec rollback possible si échec.
|
||||||
|
|
||||||
|
echo "==> 4. Redémarrage du conteneur (shared_buffers nécessite un restart complet)"
|
||||||
|
cd /root/wordpress
|
||||||
|
docker compose restart postgres
|
||||||
|
|
||||||
|
echo "==> 5. Attente du démarrage"
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if docker exec "$CONTAINER" pg_isready -U pgz_admin -d wpz_postgres > /dev/null 2>&1; then
|
||||||
|
echo " PostgreSQL prêt après ${i}s"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$i" -eq 30 ]; then
|
||||||
|
echo " ÉCHEC : PostgreSQL ne démarre pas avec la nouvelle config."
|
||||||
|
echo " Rollback automatique..."
|
||||||
|
docker cp "$BACKUP_DIR/postgresql.conf.bak" "${CONTAINER}:${CONF_PATH_IN_CONTAINER}"
|
||||||
|
docker compose restart postgres
|
||||||
|
echo " Rollback effectué. Vérifier les logs : docker logs ${CONTAINER} --tail 50"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> 6. Vérification des paramètres effectivement appliqués"
|
||||||
|
docker exec "$CONTAINER" psql -U pgz_admin -d wpz_postgres -c "
|
||||||
|
SELECT name, setting, unit
|
||||||
|
FROM pg_settings
|
||||||
|
WHERE name IN (
|
||||||
|
'shared_buffers', 'effective_cache_size', 'work_mem', 'maintenance_work_mem',
|
||||||
|
'max_wal_size', 'min_wal_size', 'random_page_cost', 'effective_io_concurrency',
|
||||||
|
'max_parallel_workers_per_gather'
|
||||||
|
)
|
||||||
|
ORDER BY name;
|
||||||
|
"
|
||||||
|
|
||||||
|
echo "==> 7. Vérification que les données et services sont intacts"
|
||||||
|
docker exec "$CONTAINER" psql -U pgz_admin -d wpz_postgres -c \
|
||||||
|
"SELECT count(*) AS partitions FROM pg_inherits WHERE inhparent = 'adsb.positions'::regclass;"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Si adsb2pg/webapp tournaient déjà, ils n'ont pas besoin d'être redémarrés"
|
||||||
|
echo "(ils se reconnectent automatiquement à PostgreSQL). Vérification :"
|
||||||
|
docker exec "$CONTAINER" psql -U pgz_admin -d wpz_postgres -c \
|
||||||
|
"SELECT max(ts) AS derniere_insertion FROM adsb.positions WHERE ts >= NOW() - INTERVAL '5 minutes';"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "Sauvegarde de l'ancienne config : $BACKUP_DIR/postgresql.conf.bak"
|
||||||
|
echo "Rollback manuel si besoin plus tard :"
|
||||||
|
echo " docker cp $BACKUP_DIR/postgresql.conf.bak ${CONTAINER}:${CONF_PATH_IN_CONTAINER}"
|
||||||
|
echo " cd /root/wordpress && docker compose restart postgres"
|
||||||
|
echo "============================================================"
|
||||||
1332
check_adsb.sh
Executable file
399
check_adsb_claude_ok_v2.sh
Executable file
@ -0,0 +1,399 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# check_adsb.sh — Vérification complète du stack ADS-B Linux-25
|
||||||
|
# Version 3.0 — JCA 2026 — sortie terminal + HTML
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
HTML_OUT="${1:-/var/www/html/status.html}" # chemin HTML (modifiable en arg)
|
||||||
|
|
||||||
|
# ── Couleurs terminal ──────────────────────────────────────────────────────────
|
||||||
|
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'; BLUE='\033[1;34m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
|
||||||
|
ERRORS=0; WARNS=0
|
||||||
|
NOW=$(date '+%Y-%m-%dT%H:%M:%S')
|
||||||
|
NOW_DISP=$(date '+%a %d/%m/%Y %H:%M:%S')
|
||||||
|
IP=$(ip -4 addr show wlp58s0 2>/dev/null | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' | head -1)
|
||||||
|
[ -z "$IP" ] && IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
# ── Buffer HTML ────────────────────────────────────────────────────────────────
|
||||||
|
HTML=""
|
||||||
|
HTML_ROWS="" # lignes de détail
|
||||||
|
|
||||||
|
# ── Fonctions terminal ─────────────────────────────────────────────────────────
|
||||||
|
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||||
|
fail() { echo -e " ${RED}✗${NC} $1"; ERRORS=$((ERRORS+1)); }
|
||||||
|
warn() { echo -e " ${YELLOW}⚠${NC} $1"; WARNS=$((WARNS+1)); }
|
||||||
|
info() { echo -e " ${CYAN}→${NC} $1"; }
|
||||||
|
hdr() { echo -e "\n${BLUE}${BOLD}═══ $1 ═══${NC}"; }
|
||||||
|
|
||||||
|
# ── Fonctions HTML ─────────────────────────────────────────────────────────────
|
||||||
|
h_section() { HTML_ROWS+="<tr class='section-row'><td colspan='3'><b>$1</b></td></tr>"; }
|
||||||
|
h_ok() { HTML_ROWS+="<tr><td class='ic ok'>✓</td><td>$1</td><td class='tag ok'>OK</td></tr>"; }
|
||||||
|
h_fail() { HTML_ROWS+="<tr><td class='ic err'>✗</td><td>$1</td><td class='tag err'>ERREUR</td></tr>"; }
|
||||||
|
h_warn() { HTML_ROWS+="<tr><td class='ic warn'>⚠</td><td>$1</td><td class='tag warn'>ATTENTION</td></tr>"; }
|
||||||
|
h_info() { HTML_ROWS+="<tr><td class='ic inf'>→</td><td colspan='2' class='info-cell'>$1</td></tr>"; }
|
||||||
|
|
||||||
|
# ── Wrapper : terminal + HTML simultanément ───────────────────────────────────
|
||||||
|
OK() { ok "$1"; h_ok "$1"; }
|
||||||
|
FAIL() { fail "$1"; h_fail "$1"; }
|
||||||
|
WARN() { warn "$1"; h_warn "$1"; }
|
||||||
|
INFO() { info "$1"; h_info "$1"; }
|
||||||
|
HDR() { hdr "$1"; h_section "▸ $1"; }
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# VÉRIFICATIONS
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ Stack ADS-B Linux-25 — Vérification complète ║${NC}"
|
||||||
|
echo -e "${BOLD}║ $NOW_DISP ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
INFO "IP : ${IP:-inconnue} — $NOW_DISP"
|
||||||
|
|
||||||
|
# ── 1. CONTENEURS ─────────────────────────────────────────────────────────────
|
||||||
|
HDR "1. Conteneurs Docker"
|
||||||
|
for ctn in adsb-readsb adsb-tar1090 adsb-adsb2pg adsb-webapp wordpress-postgres; do
|
||||||
|
STATUS=$(docker inspect --format '{{.State.Status}}' "$ctn" 2>/dev/null)
|
||||||
|
HEALTH=$(docker inspect --format '{{.State.Health.Status}}' "$ctn" 2>/dev/null)
|
||||||
|
UPTIME=$(docker inspect --format '{{.State.StartedAt}}' "$ctn" 2>/dev/null | \
|
||||||
|
xargs -I{} date -d {} '+%d/%m %H:%M' 2>/dev/null)
|
||||||
|
LABEL="$ctn"
|
||||||
|
[ -n "$HEALTH" ] && [ "$HEALTH" != "<nil>" ] && LABEL="$ctn [$HEALTH]"
|
||||||
|
if [ "$STATUS" = "running" ]; then
|
||||||
|
[ "$HEALTH" = "unhealthy" ] && WARN "$LABEL — démarré $UPTIME" \
|
||||||
|
|| OK "$LABEL — démarré $UPTIME"
|
||||||
|
else
|
||||||
|
FAIL "$ctn — STATUS=${STATUS:-absent}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 2. DONGLE RTL-SDR ─────────────────────────────────────────────────────────
|
||||||
|
HDR "2. Dongle RTL-SDR"
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "SN 00000010"; then
|
||||||
|
GAIN=$(docker logs adsb-readsb 2>&1 | grep "tuner gain set to" | tail -1 | grep -oP '[\d.]+(?= dB)')
|
||||||
|
OK "AirNav FlightStick SN:00000010 détecté — gain ${GAIN:-?} dB"
|
||||||
|
else
|
||||||
|
FAIL "Dongle SN:00000010 non détecté dans les logs readsb"
|
||||||
|
fi
|
||||||
|
ls /dev/adsb_dongle 2>/dev/null && OK "Symlink /dev/adsb_dongle présent" \
|
||||||
|
|| WARN "Symlink /dev/adsb_dongle absent"
|
||||||
|
|
||||||
|
# ── 3. DONNÉES ADS-B ──────────────────────────────────────────────────────────
|
||||||
|
HDR "3. Données ADS-B"
|
||||||
|
AC_JSON=$(curl -s --max-time 5 "http://127.0.0.1:8090/data/aircraft.json" 2>/dev/null)
|
||||||
|
if [ -n "$AC_JSON" ]; then
|
||||||
|
MSGS=$(echo "$AC_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('messages',0))" 2>/dev/null)
|
||||||
|
AVIONS=$(echo "$AC_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('aircraft',[])))" 2>/dev/null)
|
||||||
|
WITH_POS=$(echo "$AC_JSON" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
print(sum(1 for a in d.get('aircraft',[]) if a.get('lat') and a.get('lon')))" 2>/dev/null)
|
||||||
|
[ "${MSGS:-0}" -gt 0 ] 2>/dev/null \
|
||||||
|
&& OK "tar1090 actif — $(printf '%s' "$MSGS" | sed ':a;s/\B[0-9]\{3\}\>/,&/;ta') messages décodés" \
|
||||||
|
|| WARN "tar1090 répond — 0 message (démarrage récent ?)"
|
||||||
|
OK "$AVIONS avion(s) en vue dont $WITH_POS avec position GPS"
|
||||||
|
else
|
||||||
|
FAIL "tar1090 ne répond pas sur :8090/data/aircraft.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
VOL_PATH=$(docker inspect adsb_readsb-run 2>/dev/null | \
|
||||||
|
python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['Mountpoint'])" 2>/dev/null)
|
||||||
|
if [ -n "$VOL_PATH" ] && [ -f "$VOL_PATH/aircraft.pb" ]; then
|
||||||
|
AGE=$(( $(date +%s) - $(stat -c %Y "$VOL_PATH/aircraft.pb") ))
|
||||||
|
[ "$AGE" -lt 10 ] && OK "Volume readsb-run : aircraft.pb frais (${AGE}s)" \
|
||||||
|
|| WARN "Volume readsb-run : aircraft.pb âgé de ${AGE}s"
|
||||||
|
else
|
||||||
|
WARN "Volume readsb-run : aircraft.pb introuvable (mode host normal)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. PORTS RÉSEAU ───────────────────────────────────────────────────────────
|
||||||
|
HDR "4. Ports réseau"
|
||||||
|
declare -A PORT_LABELS=(
|
||||||
|
["8080"]="webapp PHP" ["8090"]="tar1090 nginx"
|
||||||
|
["30005"]="Beast TCP" ["5432"]="PostgreSQL"
|
||||||
|
["30002"]="RAW TCP" ["30003"]="SBS TCP"
|
||||||
|
)
|
||||||
|
for port in 8080 8090 30005 5432 30002 30003; do
|
||||||
|
ss -tlnp 2>/dev/null | grep -q ":${port}[[:space:]]" \
|
||||||
|
&& OK "Port $port en écoute — ${PORT_LABELS[$port]}" \
|
||||||
|
|| FAIL "Port $port absent — ${PORT_LABELS[$port]}"
|
||||||
|
done
|
||||||
|
|
||||||
|
READSB_IP=$(docker inspect adsb-readsb 2>/dev/null | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)[0]
|
||||||
|
for n,v in d['NetworkSettings']['Networks'].items():
|
||||||
|
if 'adsb' in n: print(v['IPAddress'])" 2>/dev/null)
|
||||||
|
[ -n "$READSB_IP" ] && [ "$READSB_IP" != "None" ] \
|
||||||
|
&& OK "readsb attaché à adsb-net : $READSB_IP" \
|
||||||
|
|| FAIL "readsb non attaché à adsb-net (Beast TCP impossible)"
|
||||||
|
|
||||||
|
FAIL_CNT=$(docker logs adsb-tar1090 2>&1 | grep "Beast TCP input.*failed" | \
|
||||||
|
tail -1 | grep -oP '\d+(?= times)' || echo 0)
|
||||||
|
[ "${FAIL_CNT:-0}" -gt 50 ] 2>/dev/null \
|
||||||
|
&& FAIL "Beast TCP tar1090→readsb : $FAIL_CNT échecs" \
|
||||||
|
|| OK "Connexion Beast tar1090→readsb OK"
|
||||||
|
|
||||||
|
# ── 5. POSTGRESQL ─────────────────────────────────────────────────────────────
|
||||||
|
HDR "5. PostgreSQL — schéma adsb"
|
||||||
|
PG="docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c"
|
||||||
|
|
||||||
|
PG_VER=$($PG "SELECT version();" 2>/dev/null | head -1 | xargs)
|
||||||
|
[ -n "$PG_VER" ] && OK "PostgreSQL accessible — ${PG_VER:0:55}" || FAIL "PostgreSQL inaccessible"
|
||||||
|
|
||||||
|
PART_POS=$($PG "SELECT COUNT(*) FROM pg_class c
|
||||||
|
JOIN pg_namespace n ON n.oid=c.relnamespace
|
||||||
|
WHERE n.nspname='adsb' AND c.relname LIKE 'positions_2%' AND c.relkind='r';" 2>/dev/null | xargs)
|
||||||
|
OK "${PART_POS:-0} sous-partitions adsb.positions"
|
||||||
|
|
||||||
|
PART_HIS=$($PG "SELECT COUNT(*) FROM pg_class c
|
||||||
|
JOIN pg_namespace n ON n.oid=c.relnamespace
|
||||||
|
WHERE n.nspname='adsb' AND c.relname LIKE 'aircraft_history_%' AND c.relkind='r';" 2>/dev/null | xargs)
|
||||||
|
OK "${PART_HIS:-0} sous-partitions adsb.aircraft_history"
|
||||||
|
|
||||||
|
POS_5MIN=$($PG "SELECT COUNT(*) FROM adsb.positions
|
||||||
|
WHERE ts >= NOW() - INTERVAL '5 minutes';" 2>/dev/null | xargs)
|
||||||
|
[ "${POS_5MIN:-0}" -gt 0 ] 2>/dev/null \
|
||||||
|
&& OK "$POS_5MIN positions insérées dans les 5 dernières minutes" \
|
||||||
|
|| WARN "Aucune position dans les 5 dernières minutes"
|
||||||
|
|
||||||
|
PCT_DESC=$($PG "SELECT ROUND(COUNT(aircraft_desc)*100.0/NULLIF(COUNT(*),0),1)
|
||||||
|
FROM adsb.positions WHERE ts >= NOW() - INTERVAL '30 minutes'
|
||||||
|
AND lat IS NOT NULL;" 2>/dev/null | xargs)
|
||||||
|
python3 -c "exit(0 if float('${PCT_DESC:-0}') >= 80 else 1)" 2>/dev/null \
|
||||||
|
&& OK "aircraft_desc rempli à ${PCT_DESC}% sur 30 min" \
|
||||||
|
|| WARN "aircraft_desc rempli à ${PCT_DESC:-0}% (< 80%)"
|
||||||
|
|
||||||
|
AV_TODAY=$($PG "SELECT COUNT(DISTINCT icao) FROM adsb.aircraft_history
|
||||||
|
WHERE session_start >= CURRENT_DATE;" 2>/dev/null | xargs)
|
||||||
|
SESS_TODAY=$($PG "SELECT COUNT(*) FROM adsb.aircraft_history
|
||||||
|
WHERE session_start >= CURRENT_DATE;" 2>/dev/null | xargs)
|
||||||
|
INFO "Aujourd'hui : ${AV_TODAY:-0} avions distincts, ${SESS_TODAY:-0} sessions"
|
||||||
|
|
||||||
|
DB_SIZE=$($PG "SELECT pg_size_pretty(SUM(pg_total_relation_size(c.oid)))
|
||||||
|
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
||||||
|
WHERE n.nspname='adsb';" 2>/dev/null | xargs)
|
||||||
|
INFO "Taille totale schéma adsb : ${DB_SIZE:-?}"
|
||||||
|
|
||||||
|
# ── 6. ADSB2PG ────────────────────────────────────────────────────────────────
|
||||||
|
HDR "6. adsb2pg — collecteur"
|
||||||
|
LAST_INS=$(docker logs adsb-adsb2pg 2>&1 | grep "positions insérées" | tail -1)
|
||||||
|
if [ -n "$LAST_INS" ]; then
|
||||||
|
OK "adsb2pg actif"
|
||||||
|
INFO "$LAST_INS"
|
||||||
|
LOG_TIME=$(echo "$LAST_INS" | grep -oP '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}')
|
||||||
|
AGE_LOG=$(( $(date +%s) - $(date -d "${LOG_TIME:-0}" +%s 2>/dev/null || echo $(date +%s)) ))
|
||||||
|
[ "$AGE_LOG" -lt 30 ] \
|
||||||
|
&& OK "Dernière insertion il y a ${AGE_LOG}s" \
|
||||||
|
|| WARN "Dernière insertion il y a ${AGE_LOG}s (> 30s — cycle bloqué ?)"
|
||||||
|
else
|
||||||
|
WARN "Aucun log d'insertion trouvé"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 7. WEBAPP ─────────────────────────────────────────────────────────────────
|
||||||
|
HDR "7. Webapp — interface utilisateur"
|
||||||
|
for action in live kpi trajectories; do
|
||||||
|
RESP=$(curl -s --max-time 5 \
|
||||||
|
"http://127.0.0.1:8080/api.php?action=${action}&period=24&granularity=60" 2>/dev/null)
|
||||||
|
if echo "$RESP" | python3 -c "import json,sys; json.load(sys.stdin)" 2>/dev/null; then
|
||||||
|
ERR=$(echo "$RESP" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
print(d.get('error',''))" 2>/dev/null)
|
||||||
|
[ -n "$ERR" ] && WARN "api.php?action=${action} : $ERR" \
|
||||||
|
|| OK "api.php?action=${action} : JSON valide"
|
||||||
|
else
|
||||||
|
FAIL "api.php?action=${action} : réponse non-JSON"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
TAB_CNT=$(docker exec adsb-webapp grep -c 'href="#tab' /var/www/html/index.php 2>/dev/null)
|
||||||
|
OK "${TAB_CNT:-?} onglets dans index.php"
|
||||||
|
[ "${TAB_CNT:-0}" -lt 5 ] 2>/dev/null && WARN "Moins de 5 onglets — index.php peut être obsolète"
|
||||||
|
|
||||||
|
JS_SZ=$(docker exec adsb-webapp wc -c /var/www/html/js/app.js 2>/dev/null | awk '{print $1}')
|
||||||
|
INFO "app.js : ${JS_SZ:-?} octets"
|
||||||
|
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://${IP}:8080/" 2>/dev/null)
|
||||||
|
[ "$HTTP_CODE" = "200" ] \
|
||||||
|
&& OK "Accès LAN http://${IP}:8080 : HTTP $HTTP_CODE" \
|
||||||
|
|| WARN "Accès LAN http://${IP}:8080 : HTTP ${HTTP_CODE:-?}"
|
||||||
|
|
||||||
|
# ── 8. AUTOGAIN ───────────────────────────────────────────────────────────────
|
||||||
|
HDR "8. Autogain"
|
||||||
|
CUR_GAIN=$(docker logs adsb-readsb 2>&1 | grep "tuner gain set to" | tail -1 | \
|
||||||
|
grep -oP '[\d.]+(?= dB)')
|
||||||
|
AG_STATE=$(docker logs adsb-readsb 2>&1 | grep "autogain" | \
|
||||||
|
grep -oP "(?<=state ')[\w]+" | tail -1)
|
||||||
|
[ -n "$CUR_GAIN" ] \
|
||||||
|
&& OK "Gain : ${CUR_GAIN} dB — état autogain : ${AG_STATE:-inconnu}" \
|
||||||
|
|| WARN "Gain courant inconnu"
|
||||||
|
|
||||||
|
INSUF=$(docker logs adsb-readsb 2>&1 | grep -c "Insufficient messages")
|
||||||
|
[ "$INSUF" -gt 3 ] \
|
||||||
|
&& WARN "$INSUF cycles avec messages insuffisants (antenne/portée ?)" \
|
||||||
|
|| INFO "$INSUF cycle(s) avec messages insuffisants (autogain en cours)"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# RÉSUMÉ TERMINAL
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ RÉSUMÉ ║${NC}"
|
||||||
|
echo -e "${BOLD}╠══════════════════════════════════════════════════════╣${NC}"
|
||||||
|
if [ "$ERRORS" -eq 0 ] && [ "$WARNS" -eq 0 ]; then
|
||||||
|
GLOBAL_STATUS="OK"; GLOBAL_COLOR="$GREEN"
|
||||||
|
echo -e "${BOLD}║ ${GREEN}✓ Stack opérationnel — aucune anomalie${NC}${BOLD} ║${NC}"
|
||||||
|
elif [ "$ERRORS" -eq 0 ]; then
|
||||||
|
GLOBAL_STATUS="WARN"; GLOBAL_COLOR="$YELLOW"
|
||||||
|
echo -e "${BOLD}║ ${YELLOW}⚠ Stack opérationnel — $WARNS avertissement(s)${NC}${BOLD} ║${NC}"
|
||||||
|
else
|
||||||
|
GLOBAL_STATUS="ERROR"; GLOBAL_COLOR="$RED"
|
||||||
|
echo -e "${BOLD}║ ${RED}✗ $ERRORS erreur(s), $WARNS avertissement(s)${NC}${BOLD} ║${NC}"
|
||||||
|
fi
|
||||||
|
echo -e "${BOLD}║ Dashboard : http://${IP}:8080${NC}${BOLD} ║${NC}"
|
||||||
|
echo -e "${BOLD}║ tar1090 : http://${IP}:8090${NC}${BOLD} ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# GÉNÉRATION HTML
|
||||||
|
# ══════════════════════════════════════════════════════════════════════════════
|
||||||
|
case "$GLOBAL_STATUS" in
|
||||||
|
OK) BADGE_CLS="badge-ok"; BADGE_TXT="✓ Opérationnel" ;;
|
||||||
|
WARN) BADGE_CLS="badge-warn"; BADGE_TXT="⚠ ${WARNS} avertissement(s)" ;;
|
||||||
|
ERROR) BADGE_CLS="badge-err"; BADGE_TXT="✗ ${ERRORS} erreur(s)" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
cat > "$HTML_OUT" << HTMLEOF
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<meta http-equiv="refresh" content="60">
|
||||||
|
<title>ADS-B Linux-25 — Status</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg:#0d1117; --surface:#161b22; --border:#30363d;
|
||||||
|
--text:#c9d1d9; --muted:#8b949e; --ok:#3fb950;
|
||||||
|
--warn:#d29922; --err:#f85149; --info:#58a6ff;
|
||||||
|
--blue:#1f6feb; --teal:#39d353;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',Arial,sans-serif;
|
||||||
|
font-size:14px;padding:20px}
|
||||||
|
h1{color:var(--info);font-size:1.4em;margin-bottom:4px}
|
||||||
|
.subtitle{color:var(--muted);font-size:.85em;margin-bottom:20px}
|
||||||
|
.header{display:flex;justify-content:space-between;align-items:flex-start;
|
||||||
|
flex-wrap:wrap;gap:12px;margin-bottom:24px;
|
||||||
|
padding:16px;background:var(--surface);border-radius:8px;
|
||||||
|
border:1px solid var(--border)}
|
||||||
|
.header-left h1{margin:0}
|
||||||
|
.kpi-row{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:24px}
|
||||||
|
.kpi{background:var(--surface);border:1px solid var(--border);border-radius:8px;
|
||||||
|
padding:12px 18px;min-width:130px;text-align:center}
|
||||||
|
.kpi .val{font-size:1.8em;font-weight:700;color:var(--info)}
|
||||||
|
.kpi .lbl{font-size:.75em;color:var(--muted);margin-top:2px}
|
||||||
|
.kpi.ok-kpi .val{color:var(--ok)}
|
||||||
|
.kpi.warn-kpi .val{color:var(--warn)}
|
||||||
|
.kpi.err-kpi .val{color:var(--err)}
|
||||||
|
.badge{display:inline-block;padding:6px 16px;border-radius:20px;
|
||||||
|
font-weight:700;font-size:.95em}
|
||||||
|
.badge-ok {background:rgba(63,185,80,.15);color:var(--ok);border:1px solid var(--ok)}
|
||||||
|
.badge-warn{background:rgba(210,153,34,.15);color:var(--warn);border:1px solid var(--warn)}
|
||||||
|
.badge-err {background:rgba(248,81,73,.15);color:var(--err);border:1px solid var(--err)}
|
||||||
|
.links{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:20px}
|
||||||
|
.links a{background:var(--blue);color:#fff;padding:6px 14px;border-radius:6px;
|
||||||
|
text-decoration:none;font-size:.85em}
|
||||||
|
.links a:hover{opacity:.85}
|
||||||
|
table{width:100%;border-collapse:collapse;background:var(--surface);
|
||||||
|
border:1px solid var(--border);border-radius:8px;overflow:hidden}
|
||||||
|
th{background:#1c2128;color:var(--muted);font-weight:600;text-align:left;
|
||||||
|
padding:8px 12px;font-size:.8em;text-transform:uppercase;letter-spacing:.5px}
|
||||||
|
td{padding:7px 12px;border-top:1px solid var(--border);vertical-align:middle}
|
||||||
|
tr:hover td{background:rgba(255,255,255,.03)}
|
||||||
|
.section-row td{background:#1c2128;color:var(--info);font-size:.82em;
|
||||||
|
text-transform:uppercase;letter-spacing:.8px;padding:6px 12px}
|
||||||
|
.ic{width:28px;text-align:center;font-size:1.1em}
|
||||||
|
.ic.ok {color:var(--ok)}
|
||||||
|
.ic.err {color:var(--err)}
|
||||||
|
.ic.warn{color:var(--warn)}
|
||||||
|
.ic.inf {color:var(--info)}
|
||||||
|
.tag{width:90px;text-align:center}
|
||||||
|
.tag{font-size:.75em;font-weight:700;padding:2px 8px;border-radius:12px}
|
||||||
|
.tag.ok {background:rgba(63,185,80,.15);color:var(--ok)}
|
||||||
|
.tag.err {background:rgba(248,81,73,.15);color:var(--err)}
|
||||||
|
.tag.warn{background:rgba(210,153,34,.15);color:var(--warn)}
|
||||||
|
.info-cell{color:var(--muted);font-size:.88em;font-style:italic}
|
||||||
|
.footer{margin-top:16px;color:var(--muted);font-size:.78em;text-align:right}
|
||||||
|
@media(max-width:600px){.kpi{min-width:100px}.header{flex-direction:column}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h1>✈ ADS-B Linux-25 — Status</h1>
|
||||||
|
<div class="subtitle">Généré le $NOW_DISP — actualisation auto toutes les 60s</div>
|
||||||
|
<div style="margin-top:8px"><span class="badge ${BADGE_CLS}">${BADGE_TXT}</span></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:.85em;color:var(--muted)">IP : <b style="color:var(--text)">${IP}</b></div>
|
||||||
|
<div style="font-size:.85em;color:var(--muted);margin-top:4px">
|
||||||
|
Erreurs : <b style="color:var(--err)">${ERRORS}</b>
|
||||||
|
Avert. : <b style="color:var(--warn)">${WARNS}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="kpi-row">
|
||||||
|
<div class="kpi $([ "${AVIONS:-0}" -gt 0 ] 2>/dev/null && echo ok-kpi || echo warn-kpi)">
|
||||||
|
<div class="val">${AVIONS:-0}</div><div class="lbl">Avions en vue</div></div>
|
||||||
|
<div class="kpi $([ "${WITH_POS:-0}" -gt 0 ] 2>/dev/null && echo ok-kpi || echo warn-kpi)">
|
||||||
|
<div class="val">${WITH_POS:-0}</div><div class="lbl">Avec GPS</div></div>
|
||||||
|
<div class="kpi">
|
||||||
|
<div class="val" style="color:var(--teal)">${AV_TODAY:-0}</div>
|
||||||
|
<div class="lbl">Avions aujourd'hui</div></div>
|
||||||
|
<div class="kpi">
|
||||||
|
<div class="val">${SESS_TODAY:-0}</div><div class="lbl">Sessions aujourd'hui</div></div>
|
||||||
|
<div class="kpi">
|
||||||
|
<div class="val" style="font-size:1.2em">${DB_SIZE:-?}</div>
|
||||||
|
<div class="lbl">Base adsb</div></div>
|
||||||
|
<div class="kpi">
|
||||||
|
<div class="val" style="font-size:1.2em">${CUR_GAIN:-?} dB</div>
|
||||||
|
<div class="lbl">Gain SDR</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="links">
|
||||||
|
<a href="http://${IP}:8080" target="_blank">🖥 Dashboard</a>
|
||||||
|
<a href="http://${IP}:8090" target="_blank">🗺 tar1090</a>
|
||||||
|
<a href="http://${IP}:8080/api.php?action=kpi&period=24&granularity=60" target="_blank">📊 API KPI</a>
|
||||||
|
<a href="http://${IP}:8080/api.php?action=live&period=24&granularity=60" target="_blank">📡 API Live</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>État</th><th>Détail</th><th>Statut</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
${HTML_ROWS}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
ADS-B Linux-25 — check_adsb.sh v3.0 — JCA 2026 |
|
||||||
|
Prochain refresh dans <span id="ctr">60</span>s
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let t=60;
|
||||||
|
setInterval(()=>{ t--; document.getElementById('ctr').textContent=t;
|
||||||
|
if(t<=0) location.reload(); },1000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
HTMLEOF
|
||||||
|
|
||||||
|
# Copier dans le conteneur webapp
|
||||||
|
docker cp "$HTML_OUT" adsb-webapp:/var/www/html/status.html 2>/dev/null && \
|
||||||
|
echo -e "\n ${GREEN}→${NC} Rapport HTML : http://${IP}:8080/status.html" || \
|
||||||
|
echo -e "\n ${YELLOW}⚠${NC} Copie Docker échouée — fichier local : $HTML_OUT"
|
||||||
|
|
||||||
|
exit $ERRORS
|
||||||
|
|
||||||
304
check_adsb_claude_v1.sh
Executable file
@ -0,0 +1,304 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# check_adsb.sh — Vérification complète du stack ADS-B Linux-25
|
||||||
|
# Version 2.0 — JCA 2026
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'; BLUE='\033[1;34m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
|
||||||
|
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||||
|
fail() { echo -e " ${RED}✗${NC} $1"; ERRORS=$((ERRORS+1)); }
|
||||||
|
warn() { echo -e " ${YELLOW}⚠${NC} $1"; WARNS=$((WARNS+1)); }
|
||||||
|
info() { echo -e " ${CYAN}→${NC} $1"; }
|
||||||
|
hdr() { echo -e "\n${BLUE}${BOLD}═══ $1 ═══${NC}"; }
|
||||||
|
|
||||||
|
ERRORS=0; WARNS=0
|
||||||
|
IP=$(ip -4 addr show wlp58s0 2>/dev/null | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' | head -1)
|
||||||
|
[ -z "$IP" ] && IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ Stack ADS-B Linux-25 — Vérification complète ║${NC}"
|
||||||
|
echo -e "${BOLD}║ $(date '+%a %d/%m/%Y %H:%M:%S') ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
info "IP détectée : ${IP:-inconnue}"
|
||||||
|
|
||||||
|
# ── 1. CONTENEURS ─────────────────────────────────────────────────────────────
|
||||||
|
hdr "1. Conteneurs Docker"
|
||||||
|
CONTAINERS="adsb-readsb adsb-tar1090 adsb-adsb2pg adsb-webapp wordpress-postgres"
|
||||||
|
for ctn in $CONTAINERS; do
|
||||||
|
STATUS=$(docker inspect --format '{{.State.Status}}' "$ctn" 2>/dev/null)
|
||||||
|
HEALTH=$(docker inspect --format '{{.State.Health.Status}}' "$ctn" 2>/dev/null)
|
||||||
|
if [ "$STATUS" = "running" ]; then
|
||||||
|
UPTIME=$(docker inspect --format '{{.State.StartedAt}}' "$ctn" | xargs -I{} date -d {} '+%d/%m %H:%M' 2>/dev/null)
|
||||||
|
LABEL="${ctn}"
|
||||||
|
[ -n "$HEALTH" ] && [ "$HEALTH" != "<nil>" ] && LABEL="$LABEL [$HEALTH]"
|
||||||
|
if [ "$HEALTH" = "unhealthy" ]; then
|
||||||
|
warn "$LABEL — démarré $UPTIME"
|
||||||
|
else
|
||||||
|
ok "$LABEL — démarré $UPTIME"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "$ctn — STATUS=$STATUS"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 2. DONGLE RTL-SDR ─────────────────────────────────────────────────────────
|
||||||
|
hdr "2. Dongle RTL-SDR"
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "SN 00000010"; then
|
||||||
|
ok "AirNav FlightStick SN:00000010 détecté"
|
||||||
|
GAIN=$(docker logs adsb-readsb 2>&1 | grep "tuner gain set to" | tail -1 | grep -oP '[\d.]+(?= dB)')
|
||||||
|
[ -n "$GAIN" ] && info "Gain actuel : ${GAIN} dB (autogain)"
|
||||||
|
else
|
||||||
|
fail "Dongle SN:00000010 non détecté dans les logs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le device USB sur l'hôte
|
||||||
|
if ls /dev/adsb_dongle 2>/dev/null | grep -q adsb_dongle; then
|
||||||
|
ok "Symlink /dev/adsb_dongle présent"
|
||||||
|
elif lsusb 2>/dev/null | grep -q "RTL2838\|2838:0001\|0bda:2838"; then
|
||||||
|
ok "Dongle détecté via lsusb (symlink absent)"
|
||||||
|
else
|
||||||
|
warn "Symlink /dev/adsb_dongle absent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. DONNÉES ADS-B ──────────────────────────────────────────────────────────
|
||||||
|
hdr "3. Données ADS-B"
|
||||||
|
# Vérifier aircraft.json via tar1090
|
||||||
|
AC_JSON=$(curl -s --max-time 5 "http://127.0.0.1:8090/data/aircraft.json" 2>/dev/null)
|
||||||
|
if [ -n "$AC_JSON" ]; then
|
||||||
|
MSGS=$(echo "$AC_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('messages',0))" 2>/dev/null)
|
||||||
|
AVIONS=$(echo "$AC_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('aircraft',[])))" 2>/dev/null)
|
||||||
|
WITH_POS=$(echo "$AC_JSON" | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
n=sum(1 for a in d.get('aircraft',[]) if a.get('lat') and a.get('lon'))
|
||||||
|
print(n)" 2>/dev/null)
|
||||||
|
if [ "${MSGS:-0}" -gt 0 ] 2>/dev/null; then
|
||||||
|
ok "tar1090 actif — $MSGS messages décodés"
|
||||||
|
ok "$AVIONS avion(s) en vue dont $WITH_POS avec position GPS"
|
||||||
|
else
|
||||||
|
warn "tar1090 répond mais 0 message (démarrage récent ou aucun avion ?)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "tar1090 ne répond pas sur :8090/data/aircraft.json"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Volume partagé readsb-run
|
||||||
|
VOL_PATH=$(docker inspect adsb_readsb-run 2>/dev/null | \
|
||||||
|
python3 -c "import json,sys; d=json.load(sys.stdin); print(d[0]['Mountpoint'])" 2>/dev/null)
|
||||||
|
if [ -n "$VOL_PATH" ] && ls "$VOL_PATH/aircraft.pb" 2>/dev/null | grep -q pb; then
|
||||||
|
AGE=$(( $(date +%s) - $(stat -c %Y "$VOL_PATH/aircraft.pb" 2>/dev/null || echo 0) ))
|
||||||
|
if [ "$AGE" -lt 10 ]; then
|
||||||
|
ok "Volume readsb-run : aircraft.pb mis à jour il y a ${AGE}s"
|
||||||
|
else
|
||||||
|
warn "Volume readsb-run : aircraft.pb âgé de ${AGE}s"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Volume readsb-run : aircraft.pb introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. PORTS RÉSEAU ───────────────────────────────────────────────────────────
|
||||||
|
hdr "4. Ports réseau"
|
||||||
|
declare -A PORTS=(
|
||||||
|
[":8080"]="webapp PHP"
|
||||||
|
[":8090"]="tar1090 nginx"
|
||||||
|
[":30005"]="Beast TCP readsb"
|
||||||
|
[":5432"]="PostgreSQL"
|
||||||
|
[":30002"]="RAW TCP"
|
||||||
|
[":30003"]="SBS TCP"
|
||||||
|
)
|
||||||
|
for port_str in ":8080" ":8090" ":30005" ":5432" ":30002" ":30003"; do
|
||||||
|
port="${port_str:1}"
|
||||||
|
label="${PORTS[$port_str]}"
|
||||||
|
if ss -tlnp 2>/dev/null | grep -q ":${port}[[:space:]]"; then
|
||||||
|
ok "Port ${port} en écoute — ${label}"
|
||||||
|
else
|
||||||
|
fail "Port ${port} absent — ${label}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Connexion Beast tar1090 → readsb
|
||||||
|
BEAST_ERRS=$(docker logs adsb-tar1090 2>&1 | grep "Beast TCP input.*failed" | tail -1)
|
||||||
|
if [ -n "$BEAST_ERRS" ]; then
|
||||||
|
FAIL_CNT=$(echo "$BEAST_ERRS" | grep -oP '\d+(?= times)')
|
||||||
|
if [ "${FAIL_CNT:-0}" -gt 50 ] 2>/dev/null; then
|
||||||
|
fail "Connexion Beast tar1090→readsb : $FAIL_CNT échecs (vérifier réseau adsb-net)"
|
||||||
|
else
|
||||||
|
warn "Quelques échecs Beast TCP au démarrage (normal): $FAIL_CNT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
ok "Connexion Beast tar1090→readsb : OK"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Réseau adsb-net
|
||||||
|
READSB_IP=$(docker inspect adsb-readsb 2>/dev/null | \
|
||||||
|
python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)[0]
|
||||||
|
nets=d['NetworkSettings']['Networks']
|
||||||
|
for n,v in nets.items():
|
||||||
|
if 'adsb' in n:
|
||||||
|
print(v['IPAddress'])
|
||||||
|
" 2>/dev/null)
|
||||||
|
if [ -n "$READSB_IP" ] && [ "$READSB_IP" != "None" ]; then
|
||||||
|
ok "readsb attaché à adsb-net : $READSB_IP"
|
||||||
|
else
|
||||||
|
fail "readsb non attaché au réseau adsb-net (Beast TCP impossible)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 5. POSTGRESQL ─────────────────────────────────────────────────────────────
|
||||||
|
hdr "5. PostgreSQL — schéma adsb"
|
||||||
|
PG_CMD="docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c"
|
||||||
|
|
||||||
|
# Connexion
|
||||||
|
PG_VER=$($PG_CMD "SELECT version();" 2>/dev/null | head -1 | xargs)
|
||||||
|
if [ -n "$PG_VER" ]; then
|
||||||
|
ok "PostgreSQL accessible"
|
||||||
|
info "${PG_VER:0:60}"
|
||||||
|
else
|
||||||
|
fail "PostgreSQL inaccessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Partitions positions
|
||||||
|
PART_POS=$($PG_CMD "
|
||||||
|
SELECT COUNT(*) FROM pg_class c
|
||||||
|
JOIN pg_namespace n ON n.oid=c.relnamespace
|
||||||
|
WHERE n.nspname='adsb' AND c.relname LIKE 'positions_2%' AND c.relkind='r';" 2>/dev/null | xargs)
|
||||||
|
ok "${PART_POS:-0} sous-partitions adsb.positions"
|
||||||
|
|
||||||
|
# Partitions aircraft_history
|
||||||
|
PART_HIS=$($PG_CMD "
|
||||||
|
SELECT COUNT(*) FROM pg_class c
|
||||||
|
JOIN pg_namespace n ON n.oid=c.relnamespace
|
||||||
|
WHERE n.nspname='adsb' AND c.relname LIKE 'aircraft_history_%' AND c.relkind='r';" 2>/dev/null | xargs)
|
||||||
|
ok "${PART_HIS:-0} sous-partitions adsb.aircraft_history"
|
||||||
|
|
||||||
|
# Insertions récentes positions
|
||||||
|
POS_COUNT=$($PG_CMD "
|
||||||
|
SELECT COUNT(*) FROM adsb.positions
|
||||||
|
WHERE ts >= NOW() - INTERVAL '5 minutes';" 2>/dev/null | xargs)
|
||||||
|
if [ "${POS_COUNT:-0}" -gt 0 ] 2>/dev/null; then
|
||||||
|
ok "${POS_COUNT} positions insérées dans les 5 dernières minutes"
|
||||||
|
else
|
||||||
|
warn "Aucune position insérée dans les 5 dernières minutes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pourcentage aircraft_desc rempli
|
||||||
|
PCT_DESC=$($PG_CMD "
|
||||||
|
SELECT ROUND(COUNT(aircraft_desc)*100.0/NULLIF(COUNT(*),0),1)
|
||||||
|
FROM adsb.positions
|
||||||
|
WHERE ts >= NOW() - INTERVAL '30 minutes'
|
||||||
|
AND lat IS NOT NULL;" 2>/dev/null | xargs)
|
||||||
|
if [ -n "$PCT_DESC" ] && [ "$PCT_DESC" != "NULL" ]; then
|
||||||
|
if python3 -c "exit(0 if float('${PCT_DESC:-0}') >= 80 else 1)" 2>/dev/null; then
|
||||||
|
ok "aircraft_desc rempli à ${PCT_DESC}% (30 dernières min)"
|
||||||
|
else
|
||||||
|
warn "aircraft_desc rempli à ${PCT_DESC}% seulement (< 80%)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stats générales
|
||||||
|
STATS=$($PG_CMD "
|
||||||
|
SELECT
|
||||||
|
COUNT(DISTINCT icao) AS avions_aujourd_hui,
|
||||||
|
COUNT(*) AS sessions_aujourd_hui
|
||||||
|
FROM adsb.aircraft_history
|
||||||
|
WHERE session_start >= CURRENT_DATE;" 2>/dev/null | xargs)
|
||||||
|
info "Aujourd'hui : $STATS"
|
||||||
|
|
||||||
|
# Taille de la base adsb
|
||||||
|
DB_SIZE=$($PG_CMD "
|
||||||
|
SELECT pg_size_pretty(SUM(pg_total_relation_size(c.oid)))
|
||||||
|
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
||||||
|
WHERE n.nspname='adsb';" 2>/dev/null | xargs)
|
||||||
|
info "Taille totale schéma adsb : ${DB_SIZE:-?}"
|
||||||
|
|
||||||
|
# ── 6. ADSB2PG ────────────────────────────────────────────────────────────────
|
||||||
|
hdr "6. adsb2pg — collecteur"
|
||||||
|
LAST_LOG=$(docker logs adsb-adsb2pg 2>&1 | grep "positions insérées" | tail -1)
|
||||||
|
if [ -n "$LAST_LOG" ]; then
|
||||||
|
ok "adsb2pg actif : $LAST_LOG"
|
||||||
|
# Vérifier la fraîcheur (dernière log < 30s ?)
|
||||||
|
LOG_TIME=$(docker logs adsb-adsb2pg 2>&1 | grep "positions insérées" | tail -1 | \
|
||||||
|
grep -oP '\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}')
|
||||||
|
if [ -n "$LOG_TIME" ]; then
|
||||||
|
AGE_LOG=$(( $(date +%s) - $(date -d "$LOG_TIME" +%s 2>/dev/null || echo 0) ))
|
||||||
|
if [ "$AGE_LOG" -lt 30 ]; then
|
||||||
|
ok "Dernière insertion il y a ${AGE_LOG}s (cycle 5s actif)"
|
||||||
|
else
|
||||||
|
warn "Dernière insertion il y a ${AGE_LOG}s (cycle 5s peut être bloqué)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Aucun log d'insertion trouvé dans adsb2pg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 7. WEBAPP ─────────────────────────────────────────────────────────────────
|
||||||
|
hdr "7. Webapp — interface utilisateur"
|
||||||
|
# Test API endpoints
|
||||||
|
for action in live kpi trajectories; do
|
||||||
|
RESP=$(curl -s --max-time 5 "http://127.0.0.1:8080/api.php?action=${action}&period=24&granularity=60" 2>/dev/null)
|
||||||
|
if echo "$RESP" | python3 -c "import json,sys; json.load(sys.stdin)" 2>/dev/null; then
|
||||||
|
# Vérifier pas d'erreur PHP
|
||||||
|
if echo "$RESP" | grep -q '"error"'; then
|
||||||
|
ERR=$(echo "$RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('error','?'))" 2>/dev/null)
|
||||||
|
warn "api.php?action=${action} : erreur — $ERR"
|
||||||
|
else
|
||||||
|
ok "api.php?action=${action} : OK"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "api.php?action=${action} : réponse non-JSON"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier les onglets dans index.php
|
||||||
|
TAB_COUNT=$(docker exec adsb-webapp grep -c 'href="#tab' /var/www/html/index.php 2>/dev/null)
|
||||||
|
ok "${TAB_COUNT:-?} onglets dans index.php"
|
||||||
|
[ "${TAB_COUNT:-0}" -lt 5 ] && warn "Moins de 5 onglets — index.php peut être obsolète"
|
||||||
|
|
||||||
|
# Taille du JS
|
||||||
|
JS_SIZE=$(docker exec adsb-webapp wc -c /var/www/html/js/app.js 2>/dev/null | awk '{print $1}')
|
||||||
|
info "app.js : ${JS_SIZE:-?} octets"
|
||||||
|
|
||||||
|
# Accès depuis le LAN
|
||||||
|
if [ -n "$IP" ]; then
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://${IP}:8080/" 2>/dev/null)
|
||||||
|
[ "$HTTP_CODE" = "200" ] && ok "Accès LAN http://${IP}:8080 : HTTP $HTTP_CODE" || \
|
||||||
|
warn "Accès LAN http://${IP}:8080 : HTTP $HTTP_CODE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 8. AUTOGAIN ───────────────────────────────────────────────────────────────
|
||||||
|
hdr "8. Autogain"
|
||||||
|
GAIN_LOG=$(docker logs adsb-readsb 2>&1 | grep -E "gain|autogain" | tail -5)
|
||||||
|
CURRENT_GAIN=$(docker logs adsb-readsb 2>&1 | grep "tuner gain set to" | tail -1 | \
|
||||||
|
grep -oP '[\d.]+(?= dB)')
|
||||||
|
STATE=$(docker logs adsb-readsb 2>&1 | grep "autogain" | grep -oP "(?<=state ')[\w]+" | tail -1)
|
||||||
|
|
||||||
|
if [ -n "$CURRENT_GAIN" ]; then
|
||||||
|
ok "Gain courant : ${CURRENT_GAIN} dB (état: ${STATE:-inconnu})"
|
||||||
|
else
|
||||||
|
warn "Gain courant inconnu"
|
||||||
|
fi
|
||||||
|
|
||||||
|
INSUF=$(docker logs adsb-readsb 2>&1 | grep "Insufficient messages" | wc -l)
|
||||||
|
[ "$INSUF" -gt 3 ] && warn "$INSUF cycles avec messages insuffisants (antenne/portée ?)" || \
|
||||||
|
[ "$INSUF" -gt 0 ] && info "$INSUF cycle(s) avec messages insuffisants"
|
||||||
|
|
||||||
|
# ── RÉSUMÉ ────────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ RÉSUMÉ ║${NC}"
|
||||||
|
echo -e "${BOLD}╠══════════════════════════════════════════════════════╣${NC}"
|
||||||
|
if [ "$ERRORS" -eq 0 ] && [ "$WARNS" -eq 0 ]; then
|
||||||
|
echo -e "${BOLD}║ ${GREEN}✓ Stack opérationnel — aucune anomalie détectée${NC}${BOLD} ║${NC}"
|
||||||
|
elif [ "$ERRORS" -eq 0 ]; then
|
||||||
|
echo -e "${BOLD}║ ${YELLOW}⚠ Stack opérationnel — $WARNS avertissement(s)${NC}${BOLD} ║${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${BOLD}║ ${RED}✗ $ERRORS erreur(s), $WARNS avertissement(s) détectés${NC}${BOLD} ║${NC}"
|
||||||
|
fi
|
||||||
|
echo -e "${BOLD}║ Dashboard : http://${IP}:8080${NC}${BOLD} ║${NC}"
|
||||||
|
echo -e "${BOLD}║ tar1090 : http://${IP}:8090${NC}${BOLD} ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
exit $ERRORS
|
||||||
284
check_adsb_mistral.sh
Executable file
@ -0,0 +1,284 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Script de vérification complète de la stack ADS-B
|
||||||
|
# Version : 4.0
|
||||||
|
# Date : 26/06/2026
|
||||||
|
# Auteur : JC Abiven (avec Mistral AI)
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
# Couleurs pour l'affichage
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Fonction pour afficher un message avec une couleur
|
||||||
|
print_status() {
|
||||||
|
echo -e "${1}${2}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher un titre
|
||||||
|
print_title() {
|
||||||
|
echo -e "\n${BLUE}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher une section
|
||||||
|
print_section() {
|
||||||
|
echo -e "\n${CYAN}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Récupérer l'IP de l'hôte
|
||||||
|
HOST_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
# Afficher l'en-tête
|
||||||
|
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════╗"
|
||||||
|
echo -e "║ Stack ADS-B Linux-25 — Vérification complète ║"
|
||||||
|
echo -e "║ $(date "+%a %d/%m/%Y %H:%M:%S") ║"
|
||||||
|
echo -e "╚══════════════════════════════════════════════════════╝"
|
||||||
|
print_status "$YELLOW" " → IP détectée : $HOST_IP"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 1. Vérification des conteneurs Docker
|
||||||
|
# =============================================
|
||||||
|
print_section "1. Conteneurs Docker"
|
||||||
|
|
||||||
|
DOCKER_CONTAINERS=("adsb-readsb" "adsb-tar1090" "adsb-adsb2pg" "adsb-webapp" "wordpress-postgres")
|
||||||
|
for container in "${DOCKER_CONTAINERS[@]}"; do
|
||||||
|
if docker ps | grep -q "$container"; then
|
||||||
|
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "none")
|
||||||
|
if [ "$STATUS" = "healthy" ]; then
|
||||||
|
print_status "$GREEN" " ✓ $container [healthy]"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ $container"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ $container — non démarré"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 2. Vérification du dongle RTL-SDR
|
||||||
|
# =============================================
|
||||||
|
print_section "2. Dongle RTL-SDR"
|
||||||
|
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "rtlsdr: using device #0: Generic RTL2832U OEM (AIRNAV, ADSB_1090, SN 00000010)"; then
|
||||||
|
print_status "$GREEN" " ✓ AirNav FlightStick SN:00000010 détecté"
|
||||||
|
GAIN=$(docker logs adsb-readsb 2>&1 | grep -oP "tuner gain set to \K[0-9.]+ dB" || echo "inconnu")
|
||||||
|
print_status "$GREEN" " → Gain actuel : $GAIN"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dongle RTL-SDR non détecté"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le symlink /dev/adsb_dongle
|
||||||
|
if [ -L "/dev/adsb_dongle" ]; then
|
||||||
|
print_status "$GREEN" " ✓ Symlink /dev/adsb_dongle présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Symlink /dev/adsb_dongle introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 3. Vérification des données ADS-B
|
||||||
|
# =============================================
|
||||||
|
print_section "3. Données ADS-B"
|
||||||
|
|
||||||
|
# Vérifier tar1090
|
||||||
|
if curl -s --max-time 5 http://127.0.0.1:8090 >/dev/null; then
|
||||||
|
TAR1090_STATS=$(curl -s --max-time 5 http://127.0.0.1:8090/data/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$TAR1090_STATS" ]; then
|
||||||
|
MESSAGES=$(echo "$TAR1090_STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ tar1090 actif — $MESSAGES messages décodés"
|
||||||
|
fi
|
||||||
|
|
||||||
|
AIRCRAFT_DATA=$(curl -s --max-time 5 http://127.0.0.1:8090/data/aircraft.json 2>/dev/null)
|
||||||
|
if [ -n "$AIRCRAFT_DATA" ]; then
|
||||||
|
AIRCRAFT_COUNT=$(echo "$AIRCRAFT_DATA" | jq '. | length // 0')
|
||||||
|
AIRCRAFT_WITH_POS=$(echo "$AIRCRAFT_DATA" | jq '[.[] | select(.lat != null)] | length // 0')
|
||||||
|
print_status "$GREEN" " ✓ $AIRCRAFT_COUNT avion(s) en vue dont $AIRCRAFT_WITH_POS avec position GPS"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ tar1090 non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le volume readsb-run
|
||||||
|
if docker exec adsb-readsb ls /run/readsb/aircraft.pb >/dev/null 2>&1; then
|
||||||
|
print_status "$GREEN" " ✓ Volume readsb-run : aircraft.pb présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Volume readsb-run : aircraft.pb introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 4. Vérification des ports réseau
|
||||||
|
# =============================================
|
||||||
|
print_section "4. Ports réseau"
|
||||||
|
|
||||||
|
PORTS=("8080:webapp PHP" "8090:tar1090 nginx" "30005:Beast TCP readsb" "5432:PostgreSQL" "30002:RAW TCP" "30003:SBS TCP")
|
||||||
|
for port_info in "${PORTS[@]}"; do
|
||||||
|
PORT=${port_info%%:*}
|
||||||
|
SERVICE=${port_info#*:}
|
||||||
|
if timeout 2 nc -z 127.0.0.1 "$PORT" 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Port $PORT en écoute — $SERVICE"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Port $PORT non accessible — $SERVICE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier la connectivité Beast TCP via la gateway
|
||||||
|
if timeout 2 nc -z 172.20.0.1 30005 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ readsb attaché à adsb-net : 172.20.0.2"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Problème de connectivité avec adsb-net"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 5. Vérification de PostgreSQL et du schéma adsb
|
||||||
|
# =============================================
|
||||||
|
print_section "5. PostgreSQL — schéma adsb"
|
||||||
|
|
||||||
|
# Vérifier l'accès à PostgreSQL
|
||||||
|
if docker exec wordpress-postgres pg_isready -U pgz_admin -d wpz_postgres 2>/dev/null | grep -q "accepting connections"; then
|
||||||
|
print_status "$GREEN" " ✓ PostgreSQL accessible"
|
||||||
|
PG_VERSION=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT version();" 2>/dev/null | head -1)
|
||||||
|
print_status "$GREEN" " → $PG_VERSION"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ PostgreSQL non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier les partitions
|
||||||
|
PARTITIONS_POSITIONS=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'positions_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
PARTITIONS_HISTORY=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'aircraft_history_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_POSITIONS sous-partitions adsb.positions"
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_HISTORY sous-partitions adsb.aircraft_history"
|
||||||
|
|
||||||
|
# Vérifier les insertions récentes
|
||||||
|
POSITIONS_5MIN=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '5 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $POSITIONS_5MIN positions insérées dans les 5 dernières minutes"
|
||||||
|
|
||||||
|
# Vérifier le remplissage de aircraft_desc
|
||||||
|
DESC_FILL=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT ROUND(COUNT(aircraft_desc)*100.0/COUNT(*),1) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '30 minutes' AND lat IS NOT NULL;" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ aircraft_desc rempli à $DESC_FILL% (30 dernières min)"
|
||||||
|
|
||||||
|
# Taille du schéma adsb
|
||||||
|
SCHEMA_SIZE=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT pg_size_pretty(pg_total_relation_size('adsb'));" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " → Taille totale schéma adsb : $SCHEMA_SIZE"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 6. Vérification de adsb2pg (collecteur)
|
||||||
|
# =============================================
|
||||||
|
print_section "6. adsb2pg — collecteur"
|
||||||
|
|
||||||
|
if docker ps | grep -q "adsb-adsb2pg"; then
|
||||||
|
ADSB2PG_LOGS=$(docker logs adsb-adsb2pg 2>&1 | tail -1)
|
||||||
|
if echo "$ADSB2PG_LOGS" | grep -q "positions insérées"; then
|
||||||
|
print_status "$GREEN" " ✓ adsb2pg actif : $ADSB2PG_LOGS"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ adsb2pg actif mais aucun log récent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier la dernière insertion
|
||||||
|
LAST_INSERT=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT EXTRACT(EPOCH FROM (NOW() - MAX(inserted_at))) FROM adsb.positions WHERE inserted_at >= NOW() - INTERVAL '10 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
if [ -n "$LAST_INSERT" ] && [ "$(echo "$LAST_INSERT < 10" | bc)" -eq 1 ]; then
|
||||||
|
print_status "$GREEN" " ✓ Dernière insertion il y a $(printf "%.0f" "$LAST_INSERT")s (cycle 5s actif)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dernière insertion trop ancienne ou introuvable"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ adsb2pg non démarré"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 7. Vérification de la webapp
|
||||||
|
# =============================================
|
||||||
|
print_section "7. Webapp — interface utilisateur"
|
||||||
|
|
||||||
|
# Vérifier les endpoints API
|
||||||
|
API_ENDPOINTS=("action=live" "action=kpi" "action=trajectories")
|
||||||
|
for endpoint in "${API_ENDPOINTS[@]}"; do
|
||||||
|
if curl -s --max-time 5 "http://127.0.0.1:8080/api.php?$endpoint" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ api.php?$endpoint : OK"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ api.php?$endpoint : KO"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier le nombre d'onglets dans index.php
|
||||||
|
ONGETS=$(curl -s --max-time 5 http://127.0.0.1:8080/index.php | grep -o "<li>" | wc -l)
|
||||||
|
print_status "$GREEN" " ✓ $ONGETS onglets dans index.php"
|
||||||
|
|
||||||
|
# Vérifier la taille de app.js
|
||||||
|
APP_JS_SIZE=$(curl -s --max-time 5 http://127.0.0.1:8080/app.js | wc -c)
|
||||||
|
print_status "$GREEN" " → app.js : $APP_JS_SIZE octets"
|
||||||
|
|
||||||
|
# Vérifier l'accès LAN
|
||||||
|
if curl -s --max-time 5 "http://$HOST_IP:8080" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Accès LAN http://$HOST_IP:8080 : HTTP 200"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Accès LAN http://$HOST_IP:8080 : KO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 8. Vérification de l'autogain
|
||||||
|
# =============================================
|
||||||
|
print_section "8. Autogain"
|
||||||
|
|
||||||
|
GAIN_STATUS=$(docker logs adsb-readsb 2>&1 | grep -oP "Gain: \K[0-9.]+ dB" || echo "inconnu")
|
||||||
|
print_status "$GREEN" " ✓ Gain courant : $GAIN_STATUS (état: inconnu)"
|
||||||
|
|
||||||
|
# Vérifier les cycles avec messages insuffisants
|
||||||
|
INSUFFICIENT_CYCLES=$(docker logs adsb-readsb 2>&1 | grep -c "messages insuffisants" || echo "0")
|
||||||
|
if [ "$INSUFFICIENT_CYCLES" -gt 0 ]; then
|
||||||
|
print_status "$YELLOW" " ⚠ $INSUFFICIENT_CYCLES cycle(s) avec messages insuffisants (antenne/portée ?)"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ Aucun cycle avec messages insuffisants"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 9. Vérification des statistiques readsb
|
||||||
|
# =============================================
|
||||||
|
print_section "9. Statistiques readsb"
|
||||||
|
|
||||||
|
# Essayer d'abord via l'IP du conteneur dans adsb-net
|
||||||
|
STATS=$(curl -s --max-time 5 http://172.20.0.2:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
# Essayer via docker exec
|
||||||
|
STATS=$(docker exec adsb-readsb curl -s http://localhost:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles (via docker exec) :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Impossible de récupérer les statistiques de readsb"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Résumé final
|
||||||
|
# =============================================
|
||||||
|
print_title "RÉSUMÉ"
|
||||||
|
|
||||||
|
# Compter les erreurs et avertissements
|
||||||
|
ERRORS=$(grep -c "$RED✗" <<< "$(./check_adsb_complete.sh 2>&1)" || echo "0")
|
||||||
|
WARNINGS=$(grep -c "$YELLOW⚠" <<< "$(./check_adsb_complete.sh 2>&1)" || echo "0")
|
||||||
|
|
||||||
|
if [ "$ERRORS" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then
|
||||||
|
print_status "$GREEN" " ✓ Stack opérationnel — Aucun problème détecté"
|
||||||
|
elif [ "$ERRORS" -eq 0 ]; then
|
||||||
|
print_status "$YELLOW" " ⚠ Stack opérationnel — $WARNINGS avertissement(s)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Stack en erreur — $ERRORS erreur(s), $WARNINGS avertissement(s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "$BLUE" " Dashboard : http://$HOST_IP:8080"
|
||||||
|
print_status "$BLUE" " tar1090 : http://$HOST_IP:8090"
|
||||||
|
|
||||||
88
check_adsb_mistral_v0.sh
Executable file
@ -0,0 +1,88 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Couleurs pour l'affichage
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_status() {
|
||||||
|
echo -e "${1}${2}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Vérifier que tous les conteneurs sont en cours d'exécution
|
||||||
|
print_status "$YELLOW" "=== Vérification des conteneurs Docker ==="
|
||||||
|
for service in readsb tar1090 adsb2pg webapp; do
|
||||||
|
if docker ps | grep -q "adsb-$service"; then
|
||||||
|
print_status "$GREEN" "✓ Le conteneur adsb-$service est en cours d'exécution."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Le conteneur adsb-$service n'est pas en cours d'exécution."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. Vérifier la détection du dongle RTL-SDR
|
||||||
|
print_status "$YELLOW" "\n=== Vérification du dongle RTL-SDR ==="
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "rtlsdr: using device #0: Generic RTL2832U OEM (AIRNAV, ADSB_1090, SN 00000010)"; then
|
||||||
|
print_status "$GREEN" "✓ Le dongle AirNav FlightStick (SN: 00000010) est détecté et utilisé par readsb."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Le dongle RTL-SDR n'est pas détecté ou utilisé correctement."
|
||||||
|
print_status "$YELLOW" "Dernières lignes des logs :"
|
||||||
|
docker logs adsb-readsb 2>&1 | tail -10
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Vérifier que readsb décode des messages ADS-B (via les ports)
|
||||||
|
print_status "$YELLOW" "\n=== Vérification du décodage ADS-B ==="
|
||||||
|
if timeout 2 nc -z 172.20.0.1 30005; then
|
||||||
|
print_status "$GREEN" "✓ Le port Beast (30005) émet des données (readsb décode des messages ADS-B)."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Aucune donnée détectée sur le port Beast (30005)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Vérifier les statistiques readsb (via l'IP du conteneur)
|
||||||
|
print_status "$YELLOW" "\n=== Vérification des statistiques readsb ==="
|
||||||
|
STATS=$(curl -s --max-time 5 http://172.20.0.2:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" "✓ Statistiques readsb accessibles :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Impossible de récupérer les statistiques de readsb."
|
||||||
|
print_status "$YELLOW" " → Essayez : docker exec adsb-readsb curl -s http://localhost:30003/readsb/stats.json | jq"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Vérifier que tar1090 est accessible et reçoit des données
|
||||||
|
print_status "$YELLOW" "\n=== Vérification de tar1090 ==="
|
||||||
|
if curl -s --max-time 5 http://127.0.0.1:8090 >/dev/null; then
|
||||||
|
print_status "$GREEN" "✓ tar1090 est accessible sur http://127.0.0.1:8090."
|
||||||
|
AIRCRAFT_COUNT=$(curl -s --max-time 5 http://127.0.0.1:8090/data/aircraft.json | jq '. | length // 0')
|
||||||
|
if [ "$AIRCRAFT_COUNT" -gt 0 ]; then
|
||||||
|
print_status "$GREEN" "✓ $AIRCRAFT_COUNT avion(s) détecté(s) sur tar1090."
|
||||||
|
LAST_UPDATE=$(curl -s --max-time 5 http://127.0.0.1:8090/data/aircraft.json | jq -r '.now // 0')
|
||||||
|
CURRENT_TIME=$(date +%s)
|
||||||
|
TIME_DIFF=$((CURRENT_TIME - LAST_UPDATE))
|
||||||
|
if [ "$TIME_DIFF" -le 10 ]; then
|
||||||
|
print_status "$GREEN" "✓ Les données sont récentes (dernière mise à jour il y a $TIME_DIFF secondes)."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Les données ne sont pas récentes (dernière mise à jour il y a $TIME_DIFF secondes)."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Aucun avion détecté sur tar1090."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ tar1090 n'est pas accessible sur http://127.0.0.1:8090."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Vérifier les ports critiques
|
||||||
|
print_status "$YELLOW" "\n=== Vérification des ports ==="
|
||||||
|
for port in 30002 30003 30005 8080 8090; do
|
||||||
|
if timeout 2 nc -z 127.0.0.1 $port; then
|
||||||
|
print_status "$GREEN" "✓ Le port $port est accessible sur 127.0.0.1."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Le port $port n'est pas accessible sur 127.0.0.1."
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
print_status "$YELLOW" "\n=== Fin des vérifications ==="
|
||||||
|
|
||||||
309
check_adsb_mistral_v2.sh
Executable file
@ -0,0 +1,309 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Script de vérification complète de la stack ADS-B
|
||||||
|
# Version : 5.0 (Corrigée)
|
||||||
|
# Date : 26/06/2026
|
||||||
|
# Auteur : JC Abiven (avec Mistral AI)
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
# Vérifier que jq est installé
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo -e "\033[0;31m✗ jq n'est pas installé. Installez-le avec : sudo apt install jq\033[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Couleurs pour l'affichage
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Fonction pour afficher un message avec une couleur
|
||||||
|
print_status() {
|
||||||
|
echo -e "${1}${2}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher un titre
|
||||||
|
print_title() {
|
||||||
|
echo -e "\n${BLUE}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher une section
|
||||||
|
print_section() {
|
||||||
|
echo -e "\n${CYAN}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Récupérer l'IP de l'hôte
|
||||||
|
HOST_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
# Afficher l'en-tête
|
||||||
|
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════╗"
|
||||||
|
echo -e "║ Stack ADS-B Linux-25 — Vérification complète ║"
|
||||||
|
echo -e "║ $(date "+%a %d/%m/%Y %H:%M:%S") ║"
|
||||||
|
echo -e "╚══════════════════════════════════════════════════════╝"
|
||||||
|
print_status "$YELLOW" " → IP détectée : $HOST_IP"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 1. Vérification des conteneurs Docker
|
||||||
|
# =============================================
|
||||||
|
print_section "1. Conteneurs Docker"
|
||||||
|
|
||||||
|
DOCKER_CONTAINERS=("adsb-readsb" "adsb-tar1090" "adsb-adsb2pg" "adsb-webapp" "wordpress-postgres")
|
||||||
|
for container in "${DOCKER_CONTAINERS[@]}"; do
|
||||||
|
if docker ps | grep -q "$container"; then
|
||||||
|
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "none")
|
||||||
|
if [ "$STATUS" = "healthy" ]; then
|
||||||
|
print_status "$GREEN" " ✓ $container [healthy]"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ $container"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ $container — non démarré"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 2. Vérification du dongle RTL-SDR
|
||||||
|
# =============================================
|
||||||
|
print_section "2. Dongle RTL-SDR"
|
||||||
|
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "rtlsdr: using device #0: Generic RTL2832U OEM (AIRNAV, ADSB_1090, SN 00000010)"; then
|
||||||
|
print_status "$GREEN" " ✓ AirNav FlightStick SN:00000010 détecté"
|
||||||
|
GAIN=$(docker logs adsb-readsb 2>&1 | grep -oP "tuner gain set to \K[0-9.]+ dB" | tail -1)
|
||||||
|
print_status "$GREEN" " → Gain actuel : $GAIN"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dongle RTL-SDR non détecté"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le symlink /dev/adsb_dongle
|
||||||
|
if [ -L "/dev/adsb_dongle" ]; then
|
||||||
|
print_status "$GREEN" " ✓ Symlink /dev/adsb_dongle présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Symlink /dev/adsb_dongle introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 3. Vérification des données ADS-B
|
||||||
|
# =============================================
|
||||||
|
print_section "3. Données ADS-B"
|
||||||
|
|
||||||
|
# Vérifier tar1090
|
||||||
|
if curl -s --max-time 5 http://127.0.0.1:8090 >/dev/null; then
|
||||||
|
TAR1090_STATS=$(curl -s --max-time 5 http://127.0.0.1:8090/data/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$TAR1090_STATS" ]; then
|
||||||
|
MESSAGES=$(echo "$TAR1090_STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ tar1090 actif — $MESSAGES messages décodés"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Impossible de récupérer les stats de tar1090"
|
||||||
|
fi
|
||||||
|
|
||||||
|
AIRCRAFT_DATA=$(curl -s --max-time 5 http://127.0.0.1:8090/data/aircraft.json 2>/dev/null)
|
||||||
|
if [ -n "$AIRCRAFT_DATA" ]; then
|
||||||
|
AIRCRAFT_COUNT=$(echo "$AIRCRAFT_DATA" | jq '. | length // 0')
|
||||||
|
AIRCRAFT_WITH_POS=$(echo "$AIRCRAFT_DATA" | jq '[.[] | select(.lat != null)] | length // 0')
|
||||||
|
print_status "$GREEN" " ✓ $AIRCRAFT_COUNT avion(s) en vue dont $AIRCRAFT_WITH_POS avec position GPS"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Impossible de récupérer les données des avions"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ tar1090 non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le volume readsb-run
|
||||||
|
if docker exec adsb-readsb ls /run/readsb/aircraft.pb >/dev/null 2>&1; then
|
||||||
|
print_status "$GREEN" " ✓ Volume readsb-run : aircraft.pb présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Volume readsb-run : aircraft.pb introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 4. Vérification des ports réseau
|
||||||
|
# =============================================
|
||||||
|
print_section "4. Ports réseau"
|
||||||
|
|
||||||
|
PORTS=("8080:webapp PHP" "8090:tar1090 nginx" "30005:Beast TCP readsb" "5432:PostgreSQL" "30002:RAW TCP" "30003:SBS TCP")
|
||||||
|
for port_info in "${PORTS[@]}"; do
|
||||||
|
PORT=${port_info%%:*}
|
||||||
|
SERVICE=${port_info#*:}
|
||||||
|
if timeout 2 nc -z 127.0.0.1 "$PORT" 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Port $PORT en écoute — $SERVICE"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Port $PORT non accessible — $SERVICE"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier la connectivité Beast TCP via la gateway
|
||||||
|
if timeout 2 nc -z 172.20.0.1 30005 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ readsb attaché à adsb-net : 172.20.0.2"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Problème de connectivité avec adsb-net"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 5. Vérification de PostgreSQL et du schéma adsb
|
||||||
|
# =============================================
|
||||||
|
print_section "5. PostgreSQL — schéma adsb"
|
||||||
|
|
||||||
|
# Vérifier l'accès à PostgreSQL
|
||||||
|
if docker exec wordpress-postgres pg_isready -U pgz_admin -d wpz_postgres 2>/dev/null | grep -q "accepting connections"; then
|
||||||
|
print_status "$GREEN" " ✓ PostgreSQL accessible"
|
||||||
|
PG_VERSION=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT version();" 2>/dev/null | head -1 | tr -d ' ')
|
||||||
|
print_status "$GREEN" " → $PG_VERSION"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ PostgreSQL non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier les partitions
|
||||||
|
PARTITIONS_POSITIONS=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'positions_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
PARTITIONS_HISTORY=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'aircraft_history_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_POSITIONS sous-partitions adsb.positions"
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_HISTORY sous-partitions adsb.aircraft_history"
|
||||||
|
|
||||||
|
# Vérifier les insertions récentes
|
||||||
|
POSITIONS_5MIN=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '5 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $POSITIONS_5MIN positions insérées dans les 5 dernières minutes"
|
||||||
|
|
||||||
|
# Vérifier le remplissage de aircraft_desc
|
||||||
|
DESC_FILL=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT ROUND(COUNT(aircraft_desc)*100.0/COUNT(*),1) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '30 minutes' AND lat IS NOT NULL;" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ aircraft_desc rempli à ${DESC_FILL:-0}% (30 dernières min)"
|
||||||
|
|
||||||
|
# Taille du schéma adsb
|
||||||
|
SCHEMA_SIZE=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT pg_size_pretty(pg_total_relation_size('adsb'));" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " → Taille totale schéma adsb : ${SCHEMA_SIZE:-inconnu}"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 6. Vérification de adsb2pg (collecteur)
|
||||||
|
# =============================================
|
||||||
|
print_section "6. adsb2pg — collecteur"
|
||||||
|
|
||||||
|
if docker ps | grep -q "adsb-adsb2pg"; then
|
||||||
|
ADSB2PG_LOGS=$(docker logs adsb-adsb2pg 2>&1 | tail -1)
|
||||||
|
if echo "$ADSB2PG_LOGS" | grep -q "positions insérées"; then
|
||||||
|
print_status "$GREEN" " ✓ adsb2pg actif : $ADSB2PG_LOGS"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ adsb2pg actif mais aucun log récent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier la dernière insertion (en secondes)
|
||||||
|
LAST_INSERT_SEC=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT EXTRACT(EPOCH FROM (NOW() - MAX(inserted_at))) FROM adsb.positions WHERE inserted_at >= NOW() - INTERVAL '10 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
if [ -n "$LAST_INSERT_SEC" ]; then
|
||||||
|
LAST_INSERT_SEC_INT=${LAST_INSERT_SEC%.*} # Supprimer la partie décimale
|
||||||
|
if [ "$LAST_INSERT_SEC_INT" -le 10 ] 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Dernière insertion il y a ${LAST_INSERT_SEC_INT}s (cycle 5s actif)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dernière insertion trop ancienne (${LAST_INSERT_SEC_INT}s)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Impossible de vérifier la dernière insertion"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ adsb2pg non démarré"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 7. Vérification de la webapp
|
||||||
|
# =============================================
|
||||||
|
print_section "7. Webapp — interface utilisateur"
|
||||||
|
|
||||||
|
# Vérifier les endpoints API
|
||||||
|
API_ENDPOINTS=("action=live" "action=kpi" "action=trajectories")
|
||||||
|
for endpoint in "${API_ENDPOINTS[@]}"; do
|
||||||
|
if curl -s --max-time 5 "http://127.0.0.1:8080/api.php?$endpoint" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ api.php?$endpoint : OK"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ api.php?$endpoint : KO"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier le nombre d'onglets dans index.php
|
||||||
|
ONGETS=$(curl -s --max-time 5 http://127.0.0.1:8080/index.php | grep -o "<li>" | wc -l)
|
||||||
|
print_status "$GREEN" " ✓ $ONGETS onglet(s) dans index.php"
|
||||||
|
|
||||||
|
# Vérifier la taille de app.js
|
||||||
|
APP_JS_SIZE=$(curl -s --max-time 5 http://127.0.0.1:8080/app.js | wc -c)
|
||||||
|
print_status "$GREEN" " → app.js : $APP_JS_SIZE octets"
|
||||||
|
|
||||||
|
# Vérifier l'accès LAN
|
||||||
|
if curl -s --max-time 5 "http://$HOST_IP:8080" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Accès LAN http://$HOST_IP:8080 : HTTP 200"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Accès LAN http://$HOST_IP:8080 : KO"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 8. Vérification de l'autogain
|
||||||
|
# =============================================
|
||||||
|
print_section "8. Autogain"
|
||||||
|
|
||||||
|
GAIN_STATUS=$(docker logs adsb-readsb 2>&1 | grep -oP "Gain: \K[0-9.]+ dB" | tail -1)
|
||||||
|
if [ -z "$GAIN_STATUS" ]; then
|
||||||
|
GAIN_STATUS="inconnu"
|
||||||
|
fi
|
||||||
|
print_status "$GREEN" " ✓ Gain courant : $GAIN_STATUS"
|
||||||
|
|
||||||
|
# Vérifier les cycles avec messages insuffisants
|
||||||
|
INSUFFICIENT_CYCLES=$(docker logs adsb-readsb 2>&1 | grep -c "messages insuffisants" || echo "0")
|
||||||
|
if [ "$INSUFFICIENT_CYCLES" -gt 0 ] 2>/dev/null; then
|
||||||
|
print_status "$YELLOW" " ⚠ $INSUFFICIENT_CYCLES cycle(s) avec messages insuffisants (antenne/portée ?)"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ Aucun cycle avec messages insuffisants"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 9. Vérification des statistiques readsb
|
||||||
|
# =============================================
|
||||||
|
print_section "9. Statistiques readsb"
|
||||||
|
|
||||||
|
# Essayer d'abord via l'IP du conteneur dans adsb-net
|
||||||
|
STATS=$(curl -s --max-time 5 http://172.20.0.2:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
# Essayer via docker exec
|
||||||
|
STATS=$(docker exec adsb-readsb curl -s http://localhost:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles (via docker exec) :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Impossible de récupérer les statistiques de readsb"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Résumé final
|
||||||
|
# =============================================
|
||||||
|
print_title "RÉSUMÉ"
|
||||||
|
|
||||||
|
# Compter les erreurs et avertissements dans le script actuel
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" == *"$RED✗"* ]]; then
|
||||||
|
((ERRORS++))
|
||||||
|
elif [[ "$line" == *"$YELLOW⚠"* ]]; then
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
done < <(./check_adsb_final.sh 2>&1)
|
||||||
|
|
||||||
|
if [ "$ERRORS" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then
|
||||||
|
print_status "$GREEN" " ✓ Stack opérationnel — Aucun problème détecté"
|
||||||
|
elif [ "$ERRORS" -eq 0 ]; then
|
||||||
|
print_status "$YELLOW" " ⚠ Stack opérationnel — $WARNINGS avertissement(s)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Stack en erreur — $ERRORS erreur(s), $WARNINGS avertissement(s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "$BLUE" " Dashboard : http://$HOST_IP:8080"
|
||||||
|
print_status "$BLUE" " tar1090 : http://$HOST_IP:8090"
|
||||||
|
|
||||||
322
check_adsb_mistral_v3.sh
Executable file
@ -0,0 +1,322 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Script de vérification complète de la stack ADS-B
|
||||||
|
# Version : 6.0 (Avec adresses IP pour chaque port)
|
||||||
|
# Date : 26/06/2026
|
||||||
|
# Auteur : JC Abiven (avec Mistral AI)
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
# Vérifier que jq est installé
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo -e "\033[0;31m✗ jq n'est pas installé. Installez-le avec : sudo apt install jq\033[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Couleurs pour l'affichage
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Fonction pour afficher un message avec une couleur
|
||||||
|
print_status() {
|
||||||
|
echo -e "${1}${2}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher un titre
|
||||||
|
print_title() {
|
||||||
|
echo -e "\n${BLUE}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher une section
|
||||||
|
print_section() {
|
||||||
|
echo -e "\n${CYAN}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Récupérer l'IP de l'hôte
|
||||||
|
HOST_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
# Afficher l'en-tête
|
||||||
|
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════╗"
|
||||||
|
echo -e "║ Stack ADS-B Linux-25 — Vérification complète ║"
|
||||||
|
echo -e "║ $(date "+%a %d/%m/%Y %H:%M:%S") ║"
|
||||||
|
echo -e "╚══════════════════════════════════════════════════════╝"
|
||||||
|
print_status "$YELLOW" " → IP détectée : $HOST_IP"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 1. Vérification des conteneurs Docker
|
||||||
|
# =============================================
|
||||||
|
print_section "1. Conteneurs Docker"
|
||||||
|
|
||||||
|
DOCKER_CONTAINERS=("adsb-readsb" "adsb-tar1090" "adsb-adsb2pg" "adsb-webapp" "wordpress-postgres")
|
||||||
|
for container in "${DOCKER_CONTAINERS[@]}"; do
|
||||||
|
if docker ps | grep -q "$container"; then
|
||||||
|
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "none")
|
||||||
|
if [ "$STATUS" = "healthy" ]; then
|
||||||
|
print_status "$GREEN" " ✓ $container [healthy]"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ $container"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ $container — non démarré"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 2. Vérification du dongle RTL-SDR
|
||||||
|
# =============================================
|
||||||
|
print_section "2. Dongle RTL-SDR"
|
||||||
|
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "rtlsdr: using device #0: Generic RTL2832U OEM (AIRNAV, ADSB_1090, SN 00000010)"; then
|
||||||
|
print_status "$GREEN" " ✓ AirNav FlightStick SN:00000010 détecté"
|
||||||
|
GAIN=$(docker logs adsb-readsb 2>&1 | grep -oP "tuner gain set to \K[0-9.]+ dB" | tail -1)
|
||||||
|
print_status "$GREEN" " → Gain actuel : $GAIN"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dongle RTL-SDR non détecté"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le symlink /dev/adsb_dongle
|
||||||
|
if [ -L "/dev/adsb_dongle" ]; then
|
||||||
|
print_status "$GREEN" " ✓ Symlink /dev/adsb_dongle présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Symlink /dev/adsb_dongle introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 3. Vérification des données ADS-B
|
||||||
|
# =============================================
|
||||||
|
print_section "3. Données ADS-B"
|
||||||
|
|
||||||
|
# Vérifier tar1090
|
||||||
|
if curl -s --max-time 5 http://127.0.0.1:8090 >/dev/null; then
|
||||||
|
TAR1090_STATS=$(curl -s --max-time 5 http://127.0.0.1:8090/data/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$TAR1090_STATS" ]; then
|
||||||
|
MESSAGES=$(echo "$TAR1090_STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ tar1090 actif — $MESSAGES messages décodés"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Impossible de récupérer les stats de tar1090"
|
||||||
|
fi
|
||||||
|
|
||||||
|
AIRCRAFT_DATA=$(curl -s --max-time 5 http://127.0.0.1:8090/data/aircraft.json 2>/dev/null)
|
||||||
|
if [ -n "$AIRCRAFT_DATA" ]; then
|
||||||
|
AIRCRAFT_COUNT=$(echo "$AIRCRAFT_DATA" | jq '. | length // 0')
|
||||||
|
AIRCRAFT_WITH_POS=$(echo "$AIRCRAFT_DATA" | jq '[.[] | select(.lat != null and .lon != null)] | length // 0')
|
||||||
|
print_status "$GREEN" " ✓ $AIRCRAFT_COUNT avion(s) en vue dont $AIRCRAFT_WITH_POS avec position GPS"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Impossible de récupérer les données des avions"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ tar1090 non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le volume readsb-run
|
||||||
|
if docker exec adsb-readsb ls /run/readsb/aircraft.pb >/dev/null 2>&1; then
|
||||||
|
print_status "$GREEN" " ✓ Volume readsb-run : aircraft.pb présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Volume readsb-run : aircraft.pb introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 4. Vérification des ports réseau (avec adresses IP)
|
||||||
|
# =============================================
|
||||||
|
print_section "4. Ports réseau"
|
||||||
|
|
||||||
|
# Fonction pour obtenir l'adresse IP associée à un port
|
||||||
|
get_ip_for_port() {
|
||||||
|
local port=$1
|
||||||
|
ss -tulnp | grep ":$port " | awk '{print $5}' | cut -d: -f1 | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
PORTS=("8080:webapp PHP:127.0.0.1" "8090:tar1090 nginx:127.0.0.1" "30005:Beast TCP readsb:0.0.0.0" "5432:PostgreSQL:127.0.0.1" "30002:RAW TCP:127.0.0.1" "30003:SBS TCP:127.0.0.1")
|
||||||
|
for port_info in "${PORTS[@]}"; do
|
||||||
|
PORT=${port_info%%:*}
|
||||||
|
SERVICE=${port_info#*:}
|
||||||
|
SERVICE=${SERVICE%:*}
|
||||||
|
EXPECTED_IP=${port_info##*:}
|
||||||
|
|
||||||
|
# Obtenir l'IP réelle
|
||||||
|
ACTUAL_IP=$(get_ip_for_port "$PORT" || echo "inconnu")
|
||||||
|
|
||||||
|
if timeout 2 nc -z "$EXPECTED_IP" "$PORT" 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Port $PORT ($SERVICE) — IP: $ACTUAL_IP (attendu: $EXPECTED_IP)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Port $PORT ($SERVICE) — IP: $ACTUAL_IP (attendu: $EXPECTED_IP)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier la connectivité Beast TCP via la gateway
|
||||||
|
if timeout 2 nc -z 172.20.0.1 30005 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ readsb attaché à adsb-net : 172.20.0.2 (gateway: 172.20.0.1)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Problème de connectivité avec adsb-net (172.20.0.1:30005)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 5. Vérification de PostgreSQL et du schéma adsb
|
||||||
|
# =============================================
|
||||||
|
print_section "5. PostgreSQL — schéma adsb"
|
||||||
|
|
||||||
|
# Vérifier l'accès à PostgreSQL
|
||||||
|
if docker exec wordpress-postgres pg_isready -U pgz_admin -d wpz_postgres 2>/dev/null | grep -q "accepting connections"; then
|
||||||
|
print_status "$GREEN" " ✓ PostgreSQL accessible"
|
||||||
|
PG_VERSION=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT version();" 2>/dev/null | head -1 | tr -d ' ')
|
||||||
|
print_status "$GREEN" " → $PG_VERSION"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ PostgreSQL non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier les partitions
|
||||||
|
PARTITIONS_POSITIONS=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'positions_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
PARTITIONS_HISTORY=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'aircraft_history_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_POSITIONS sous-partitions adsb.positions"
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_HISTORY sous-partitions adsb.aircraft_history"
|
||||||
|
|
||||||
|
# Vérifier les insertions récentes
|
||||||
|
POSITIONS_5MIN=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '5 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $POSITIONS_5MIN positions insérées dans les 5 dernières minutes"
|
||||||
|
|
||||||
|
# Vérifier le remplissage de aircraft_desc
|
||||||
|
DESC_FILL=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT ROUND(COUNT(aircraft_desc)*100.0/COUNT(*),1) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '30 minutes' AND lat IS NOT NULL;" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ aircraft_desc rempli à ${DESC_FILL:-0}% (30 dernières min)"
|
||||||
|
|
||||||
|
# Taille du schéma adsb
|
||||||
|
SCHEMA_SIZE=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT pg_size_pretty(pg_total_relation_size('adsb'));" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " → Taille totale schéma adsb : ${SCHEMA_SIZE:-inconnu}"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 6. Vérification de adsb2pg (collecteur)
|
||||||
|
# =============================================
|
||||||
|
print_section "6. adsb2pg — collecteur"
|
||||||
|
|
||||||
|
if docker ps | grep -q "adsb-adsb2pg"; then
|
||||||
|
ADSB2PG_LOGS=$(docker logs adsb-adsb2pg 2>&1 | tail -1)
|
||||||
|
if echo "$ADSB2PG_LOGS" | grep -q "positions insérées"; then
|
||||||
|
print_status "$GREEN" " ✓ adsb2pg actif : $ADSB2PG_LOGS"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ adsb2pg actif mais aucun log récent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier la dernière insertion (en secondes)
|
||||||
|
LAST_INSERT_SEC=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT EXTRACT(EPOCH FROM (NOW() - MAX(inserted_at))) FROM adsb.positions WHERE inserted_at >= NOW() - INTERVAL '10 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
if [ -n "$LAST_INSERT_SEC" ]; then
|
||||||
|
LAST_INSERT_SEC_INT=${LAST_INSERT_SEC%.*} # Supprimer la partie décimale
|
||||||
|
if [ "$LAST_INSERT_SEC_INT" -le 10 ] 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Dernière insertion il y a ${LAST_INSERT_SEC_INT}s (cycle 5s actif)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dernière insertion trop ancienne (${LAST_INSERT_SEC_INT}s)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Impossible de vérifier la dernière insertion"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ adsb2pg non démarré"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 7. Vérification de la webapp
|
||||||
|
# =============================================
|
||||||
|
print_section "7. Webapp — interface utilisateur"
|
||||||
|
|
||||||
|
# Vérifier les endpoints API
|
||||||
|
API_ENDPOINTS=("action=live" "action=kpi" "action=trajectories")
|
||||||
|
for endpoint in "${API_ENDPOINTS[@]}"; do
|
||||||
|
if curl -s --max-time 5 "http://127.0.0.1:8080/api.php?$endpoint" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ api.php?$endpoint : OK (IP: 127.0.0.1)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ api.php?$endpoint : KO (IP: 127.0.0.1)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier le nombre d'onglets dans index.php
|
||||||
|
ONGETS=$(curl -s --max-time 5 http://127.0.0.1:8080/index.php | grep -o "<li>" | wc -l)
|
||||||
|
print_status "$GREEN" " ✓ $ONGETS onglet(s) dans index.php (IP: 127.0.0.1)"
|
||||||
|
|
||||||
|
# Vérifier la taille de app.js
|
||||||
|
APP_JS_SIZE=$(curl -s --max-time 5 http://127.0.0.1:8080/app.js | wc -c)
|
||||||
|
print_status "$GREEN" " → app.js : $APP_JS_SIZE octets (IP: 127.0.0.1)"
|
||||||
|
|
||||||
|
# Vérifier l'accès LAN
|
||||||
|
if curl -s --max-time 5 "http://$HOST_IP:8080" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Accès LAN http://$HOST_IP:8080 : HTTP 200 (IP: $HOST_IP)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Accès LAN http://$HOST_IP:8080 : KO (IP: $HOST_IP)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 8. Vérification de l'autogain
|
||||||
|
# =============================================
|
||||||
|
print_section "8. Autogain"
|
||||||
|
|
||||||
|
GAIN_STATUS=$(docker logs adsb-readsb 2>&1 | grep -oP "Gain: \K[0-9.]+ dB" | tail -1)
|
||||||
|
if [ -z "$GAIN_STATUS" ]; then
|
||||||
|
GAIN_STATUS="inconnu"
|
||||||
|
fi
|
||||||
|
print_status "$GREEN" " ✓ Gain courant : $GAIN_STATUS"
|
||||||
|
|
||||||
|
# Vérifier les cycles avec messages insuffisants
|
||||||
|
INSUFFICIENT_CYCLES=$(docker logs adsb-readsb 2>&1 | grep -c "messages insuffisants" || echo "0")
|
||||||
|
if [ "$INSUFFICIENT_CYCLES" -gt 0 ] 2>/dev/null; then
|
||||||
|
print_status "$YELLOW" " ⚠ $INSUFFICIENT_CYCLES cycle(s) avec messages insuffisants (antenne/portée ?)"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ Aucun cycle avec messages insuffisants"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 9. Vérification des statistiques readsb
|
||||||
|
# =============================================
|
||||||
|
print_section "9. Statistiques readsb"
|
||||||
|
|
||||||
|
# Essayer d'abord via l'IP du conteneur dans adsb-net
|
||||||
|
STATS=$(curl -s --max-time 5 http://172.20.0.2:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles (IP: 172.20.0.2:30003) :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
# Essayer via docker exec
|
||||||
|
STATS=$(docker exec adsb-readsb curl -s http://localhost:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles (via docker exec, IP: localhost:30003) :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Impossible de récupérer les statistiques de readsb (IP: 172.20.0.2 ou localhost:30003)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Résumé final
|
||||||
|
# =============================================
|
||||||
|
print_title "RÉSUMÉ"
|
||||||
|
|
||||||
|
# Compter les erreurs et avertissements dans le script actuel
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
SCRIPT_OUTPUT=$(./check_adsb_with_ips.sh 2>&1)
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" == *"$RED✗"* ]]; then
|
||||||
|
((ERRORS++))
|
||||||
|
elif [[ "$line" == *"$YELLOW⚠"* ]]; then
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
done <<< "$SCRIPT_OUTPUT"
|
||||||
|
|
||||||
|
if [ "$ERRORS" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then
|
||||||
|
print_status "$GREEN" " ✓ Stack opérationnel — Aucun problème détecté"
|
||||||
|
elif [ "$ERRORS" -eq 0 ]; then
|
||||||
|
print_status "$YELLOW" " ⚠ Stack opérationnel — $WARNINGS avertissement(s)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Stack en erreur — $ERRORS erreur(s), $WARNINGS avertissement(s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "$BLUE" " Dashboard : http://$HOST_IP:8080"
|
||||||
|
print_status "$BLUE" " tar1090 : http://$HOST_IP:8090"
|
||||||
|
|
||||||
347
check_adsb_mistral_v4.sh
Executable file
@ -0,0 +1,347 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Script de vérification complète de la stack ADS-B
|
||||||
|
# Version : 7.0 (Avec port/IP PostgreSQL et corrections jq)
|
||||||
|
# Date : 26/06/2026
|
||||||
|
# Auteur : JC Abiven (avec Mistral AI)
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
# Vérifier que jq est installé
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo -e "\033[0;31m✗ jq n'est pas installé. Installez-le avec : sudo apt install jq\033[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Couleurs pour l'affichage
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Fonction pour afficher un message avec une couleur
|
||||||
|
print_status() {
|
||||||
|
echo -e "${1}${2}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher un titre
|
||||||
|
print_title() {
|
||||||
|
echo -e "\n${BLUE}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher une section
|
||||||
|
print_section() {
|
||||||
|
echo -e "\n${CYAN}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Récupérer l'IP de l'hôte
|
||||||
|
HOST_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
# Afficher l'en-tête
|
||||||
|
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════╗"
|
||||||
|
echo -e "║ Stack ADS-B Linux-25 — Vérification complète ║"
|
||||||
|
echo -e "║ $(date "+%a %d/%m/%Y %H:%M:%S") ║"
|
||||||
|
echo -e "╚══════════════════════════════════════════════════════╝"
|
||||||
|
print_status "$YELLOW" " → IP détectée : $HOST_IP"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 1. Vérification des conteneurs Docker
|
||||||
|
# =============================================
|
||||||
|
print_section "1. Conteneurs Docker"
|
||||||
|
|
||||||
|
DOCKER_CONTAINERS=("adsb-readsb" "adsb-tar1090" "adsb-adsb2pg" "adsb-webapp" "wordpress-postgres")
|
||||||
|
for container in "${DOCKER_CONTAINERS[@]}"; do
|
||||||
|
if docker ps | grep -q "$container"; then
|
||||||
|
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "none")
|
||||||
|
if [ "$STATUS" = "healthy" ]; then
|
||||||
|
print_status "$GREEN" " ✓ $container [healthy]"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ $container"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ $container — non démarré"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 2. Vérification du dongle RTL-SDR
|
||||||
|
# =============================================
|
||||||
|
print_section "2. Dongle RTL-SDR"
|
||||||
|
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "rtlsdr: using device #0: Generic RTL2832U OEM (AIRNAV, ADSB_1090, SN 00000010)"; then
|
||||||
|
print_status "$GREEN" " ✓ AirNav FlightStick SN:00000010 détecté"
|
||||||
|
GAIN=$(docker logs adsb-readsb 2>&1 | grep -oP "tuner gain set to \K[0-9.]+ dB" | tail -1)
|
||||||
|
print_status "$GREEN" " → Gain actuel : $GAIN"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dongle RTL-SDR non détecté"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le symlink /dev/adsb_dongle
|
||||||
|
if [ -L "/dev/adsb_dongle" ]; then
|
||||||
|
print_status "$GREEN" " ✓ Symlink /dev/adsb_dongle présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Symlink /dev/adsb_dongle introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 3. Vérification des données ADS-B
|
||||||
|
# =============================================
|
||||||
|
print_section "3. Données ADS-B"
|
||||||
|
|
||||||
|
# Vérifier tar1090
|
||||||
|
if curl -s --max-time 5 http://127.0.0.1:8090 >/dev/null; then
|
||||||
|
TAR1090_STATS=$(curl -s --max-time 5 http://127.0.0.1:8090/data/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$TAR1090_STATS" ]; then
|
||||||
|
MESSAGES=$(echo "$TAR1090_STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ tar1090 actif — $MESSAGES messages décodés"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Impossible de récupérer les stats de tar1090"
|
||||||
|
fi
|
||||||
|
|
||||||
|
AIRCRAFT_DATA=$(curl -s --max-time 5 http://127.0.0.1:8090/data/aircraft.json 2>/dev/null)
|
||||||
|
if [ -n "$AIRCRAFT_DATA" ]; then
|
||||||
|
AIRCRAFT_COUNT=$(echo "$AIRCRAFT_DATA" | jq '. | length // 0')
|
||||||
|
# Correction : utiliser `select` pour éviter l'erreur jq
|
||||||
|
AIRCRAFT_WITH_POS=$(echo "$AIRCRAFT_DATA" | jq '[.[] | select(.lat != null and .lon != null)] | length // 0')
|
||||||
|
print_status "$GREEN" " ✓ $AIRCRAFT_COUNT avion(s) en vue dont $AIRCRAFT_WITH_POS avec position GPS"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Impossible de récupérer les données des avions"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ tar1090 non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le volume readsb-run
|
||||||
|
if docker exec adsb-readsb ls /run/readsb/aircraft.pb >/dev/null 2>&1; then
|
||||||
|
print_status "$GREEN" " ✓ Volume readsb-run : aircraft.pb présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Volume readsb-run : aircraft.pb introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 4. Vérification des ports réseau (avec adresses IP)
|
||||||
|
# =============================================
|
||||||
|
print_section "4. Ports réseau"
|
||||||
|
|
||||||
|
# Fonction pour obtenir l'adresse IP et le port d'écoute
|
||||||
|
get_listening_info() {
|
||||||
|
local port=$1
|
||||||
|
# Essayer avec ss d'abord
|
||||||
|
if command -v ss &> /dev/null; then
|
||||||
|
ss -tulnp | grep ":$port " | awk '{print $5}' | head -1
|
||||||
|
else
|
||||||
|
# Fallback sur netstat
|
||||||
|
netstat -tulnp 2>/dev/null | grep ":$port " | awk '{print $4}' | head -1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Liste des ports à vérifier : format "port:service:ip_attendue"
|
||||||
|
PORTS=(
|
||||||
|
"8080:webapp PHP:127.0.0.1"
|
||||||
|
"8090:tar1090 nginx:0.0.0.0"
|
||||||
|
"30005:Beast TCP readsb:0.0.0.0"
|
||||||
|
"5432:PostgreSQL:127.0.0.1"
|
||||||
|
"30002:RAW TCP:127.0.0.1"
|
||||||
|
"30003:SBS TCP:127.0.0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
for port_info in "${PORTS[@]}"; do
|
||||||
|
PORT=${port_info%%:*}
|
||||||
|
SERVICE=${port_info#*:}
|
||||||
|
SERVICE=${SERVICE%:*}
|
||||||
|
EXPECTED_IP=${port_info##*:}
|
||||||
|
|
||||||
|
# Obtenir l'IP et le port réel
|
||||||
|
LISTENING_INFO=$(get_listening_info "$PORT" || echo "inconnu")
|
||||||
|
ACTUAL_IP=$(echo "$LISTENING_INFO" | cut -d: -f1)
|
||||||
|
ACTUAL_PORT=$(echo "$LISTENING_INFO" | cut -d: -f2)
|
||||||
|
|
||||||
|
if timeout 2 nc -z "$EXPECTED_IP" "$PORT" 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Port $PORT ($SERVICE) — IP: ${ACTUAL_IP:-inconnu}:${ACTUAL_PORT:-$PORT} (attendu: $EXPECTED_IP:$PORT)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Port $PORT ($SERVICE) — IP: ${ACTUAL_IP:-inconnu}:${ACTUAL_PORT:-$PORT} (attendu: $EXPECTED_IP:$PORT)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier la connectivité Beast TCP via la gateway
|
||||||
|
if timeout 2 nc -z 172.20.0.1 30005 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ readsb attaché à adsb-net : 172.20.0.2 (gateway: 172.20.0.1)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Problème de connectivité avec adsb-net (172.20.0.1:30005)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 5. Vérification de PostgreSQL et du schéma adsb
|
||||||
|
# =============================================
|
||||||
|
print_section "5. PostgreSQL — schéma adsb"
|
||||||
|
|
||||||
|
# Vérifier l'accès à PostgreSQL
|
||||||
|
if docker exec wordpress-postgres pg_isready -U pgz_admin -d wpz_postgres 2>/dev/null | grep -q "accepting connections"; then
|
||||||
|
print_status "$GREEN" " ✓ PostgreSQL accessible"
|
||||||
|
PG_VERSION=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT version();" 2>/dev/null | head -1 | tr -d ' ')
|
||||||
|
print_status "$GREEN" " → $PG_VERSION"
|
||||||
|
|
||||||
|
# Obtenir le port et l'IP d'écoute de PostgreSQL
|
||||||
|
PG_LISTEN_INFO=$(docker exec wordpress-postgres ss -tulnp 2>/dev/null | grep "5432" | awk '{print $5}' | head -1 || echo "inconnu")
|
||||||
|
PG_ACTUAL_IP=$(echo "$PG_LISTEN_INFO" | cut -d: -f1)
|
||||||
|
PG_ACTUAL_PORT=$(echo "$PG_LISTEN_INFO" | cut -d: -f2)
|
||||||
|
print_status "$GREEN" " → Écoute sur : ${PG_ACTUAL_IP:-127.0.0.1}:${PG_ACTUAL_PORT:-5432}"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ PostgreSQL non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier les partitions
|
||||||
|
PARTITIONS_POSITIONS=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'positions_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
PARTITIONS_HISTORY=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'aircraft_history_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_POSITIONS sous-partitions adsb.positions"
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_HISTORY sous-partitions adsb.aircraft_history"
|
||||||
|
|
||||||
|
# Vérifier les insertions récentes
|
||||||
|
POSITIONS_5MIN=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '5 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $POSITIONS_5MIN positions insérées dans les 5 dernières minutes"
|
||||||
|
|
||||||
|
# Vérifier le remplissage de aircraft_desc
|
||||||
|
DESC_FILL=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT ROUND(COUNT(aircraft_desc)*100.0/COUNT(*),1) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '30 minutes' AND lat IS NOT NULL;" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ aircraft_desc rempli à ${DESC_FILL:-0}% (30 dernières min)"
|
||||||
|
|
||||||
|
# Taille du schéma adsb
|
||||||
|
SCHEMA_SIZE=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT pg_size_pretty(pg_total_relation_size('adsb'));" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " → Taille totale schéma adsb : ${SCHEMA_SIZE:-inconnu}"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 6. Vérification de adsb2pg (collecteur)
|
||||||
|
# =============================================
|
||||||
|
print_section "6. adsb2pg — collecteur"
|
||||||
|
|
||||||
|
if docker ps | grep -q "adsb-adsb2pg"; then
|
||||||
|
ADSB2PG_LOGS=$(docker logs adsb-adsb2pg 2>&1 | tail -1)
|
||||||
|
if echo "$ADSB2PG_LOGS" | grep -q "positions insérées"; then
|
||||||
|
print_status "$GREEN" " ✓ adsb2pg actif : $ADSB2PG_LOGS"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ adsb2pg actif mais aucun log récent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier la dernière insertion (en secondes)
|
||||||
|
LAST_INSERT_SEC=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT EXTRACT(EPOCH FROM (NOW() - MAX(inserted_at))) FROM adsb.positions WHERE inserted_at >= NOW() - INTERVAL '10 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
if [ -n "$LAST_INSERT_SEC" ]; then
|
||||||
|
LAST_INSERT_SEC_INT=${LAST_INSERT_SEC%.*} # Supprimer la partie décimale
|
||||||
|
if [ "$LAST_INSERT_SEC_INT" -le 10 ] 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Dernière insertion il y a ${LAST_INSERT_SEC_INT}s (cycle 5s actif)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dernière insertion trop ancienne (${LAST_INSERT_SEC_INT}s)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Impossible de vérifier la dernière insertion"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ adsb2pg non démarré"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 7. Vérification de la webapp
|
||||||
|
# =============================================
|
||||||
|
print_section "7. Webapp — interface utilisateur"
|
||||||
|
|
||||||
|
# Vérifier les endpoints API
|
||||||
|
API_ENDPOINTS=("action=live" "action=kpi" "action=trajectories")
|
||||||
|
for endpoint in "${API_ENDPOINTS[@]}"; do
|
||||||
|
if curl -s --max-time 5 "http://127.0.0.1:8080/api.php?$endpoint" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ api.php?$endpoint : OK (IP: 127.0.0.1:8080)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ api.php?$endpoint : KO (IP: 127.0.0.1:8080)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier le nombre d'onglets dans index.php
|
||||||
|
ONGETS=$(curl -s --max-time 5 http://127.0.0.1:8080/index.php | grep -o "<li>" | wc -l)
|
||||||
|
print_status "$GREEN" " ✓ $ONGETS onglet(s) dans index.php (IP: 127.0.0.1:8080)"
|
||||||
|
|
||||||
|
# Vérifier la taille de app.js
|
||||||
|
APP_JS_SIZE=$(curl -s --max-time 5 http://127.0.0.1:8080/app.js | wc -c)
|
||||||
|
print_status "$GREEN" " → app.js : $APP_JS_SIZE octets (IP: 127.0.0.1:8080)"
|
||||||
|
|
||||||
|
# Vérifier l'accès LAN
|
||||||
|
if curl -s --max-time 5 "http://$HOST_IP:8080" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Accès LAN http://$HOST_IP:8080 : HTTP 200 (IP: $HOST_IP:8080)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Accès LAN http://$HOST_IP:8080 : KO (IP: $HOST_IP:8080)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 8. Vérification de l'autogain
|
||||||
|
# =============================================
|
||||||
|
print_section "8. Autogain"
|
||||||
|
|
||||||
|
GAIN_STATUS=$(docker logs adsb-readsb 2>&1 | grep -oP "Gain: \K[0-9.]+ dB" | tail -1)
|
||||||
|
if [ -z "$GAIN_STATUS" ]; then
|
||||||
|
GAIN_STATUS="inconnu"
|
||||||
|
fi
|
||||||
|
print_status "$GREEN" " ✓ Gain courant : $GAIN_STATUS"
|
||||||
|
|
||||||
|
# Vérifier les cycles avec messages insuffisants
|
||||||
|
INSUFFICIENT_CYCLES=$(docker logs adsb-readsb 2>&1 | grep -c "messages insuffisants" || echo "0")
|
||||||
|
if [ "$INSUFFICIENT_CYCLES" -gt 0 ] 2>/dev/null; then
|
||||||
|
print_status "$YELLOW" " ⚠ $INSUFFICIENT_CYCLES cycle(s) avec messages insuffisants (antenne/portée ?)"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ Aucun cycle avec messages insuffisants"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 9. Vérification des statistiques readsb
|
||||||
|
# =============================================
|
||||||
|
print_section "9. Statistiques readsb"
|
||||||
|
|
||||||
|
# Essayer d'abord via l'IP du conteneur dans adsb-net
|
||||||
|
STATS=$(curl -s --max-time 5 http://172.20.0.2:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles (IP: 172.20.0.2:30003) :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
# Essayer via docker exec
|
||||||
|
STATS=$(docker exec adsb-readsb curl -s http://localhost:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles (via docker exec, IP: localhost:30003) :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Impossible de récupérer les statistiques de readsb (IP: 172.20.0.2 ou localhost:30003)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Résumé final
|
||||||
|
# =============================================
|
||||||
|
print_title "RÉSUMÉ"
|
||||||
|
|
||||||
|
# Compter les erreurs et avertissements dans le script actuel
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
SCRIPT_OUTPUT=$(./check_adsb_complete_final.sh 2>&1)
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" == *"$RED✗"* ]]; then
|
||||||
|
((ERRORS++))
|
||||||
|
elif [[ "$line" == *"$YELLOW⚠"* ]]; then
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
done <<< "$SCRIPT_OUTPUT"
|
||||||
|
|
||||||
|
if [ "$ERRORS" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then
|
||||||
|
print_status "$GREEN" " ✓ Stack opérationnel — Aucun problème détecté"
|
||||||
|
elif [ "$ERRORS" -eq 0 ]; then
|
||||||
|
print_status "$YELLOW" " ⚠ Stack opérationnel — $WARNINGS avertissement(s)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Stack en erreur — $ERRORS erreur(s), $WARNINGS avertissement(s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "$BLUE" " Dashboard : http://$HOST_IP:8080"
|
||||||
|
print_status "$BLUE" " tar1090 : http://$HOST_IP:8090"
|
||||||
|
|
||||||
|
|
||||||
350
check_adsb_mistral_v5.sh
Executable file
@ -0,0 +1,350 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Script de vérification complète de la stack ADS-B
|
||||||
|
# Version : 6.0 (7 bugs corrigés)
|
||||||
|
# Date : 26/06/2026
|
||||||
|
# Auteur : JC Abiven (avec Mistral AI)
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
# Vérifier que jq est installé
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo -e "\033[0;31m✗ jq n'est pas installé. Installez-le avec : sudo apt install jq\033[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Couleurs pour l'affichage
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# Fonction pour afficher un message avec une couleur
|
||||||
|
print_status() {
|
||||||
|
echo -e "${1}${2}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher un titre
|
||||||
|
print_title() {
|
||||||
|
echo -e "\n${BLUE}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fonction pour afficher une section
|
||||||
|
print_section() {
|
||||||
|
echo -e "\n${CYAN}═══ ${1} ═══${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Récupérer l'IP de l'hôte
|
||||||
|
HOST_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
# Afficher l'en-tête
|
||||||
|
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════╗"
|
||||||
|
echo -e "║ Stack ADS-B Linux-25 — Vérification complète ║"
|
||||||
|
echo -e "║ $(date "+%a %d/%m/%Y %H:%M:%S") ║"
|
||||||
|
echo -e "╚══════════════════════════════════════════════════════╝"
|
||||||
|
print_status "$YELLOW" " → IP détectée : $HOST_IP"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 1. Vérification des conteneurs Docker
|
||||||
|
# =============================================
|
||||||
|
print_section "1. Conteneurs Docker"
|
||||||
|
|
||||||
|
DOCKER_CONTAINERS=("adsb-readsb" "adsb-tar1090" "adsb-adsb2pg" "adsb-webapp" "wordpress-postgres")
|
||||||
|
for container in "${DOCKER_CONTAINERS[@]}"; do
|
||||||
|
if docker ps | grep -q "$container"; then
|
||||||
|
STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || echo "none")
|
||||||
|
if [ "$STATUS" = "healthy" ]; then
|
||||||
|
print_status "$GREEN" " ✓ $container [healthy]"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ $container"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ $container — non démarré"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 2. Vérification du dongle RTL-SDR
|
||||||
|
# =============================================
|
||||||
|
print_section "2. Dongle RTL-SDR"
|
||||||
|
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "rtlsdr: using device #0: Generic RTL2832U OEM (AIRNAV, ADSB_1090, SN 00000010)"; then
|
||||||
|
print_status "$GREEN" " ✓ AirNav FlightStick SN:00000010 détecté"
|
||||||
|
# Correction : Pattern grep pour extraire le gain (ex: "49.6 dB")
|
||||||
|
GAIN=$(docker logs adsb-readsb 2>&1 | grep -oP '\d+\.\d+ dB' | tail -1 || echo "inconnu")
|
||||||
|
print_status "$GREEN" " → Gain actuel : $GAIN"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dongle RTL-SDR non détecté"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le symlink /dev/adsb_dongle
|
||||||
|
if [ -L "/dev/adsb_dongle" ]; then
|
||||||
|
print_status "$GREEN" " ✓ Symlink /dev/adsb_dongle présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Symlink /dev/adsb_dongle introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 3. Vérification des données ADS-B
|
||||||
|
# =============================================
|
||||||
|
print_section "3. Données ADS-B"
|
||||||
|
|
||||||
|
# Vérifier tar1090
|
||||||
|
if curl -s --max-time 5 http://127.0.0.1:8090 >/dev/null; then
|
||||||
|
# Correction : Utiliser .messages au lieu de .totalMessages (champ réel dans tar1090)
|
||||||
|
TAR1090_STATS=$(curl -s --max-time 5 http://127.0.0.1:8090/data/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$TAR1090_STATS" ]; then
|
||||||
|
MESSAGES=$(echo "$TAR1090_STATS" | jq -r '.messages // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ tar1090 actif — $MESSAGES messages décodés"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Impossible de récupérer les stats de tar1090"
|
||||||
|
fi
|
||||||
|
|
||||||
|
AIRCRAFT_DATA=$(curl -s --max-time 5 http://127.0.0.1:8090/data/aircraft.json 2>/dev/null)
|
||||||
|
if [ -n "$AIRCRAFT_DATA" ]; then
|
||||||
|
# Correction : Utiliser .aircraft[] pour itérer sur les avions (pas sur tout le JSON)
|
||||||
|
AIRCRAFT_COUNT=$(echo "$AIRCRAFT_DATA" | jq '.aircraft | length // 0')
|
||||||
|
# Correction : Filtre valide pour .lat et .lon (évite l'erreur "Cannot index number with string")
|
||||||
|
AIRCRAFT_WITH_POS=$(echo "$AIRCRAFT_DATA" | jq '[.aircraft[] | select(.lat != null and .lon != null)] | length // 0')
|
||||||
|
print_status "$GREEN" " ✓ $AIRCRAFT_COUNT avion(s) en vue dont $AIRCRAFT_WITH_POS avec position GPS"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Impossible de récupérer les données des avions"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ tar1090 non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier le volume readsb-run
|
||||||
|
if docker exec adsb-readsb ls /run/readsb/aircraft.pb >/dev/null 2>&1; then
|
||||||
|
print_status "$GREEN" " ✓ Volume readsb-run : aircraft.pb présent"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Volume readsb-run : aircraft.pb introuvable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 4. Vérification des ports réseau (avec adresses IP)
|
||||||
|
# =============================================
|
||||||
|
print_section "4. Ports réseau"
|
||||||
|
|
||||||
|
# Fonction pour obtenir l'adresse IP et le port d'écoute
|
||||||
|
get_listening_info() {
|
||||||
|
local port=$1
|
||||||
|
if command -v ss &> /dev/null; then
|
||||||
|
ss -tulnp | grep -E ":$port\s" | awk '{print $5}' | head -1
|
||||||
|
else
|
||||||
|
netstat -tulnp 2>/dev/null | grep -E ":$port\s" | awk '{print $4}' | head -1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Liste des ports à vérifier : format "port:service:ip_attendue"
|
||||||
|
PORTS=(
|
||||||
|
"8080:webapp PHP:127.0.0.1"
|
||||||
|
"8090:tar1090 nginx:0.0.0.0"
|
||||||
|
"30005:Beast TCP readsb:0.0.0.0"
|
||||||
|
"5432:PostgreSQL:127.0.0.1"
|
||||||
|
"30002:RAW TCP:127.0.0.1"
|
||||||
|
"30003:SBS TCP:127.0.0.1"
|
||||||
|
)
|
||||||
|
|
||||||
|
for port_info in "${PORTS[@]}"; do
|
||||||
|
PORT=${port_info%%:*}
|
||||||
|
SERVICE=${port_info#*:}
|
||||||
|
SERVICE=${SERVICE%:*}
|
||||||
|
EXPECTED_IP=${port_info##*:}
|
||||||
|
|
||||||
|
LISTENING_INFO=$(get_listening_info "$PORT" || echo "inconnu")
|
||||||
|
ACTUAL_IP=$(echo "$LISTENING_INFO" | cut -d: -f1)
|
||||||
|
ACTUAL_PORT=$(echo "$LISTENING_INFO" | cut -d: -f2)
|
||||||
|
|
||||||
|
if timeout 2 nc -z "$EXPECTED_IP" "$PORT" 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Port $PORT ($SERVICE) — IP: ${ACTUAL_IP:-*}:${ACTUAL_PORT:-$PORT} (attendu: $EXPECTED_IP:$PORT)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Port $PORT ($SERVICE) — IP: ${ACTUAL_IP:-*}:${ACTUAL_PORT:-$PORT} (attendu: $EXPECTED_IP:$PORT)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier la connectivité Beast TCP via la gateway
|
||||||
|
if timeout 2 nc -z 172.20.0.1 30005 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ readsb attaché à adsb-net : 172.20.0.2 (gateway: 172.20.0.1)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Problème de connectivité avec adsb-net (172.20.0.1:30005)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 5. Vérification de PostgreSQL et du schéma adsb
|
||||||
|
# =============================================
|
||||||
|
print_section "5. PostgreSQL — schéma adsb"
|
||||||
|
|
||||||
|
# Vérifier l'accès à PostgreSQL
|
||||||
|
if docker exec wordpress-postgres pg_isready -U pgz_admin -d wpz_postgres 2>/dev/null | grep -q "accepting connections"; then
|
||||||
|
print_status "$GREEN" " ✓ PostgreSQL accessible"
|
||||||
|
PG_VERSION=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT version();" 2>/dev/null | head -1 | tr -d ' ')
|
||||||
|
print_status "$GREEN" " → $PG_VERSION"
|
||||||
|
|
||||||
|
# Obtenir le port et l'IP d'écoute de PostgreSQL
|
||||||
|
PG_LISTEN_INFO=$(docker exec wordpress-postgres ss -tulnp 2>/dev/null | grep -E ":5432\s" | awk '{print $5}' | head -1 || echo "127.0.0.1:5432")
|
||||||
|
PG_ACTUAL_IP=$(echo "$PG_LISTEN_INFO" | cut -d: -f1)
|
||||||
|
PG_ACTUAL_PORT=$(echo "$PG_LISTEN_INFO" | cut -d: -f2)
|
||||||
|
print_status "$GREEN" " → Écoute sur : ${PG_ACTUAL_IP}:${PG_ACTUAL_PORT}"
|
||||||
|
|
||||||
|
# Correction : Utiliser pg_class + pg_namespace pour obtenir la taille du schéma
|
||||||
|
SCHEMA_SIZE=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "
|
||||||
|
SELECT pg_size_pretty(SUM(pg_total_relation_size(C.oid)))
|
||||||
|
FROM pg_class C
|
||||||
|
JOIN pg_namespace N ON N.oid = C.relnamespace
|
||||||
|
WHERE N.nspname = 'adsb';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " → Taille totale schéma adsb : ${SCHEMA_SIZE:-inconnu}"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ PostgreSQL non accessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier les partitions
|
||||||
|
PARTITIONS_POSITIONS=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'positions_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
PARTITIONS_HISTORY=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'adsb' AND tablename LIKE 'aircraft_history_%';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_POSITIONS sous-partitions adsb.positions"
|
||||||
|
print_status "$GREEN" " ✓ $PARTITIONS_HISTORY sous-partitions adsb.aircraft_history"
|
||||||
|
|
||||||
|
# Vérifier les insertions récentes
|
||||||
|
POSITIONS_5MIN=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT COUNT(*) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '5 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ $POSITIONS_5MIN positions insérées dans les 5 dernières minutes"
|
||||||
|
|
||||||
|
# Vérifier le remplissage de aircraft_desc
|
||||||
|
DESC_FILL=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT ROUND(COUNT(aircraft_desc)*100.0/COUNT(*),1) FROM adsb.positions WHERE ts >= NOW() - INTERVAL '30 minutes' AND lat IS NOT NULL;" 2>/dev/null | tr -d ' ')
|
||||||
|
print_status "$GREEN" " ✓ aircraft_desc rempli à ${DESC_FILL:-0}% (30 dernières min)"
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 6. Vérification de adsb2pg (collecteur)
|
||||||
|
# =============================================
|
||||||
|
print_section "6. adsb2pg — collecteur"
|
||||||
|
|
||||||
|
if docker ps | grep -q "adsb-adsb2pg"; then
|
||||||
|
ADSB2PG_LOGS=$(docker logs adsb-adsb2pg 2>&1 | tail -1)
|
||||||
|
if echo "$ADSB2PG_LOGS" | grep -q "positions insérées"; then
|
||||||
|
print_status "$GREEN" " ✓ adsb2pg actif : $ADSB2PG_LOGS"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ adsb2pg actif mais aucun log récent"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier la dernière insertion (en secondes)
|
||||||
|
LAST_INSERT_SEC=$(docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c "SELECT EXTRACT(EPOCH FROM (NOW() - MAX(inserted_at))) FROM adsb.positions WHERE inserted_at >= NOW() - INTERVAL '10 minutes';" 2>/dev/null | tr -d ' ')
|
||||||
|
if [ -n "$LAST_INSERT_SEC" ]; then
|
||||||
|
LAST_INSERT_SEC_INT=${LAST_INSERT_SEC%.*} # Supprimer la partie décimale
|
||||||
|
if [ "$LAST_INSERT_SEC_INT" -le 10 ] 2>/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Dernière insertion il y a ${LAST_INSERT_SEC_INT}s (cycle 5s actif)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Dernière insertion trop ancienne (${LAST_INSERT_SEC_INT}s)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Impossible de vérifier la dernière insertion"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ adsb2pg non démarré"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 7. Vérification de la webapp
|
||||||
|
# =============================================
|
||||||
|
print_section "7. Webapp — interface utilisateur"
|
||||||
|
|
||||||
|
# Vérifier les endpoints API
|
||||||
|
API_ENDPOINTS=("action=live" "action=kpi" "action=trajectories")
|
||||||
|
for endpoint in "${API_ENDPOINTS[@]}"; do
|
||||||
|
if curl -s --max-time 5 "http://127.0.0.1:8080/api.php?$endpoint" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ api.php?$endpoint : OK (IP: 127.0.0.1:8080)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ api.php?$endpoint : KO (IP: 127.0.0.1:8080)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Correction : Compter les <li> directement dans le conteneur webapp (pas via curl)
|
||||||
|
ONGETS=$(docker exec adsb-webapp grep -c "<li>" /var/www/html/index.php 2>/dev/null || echo "0")
|
||||||
|
print_status "$GREEN" " ✓ $ONGETS onglet(s) dans index.php (IP: 127.0.0.1:8080)"
|
||||||
|
|
||||||
|
# Correction : Taille de app.js directement dans le conteneur
|
||||||
|
APP_JS_SIZE=$(docker exec adsb-webapp wc -c < /var/www/html/app.js 2>/dev/null | awk '{print $1}' || echo "0")
|
||||||
|
print_status "$GREEN" " → app.js : $APP_JS_SIZE octets (IP: 127.0.0.1:8080)"
|
||||||
|
|
||||||
|
# Vérifier l'accès LAN
|
||||||
|
if curl -s --max-time 5 "http://$HOST_IP:8080" >/dev/null; then
|
||||||
|
print_status "$GREEN" " ✓ Accès LAN http://$HOST_IP:8080 : HTTP 200 (IP: $HOST_IP:8080)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Accès LAN http://$HOST_IP:8080 : KO (IP: $HOST_IP:8080)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 8. Vérification de l'autogain
|
||||||
|
# =============================================
|
||||||
|
print_section "8. Autogain"
|
||||||
|
|
||||||
|
# Correction : Pattern grep pour extraire le gain (ex: "49.6 dB")
|
||||||
|
GAIN_STATUS=$(docker logs adsb-readsb 2>&1 | grep -oP '\d+\.\d+ dB' | tail -1 || echo "inconnu")
|
||||||
|
print_status "$GREEN" " ✓ Gain courant : $GAIN_STATUS"
|
||||||
|
|
||||||
|
# Vérifier les cycles avec messages insuffisants
|
||||||
|
INSUFFICIENT_CYCLES=$(docker logs adsb-readsb 2>&1 | grep -c "messages insuffisants" || echo "0")
|
||||||
|
if [ "$INSUFFICIENT_CYCLES" -gt 0 ] 2>/dev/null; then
|
||||||
|
print_status "$YELLOW" " ⚠ $INSUFFICIENT_CYCLES cycle(s) avec messages insuffisants (antenne/portée ?)"
|
||||||
|
else
|
||||||
|
print_status "$GREEN" " ✓ Aucun cycle avec messages insuffisants"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# 9. Statistiques readsb
|
||||||
|
# =============================================
|
||||||
|
print_section "9. Statistiques readsb"
|
||||||
|
|
||||||
|
# Correction : Endpoint correct pour les stats readsb (via tar1090 ou lighttpd interne)
|
||||||
|
STATS=$(curl -s --max-time 5 http://127.0.0.1:8090/data/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles (IP: 127.0.0.1:8090) :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
# Fallback : Essayer via lighttpd interne (port 80 dans le conteneur readsb)
|
||||||
|
STATS=$(docker exec adsb-readsb curl -s http://localhost/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" " ✓ Statistiques readsb accessibles (via lighttpd, IP: localhost:80) :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
print_status "$YELLOW" " ⚠ Impossible de récupérer les statistiques de readsb (essayez : docker exec adsb-readsb curl -s http://localhost/readsb/stats.json | jq)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# Résumé final
|
||||||
|
# =============================================
|
||||||
|
print_title "RÉSUMÉ"
|
||||||
|
|
||||||
|
# Compter les erreurs et avertissements dans le script actuel
|
||||||
|
ERRORS=0
|
||||||
|
WARNINGS=0
|
||||||
|
SCRIPT_OUTPUT=$(mktemp)
|
||||||
|
./check_adsb_v6_fixed.sh > "$SCRIPT_OUTPUT" 2>&1
|
||||||
|
while IFS= read -r line; do
|
||||||
|
if [[ "$line" == *"$RED✗"* ]]; then
|
||||||
|
((ERRORS++))
|
||||||
|
elif [[ "$line" == *"$YELLOW⚠"* ]]; then
|
||||||
|
((WARNINGS++))
|
||||||
|
fi
|
||||||
|
done < "$SCRIPT_OUTPUT"
|
||||||
|
rm "$SCRIPT_OUTPUT"
|
||||||
|
|
||||||
|
if [ "$ERRORS" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then
|
||||||
|
print_status "$GREEN" " ✓ Stack opérationnel — Aucun problème détecté"
|
||||||
|
elif [ "$ERRORS" -eq 0 ]; then
|
||||||
|
print_status "$YELLOW" " ⚠ Stack opérationnel — $WARNINGS avertissement(s)"
|
||||||
|
else
|
||||||
|
print_status "$RED" " ✗ Stack en erreur — $ERRORS erreur(s), $WARNINGS avertissement(s)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "$BLUE" " Dashboard : http://$HOST_IP:8080"
|
||||||
|
print_status "$BLUE" " tar1090 : http://$HOST_IP:8090"
|
||||||
|
|
||||||
281
check_adsb_mistral_v5_ByClaude.sh
Executable file
@ -0,0 +1,281 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# =============================================
|
||||||
|
# Script de vérification complète de la stack ADS-B
|
||||||
|
# Version : 9.0 (Corrigé par Claude)
|
||||||
|
# Date : 26/06/2026
|
||||||
|
# Auteur : JC Abiven
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
echo -e "\033[0;31m✗ jq n'est pas installé : sudo apt install jq\033[0m"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'; MAGENTA='\033[0;35m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||||
|
|
||||||
|
ok() { echo -e "${GREEN} ✓${NC} $1"; }
|
||||||
|
fail() { echo -e "${RED} ✗${NC} $1"; ERRORS=$((ERRORS+1)); }
|
||||||
|
warn() { echo -e "${YELLOW} ⚠${NC} $1"; WARNS=$((WARNS+1)); }
|
||||||
|
info() { echo -e "${CYAN} →${NC} $1"; }
|
||||||
|
sec() { echo -e "\n${CYAN}═══ $1 ═══${NC}"; }
|
||||||
|
|
||||||
|
ERRORS=0; WARNS=0
|
||||||
|
HOST_IP=$(ip -4 addr show wlp58s0 2>/dev/null | grep -oP '(?<=inet )\d+\.\d+\.\d+\.\d+' | head -1)
|
||||||
|
[ -z "$HOST_IP" ] && HOST_IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════╗"
|
||||||
|
echo -e "║ Stack ADS-B Linux-25 — Vérification complète ║"
|
||||||
|
echo -e "║ $(date '+%a %d/%m/%Y %H:%M:%S') ║"
|
||||||
|
echo -e "╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
info "IP détectée : $HOST_IP"
|
||||||
|
|
||||||
|
# ── 1. Conteneurs ─────────────────────────────────────────────────────────────
|
||||||
|
sec "1. Conteneurs Docker"
|
||||||
|
for ctn in adsb-readsb adsb-tar1090 adsb-adsb2pg adsb-webapp wordpress-postgres; do
|
||||||
|
STATUS=$(docker inspect --format '{{.State.Status}}' "$ctn" 2>/dev/null)
|
||||||
|
HEALTH=$(docker inspect --format '{{.State.Health.Status}}' "$ctn" 2>/dev/null)
|
||||||
|
UPTIME=$(docker inspect --format '{{.State.StartedAt}}' "$ctn" 2>/dev/null | \
|
||||||
|
xargs -I{} date -d {} '+%d/%m %H:%M' 2>/dev/null)
|
||||||
|
if [ "$STATUS" = "running" ]; then
|
||||||
|
LABEL="$ctn"
|
||||||
|
[ "$HEALTH" = "healthy" ] && LABEL="$ctn [healthy]"
|
||||||
|
[ "$HEALTH" = "unhealthy" ] && LABEL="$ctn [unhealthy]" && WARNS=$((WARNS+1))
|
||||||
|
[ "$HEALTH" = "unhealthy" ] \
|
||||||
|
&& warn "$LABEL — démarré $UPTIME" \
|
||||||
|
|| ok "$LABEL — démarré $UPTIME"
|
||||||
|
else
|
||||||
|
fail "$ctn — STATUS=${STATUS:-absent}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 2. Dongle RTL-SDR ─────────────────────────────────────────────────────────
|
||||||
|
sec "2. Dongle RTL-SDR"
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "SN 00000010"; then
|
||||||
|
# BUG CORRIGÉ : grep -oP avec lookbehind correct
|
||||||
|
GAIN=$(docker logs adsb-readsb 2>&1 | grep "tuner gain set to" | tail -1 \
|
||||||
|
| grep -oP '[\d.]+(?= dB)')
|
||||||
|
ok "AirNav FlightStick SN:00000010 détecté — gain ${GAIN:-?} dB"
|
||||||
|
else
|
||||||
|
fail "Dongle SN:00000010 non détecté"
|
||||||
|
fi
|
||||||
|
[ -L "/dev/adsb_dongle" ] && ok "Symlink /dev/adsb_dongle présent" \
|
||||||
|
|| warn "Symlink /dev/adsb_dongle introuvable"
|
||||||
|
|
||||||
|
# ── 3. Données ADS-B ──────────────────────────────────────────────────────────
|
||||||
|
sec "3. Données ADS-B"
|
||||||
|
AC_JSON=$(curl -s --max-time 5 "http://127.0.0.1:8090/data/aircraft.json" 2>/dev/null)
|
||||||
|
if [ -n "$AC_JSON" ]; then
|
||||||
|
# BUG CORRIGÉ : utiliser .aircraft[] et pas .[] (qui itère aussi sur messages/now)
|
||||||
|
MSGS=$(echo "$AC_JSON" | jq -r '.messages // 0')
|
||||||
|
AVIONS=$(echo "$AC_JSON" | jq '[.aircraft // []] | length')
|
||||||
|
WITH_POS=$(echo "$AC_JSON" | jq '[.aircraft[]? | select(.lat != null and .lon != null)] | length')
|
||||||
|
[ "${MSGS:-0}" -gt 0 ] \
|
||||||
|
&& ok "tar1090 actif — $(printf '%d' "$MSGS") messages décodés" \
|
||||||
|
|| warn "tar1090 répond — 0 message (démarrage récent ?)"
|
||||||
|
ok "$AVIONS avion(s) en vue dont $WITH_POS avec position GPS"
|
||||||
|
else
|
||||||
|
fail "tar1090 ne répond pas sur :8090/data/aircraft.json"
|
||||||
|
AVIONS=0; WITH_POS=0; MSGS=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# BUG CORRIGÉ : vérifier via docker exec (le volume tmpfs n'est pas accessible hôte)
|
||||||
|
if docker exec adsb-readsb test -f /run/readsb/aircraft.pb 2>/dev/null; then
|
||||||
|
ok "Volume readsb-run : aircraft.pb présent dans le conteneur readsb"
|
||||||
|
elif docker exec adsb-tar1090 test -f /run/readsb/aircraft.pb 2>/dev/null; then
|
||||||
|
ok "Volume readsb-run : aircraft.pb présent dans le conteneur tar1090"
|
||||||
|
else
|
||||||
|
warn "aircraft.pb introuvable dans les conteneurs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. Ports réseau ───────────────────────────────────────────────────────────
|
||||||
|
sec "4. Ports réseau"
|
||||||
|
declare -A PORT_LABELS=(
|
||||||
|
["8080"]="webapp PHP" ["8090"]="tar1090 nginx"
|
||||||
|
["30005"]="Beast TCP" ["5432"]="PostgreSQL"
|
||||||
|
["30002"]="RAW TCP" ["30003"]="SBS TCP"
|
||||||
|
)
|
||||||
|
for port in 8080 8090 30005 5432 30002 30003; do
|
||||||
|
LISTEN=$(ss -tlnp 2>/dev/null | grep ":${port}[[:space:]]" | awk '{print $4}' | head -1)
|
||||||
|
if [ -n "$LISTEN" ]; then
|
||||||
|
ok "Port $port en écoute sur $LISTEN — ${PORT_LABELS[$port]}"
|
||||||
|
else
|
||||||
|
fail "Port $port absent — ${PORT_LABELS[$port]}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Connexion Beast via gateway adsb-net
|
||||||
|
READSB_IP=$(docker inspect adsb-readsb 2>/dev/null | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
d=json.load(sys.stdin)[0]
|
||||||
|
for n,v in d['NetworkSettings']['Networks'].items():
|
||||||
|
if 'adsb' in n: print(v['IPAddress'])" 2>/dev/null)
|
||||||
|
if [ -n "$READSB_IP" ] && [ "$READSB_IP" != "None" ]; then
|
||||||
|
ok "readsb attaché à adsb-net : $READSB_IP (gateway 172.20.0.1)"
|
||||||
|
else
|
||||||
|
fail "readsb non attaché à adsb-net — Beast TCP impossible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 5. PostgreSQL ─────────────────────────────────────────────────────────────
|
||||||
|
sec "5. PostgreSQL — schéma adsb"
|
||||||
|
PG="docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -t -c"
|
||||||
|
|
||||||
|
PG_VER=$($PG "SELECT version();" 2>/dev/null | head -1 | sed 's/ */ /g' | xargs)
|
||||||
|
if [ -n "$PG_VER" ]; then
|
||||||
|
ok "PostgreSQL accessible"
|
||||||
|
info "${PG_VER:0:70}"
|
||||||
|
else
|
||||||
|
fail "PostgreSQL inaccessible"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# BUG CORRIGÉ : pg_tables plutôt que pg_class pour les partitions feuilles
|
||||||
|
PART_POS=$($PG "SELECT COUNT(*) FROM pg_tables
|
||||||
|
WHERE schemaname='adsb' AND tablename LIKE 'positions_%';" 2>/dev/null | xargs)
|
||||||
|
ok "${PART_POS:-0} sous-partitions adsb.positions"
|
||||||
|
|
||||||
|
PART_HIS=$($PG "SELECT COUNT(*) FROM pg_tables
|
||||||
|
WHERE schemaname='adsb' AND tablename LIKE 'aircraft_history_%';" 2>/dev/null | xargs)
|
||||||
|
ok "${PART_HIS:-0} sous-partitions adsb.aircraft_history"
|
||||||
|
|
||||||
|
POS_5MIN=$($PG "SELECT COUNT(*) FROM adsb.positions
|
||||||
|
WHERE ts >= NOW() - INTERVAL '5 minutes';" 2>/dev/null | xargs)
|
||||||
|
[ "${POS_5MIN:-0}" -gt 0 ] 2>/dev/null \
|
||||||
|
&& ok "$POS_5MIN positions insérées dans les 5 dernières minutes" \
|
||||||
|
|| warn "Aucune position dans les 5 dernières minutes"
|
||||||
|
|
||||||
|
PCT_DESC=$($PG "SELECT ROUND(COUNT(aircraft_desc)*100.0/NULLIF(COUNT(*),0),1)
|
||||||
|
FROM adsb.positions WHERE ts >= NOW() - INTERVAL '30 minutes'
|
||||||
|
AND lat IS NOT NULL;" 2>/dev/null | xargs)
|
||||||
|
python3 -c "exit(0 if float('${PCT_DESC:-0}') >= 80 else 1)" 2>/dev/null \
|
||||||
|
&& ok "aircraft_desc rempli à ${PCT_DESC}% (30 dernières min)" \
|
||||||
|
|| warn "aircraft_desc rempli à ${PCT_DESC:-0}% (< 80%)"
|
||||||
|
|
||||||
|
AV_TODAY=$($PG "SELECT COUNT(DISTINCT icao) FROM adsb.aircraft_history
|
||||||
|
WHERE session_start >= CURRENT_DATE;" 2>/dev/null | xargs)
|
||||||
|
SESS_TODAY=$($PG "SELECT COUNT(*) FROM adsb.aircraft_history
|
||||||
|
WHERE session_start >= CURRENT_DATE;" 2>/dev/null | xargs)
|
||||||
|
info "Aujourd'hui : ${AV_TODAY:-0} avions distincts, ${SESS_TODAY:-0} sessions"
|
||||||
|
|
||||||
|
# BUG CORRIGÉ : pg_size_pretty sur le schéma via pg_class
|
||||||
|
DB_SIZE=$($PG "SELECT pg_size_pretty(SUM(pg_total_relation_size(c.oid)))
|
||||||
|
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
||||||
|
WHERE n.nspname='adsb';" 2>/dev/null | xargs)
|
||||||
|
info "Taille totale schéma adsb : ${DB_SIZE:-?}"
|
||||||
|
|
||||||
|
# ── 6. adsb2pg ────────────────────────────────────────────────────────────────
|
||||||
|
sec "6. adsb2pg — collecteur"
|
||||||
|
LAST_INS=$(docker logs adsb-adsb2pg 2>&1 | grep "positions insérées" | tail -1)
|
||||||
|
if [ -n "$LAST_INS" ]; then
|
||||||
|
ok "adsb2pg actif"
|
||||||
|
info "$LAST_INS"
|
||||||
|
# BUG CORRIGÉ : calculer l'âge depuis PostgreSQL (plus fiable que les logs)
|
||||||
|
LAST_SEC=$($PG "SELECT EXTRACT(EPOCH FROM (NOW() - MAX(inserted_at)))::INT
|
||||||
|
FROM adsb.positions WHERE inserted_at >= NOW() - INTERVAL '10 minutes';" \
|
||||||
|
2>/dev/null | xargs)
|
||||||
|
LAST_SEC_INT=${LAST_SEC%.*}
|
||||||
|
if [ -n "$LAST_SEC_INT" ] && [ "$LAST_SEC_INT" -le 15 ] 2>/dev/null; then
|
||||||
|
ok "Dernière insertion il y a ${LAST_SEC_INT}s (cycle 5s actif)"
|
||||||
|
elif [ -n "$LAST_SEC_INT" ]; then
|
||||||
|
warn "Dernière insertion il y a ${LAST_SEC_INT}s (> 15s)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Aucun log d'insertion trouvé dans adsb2pg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 7. Webapp ─────────────────────────────────────────────────────────────────
|
||||||
|
sec "7. Webapp — interface utilisateur"
|
||||||
|
for action in live kpi trajectories; do
|
||||||
|
RESP=$(curl -s --max-time 5 \
|
||||||
|
"http://127.0.0.1:8080/api.php?action=${action}&period=24&granularity=60" 2>/dev/null)
|
||||||
|
ERR=$(echo "$RESP" | jq -r '.error // empty' 2>/dev/null)
|
||||||
|
if echo "$RESP" | jq . >/dev/null 2>&1; then
|
||||||
|
[ -n "$ERR" ] && warn "api.php?action=${action} : $ERR" \
|
||||||
|
|| ok "api.php?action=${action} : JSON valide"
|
||||||
|
else
|
||||||
|
fail "api.php?action=${action} : réponse non-JSON"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# BUG CORRIGÉ : compter les onglets directement dans le conteneur
|
||||||
|
TAB_CNT=$(docker exec adsb-webapp grep -c 'href="#tab' /var/www/html/index.php 2>/dev/null)
|
||||||
|
ok "${TAB_CNT:-?} onglets dans index.php"
|
||||||
|
[ "${TAB_CNT:-0}" -lt 5 ] 2>/dev/null && warn "Moins de 5 onglets — index.php peut être obsolète"
|
||||||
|
|
||||||
|
# BUG CORRIGÉ : taille app.js directement dans le conteneur
|
||||||
|
JS_SZ=$(docker exec adsb-webapp wc -c /var/www/html/js/app.js 2>/dev/null | awk '{print $1}')
|
||||||
|
info "app.js : ${JS_SZ:-?} octets"
|
||||||
|
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://${HOST_IP}:8080/" 2>/dev/null)
|
||||||
|
[ "$HTTP_CODE" = "200" ] \
|
||||||
|
&& ok "Accès LAN http://${HOST_IP}:8080 : HTTP $HTTP_CODE" \
|
||||||
|
|| warn "Accès LAN http://${HOST_IP}:8080 : HTTP ${HTTP_CODE:-?}"
|
||||||
|
|
||||||
|
# ── 8. Autogain ───────────────────────────────────────────────────────────────
|
||||||
|
sec "8. Autogain"
|
||||||
|
# BUG CORRIGÉ : pattern grep correct pour le gain
|
||||||
|
CUR_GAIN=$(docker logs adsb-readsb 2>&1 | grep "tuner gain set to" | tail -1 \
|
||||||
|
| grep -oP '[\d.]+(?= dB)')
|
||||||
|
AG_STATE=$(docker logs adsb-readsb 2>&1 | grep -oP "(?<=state ')[\w]+" | tail -1)
|
||||||
|
[ -n "$CUR_GAIN" ] \
|
||||||
|
&& ok "Gain courant : ${CUR_GAIN} dB — état : ${AG_STATE:-inconnu}" \
|
||||||
|
|| warn "Gain courant inconnu"
|
||||||
|
|
||||||
|
# BUG CORRIGÉ : recherche en anglais (les logs readsb sont en anglais)
|
||||||
|
INSUF=$(docker logs adsb-readsb 2>&1 | grep -c "Insufficient messages")
|
||||||
|
[ "$INSUF" -gt 3 ] \
|
||||||
|
&& warn "$INSUF cycles avec messages insuffisants (antenne/portée ?)" \
|
||||||
|
|| info "$INSUF cycle(s) avec messages insuffisants"
|
||||||
|
|
||||||
|
# ── 9. Statistiques readsb ────────────────────────────────────────────────────
|
||||||
|
sec "9. Statistiques readsb"
|
||||||
|
# BUG CORRIGÉ : les stats sont sur lighttpd port 8080 interne, pas 30003
|
||||||
|
# Le bon endpoint est /data/stats.json via tar1090
|
||||||
|
STATS_JSON=$(curl -s --max-time 5 "http://127.0.0.1:8090/data/stats.json" 2>/dev/null)
|
||||||
|
if [ -n "$STATS_JSON" ] && echo "$STATS_JSON" | jq . >/dev/null 2>&1; then
|
||||||
|
# Stats last 1 min
|
||||||
|
MSGS_1MIN=$(echo "$STATS_JSON" | jq -r '.last1min.local.messages_strong_signal // .last1min.local.decoded // 0' 2>/dev/null)
|
||||||
|
MSGS_TOTAL=$(echo "$STATS_JSON" | jq -r '.total.local.messages // 0' 2>/dev/null)
|
||||||
|
RANGE=$(echo "$STATS_JSON" | jq -r '.last1min.local.max_distance_meters // 0' 2>/dev/null)
|
||||||
|
RANGE_KM=$(python3 -c "print(round(${RANGE:-0}/1000,1))" 2>/dev/null)
|
||||||
|
TRACKS=$(echo "$STATS_JSON" | jq -r '.last1min.local.tracks_with_pos // 0' 2>/dev/null)
|
||||||
|
ok "Statistiques readsb disponibles (via tar1090 :8090)"
|
||||||
|
info "Messages totaux : ${MSGS_TOTAL:-0}"
|
||||||
|
info "Messages dernière minute : ${MSGS_1MIN:-0}"
|
||||||
|
info "Pistes avec position (1min) : ${TRACKS:-0}"
|
||||||
|
info "Portée max (1min) : ${RANGE_KM:-0} km"
|
||||||
|
else
|
||||||
|
# Fallback : stats depuis lighttpd interne readsb
|
||||||
|
STATS2=$(docker exec adsb-readsb wget -qO- "http://localhost:8080/data/stats.json" 2>/dev/null)
|
||||||
|
if [ -n "$STATS2" ] && echo "$STATS2" | jq . >/dev/null 2>&1; then
|
||||||
|
MSGS_TOTAL=$(echo "$STATS2" | jq -r '.total.local.messages // 0')
|
||||||
|
ok "Statistiques readsb (via lighttpd interne) — Messages totaux : $MSGS_TOTAL"
|
||||||
|
else
|
||||||
|
warn "Statistiques readsb non disponibles"
|
||||||
|
info "Utiliser : docker exec adsb-readsb cat /run/readsb/stats.pb"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fichiers stats protobuf (toujours disponibles)
|
||||||
|
STATS_AGE=$(docker exec adsb-readsb stat -c %Y /run/readsb/stats.pb 2>/dev/null)
|
||||||
|
if [ -n "$STATS_AGE" ]; then
|
||||||
|
AGE_S=$(( $(date +%s) - STATS_AGE ))
|
||||||
|
ok "stats.pb dans readsb-run, mis à jour il y a ${AGE_S}s"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── RÉSUMÉ ────────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo -e "${MAGENTA}╔══════════════════════════════════════════════════════╗"
|
||||||
|
echo -e "║ RÉSUMÉ ║"
|
||||||
|
echo -e "╠══════════════════════════════════════════════════════╣${NC}"
|
||||||
|
if [ "$ERRORS" -eq 0 ] && [ "$WARNS" -eq 0 ]; then
|
||||||
|
echo -e "${MAGENTA}║${NC} ${GREEN}✓ Stack opérationnel — aucune anomalie détectée${NC} ${MAGENTA}║${NC}"
|
||||||
|
elif [ "$ERRORS" -eq 0 ]; then
|
||||||
|
echo -e "${MAGENTA}║${NC} ${YELLOW}⚠ Stack opérationnel — $WARNS avertissement(s)${NC} ${MAGENTA}║${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${MAGENTA}║${NC} ${RED}✗ $ERRORS erreur(s), $WARNS avertissement(s)${NC} ${MAGENTA}║${NC}"
|
||||||
|
fi
|
||||||
|
echo -e "${MAGENTA}║${NC} ${BLUE}Dashboard : http://${HOST_IP}:8080${NC} ${MAGENTA}║${NC}"
|
||||||
|
echo -e "${MAGENTA}║${NC} ${BLUE}tar1090 : http://${HOST_IP}:8090${NC} ${MAGENTA}║${NC}"
|
||||||
|
echo -e "${MAGENTA}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
exit $ERRORS
|
||||||
82
check_adsb_old.sh
Executable file
@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Couleurs pour l'affichage
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
print_status() {
|
||||||
|
echo -e "${1}${2}${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 1. Vérifier que le conteneur readsb est en cours d'exécution
|
||||||
|
print_status "$YELLOW" "=== Vérification du conteneur readsb ==="
|
||||||
|
if docker ps | grep -q "adsb-readsb"; then
|
||||||
|
print_status "$GREEN" "✓ Le conteneur adsb-readsb est en cours d'exécution."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Le conteneur adsb-readsb n'est pas en cours d'exécution."
|
||||||
|
docker ps
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Vérifier les logs de readsb pour la détection du dongle
|
||||||
|
print_status "$YELLOW" "\n=== Vérification de la détection du dongle RTL-SDR ==="
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "RTL2832U\|AIRNAV\|ADSB_1090"; then
|
||||||
|
print_status "$GREEN" "✓ Le dongle RTL2832U (AirNav FlightStick) est détecté par readsb."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Le dongle RTL2832U n'est pas détecté dans les logs."
|
||||||
|
print_status "$YELLOW" "Dernières lignes des logs :"
|
||||||
|
docker logs adsb-readsb 2>&1 | tail -20
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Vérifier que readsb décode des messages ADS-B
|
||||||
|
print_status "$YELLOW" "\n=== Vérification du décodage ADS-B ==="
|
||||||
|
if docker logs adsb-readsb 2>&1 | grep -q "Decoded\|Accepted"; then
|
||||||
|
print_status "$GREEN" "✓ readsb décode des messages ADS-B."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Aucun message ADS-B décodé trouvé dans les logs."
|
||||||
|
print_status "$YELLOW" "Dernières lignes des logs :"
|
||||||
|
docker logs adsb-readsb 2>&1 | tail -20
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Vérifier que le port 30005 (Beast) émet des données
|
||||||
|
print_status "$YELLOW" "\n=== Vérification du port 30005 (Beast) ==="
|
||||||
|
if timeout 5 bash -c "echo > /dev/tcp/127.0.0.1/30005" 2>/dev/null; then
|
||||||
|
print_status "$GREEN" "✓ Le port 30005 est accessible localement."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Le port 30005 n'est pas accessible localement."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Vérifier que tar1090 est accessible
|
||||||
|
print_status "$YELLOW" "\n=== Vérification de tar1090 ==="
|
||||||
|
if curl -s http://127.0.0.1:8090 >/dev/null; then
|
||||||
|
print_status "$GREEN" "✓ tar1090 est accessible sur http://127.0.0.1:8090."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ tar1090 n'est pas accessible sur http://127.0.0.1:8090."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Vérifier les statistiques de readsb
|
||||||
|
print_status "$YELLOW" "\n=== Vérification des statistiques readsb ==="
|
||||||
|
STATS=$(curl -s http://localhost:30003/readsb/stats.json 2>/dev/null)
|
||||||
|
if [ -n "$STATS" ]; then
|
||||||
|
TOTAL_MSG=$(echo "$STATS" | jq -r '.totalMessages // "N/A"')
|
||||||
|
MSG_PER_SEC=$(echo "$STATS" | jq -r '.messagesPerSecond // "N/A"')
|
||||||
|
print_status "$GREEN" "✓ Statistiques readsb accessibles :"
|
||||||
|
print_status "$GREEN" " - Messages totaux : $TOTAL_MSG"
|
||||||
|
print_status "$GREEN" " - Messages/seconde : $MSG_PER_SEC"
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Impossible de récupérer les statistiques de readsb."
|
||||||
|
print_status "$YELLOW" "Vérifiez que le port 30003 est accessible depuis cette machine."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 7. Vérifier la présence d'avions sur tar1090
|
||||||
|
print_status "$YELLOW" "\n=== Vérification des avions sur tar1090 ==="
|
||||||
|
AIRCRAFT_COUNT=$(curl -s http://127.0.0.1:8090/data/aircraft.json | jq '. | length // 0')
|
||||||
|
if [ "$AIRCRAFT_COUNT" -gt 0 ]; then
|
||||||
|
print_status "$GREEN" "✓ $AIRCRAFT_COUNT avion(s) détecté(s) sur tar1090."
|
||||||
|
else
|
||||||
|
print_status "$RED" "✗ Aucun avion détecté sur tar1090 (vérifiez la portée ou l'antenne)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_status "$YELLOW" "\n=== Fin des vérifications ==="
|
||||||
266
docker-compose.yml
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
---
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Stack ADS-B Linux-25 — 1 à 3 récepteurs, pilotés par profiles Compose
|
||||||
|
#
|
||||||
|
# Les services d'un récepteur sont conditionnés au profile rx<n>. La liste des
|
||||||
|
# profiles actifs et les numéros de série sont générés depuis le matériel
|
||||||
|
# réellement présent :
|
||||||
|
#
|
||||||
|
# /data/adsb/scripts/gen_env.sh
|
||||||
|
# docker compose --env-file .env.receivers up -d --remove-orphans
|
||||||
|
#
|
||||||
|
# Secrets : PG_PASSWORD est lu depuis .env (non versionné), plus en dur ici.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
x-readsb-common: &readsb-common
|
||||||
|
image: ghcr.io/sdr-enthusiasts/docker-readsb-protobuf:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
privileged: true
|
||||||
|
devices:
|
||||||
|
- /dev/bus/usb:/dev/bus/usb
|
||||||
|
networks:
|
||||||
|
- adsb-net
|
||||||
|
tmpfs:
|
||||||
|
- /var/log:size=32M
|
||||||
|
|
||||||
|
x-readsb-env: &readsb-env
|
||||||
|
TZ: Europe/Paris
|
||||||
|
READSB_DCFILTER: "true"
|
||||||
|
READSB_DEVICE_TYPE: rtlsdr
|
||||||
|
READSB_FIX: "true"
|
||||||
|
READSB_LAT: "48.8236"
|
||||||
|
READSB_LON: "2.2770"
|
||||||
|
# READSB_MODEAC: "true"
|
||||||
|
READSB_RX_LOCATION_ACCURACY: "2"
|
||||||
|
READSB_STATS_RANGE: "true"
|
||||||
|
READSB_NET_ENABLE: "true"
|
||||||
|
|
||||||
|
x-tar1090-common: &tar1090-common
|
||||||
|
image: ghcr.io/sdr-enthusiasts/docker-tar1090:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- adsb-net
|
||||||
|
tmpfs:
|
||||||
|
- /run/tar1090:size=64M
|
||||||
|
- /var/log:size=32M
|
||||||
|
|
||||||
|
x-tar1090-env: &tar1090-env
|
||||||
|
TZ: Europe/Paris
|
||||||
|
LAT: "48.8236"
|
||||||
|
LONG: "2.2770"
|
||||||
|
TAR1090_DEFAULTCENTERLAT: "48.8236"
|
||||||
|
TAR1090_DEFAULTCENTERLON: "2.2770"
|
||||||
|
TAR1090_DEFAULTZOOM: "10"
|
||||||
|
TAR1090_PLANECOUNTINTITLE: "true"
|
||||||
|
TAR1090_ENABLE_AC_DB: "true"
|
||||||
|
|
||||||
|
x-pg-env: &pg-env
|
||||||
|
PG_HOST: 127.0.0.1
|
||||||
|
PG_PORT: "5432"
|
||||||
|
PG_USER: pgz_admin
|
||||||
|
PG_PASSWORD: ${PG_PASSWORD:?PG_PASSWORD manquant — le définir dans /data/adsb/.env}
|
||||||
|
PG_DB: wpz_postgres
|
||||||
|
PG_SCHEMA: adsb
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# Services permanents (indépendants du nombre de dongles)
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
webapp:
|
||||||
|
build: ./webapp
|
||||||
|
container_name: adsb-webapp
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
<<: *pg-env
|
||||||
|
TZ: Europe/Paris
|
||||||
|
TAR1090_URL: http://127.0.0.1:8090
|
||||||
|
APP_TITLE: ADS-B Linux-25
|
||||||
|
STATION_LAT: "48.8236"
|
||||||
|
STATION_LON: "2.2770"
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# Slot rx1 — ports 8090 / 30002 / 30003 / 30005
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
readsb:
|
||||||
|
<<: *readsb-common
|
||||||
|
container_name: adsb-readsb
|
||||||
|
profiles: ["rx1"]
|
||||||
|
environment:
|
||||||
|
<<: *readsb-env
|
||||||
|
READSB_RTLSDR_DEVICE: ${RX1_SERIAL}
|
||||||
|
READSB_GAIN: ${RX1_GAIN:-autogain}
|
||||||
|
READSB_NET_SBS_OUTPUT_PORT: "30003"
|
||||||
|
READSB_NET_BEAST_OUTPUT_PORT: "30005"
|
||||||
|
READSB_NET_RAW_OUTPUT_PORT: "30002"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:30003:30003"
|
||||||
|
- "30005:30005"
|
||||||
|
- "127.0.0.1:30002:30002"
|
||||||
|
volumes:
|
||||||
|
- readsb-run:/run/readsb
|
||||||
|
|
||||||
|
tar1090:
|
||||||
|
<<: *tar1090-common
|
||||||
|
container_name: adsb-tar1090
|
||||||
|
profiles: ["rx1"]
|
||||||
|
depends_on:
|
||||||
|
- readsb
|
||||||
|
ports:
|
||||||
|
- "8090:8090"
|
||||||
|
environment:
|
||||||
|
<<: *tar1090-env
|
||||||
|
BEASTHOST: readsb
|
||||||
|
BEASTPORT: "30005"
|
||||||
|
TAR1090_PAGETITLE: ADS-B Linux-25
|
||||||
|
TAR1090_NGINX_PORT: "8090"
|
||||||
|
volumes:
|
||||||
|
- readsb-run:/run/readsb
|
||||||
|
|
||||||
|
adsb2pg:
|
||||||
|
build: ./adsb2pg
|
||||||
|
container_name: adsb-adsb2pg
|
||||||
|
profiles: ["rx1"]
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
<<: *pg-env
|
||||||
|
TZ: Europe/Paris
|
||||||
|
DUMP1090_URL: http://127.0.0.1:8090/data/aircraft.json
|
||||||
|
POLL_INTERVAL: "5"
|
||||||
|
RECEIVER_SERIAL: ${RX1_SERIAL}
|
||||||
|
TAR1090_DB_PATH: /tar1090-db
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# Slot rx2 — ports 8091 / 30012 / 30013 / 30015
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
readsb2:
|
||||||
|
<<: *readsb-common
|
||||||
|
container_name: adsb-readsb2
|
||||||
|
profiles: ["rx2"]
|
||||||
|
environment:
|
||||||
|
<<: *readsb-env
|
||||||
|
READSB_RTLSDR_DEVICE: ${RX2_SERIAL}
|
||||||
|
READSB_GAIN: ${RX2_GAIN:-autogain}
|
||||||
|
READSB_NET_SBS_OUTPUT_PORT: "30013"
|
||||||
|
READSB_NET_BEAST_OUTPUT_PORT: "30015"
|
||||||
|
READSB_NET_RAW_OUTPUT_PORT: "30012"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:30013:30013"
|
||||||
|
- "30015:30015"
|
||||||
|
- "127.0.0.1:30012:30012"
|
||||||
|
volumes:
|
||||||
|
- readsb2-run:/run/readsb
|
||||||
|
|
||||||
|
tar10902:
|
||||||
|
<<: *tar1090-common
|
||||||
|
container_name: adsb-tar10902
|
||||||
|
profiles: ["rx2"]
|
||||||
|
depends_on:
|
||||||
|
- readsb2
|
||||||
|
ports:
|
||||||
|
- "8091:8091"
|
||||||
|
environment:
|
||||||
|
<<: *tar1090-env
|
||||||
|
BEASTHOST: readsb2
|
||||||
|
BEASTPORT: "30015"
|
||||||
|
TAR1090_PAGETITLE: ADS-B Linux-25 (récepteur 2)
|
||||||
|
TAR1090_NGINX_PORT: "8091"
|
||||||
|
volumes:
|
||||||
|
- readsb2-run:/run/readsb
|
||||||
|
|
||||||
|
adsb2pg2:
|
||||||
|
build: ./adsb2pg
|
||||||
|
container_name: adsb-adsb2pg2
|
||||||
|
profiles: ["rx2"]
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
<<: *pg-env
|
||||||
|
TZ: Europe/Paris
|
||||||
|
DUMP1090_URL: http://127.0.0.1:8091/data/aircraft.json
|
||||||
|
POLL_INTERVAL: "5"
|
||||||
|
RECEIVER_SERIAL: ${RX2_SERIAL}
|
||||||
|
TAR1090_DB_PATH: /tar1090-db
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
# Slot rx3 — ports 8092 / 30022 / 30023 / 30025
|
||||||
|
# ═══════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
readsb3:
|
||||||
|
<<: *readsb-common
|
||||||
|
container_name: adsb-readsb3
|
||||||
|
profiles: ["rx3"]
|
||||||
|
environment:
|
||||||
|
<<: *readsb-env
|
||||||
|
READSB_RTLSDR_DEVICE: ${RX3_SERIAL}
|
||||||
|
READSB_GAIN: ${RX3_GAIN:-autogain}
|
||||||
|
READSB_NET_SBS_OUTPUT_PORT: "30023"
|
||||||
|
READSB_NET_BEAST_OUTPUT_PORT: "30025"
|
||||||
|
READSB_NET_RAW_OUTPUT_PORT: "30022"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:30023:30023"
|
||||||
|
- "30025:30025"
|
||||||
|
- "127.0.0.1:30022:30022"
|
||||||
|
volumes:
|
||||||
|
- readsb3-run:/run/readsb
|
||||||
|
|
||||||
|
tar10903:
|
||||||
|
<<: *tar1090-common
|
||||||
|
container_name: adsb-tar10903
|
||||||
|
profiles: ["rx3"]
|
||||||
|
depends_on:
|
||||||
|
- readsb3
|
||||||
|
ports:
|
||||||
|
- "8092:8092"
|
||||||
|
environment:
|
||||||
|
<<: *tar1090-env
|
||||||
|
BEASTHOST: readsb3
|
||||||
|
BEASTPORT: "30025"
|
||||||
|
TAR1090_PAGETITLE: ADS-B Linux-25 (récepteur 3)
|
||||||
|
TAR1090_NGINX_PORT: "8092"
|
||||||
|
volumes:
|
||||||
|
- readsb3-run:/run/readsb
|
||||||
|
|
||||||
|
adsb2pg3:
|
||||||
|
build: ./adsb2pg
|
||||||
|
container_name: adsb-adsb2pg3
|
||||||
|
profiles: ["rx3"]
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
environment:
|
||||||
|
<<: *pg-env
|
||||||
|
TZ: Europe/Paris
|
||||||
|
DUMP1090_URL: http://127.0.0.1:8092/data/aircraft.json
|
||||||
|
POLL_INTERVAL: "5"
|
||||||
|
RECEIVER_SERIAL: ${RX3_SERIAL}
|
||||||
|
TAR1090_DB_PATH: /tar1090-db
|
||||||
|
|
||||||
|
networks:
|
||||||
|
adsb-net:
|
||||||
|
name: adsb-net
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
readsb-run:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: tmpfs
|
||||||
|
device: tmpfs
|
||||||
|
o: size=64m
|
||||||
|
readsb2-run:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: tmpfs
|
||||||
|
device: tmpfs
|
||||||
|
o: size=64m
|
||||||
|
readsb3-run:
|
||||||
|
driver: local
|
||||||
|
driver_opts:
|
||||||
|
type: tmpfs
|
||||||
|
device: tmpfs
|
||||||
|
o: size=64m
|
||||||
182
export_adsb_bundle.sh
Executable file
@ -0,0 +1,182 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# export_adsb_bundle.sh — Empaquette le stack ADS-B fonctionnel de Linux-25
|
||||||
|
# Version 2.0 — JCA 2026
|
||||||
|
#
|
||||||
|
# À LANCER SUR LINUX-25 (la machine source, celle qui fonctionne déjà).
|
||||||
|
# Produit une archive .tar.gz portable, prête à copier sur la machine neuve
|
||||||
|
# et à installer avec install_adsb_stack.sh.
|
||||||
|
#
|
||||||
|
# v2.0 (05/07/2026) : inclut les correctifs découverts lors des incidents
|
||||||
|
# du 03-05/07/2026 :
|
||||||
|
# - adsb2pg.py : clean_altitude() (bug "ground") + rollback() sur exception
|
||||||
|
# - watchdog_readsb.sh v2 : seuil adaptatif jour/nuit (évite le flapping
|
||||||
|
# nocturne sur les creux de trafic réels)
|
||||||
|
# - schema_adsb_base.sql : schéma générique SANS les partitions datées
|
||||||
|
# spécifiques à Linux-25 (w25-w28, 2026/2027) — la nouvelle machine
|
||||||
|
# amorce ses propres partitions à la date réelle d'installation
|
||||||
|
# - check_adsb.sh v3.4 : watchdog crontab, continuité partitions, RestartCount
|
||||||
|
# - cron d'automatisation des partitions (dimanche 20h, +14 jours)
|
||||||
|
#
|
||||||
|
# Usage : ./export_adsb_bundle.sh [chemin_sortie.tar.gz]
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ADSB_DIR="/data/adsb"
|
||||||
|
OUT_FILE="${1:-/tmp/adsb_bundle_$(date +%Y%m%d_%H%M%S).tar.gz}"
|
||||||
|
WORK_DIR=$(mktemp -d)
|
||||||
|
BUNDLE_DIR="$WORK_DIR/adsb_bundle"
|
||||||
|
|
||||||
|
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||||
|
fail() { echo -e " ${RED}✗${NC} $1"; }
|
||||||
|
warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
|
||||||
|
info() { echo -e " ${CYAN}→${NC} $1"; }
|
||||||
|
step() { echo -e "\n${BOLD}▸ $1${NC}"; }
|
||||||
|
|
||||||
|
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||||
|
|
||||||
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ Export bundle ADS-B — source : $(hostname) ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
mkdir -p "$BUNDLE_DIR"/{adsb2pg,webapp,scripts,sql,docs}
|
||||||
|
|
||||||
|
# ── 1. docker-compose.yml (nettoyé des secrets) ───────────────────────────────
|
||||||
|
step "1. docker-compose.yml"
|
||||||
|
if [ -f "$ADSB_DIR/docker-compose.yml" ]; then
|
||||||
|
# Remplace le mot de passe en dur par un placeholder — install_adsb_stack.sh
|
||||||
|
# en génère un nouveau et l'injecte à l'installation.
|
||||||
|
sed -E 's/(PG_PASSWORD=)[^[:space:]]+/\1__PG_PASSWORD_PLACEHOLDER__/' \
|
||||||
|
"$ADSB_DIR/docker-compose.yml" > "$BUNDLE_DIR/docker-compose.yml"
|
||||||
|
ok "docker-compose.yml exporté (mot de passe neutralisé)"
|
||||||
|
else
|
||||||
|
fail "docker-compose.yml introuvable dans $ADSB_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 2. adsb2pg (code source complet) ──────────────────────────────────────────
|
||||||
|
step "2. adsb2pg"
|
||||||
|
if [ -d "$ADSB_DIR/adsb2pg" ]; then
|
||||||
|
# --exclude .git : tar1090-db est un dépôt Git cloné en sous-répertoire —
|
||||||
|
# son historique n'a aucune utilité dans l'image Docker et alourdit
|
||||||
|
# inutilement le bundle (souvent plusieurs dizaines de Mo).
|
||||||
|
if command -v rsync &>/dev/null; then
|
||||||
|
rsync -a --exclude='.git' "$ADSB_DIR/adsb2pg/" "$BUNDLE_DIR/adsb2pg/"
|
||||||
|
else
|
||||||
|
cp -r "$ADSB_DIR/adsb2pg/." "$BUNDLE_DIR/adsb2pg/"
|
||||||
|
find "$BUNDLE_DIR/adsb2pg" -type d -name ".git" -exec rm -rf {} + 2>/dev/null
|
||||||
|
fi
|
||||||
|
GIT_LEFT=$(find "$BUNDLE_DIR/adsb2pg" -type d -name ".git" | wc -l)
|
||||||
|
if [ "$GIT_LEFT" -eq 0 ]; then
|
||||||
|
ok "Code adsb2pg copié ($(find "$BUNDLE_DIR/adsb2pg" -name '*.py' | wc -l) fichier(s) .py), .git exclu"
|
||||||
|
else
|
||||||
|
warn ".git non exclu proprement — vérifier manuellement la taille du bundle"
|
||||||
|
fi
|
||||||
|
if grep -q "clean_altitude" "$BUNDLE_DIR/adsb2pg/adsb2pg.py" 2>/dev/null; then
|
||||||
|
ok "Correctif clean_altitude() présent"
|
||||||
|
else
|
||||||
|
warn "Correctif clean_altitude() ABSENT — bundle basé sur une version non corrigée !"
|
||||||
|
fi
|
||||||
|
if grep -q "conn.rollback()" "$BUNDLE_DIR/adsb2pg/adsb2pg.py" 2>/dev/null; then
|
||||||
|
ok "Correctif rollback() présent"
|
||||||
|
else
|
||||||
|
warn "Correctif rollback() ABSENT — bundle basé sur une version non corrigée !"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
fail "Répertoire adsb2pg introuvable dans $ADSB_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 3. webapp (code source complet) ───────────────────────────────────────────
|
||||||
|
step "3. webapp"
|
||||||
|
if [ -d "$ADSB_DIR/webapp" ]; then
|
||||||
|
cp -r "$ADSB_DIR/webapp/." "$BUNDLE_DIR/webapp/"
|
||||||
|
ok "Code webapp copié"
|
||||||
|
else
|
||||||
|
fail "Répertoire webapp introuvable dans $ADSB_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. Schéma SQL générique (SANS les partitions datées) ──────────────────────
|
||||||
|
step "4. Schéma PostgreSQL"
|
||||||
|
if [ -f "$ADSB_DIR/schema_adsb_base.sql" ]; then
|
||||||
|
cp "$ADSB_DIR/schema_adsb_base.sql" "$BUNDLE_DIR/sql/schema_adsb_base.sql"
|
||||||
|
ok "schema_adsb_base.sql copié depuis $ADSB_DIR"
|
||||||
|
else
|
||||||
|
warn "schema_adsb_base.sql absent de $ADSB_DIR — utilisation de la copie du bundle courant"
|
||||||
|
info "Pense à le régénérer si le schéma a changé depuis :"
|
||||||
|
info " docker exec wordpress-postgres pg_dump -U pgz_admin -d wpz_postgres --schema-only -n adsb > schema_full.sql"
|
||||||
|
info " puis en retirer les partitions datées (w25-w28, aircraft_history_2026/2027)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 5. Scripts opérationnels (watchdog v2, check_adsb v3.4) ───────────────────
|
||||||
|
step "5. Scripts opérationnels"
|
||||||
|
for f in scripts/watchdog_readsb.sh check_adsb.sh; do
|
||||||
|
SRC="$ADSB_DIR/$f"
|
||||||
|
DEST="$BUNDLE_DIR/scripts/$(basename "$f")"
|
||||||
|
if [ -f "$SRC" ]; then
|
||||||
|
cp "$SRC" "$DEST"
|
||||||
|
chmod +x "$DEST"
|
||||||
|
ok "$(basename "$f") copié"
|
||||||
|
else
|
||||||
|
warn "$SRC introuvable — à ajouter manuellement au bundle avant install"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Vérifier que le watchdog copié est bien la v2 (seuil adaptatif)
|
||||||
|
if grep -q "THRESHOLD" "$BUNDLE_DIR/scripts/watchdog_readsb.sh" 2>/dev/null; then
|
||||||
|
ok "watchdog_readsb.sh : v2 (seuil adaptatif jour/nuit) confirmée"
|
||||||
|
else
|
||||||
|
warn "watchdog_readsb.sh : semble être la v1 (pas de seuil adaptatif) — flapping nocturne possible sur la nouvelle machine"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 6. udev rule (RTL-SDR) ────────────────────────────────────────────────────
|
||||||
|
step "6. Règle udev RTL-SDR"
|
||||||
|
if [ -f /etc/udev/rules.d/99-adsb-docker.rules ]; then
|
||||||
|
cp /etc/udev/rules.d/99-adsb-docker.rules "$BUNDLE_DIR/scripts/99-adsb-docker.rules"
|
||||||
|
ok "Règle udev copiée"
|
||||||
|
else
|
||||||
|
warn "Règle udev absente sur cette machine — la nouvelle machine n'aura pas de relance auto USB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 7. Documentation / notes de version ───────────────────────────────────────
|
||||||
|
step "7. Documentation"
|
||||||
|
cat > "$BUNDLE_DIR/docs/CHANGELOG_BUNDLE.md" << 'EOF'
|
||||||
|
# Bundle ADS-B — historique des correctifs inclus
|
||||||
|
|
||||||
|
## v2.0 (05/07/2026)
|
||||||
|
- **adsb2pg.py** : fix `clean_altitude()` — readsb/tar1090 renvoient la chaîne
|
||||||
|
littérale `"ground"` pour l'altitude d'un avion au sol, ce qui plantait
|
||||||
|
l'INSERT PostgreSQL (colonne integer). Sans `conn.rollback()` derrière,
|
||||||
|
cette unique erreur bloquait tout le collecteur pendant 13h (incident du
|
||||||
|
03/07/2026 sur Linux-25).
|
||||||
|
- **watchdog_readsb.sh v2** : seuil adaptatif jour (1 cycle/5min) / nuit
|
||||||
|
(3 cycles/15min) — la v1 redémarrait le dongle sur de simples creux de
|
||||||
|
trafic nocturne, confondus avec un vrai gel matériel.
|
||||||
|
- **schema_adsb_base.sql** : schéma généré à partir d'un pg_dump --schema-only
|
||||||
|
de Linux-25, DÉLIBÉRÉMENT expurgé des partitions datées (semaines/années
|
||||||
|
spécifiques à Linux-25). L'installeur amorce les partitions à la date
|
||||||
|
réelle d'installation via `create_week_partition()`/`create_year_partition()`.
|
||||||
|
- **check_adsb.sh v3.4** : ajout des vérifications watchdog (crontab, fraîcheur,
|
||||||
|
flapping), continuité des partitions, RestartCount Docker, sync NTP.
|
||||||
|
- Automatisation cron de création des partitions (dimanche 20h, +14 jours).
|
||||||
|
|
||||||
|
## Notes de déploiement
|
||||||
|
- Le récepteur RTL-SDR de la nouvelle machine aura un numéro de série USB
|
||||||
|
différent — install_adsb_stack.sh le détecte automatiquement et met à jour
|
||||||
|
RECEIVER_SERIAL dans docker-compose.yml + une ligne dans adsb.receivers.
|
||||||
|
- Le mot de passe PostgreSQL est régénéré à l'installation, jamais réutilisé
|
||||||
|
tel quel entre machines.
|
||||||
|
EOF
|
||||||
|
ok "CHANGELOG_BUNDLE.md généré"
|
||||||
|
|
||||||
|
# ── 8. Archivage ───────────────────────────────────────────────────────────────
|
||||||
|
step "8. Création de l'archive"
|
||||||
|
tar czf "$OUT_FILE" -C "$WORK_DIR" adsb_bundle
|
||||||
|
SIZE=$(du -sh "$OUT_FILE" | cut -f1)
|
||||||
|
ok "Archive créée : $OUT_FILE ($SIZE)"
|
||||||
|
|
||||||
|
echo -e "\n${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ Bundle prêt : $OUT_FILE"
|
||||||
|
echo -e "${BOLD}║ Copie sur la nouvelle machine puis lance : ║${NC}"
|
||||||
|
echo -e "${BOLD}║ tar xzf $(basename "$OUT_FILE") ║${NC}"
|
||||||
|
echo -e "${BOLD}║ cd adsb_bundle && sudo ./install_adsb_stack.sh ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
78
fix_permissions.sh
Executable file
@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# Correction permissions UID après bascule postgres:16-alpine -> postgis/postgis:16-3.5
|
||||||
|
# Le conteneur boucle en restart : "could not open log file ... Permission denied"
|
||||||
|
# Cause : changement d'UID de l'utilisateur postgres entre l'image Alpine (musl)
|
||||||
|
# et l'image Debian (glibc) sous-jacente à postgis/postgis.
|
||||||
|
# ============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "==> 1. Identifier l'UID/GID postgres DANS la nouvelle image"
|
||||||
|
# On lance un conteneur jetable avec la même image pour lire son UID interne,
|
||||||
|
# sans dépendre du conteneur en échec qui boucle en restart.
|
||||||
|
NEW_UID=$(docker run --rm postgis/postgis:16-3.5 id -u postgres)
|
||||||
|
NEW_GID=$(docker run --rm postgis/postgis:16-3.5 id -g postgres)
|
||||||
|
echo " UID postgres (nouvelle image) : $NEW_UID"
|
||||||
|
echo " GID postgres (nouvelle image) : $NEW_GID"
|
||||||
|
|
||||||
|
echo "==> 2. Stopper le conteneur qui boucle en restart"
|
||||||
|
cd /root/wordpress
|
||||||
|
docker compose stop postgres
|
||||||
|
|
||||||
|
echo "==> 3. Corriger les permissions sur les volumes concernés"
|
||||||
|
# pg-logs-std : cause de l'échec actuel
|
||||||
|
# pg-conf-std : même cause potentielle, à corriger par précaution
|
||||||
|
# postgres_data : à corriger aussi -- sinon le prochain démarrage échouera
|
||||||
|
# différemment une fois le problème de logs résolu (même cause, autre symptôme)
|
||||||
|
|
||||||
|
# Volumes Docker "local" sont physiquement sous /var/lib/docker/volumes/<nom>/_data
|
||||||
|
# On passe par un conteneur utilitaire pour ne pas dépendre du chemin hôte exact
|
||||||
|
# (peut varier si docker tourne en snap, comme mentionné dans l'architecture v4)
|
||||||
|
|
||||||
|
docker run --rm \
|
||||||
|
-v pg-logs-std:/var/log/postgresql \
|
||||||
|
-v pg-conf-std:/etc/postgresql \
|
||||||
|
-v postgres_data:/var/lib/postgresql/data \
|
||||||
|
postgis/postgis:16-3.5 \
|
||||||
|
chown -R "${NEW_UID}:${NEW_GID}" \
|
||||||
|
/var/log/postgresql \
|
||||||
|
/etc/postgresql \
|
||||||
|
/var/lib/postgresql/data
|
||||||
|
|
||||||
|
echo "==> 4. Vérification des permissions appliquées"
|
||||||
|
docker run --rm \
|
||||||
|
-v pg-logs-std:/var/log/postgresql \
|
||||||
|
-v pg-conf-std:/etc/postgresql \
|
||||||
|
-v postgres_data:/var/lib/postgresql/data \
|
||||||
|
postgis/postgis:16-3.5 \
|
||||||
|
bash -c "ls -la /var/log/postgresql | head -5; echo '---'; ls -la /etc/postgresql | head -5; echo '---'; ls -la /var/lib/postgresql/data | head -5"
|
||||||
|
|
||||||
|
echo "==> 5. Redémarrage du conteneur postgres"
|
||||||
|
docker compose up -d postgres
|
||||||
|
|
||||||
|
echo "==> 6. Attente du démarrage"
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if docker exec wordpress-postgres pg_isready -U pgz_admin -d wpz_postgres > /dev/null 2>&1; then
|
||||||
|
echo " PostgreSQL prêt après ${i}s"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$i" -eq 30 ]; then
|
||||||
|
echo " ÉCHEC : PostgreSQL toujours pas prêt après 30s"
|
||||||
|
echo " Vérifier : docker logs wordpress-postgres --tail 50"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> 7. Vérification des données"
|
||||||
|
docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c \
|
||||||
|
"SELECT count(*) AS partitions FROM pg_inherits WHERE inhparent = 'adsb.positions'::regclass;"
|
||||||
|
docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c \
|
||||||
|
"SELECT count(*) AS receivers FROM adsb.receivers;"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Si les comptes ci-dessus correspondent à avant (185 partitions, 1 receiver),"
|
||||||
|
echo "les données sont intactes. On peut alors activer PostGIS :"
|
||||||
|
echo " docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c \"CREATE EXTENSION IF NOT EXISTS postgis;\""
|
||||||
|
echo "Puis redémarrer adsb2pg et webapp :"
|
||||||
|
echo " cd /data/adsb && docker compose start adsb2pg webapp"
|
||||||
1049
install_adsb_stack.sh
Executable file
90
migrate_to_postgis.sh
Executable file
@ -0,0 +1,90 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ============================================================================
|
||||||
|
# Bascule postgres:16-alpine -> postgis/postgis:16-3.4
|
||||||
|
# Stack WordPress + ADS-B, Linux-25
|
||||||
|
# Site personnel : indisponibilité tolérée, on fait ça en une seule fenêtre.
|
||||||
|
# ============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
COMPOSE_DIR="/root/wordpress"
|
||||||
|
BACKUP_DIR="/backup/postgres_migration_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
|
||||||
|
echo "==> 1. Sauvegarde de précaution AVANT toute modification"
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
# Dump logique complet (indépendant du format binaire, donc restaurable
|
||||||
|
# même si jamais l'image change quelque chose de fondamental)
|
||||||
|
docker exec wordpress-postgres pg_dump -U pgz_admin -d wpz_postgres -F c \
|
||||||
|
-f /tmp/wpz_postgres_pre_postgis.dump
|
||||||
|
docker cp wordpress-postgres:/tmp/wpz_postgres_pre_postgis.dump \
|
||||||
|
"$BACKUP_DIR/wpz_postgres_pre_postgis.dump"
|
||||||
|
|
||||||
|
echo " Dump sauvegardé : $BACKUP_DIR/wpz_postgres_pre_postgis.dump"
|
||||||
|
ls -lh "$BACKUP_DIR/wpz_postgres_pre_postgis.dump"
|
||||||
|
|
||||||
|
echo "==> 2. Sauvegarde du docker-compose.yml actuel"
|
||||||
|
cp "$COMPOSE_DIR/docker-compose.yml" "$BACKUP_DIR/docker-compose.yml.bak"
|
||||||
|
|
||||||
|
echo "==> 3. Édition de l'image dans docker-compose.yml"
|
||||||
|
sed -i 's|image: postgres:16-alpine|image: postgis/postgis:16-3.5|' \
|
||||||
|
"$COMPOSE_DIR/docker-compose.yml"
|
||||||
|
|
||||||
|
grep -A1 "container_name: wordpress-postgres" "$COMPOSE_DIR/docker-compose.yml" || true
|
||||||
|
grep "postgis/postgis" "$COMPOSE_DIR/docker-compose.yml"
|
||||||
|
|
||||||
|
echo "==> 4. Pull de la nouvelle image"
|
||||||
|
cd "$COMPOSE_DIR"
|
||||||
|
docker compose pull postgres
|
||||||
|
|
||||||
|
echo "==> 5. Arrêt propre des conteneurs dépendants (ADS-B) avant bascule"
|
||||||
|
echo " (évite des erreurs de connexion pendant le redémarrage PG)"
|
||||||
|
cd /data/adsb
|
||||||
|
docker compose stop adsb2pg webapp
|
||||||
|
|
||||||
|
echo "==> 6. Recreate du conteneur postgres avec la nouvelle image"
|
||||||
|
cd "$COMPOSE_DIR"
|
||||||
|
docker compose up -d postgres
|
||||||
|
|
||||||
|
echo "==> 7. Attente du démarrage (healthcheck implicite via pg_isready)"
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
if docker exec wordpress-postgres pg_isready -U pgz_admin -d wpz_postgres > /dev/null 2>&1; then
|
||||||
|
echo " PostgreSQL prêt après ${i}s"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "==> 8. Vérification : les données existantes sont toujours là"
|
||||||
|
docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c \
|
||||||
|
"SELECT count(*) AS partitions FROM pg_inherits WHERE inhparent = 'adsb.positions'::regclass;"
|
||||||
|
docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c \
|
||||||
|
"SELECT count(*) AS receivers FROM adsb.receivers;"
|
||||||
|
|
||||||
|
echo "==> 9. Activation de PostGIS"
|
||||||
|
docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c \
|
||||||
|
"CREATE EXTENSION IF NOT EXISTS postgis;"
|
||||||
|
docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c \
|
||||||
|
"SELECT postgis_full_version();"
|
||||||
|
|
||||||
|
echo "==> 10. Redémarrage des conteneurs ADS-B"
|
||||||
|
cd /data/adsb
|
||||||
|
docker compose start adsb2pg webapp
|
||||||
|
sleep 5
|
||||||
|
docker compose ps
|
||||||
|
|
||||||
|
echo "==> 11. Vérification finale -- adsb2pg insère toujours correctement"
|
||||||
|
sleep 10
|
||||||
|
docker exec wordpress-postgres psql -U pgz_admin -d wpz_postgres -c \
|
||||||
|
"SELECT max(ts) AS derniere_insertion, count(*) AS lignes_30min
|
||||||
|
FROM adsb.positions
|
||||||
|
WHERE ts >= NOW() - INTERVAL '5 minutes';"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================================"
|
||||||
|
echo "Migration terminée. En cas de problème, restauration :"
|
||||||
|
echo " cp $BACKUP_DIR/docker-compose.yml.bak $COMPOSE_DIR/docker-compose.yml"
|
||||||
|
echo " cd $COMPOSE_DIR && docker compose up -d postgres"
|
||||||
|
echo " (puis restaurer le dump si nécessaire :"
|
||||||
|
echo " docker exec -i wordpress-postgres pg_restore -U pgz_admin -d wpz_postgres --clean"
|
||||||
|
echo " < $BACKUP_DIR/wpz_postgres_pre_postgis.dump)"
|
||||||
|
echo "============================================================"
|
||||||
283
monitor_postgres_tables.sh
Executable file
@ -0,0 +1,283 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# monitor_postgres_tables.sh — Surveillance tailles tables ADS-B PostgreSQL
|
||||||
|
# Version : 2.0 — JCA 2026
|
||||||
|
# Usage : ./monitor_postgres_tables.sh [--alert THRESHOLD_MB] [--init]
|
||||||
|
# --alert N : alerte si positions ou aircraft_history dépasse N Mo (défaut 5000)
|
||||||
|
# --init : crée la table adsb.table_size_history si elle n'existe pas
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# ── Configuration ─────────────────────────────────────────────────────────────
|
||||||
|
PG_USER="pgz_admin"
|
||||||
|
PG_DB="wpz_postgres"
|
||||||
|
PG_SCHEMA="adsb"
|
||||||
|
ALERT_THRESHOLD_MB=5000
|
||||||
|
|
||||||
|
# ── Couleurs ──────────────────────────────────────────────────────────────────
|
||||||
|
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'; BLUE='\033[1;34m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
|
||||||
|
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||||
|
fail() { echo -e " ${RED}✗${NC} $1"; }
|
||||||
|
warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
|
||||||
|
info() { echo -e " ${CYAN}→${NC} $1"; }
|
||||||
|
hdr() { echo -e "\n${BLUE}${BOLD}── $1 ──${NC}"; }
|
||||||
|
|
||||||
|
# ── Parsing des arguments ─────────────────────────────────────────────────────
|
||||||
|
DO_INIT=false
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--alert) ALERT_THRESHOLD_MB="$2"; shift 2 ;;
|
||||||
|
--init) DO_INIT=true; shift ;;
|
||||||
|
*) echo "Usage: $0 [--alert THRESHOLD_MB] [--init]"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Helper psql ───────────────────────────────────────────────────────────────
|
||||||
|
pg() { docker exec wordpress-postgres psql -U "$PG_USER" -d "$PG_DB" -t -c "$1" 2>/dev/null | xargs; }
|
||||||
|
|
||||||
|
# ── Vérification Docker ───────────────────────────────────────────────────────
|
||||||
|
if ! docker inspect wordpress-postgres &>/dev/null; then
|
||||||
|
fail "Conteneur wordpress-postgres inaccessible"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Init : création de la table d'historique ──────────────────────────────────
|
||||||
|
if [ "$DO_INIT" = true ]; then
|
||||||
|
echo -e "${BOLD}Création de adsb.table_size_history...${NC}"
|
||||||
|
TMP_INIT=$(mktemp /tmp/pg_init_XXXXXX.sql)
|
||||||
|
trap "rm -f $TMP_INIT" EXIT
|
||||||
|
cat > "$TMP_INIT" << 'ENDINIT'
|
||||||
|
CREATE TABLE IF NOT EXISTS adsb.table_size_history (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
captured_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
table_name VARCHAR(60) NOT NULL,
|
||||||
|
total_bytes BIGINT NOT NULL,
|
||||||
|
table_bytes BIGINT NOT NULL,
|
||||||
|
index_bytes BIGINT NOT NULL,
|
||||||
|
row_estimate BIGINT NOT NULL,
|
||||||
|
partition_count INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS tsh_captured_at_idx ON adsb.table_size_history (captured_at DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS tsh_table_name_idx ON adsb.table_size_history (table_name, captured_at DESC);
|
||||||
|
COMMENT ON TABLE adsb.table_size_history IS 'Historique tailles tables ADS-B — monitor_postgres_tables.sh';
|
||||||
|
ENDINIT
|
||||||
|
docker cp "$TMP_INIT" wordpress-postgres:/tmp/pg_init.sql 2>/dev/null
|
||||||
|
docker exec wordpress-postgres psql -U "$PG_USER" -d "$PG_DB" -f /tmp/pg_init.sql 2>&1
|
||||||
|
RET=$?
|
||||||
|
docker exec wordpress-postgres rm -f /tmp/pg_init.sql 2>/dev/null
|
||||||
|
rm -f "$TMP_INIT"
|
||||||
|
if [ $RET -eq 0 ]; then
|
||||||
|
ok "Table adsb.table_size_history créée (ou déjà existante)"
|
||||||
|
else
|
||||||
|
fail "Erreur lors de la création de la table"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── En-tête ───────────────────────────────────────────────────────────────────
|
||||||
|
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ Surveillance tailles tables ADS-B PostgreSQL ║${NC}"
|
||||||
|
echo -e "${BOLD}║ $NOW ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
# ── Collecte des métriques via une seule requête ──────────────────────────────
|
||||||
|
hdr "Collecte des métriques"
|
||||||
|
|
||||||
|
# Écrire la requête dans un fichier temporaire (heredoc dans $() est instable en bash)
|
||||||
|
TMP_SQL=$(mktemp /tmp/pg_monitor_XXXXXX.sql)
|
||||||
|
trap "rm -f $TMP_SQL" EXIT
|
||||||
|
|
||||||
|
cat > "$TMP_SQL" << 'ENDSQL'
|
||||||
|
WITH parent_tables AS (
|
||||||
|
SELECT
|
||||||
|
pt.relname AS table_name,
|
||||||
|
pg_total_relation_size(pt.oid) AS total_bytes,
|
||||||
|
pg_relation_size(pt.oid) AS table_bytes,
|
||||||
|
pg_indexes_size(pt.oid) AS index_bytes,
|
||||||
|
COALESCE((
|
||||||
|
SELECT SUM(child.reltuples::BIGINT)
|
||||||
|
FROM pg_inherits inh
|
||||||
|
JOIN pg_class child ON child.oid = inh.inhrelid
|
||||||
|
JOIN pg_class parent ON parent.oid = inh.inhparent
|
||||||
|
WHERE parent.relname = pt.relname
|
||||||
|
AND parent.relnamespace = pt.relnamespace
|
||||||
|
), pt.reltuples::BIGINT) AS row_estimate,
|
||||||
|
(SELECT COUNT(*) FROM pg_inherits WHERE inhparent = pt.oid) AS partition_count
|
||||||
|
FROM pg_class pt
|
||||||
|
JOIN pg_namespace ns ON ns.oid = pt.relnamespace
|
||||||
|
WHERE ns.nspname = 'adsb'
|
||||||
|
AND pt.relkind IN ('r','p')
|
||||||
|
AND pt.relname IN ('positions', 'aircraft_history', 'receivers')
|
||||||
|
),
|
||||||
|
aggregated AS (
|
||||||
|
SELECT
|
||||||
|
pt.table_name,
|
||||||
|
CASE WHEN pt.partition_count > 0 THEN COALESCE((
|
||||||
|
SELECT SUM(pg_total_relation_size(child.oid))
|
||||||
|
FROM pg_inherits inh
|
||||||
|
JOIN pg_class child ON child.oid = inh.inhrelid
|
||||||
|
JOIN pg_class parent ON parent.oid = inh.inhparent
|
||||||
|
WHERE parent.relname = pt.table_name
|
||||||
|
AND parent.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname='adsb')
|
||||||
|
), 0) ELSE pt.total_bytes END AS total_bytes,
|
||||||
|
CASE WHEN pt.partition_count > 0 THEN COALESCE((
|
||||||
|
SELECT SUM(pg_relation_size(child.oid))
|
||||||
|
FROM pg_inherits inh
|
||||||
|
JOIN pg_class child ON child.oid = inh.inhrelid
|
||||||
|
JOIN pg_class parent ON parent.oid = inh.inhparent
|
||||||
|
WHERE parent.relname = pt.table_name
|
||||||
|
AND parent.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname='adsb')
|
||||||
|
), 0) ELSE pt.table_bytes END AS table_bytes,
|
||||||
|
CASE WHEN pt.partition_count > 0 THEN COALESCE((
|
||||||
|
SELECT SUM(pg_indexes_size(child.oid))
|
||||||
|
FROM pg_inherits inh
|
||||||
|
JOIN pg_class child ON child.oid = inh.inhrelid
|
||||||
|
JOIN pg_class parent ON parent.oid = inh.inhparent
|
||||||
|
WHERE parent.relname = pt.table_name
|
||||||
|
AND parent.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname='adsb')
|
||||||
|
), 0) ELSE pt.index_bytes END AS index_bytes,
|
||||||
|
pt.row_estimate,
|
||||||
|
pt.partition_count
|
||||||
|
FROM parent_tables pt
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
table_name,
|
||||||
|
total_bytes,
|
||||||
|
table_bytes,
|
||||||
|
index_bytes,
|
||||||
|
row_estimate,
|
||||||
|
partition_count,
|
||||||
|
pg_size_pretty(total_bytes) AS total_pretty,
|
||||||
|
pg_size_pretty(table_bytes) AS data_pretty,
|
||||||
|
pg_size_pretty(index_bytes) AS idx_pretty,
|
||||||
|
ROUND(total_bytes / 1024.0 / 1024.0, 1) AS total_mb
|
||||||
|
FROM aggregated
|
||||||
|
ORDER BY total_bytes DESC;
|
||||||
|
ENDSQL
|
||||||
|
|
||||||
|
# Copier le fichier SQL dans le conteneur et exécuter
|
||||||
|
docker cp "$TMP_SQL" wordpress-postgres:/tmp/pg_monitor.sql 2>/dev/null
|
||||||
|
METRICS=$(docker exec wordpress-postgres psql -U "$PG_USER" -d "$PG_DB" -t -f /tmp/pg_monitor.sql 2>&1)
|
||||||
|
docker exec wordpress-postgres rm -f /tmp/pg_monitor.sql 2>/dev/null
|
||||||
|
|
||||||
|
if [ -z "$METRICS" ]; then
|
||||||
|
fail "Impossible de collecter les métriques PostgreSQL"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "Métriques collectées"
|
||||||
|
|
||||||
|
# ── Affichage et insertion ────────────────────────────────────────────────────
|
||||||
|
hdr "Tailles actuelles"
|
||||||
|
|
||||||
|
ALERT_MSGS=()
|
||||||
|
|
||||||
|
while IFS='|' read -r table_name total_bytes table_bytes index_bytes row_estimate partition_count total_pretty data_pretty idx_pretty total_mb; do
|
||||||
|
# Nettoyer les espaces
|
||||||
|
table_name=$(echo "$table_name" | xargs)
|
||||||
|
total_bytes=$(echo "$total_bytes" | xargs)
|
||||||
|
table_bytes=$(echo "$table_bytes" | xargs)
|
||||||
|
index_bytes=$(echo "$index_bytes" | xargs)
|
||||||
|
row_estimate=$(echo "$row_estimate" | xargs)
|
||||||
|
partition_count=$(echo "$partition_count" | xargs)
|
||||||
|
total_pretty=$(echo "$total_pretty" | xargs)
|
||||||
|
data_pretty=$(echo "$data_pretty" | xargs)
|
||||||
|
idx_pretty=$(echo "$idx_pretty" | xargs)
|
||||||
|
total_mb=$(echo "$total_mb" | xargs)
|
||||||
|
|
||||||
|
[ -z "$table_name" ] && continue
|
||||||
|
|
||||||
|
# Récupérer la mesure précédente
|
||||||
|
PREV=$(docker exec wordpress-postgres psql -U "$PG_USER" -d "$PG_DB" -t -c "
|
||||||
|
SELECT total_bytes, captured_at::TEXT
|
||||||
|
FROM adsb.table_size_history
|
||||||
|
WHERE table_name = '$table_name'
|
||||||
|
ORDER BY captured_at DESC
|
||||||
|
LIMIT 1;" 2>/dev/null | xargs)
|
||||||
|
|
||||||
|
PREV_BYTES=$(echo "$PREV" | awk '{print $1}')
|
||||||
|
PREV_DATE=$(echo "$PREV" | awk '{print $2, $3}')
|
||||||
|
|
||||||
|
# Calculer la croissance
|
||||||
|
GROWTH_STR=""
|
||||||
|
if [ -n "$PREV_BYTES" ] && [ "$PREV_BYTES" -gt 0 ] 2>/dev/null; then
|
||||||
|
GROWTH_BYTES=$(( total_bytes - PREV_BYTES ))
|
||||||
|
GROWTH_MB=$(awk "BEGIN{printf \"%.1f\", $GROWTH_BYTES/1024/1024}")
|
||||||
|
GROWTH_PCT=$(awk "BEGIN{printf \"%.2f\", ($GROWTH_BYTES/$PREV_BYTES)*100}")
|
||||||
|
if [ "$GROWTH_BYTES" -gt 0 ]; then
|
||||||
|
GROWTH_STR=" ${GREEN}(+${GROWTH_MB} MB / +${GROWTH_PCT}% depuis $PREV_DATE)${NC}"
|
||||||
|
elif [ "$GROWTH_BYTES" -lt 0 ]; then
|
||||||
|
ABS_MB=$(awk "BEGIN{printf \"%.1f\", -$GROWTH_BYTES/1024/1024}")
|
||||||
|
GROWTH_STR=" ${YELLOW}(-${ABS_MB} MB depuis $PREV_DATE)${NC}"
|
||||||
|
else
|
||||||
|
GROWTH_STR=" ${CYAN}(stable depuis $PREV_DATE)${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Affichage
|
||||||
|
echo -e " ${BOLD}${table_name}${NC}"
|
||||||
|
echo -e " Total : ${BOLD}${total_pretty}${NC}${GROWTH_STR}"
|
||||||
|
echo -e " Données : ${data_pretty} | Index : ${idx_pretty}"
|
||||||
|
echo -e " Lignes : ~$(printf "%'d" "${row_estimate:-0}" 2>/dev/null || echo "$row_estimate") | Partitions : ${partition_count}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Alerte seuil
|
||||||
|
TOTAL_MB_INT=$(echo "$total_mb" | cut -d. -f1)
|
||||||
|
if [ "${TOTAL_MB_INT:-0}" -gt "$ALERT_THRESHOLD_MB" ] 2>/dev/null; then
|
||||||
|
ALERT_MSGS+=("$table_name : ${total_pretty} dépasse le seuil de ${ALERT_THRESHOLD_MB} MB")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Insertion dans l'historique
|
||||||
|
INSERT_RESULT=$(docker exec wordpress-postgres psql -U "$PG_USER" -d "$PG_DB" -t -c "
|
||||||
|
INSERT INTO adsb.table_size_history
|
||||||
|
(table_name, total_bytes, table_bytes, index_bytes, row_estimate, partition_count)
|
||||||
|
VALUES
|
||||||
|
('$table_name', $total_bytes, $table_bytes, $index_bytes,
|
||||||
|
${row_estimate:-0}, ${partition_count:-0})
|
||||||
|
RETURNING id;" 2>/dev/null | xargs)
|
||||||
|
|
||||||
|
if [ -n "$INSERT_RESULT" ]; then
|
||||||
|
info "Enregistrement #${INSERT_RESULT} inséré dans adsb.table_size_history"
|
||||||
|
else
|
||||||
|
warn "Impossible d'insérer dans adsb.table_size_history (table créée ? Lancer avec --init)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
done <<< "$METRICS"
|
||||||
|
|
||||||
|
# ── Évolution sur 7 jours ─────────────────────────────────────────────────────
|
||||||
|
hdr "Évolution sur 7 jours"
|
||||||
|
|
||||||
|
docker exec wordpress-postgres psql -U "$PG_USER" -d "$PG_DB" -c "
|
||||||
|
SELECT
|
||||||
|
table_name AS \"Table\",
|
||||||
|
TO_CHAR(captured_at, 'YYYY-MM-DD HH24:MI') AS \"Horodatage\",
|
||||||
|
pg_size_pretty(total_bytes) AS \"Total\",
|
||||||
|
pg_size_pretty(total_bytes
|
||||||
|
- LAG(total_bytes) OVER (PARTITION BY table_name ORDER BY captured_at))
|
||||||
|
AS \"Delta\",
|
||||||
|
partition_count AS \"Parts\",
|
||||||
|
TO_CHAR(row_estimate, 'FM999,999,999,999') AS \"Lignes estimées\"
|
||||||
|
FROM adsb.table_size_history
|
||||||
|
WHERE captured_at >= NOW() - INTERVAL '7 days'
|
||||||
|
AND table_name IN ('positions','aircraft_history')
|
||||||
|
ORDER BY table_name, captured_at DESC
|
||||||
|
LIMIT 30;" 2>/dev/null || warn "Table adsb.table_size_history absente — lancer avec --init"
|
||||||
|
|
||||||
|
# ── Alertes ───────────────────────────────────────────────────────────────────
|
||||||
|
if [ ${#ALERT_MSGS[@]} -gt 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${RED}${BOLD}═══ ALERTES ═══${NC}"
|
||||||
|
for msg in "${ALERT_MSGS[@]}"; do
|
||||||
|
echo -e " ${RED}⚠ $msg${NC}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Résumé ────────────────────────────────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ Historique : SELECT * FROM adsb.table_size_history ║${NC}"
|
||||||
|
echo -e "${BOLD}║ Seuil alerte : ${ALERT_THRESHOLD_MB} MB ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
1
postgres/conf
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/var/snap/docker/common/var-lib-docker/volumes/pg-conf-std/_data
|
||||||
1
postgres/logs
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
/var/snap/docker/common/var-lib-docker/volumes/pg-logs-std/_data
|
||||||
65
queries.sql
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
-- Requêtes utiles pour explorer les données ADS-B
|
||||||
|
-- Base : wpz_postgres, schéma : adsb
|
||||||
|
|
||||||
|
-- Nombre de positions par jour (7 derniers jours)
|
||||||
|
SELECT
|
||||||
|
DATE_TRUNC('day', ts AT TIME ZONE 'Europe/Paris') AS jour,
|
||||||
|
COUNT(*) AS nb_positions,
|
||||||
|
COUNT(DISTINCT icao) AS nb_avions
|
||||||
|
FROM adsb.positions
|
||||||
|
WHERE ts >= NOW() - INTERVAL '7 days'
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY 1 DESC;
|
||||||
|
|
||||||
|
-- Avions les plus vus aujourd'hui
|
||||||
|
SELECT
|
||||||
|
icao,
|
||||||
|
MAX(callsign) AS callsign,
|
||||||
|
COUNT(*) AS nb_positions,
|
||||||
|
MIN(altitude) AS alt_min,
|
||||||
|
MAX(altitude) AS alt_max,
|
||||||
|
ROUND(AVG(speed)) AS vitesse_moy,
|
||||||
|
MIN(ts) AS premiere_vue,
|
||||||
|
MAX(ts) AS derniere_vue
|
||||||
|
FROM adsb.positions
|
||||||
|
WHERE ts >= DATE_TRUNC('day', NOW() AT TIME ZONE 'Europe/Paris')
|
||||||
|
GROUP BY icao
|
||||||
|
ORDER BY nb_positions DESC
|
||||||
|
LIMIT 20;
|
||||||
|
|
||||||
|
-- Trajectoire d'un avion spécifique (remplacer l'ICAO)
|
||||||
|
SELECT ts, callsign, altitude, speed, track, lat, lon, vrate
|
||||||
|
FROM adsb.positions
|
||||||
|
WHERE icao = '440AF4'
|
||||||
|
AND ts >= NOW() - INTERVAL '24 hours'
|
||||||
|
ORDER BY ts;
|
||||||
|
|
||||||
|
-- Avions en dessous de 5000 ft (approche Orly/CDG)
|
||||||
|
SELECT DISTINCT ON (icao)
|
||||||
|
icao, callsign, altitude, speed, lat, lon, ts
|
||||||
|
FROM adsb.positions
|
||||||
|
WHERE ts >= NOW() - INTERVAL '1 hour'
|
||||||
|
AND altitude IS NOT NULL
|
||||||
|
AND altitude < 5000
|
||||||
|
ORDER BY icao, ts DESC;
|
||||||
|
|
||||||
|
-- Statistiques globales
|
||||||
|
SELECT
|
||||||
|
COUNT(*) AS total_positions,
|
||||||
|
COUNT(DISTINCT icao) AS avions_distincts,
|
||||||
|
MIN(ts) AS depuis,
|
||||||
|
MAX(ts) AS jusqua,
|
||||||
|
ROUND(AVG(altitude)) AS altitude_moyenne,
|
||||||
|
ROUND(AVG(speed)) AS vitesse_moyenne
|
||||||
|
FROM adsb.positions;
|
||||||
|
|
||||||
|
-- Taille des partitions
|
||||||
|
SELECT
|
||||||
|
c.relname AS partition,
|
||||||
|
pg_size_pretty(pg_relation_size(c.oid)) AS taille,
|
||||||
|
pg_size_pretty(pg_total_relation_size(c.oid)) AS taille_totale
|
||||||
|
FROM pg_class c
|
||||||
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
|
WHERE n.nspname = 'adsb'
|
||||||
|
AND c.relkind = 'r'
|
||||||
|
ORDER BY pg_relation_size(c.oid) DESC;
|
||||||
53
scripts/detect_dongles.sh
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# detect_dongles.sh v1.1 — source de vérité unique sur les récepteurs ADS-B
|
||||||
|
# Emplacement : /data/adsb/scripts/detect_dongles.sh
|
||||||
|
#
|
||||||
|
# Se source (ne s'exécute pas). Fournit :
|
||||||
|
# SLOTS[] catalogue statique des slots (ports, conteneurs, profile)
|
||||||
|
# MAX_RECEIVERS nombre de slots disponibles
|
||||||
|
# detect_serials() un numéro de série par ligne, ordre d'énumération USB
|
||||||
|
# detect_count() nombre de dongles présents
|
||||||
|
# slot_ports() calcule beast/raw/sbs pour un numéro de slot
|
||||||
|
#
|
||||||
|
# v1.1 (31/07/2026) : SLOTS remonté ici pour être partagé par check_adsb.sh et
|
||||||
|
# watchdog_readsb.sh — la duplication du catalogue entre les deux scripts était
|
||||||
|
# une source de désynchronisation.
|
||||||
|
#
|
||||||
|
# detect_serials fonctionne même si un dongle est déjà « claim » par un
|
||||||
|
# conteneur : rtl_test énumère AVANT de tenter l'ouverture, et l'erreur
|
||||||
|
# usb_claim_interface -6 qui suit n'affecte pas la liste produite.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# slot:port_tar1090:ctn_readsb:ctn_tar1090:ctn_adsb2pg:profile
|
||||||
|
SLOTS=(
|
||||||
|
"1:8090:adsb-readsb:adsb-tar1090:adsb-adsb2pg:rx1"
|
||||||
|
"2:8091:adsb-readsb2:adsb-tar10902:adsb-adsb2pg2:rx2"
|
||||||
|
"3:8092:adsb-readsb3:adsb-tar10903:adsb-adsb2pg3:rx3"
|
||||||
|
)
|
||||||
|
MAX_RECEIVERS=${#SLOTS[@]}
|
||||||
|
|
||||||
|
detect_serials() {
|
||||||
|
if command -v rtl_test >/dev/null 2>&1; then
|
||||||
|
rtl_test -t 2>&1 \
|
||||||
|
| sed -n 's/^[[:space:]]*[0-9]\+:.*SN:[[:space:]]*\([0-9A-Za-z]\+\).*/\1/p'
|
||||||
|
else
|
||||||
|
local n i
|
||||||
|
n=$(lsusb 2>/dev/null | grep -ci 'RTL2838\|RTL2832')
|
||||||
|
for ((i = 1; i <= n; i++)); do echo "unknown${i}"; done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_count() { detect_serials | grep -c . ; }
|
||||||
|
|
||||||
|
# slot_ports <n> → écrit "beast raw sbs"
|
||||||
|
slot_ports() {
|
||||||
|
local n=$1
|
||||||
|
echo "$((30005 + (n - 1) * 10)) $((30002 + (n - 1) * 10)) $((30003 + (n - 1) * 10))"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||||
|
mapfile -t _s < <(detect_serials)
|
||||||
|
echo "${#_s[@]} dongle(s) RTL-SDR détecté(s)"
|
||||||
|
for i in "${!_s[@]}"; do echo " slot rx$((i + 1)) → SN ${_s[$i]}"; done
|
||||||
|
fi
|
||||||
53
scripts/fetch_flags.sh
Executable file
@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# fetch_flags.sh — récupère les drapeaux en local, pour ne plus dépendre du CDN
|
||||||
|
#
|
||||||
|
# Par défaut la webapp charge les drapeaux depuis flagcdn.com (domaine public,
|
||||||
|
# libre d'usage, sans attribution). Ce script les rapatrie dans
|
||||||
|
# webapp/src/assets/flags/ pour un fonctionnement entièrement hors ligne.
|
||||||
|
#
|
||||||
|
# Après exécution, passer FLAG_LOCAL à true dans js/app.js :
|
||||||
|
# sed -i 's/const FLAG_LOCAL = false;/const FLAG_LOCAL = true;/' \
|
||||||
|
# /data/adsb/webapp/src/js/app.js
|
||||||
|
#
|
||||||
|
# Poids total : environ 200 Ko pour ~100 pays.
|
||||||
|
#
|
||||||
|
# Licence : les drapeaux nationaux sont des symboles d'État, non couverts par
|
||||||
|
# le droit d'auteur. flagcdn.com les distribue en domaine public.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEST="${1:-/data/adsb/webapp/src/assets/flags}"
|
||||||
|
BASE="https://flagcdn.com/24x18"
|
||||||
|
|
||||||
|
# Codes ISO alignés sur la table COUNTRY_ISO de app.js
|
||||||
|
CODES="fr de es it pt be nl lu ch at gb ie dk no se fi is pl cz sk hu ro bg gr
|
||||||
|
hr si rs ba al mt cy ee lv lt ua by ru tr us ca mx br ar cl co pe ve cn jp kr
|
||||||
|
in pk th vn sg my id ph au nz il sa qa ae kw bh om jo lb eg ma dz tn ly za ng
|
||||||
|
ke et gh sn ci cm ga mc sm ad li im gg je bm ky aw bs pa cr do cu jm kz uz az
|
||||||
|
ge am md me mk ir iq sy af bd lk np mm kh la mn tw hk mo"
|
||||||
|
|
||||||
|
mkdir -p "$DEST"
|
||||||
|
|
||||||
|
ok=0; ko=0
|
||||||
|
for c in $CODES; do
|
||||||
|
if [ -s "$DEST/$c.png" ]; then
|
||||||
|
ok=$((ok+1)); continue
|
||||||
|
fi
|
||||||
|
if curl -fsSL --max-time 10 "$BASE/$c.png" -o "$DEST/$c.png" 2>/dev/null; then
|
||||||
|
ok=$((ok+1))
|
||||||
|
else
|
||||||
|
rm -f "$DEST/$c.png"
|
||||||
|
echo " échec : $c" >&2
|
||||||
|
ko=$((ko+1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "$ok drapeau(x) disponible(s) dans $DEST"
|
||||||
|
[ "$ko" -gt 0 ] && echo "$ko échec(s) — ces pays retomberont sur le pictogramme « inconnu »"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Pour activer l'hébergement local :"
|
||||||
|
echo " sed -i 's/const FLAG_LOCAL = false;/const FLAG_LOCAL = true;/' \\"
|
||||||
|
echo " /data/adsb/webapp/src/js/app.js"
|
||||||
|
echo " docker cp /data/adsb/webapp/src/js/app.js adsb-webapp:/var/www/html/js/app.js"
|
||||||
74
scripts/gen_env.sh
Executable file
@ -0,0 +1,74 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# gen_env.sh — génère /data/adsb/.env.receivers à partir des dongles présents
|
||||||
|
# Emplacement : /data/adsb/scripts/gen_env.sh
|
||||||
|
#
|
||||||
|
# Produit :
|
||||||
|
# RX<n>_SERIAL numéro de série affecté au slot n
|
||||||
|
# RX<n>_GAIN gain du slot n (repris de .env.gains si présent, sinon autogain)
|
||||||
|
# COMPOSE_PROFILES liste des profiles à activer (rx1[,rx2[,rx3]])
|
||||||
|
#
|
||||||
|
# Usage :
|
||||||
|
# /data/adsb/scripts/gen_env.sh
|
||||||
|
# docker compose --env-file .env.receivers up -d --remove-orphans
|
||||||
|
#
|
||||||
|
# Le --remove-orphans est OBLIGATOIRE : Compose ne supprime pas de lui-même les
|
||||||
|
# conteneurs d'un profile devenu inactif. Sans lui, un readsb dont le dongle a
|
||||||
|
# été débranché continue de tourner à vide et de disputer le bus USB.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_DIR="/data/adsb"
|
||||||
|
ENV_FILE="${BASE_DIR}/.env.receivers"
|
||||||
|
GAINS_FILE="${BASE_DIR}/.env.gains" # optionnel : RX1_GAIN=49.6 etc.
|
||||||
|
MAX_RECEIVERS=3
|
||||||
|
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "${BASE_DIR}/scripts/detect_dongles.sh"
|
||||||
|
|
||||||
|
mapfile -t SERIALS < <(detect_serials)
|
||||||
|
|
||||||
|
# Gains persistants par slot (facultatif)
|
||||||
|
declare -A GAINS=()
|
||||||
|
if [ -r "$GAINS_FILE" ]; then
|
||||||
|
while IFS='=' read -r k v; do
|
||||||
|
[[ "$k" =~ ^RX[0-9]+_GAIN$ ]] && GAINS["$k"]="$v"
|
||||||
|
done < "$GAINS_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
TMP=$(mktemp)
|
||||||
|
trap 'rm -f "$TMP"' EXIT
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Généré par gen_env.sh le $(date '+%Y-%m-%d %H:%M:%S') — NE PAS ÉDITER"
|
||||||
|
echo "# Dongles détectés : ${#SERIALS[@]}"
|
||||||
|
} > "$TMP"
|
||||||
|
|
||||||
|
PROFILES=()
|
||||||
|
for i in "${!SERIALS[@]}"; do
|
||||||
|
n=$((i + 1))
|
||||||
|
if [ "$n" -gt "$MAX_RECEIVERS" ]; then
|
||||||
|
echo "AVERTISSEMENT : ${#SERIALS[@]} dongles détectés, seuls $MAX_RECEIVERS slots existent — ${SERIALS[$i]} ignoré" >&2
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "RX${n}_SERIAL=${SERIALS[$i]}" >> "$TMP"
|
||||||
|
echo "RX${n}_GAIN=${GAINS[RX${n}_GAIN]:-autogain}" >> "$TMP"
|
||||||
|
PROFILES+=("rx${n}")
|
||||||
|
done
|
||||||
|
|
||||||
|
# Les slots non pourvus doivent quand même exister comme variables, sinon
|
||||||
|
# Compose émet un warning à l'interpolation même pour un service désactivé.
|
||||||
|
for ((n = ${#PROFILES[@]} + 1; n <= MAX_RECEIVERS; n++)); do
|
||||||
|
echo "RX${n}_SERIAL=" >> "$TMP"
|
||||||
|
echo "RX${n}_GAIN=autogain" >> "$TMP"
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ ${#PROFILES[@]} -eq 0 ]; then
|
||||||
|
echo "COMPOSE_PROFILES=" >> "$TMP"
|
||||||
|
echo "ERREUR : aucun dongle RTL-SDR détecté — aucun récepteur ne sera démarré" >&2
|
||||||
|
else
|
||||||
|
printf 'COMPOSE_PROFILES=%s\n' "$(IFS=,; echo "${PROFILES[*]}")" >> "$TMP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
install -m 0640 "$TMP" "$ENV_FILE"
|
||||||
|
cat "$ENV_FILE"
|
||||||
198
scripts/watchdog_readsb.sh
Executable file
@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# watchdog_readsb.sh — surveille le(s) récepteur(s) ADS-B et redémarre le
|
||||||
|
# maillon réellement en cause quand le flux de messages se fige.
|
||||||
|
#
|
||||||
|
# v4 (31/07/2026) — refonte après l'incident du 31/07 (36 redémarrages en 24h
|
||||||
|
# sur un récepteur qui n'était pas en panne). Quatre défauts corrigés :
|
||||||
|
#
|
||||||
|
# 1. RECEIVERS était statique et listait un dongle inexistant (00000012).
|
||||||
|
# Le watchdog redémarrait donc en boucle un conteneur sans matériel.
|
||||||
|
# → RECEIVERS est maintenant dérivé des dongles réellement énumérés,
|
||||||
|
# via detect_dongles.sh (même source que check_adsb.sh).
|
||||||
|
#
|
||||||
|
# 2. THRESHOLD=1 le jour : redémarrage après 5 min sans progression. Or le
|
||||||
|
# service autogain a besoin de plusieurs heures de trafic pour converger,
|
||||||
|
# et chaque redémarrage le relançait de zéro. Le watchdog empêchait donc
|
||||||
|
# la seule chose qui aurait corrigé le gain (bloqué à 7.7 dB).
|
||||||
|
# → seuil minimum 3 cycles, + délai de grâce après démarrage.
|
||||||
|
#
|
||||||
|
# 3. Aucun plafond : un redémarrage qui ne corrige rien était répété
|
||||||
|
# indéfiniment, noyant le vrai diagnostic sous le bruit.
|
||||||
|
# → budget de MAX_RESTARTS_24H, au-delà duquel on alerte sans agir.
|
||||||
|
#
|
||||||
|
# 4. Le diagnostic reposait uniquement sur aircraft.json (servi par tar1090).
|
||||||
|
# Un tar1090 déconnecté de readsb produisait donc le même symptôme qu'un
|
||||||
|
# dongle mort — c'est exactement ce qui s'est produit le 31/07 après une
|
||||||
|
# perte d'alias réseau : readsb décodait, tar1090 servait 0 message, et
|
||||||
|
# le watchdog redémarrait readsb, le maillon sain.
|
||||||
|
# → sonde directe du port SBS : si readsb émet, c'est tar1090 qu'on
|
||||||
|
# redémarre, pas readsb.
|
||||||
|
#
|
||||||
|
# Cron : */5 * * * * /data/adsb/scripts/watchdog_readsb.sh
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
BASE_DIR="/data/adsb"
|
||||||
|
LOG_FILE="/var/log/readsb_watchdog.log"
|
||||||
|
DETECT_LIB="${BASE_DIR}/scripts/detect_dongles.sh"
|
||||||
|
|
||||||
|
# ── Paramètres de prudence ────────────────────────────────────────────────────
|
||||||
|
MIN_UPTIME=1800 # s — ne jamais redémarrer un conteneur démarré depuis
|
||||||
|
# moins de 30 min (laisse converger l'autogain)
|
||||||
|
MAX_RESTARTS_24H=6 # au-delà, on alerte sans redémarrer : si 6 redémarrages
|
||||||
|
# n'ont pas corrigé le problème, le 7e ne le fera pas
|
||||||
|
SBS_PROBE_TIMEOUT=6 # s — durée de la sonde sur le port SBS
|
||||||
|
SBS_MIN_BYTES=50 # octets reçus au-delà desquels readsb est jugé actif
|
||||||
|
|
||||||
|
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $*" >> "$LOG_FILE"; }
|
||||||
|
|
||||||
|
# ── Seuil adaptatif jour/nuit ─────────────────────────────────────────────────
|
||||||
|
# Le creux de trafic nocturne reste réel : on tolère plus longtemps la nuit.
|
||||||
|
# Mais le plancher est désormais à 3 cycles (15 min) même en journée.
|
||||||
|
HOUR=$(date +%H)
|
||||||
|
if [ "$((10#$HOUR))" -ge 6 ] && [ "$((10#$HOUR))" -lt 22 ]; then
|
||||||
|
THRESHOLD=3; PERIOD="jour"
|
||||||
|
else
|
||||||
|
THRESHOLD=5; PERIOD="nuit"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Construction dynamique des récepteurs ─────────────────────────────────────
|
||||||
|
if [ ! -r "$DETECT_LIB" ]; then
|
||||||
|
log "[FATAL] $DETECT_LIB introuvable — watchdog inopérant"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# shellcheck source=/dev/null
|
||||||
|
source "$DETECT_LIB"
|
||||||
|
|
||||||
|
mapfile -t PRESENT_SERIALS < <(detect_serials)
|
||||||
|
|
||||||
|
if [ "${#PRESENT_SERIALS[@]}" -eq 0 ]; then
|
||||||
|
log "[ALERTE] Aucun dongle RTL-SDR détecté sur le bus USB — rien à surveiller. Vérifier le branchement physique."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Budget de redémarrages ────────────────────────────────────────────────────
|
||||||
|
# Historique des redémarrages par conteneur, élagué à 24h glissantes.
|
||||||
|
restart_count_24h() {
|
||||||
|
local ctn="$1" f="/tmp/readsb_restart_hist_${ctn}" now cutoff
|
||||||
|
now=$(date +%s); cutoff=$((now - 86400))
|
||||||
|
[ -f "$f" ] || { echo 0; return; }
|
||||||
|
awk -v c="$cutoff" '$1 >= c' "$f" > "${f}.tmp" && mv "${f}.tmp" "$f"
|
||||||
|
wc -l < "$f" | tr -d ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
record_restart() {
|
||||||
|
echo "$(date +%s)" >> "/tmp/readsb_restart_hist_$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Uptime d'un conteneur, en secondes ────────────────────────────────────────
|
||||||
|
container_uptime() {
|
||||||
|
local started
|
||||||
|
started=$(docker inspect --format '{{.State.StartedAt}}' "$1" 2>/dev/null) || return 1
|
||||||
|
[ -z "$started" ] && return 1
|
||||||
|
echo $(( $(date +%s) - $(date -d "$started" +%s 2>/dev/null || echo 0) ))
|
||||||
|
}
|
||||||
|
|
||||||
|
container_running() {
|
||||||
|
[ "$(docker inspect --format '{{.State.Status}}' "$1" 2>/dev/null)" = "running" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Sonde SBS : readsb émet-il réellement des messages décodés ? ───────────────
|
||||||
|
# Retourne 0 si du trafic ADS-B sort du récepteur lui-même.
|
||||||
|
sbs_has_traffic() {
|
||||||
|
local port="$1" bytes
|
||||||
|
if command -v nc >/dev/null 2>&1; then
|
||||||
|
bytes=$(timeout "$SBS_PROBE_TIMEOUT" nc 127.0.0.1 "$port" 2>/dev/null \
|
||||||
|
| head -c 400 | wc -c)
|
||||||
|
else
|
||||||
|
bytes=$(timeout "$SBS_PROBE_TIMEOUT" bash -c \
|
||||||
|
"exec 3<>/dev/tcp/127.0.0.1/$port && head -c 400 <&3" 2>/dev/null | wc -c)
|
||||||
|
fi
|
||||||
|
[ "${bytes:-0}" -ge "$SBS_MIN_BYTES" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Redémarrage encadré ───────────────────────────────────────────────────────
|
||||||
|
guarded_restart() {
|
||||||
|
local ctn="$1" reason="$2" up n
|
||||||
|
|
||||||
|
if ! container_running "$ctn"; then
|
||||||
|
log "[$ctn] non démarré — pas de redémarrage watchdog (relève de docker compose / adsb-stack.service)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
up=$(container_uptime "$ctn")
|
||||||
|
if [ -n "$up" ] && [ "$up" -lt "$MIN_UPTIME" ]; then
|
||||||
|
log "[$ctn] $reason — mais uptime ${up}s < ${MIN_UPTIME}s, on laisse converger (autogain / initialisation)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
n=$(restart_count_24h "$ctn")
|
||||||
|
if [ "$n" -ge "$MAX_RESTARTS_24H" ]; then
|
||||||
|
log "[ALERTE] [$ctn] $reason — $n redémarrages sur 24h, plafond atteint : AUCUNE action. Le redémarrage ne corrige pas ce problème, diagnostic manuel requis (gain, antenne, câble, alias réseau)."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "[$ctn] $reason — redémarrage ($((n + 1))/${MAX_RESTARTS_24H} sur 24h)"
|
||||||
|
docker restart "$ctn" >/dev/null 2>&1 && record_restart "$ctn"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Boucle principale ─────────────────────────────────────────────────────────
|
||||||
|
for i in "${!PRESENT_SERIALS[@]}"; do
|
||||||
|
n=$((i + 1))
|
||||||
|
[ "$n" -gt "$MAX_RECEIVERS" ] && break
|
||||||
|
|
||||||
|
IFS=':' read -r SLOT PORT CTN_RS CTN_T1090 CTN_PG PROFILE <<< "${SLOTS[$i]}"
|
||||||
|
RX_SERIAL="${PRESENT_SERIALS[$i]}"
|
||||||
|
read -r BEAST_PORT RAW_PORT SBS_PORT <<< "$(slot_ports "$SLOT")"
|
||||||
|
|
||||||
|
STATE_FILE="/tmp/readsb_last_msg_count_${RX_SERIAL}"
|
||||||
|
STREAK_FILE="/tmp/readsb_stall_streak_${RX_SERIAL}"
|
||||||
|
|
||||||
|
# Le conteneur readsb doit exister ; sinon rien à surveiller sur ce slot.
|
||||||
|
if ! docker inspect "$CTN_RS" >/dev/null 2>&1; then
|
||||||
|
log "[$CTN_RS] conteneur absent alors que le dongle $RX_SERIAL est présent — le stack n'est pas déployé pour ce slot (docker compose up -d --remove-orphans)"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT=$(curl -s --max-time 10 "http://127.0.0.1:${PORT}/data/aircraft.json" \
|
||||||
|
| grep -oP '"messages"\s*:\s*\K[0-9]+')
|
||||||
|
|
||||||
|
if [ -z "$CURRENT" ]; then
|
||||||
|
log "[$CTN_RS] aircraft.json illisible (port ${PORT}) — tar1090 muet ; check ignoré ce cycle"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
LAST=$(cat "$STATE_FILE" 2>/dev/null || echo -1)
|
||||||
|
STREAK=$(cat "$STREAK_FILE" 2>/dev/null || echo 0)
|
||||||
|
|
||||||
|
if [ "$CURRENT" == "$LAST" ]; then
|
||||||
|
STREAK=$((STREAK + 1))
|
||||||
|
|
||||||
|
if [ "$STREAK" -ge "$THRESHOLD" ]; then
|
||||||
|
# Compteur figé confirmé. Avant d'accuser readsb, on demande
|
||||||
|
# directement au récepteur s'il émet : c'est ce test qui distingue
|
||||||
|
# un dongle sourd d'un tar1090 déconnecté.
|
||||||
|
if sbs_has_traffic "$SBS_PORT"; then
|
||||||
|
log "[$CTN_RS] compteur tar1090 figé à $CURRENT depuis ${STREAK} cycle(s) MAIS le port SBS ${SBS_PORT} émet — readsb est sain, le maillon en cause est tar1090"
|
||||||
|
guarded_restart "$CTN_T1090" "aircraft.json figé alors que readsb émet (alias réseau BEASTHOST perdu ?)"
|
||||||
|
else
|
||||||
|
guarded_restart "$CTN_RS" "compteur messages figé à $CURRENT depuis ${STREAK} cycle(s) [$PERIOD, seuil=$THRESHOLD] et port SBS ${SBS_PORT} silencieux"
|
||||||
|
fi
|
||||||
|
STREAK=0
|
||||||
|
else
|
||||||
|
log "[$CTN_RS] compteur inchangé à $CURRENT (${STREAK}/${THRESHOLD} cycles) [$PERIOD] - en observation"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# Progression : on efface aussi l'historique de redémarrages, le
|
||||||
|
# récepteur est manifestement revenu à la normale.
|
||||||
|
if [ "$STREAK" -gt 0 ]; then
|
||||||
|
log "[$CTN_RS] flux rétabli ($LAST → $CURRENT) - compteur de blocage remis à zéro"
|
||||||
|
fi
|
||||||
|
STREAK=0
|
||||||
|
rm -f "/tmp/readsb_restart_hist_${CTN_RS}" "/tmp/readsb_restart_hist_${CTN_T1090}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$CURRENT" > "$STATE_FILE"
|
||||||
|
echo "$STREAK" > "$STREAK_FILE"
|
||||||
|
done
|
||||||
63
sql/01_audit_partitions.sql
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- 01_audit_partitions.sql — inventaire RÉEL des partitions de adsb.positions
|
||||||
|
--
|
||||||
|
-- Lecture seule. Ne modifie rien.
|
||||||
|
--
|
||||||
|
-- reltuples = -1 signifie « jamais analysée » en PostgreSQL 16, PAS « vide ».
|
||||||
|
-- Une somme à -16 (16 enfants × -1) est donc un compte INCONNU. Ce script
|
||||||
|
-- compte les lignes pour de vrai avant toute décision de suppression.
|
||||||
|
--
|
||||||
|
-- Usage :
|
||||||
|
-- docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \
|
||||||
|
-- -v ON_ERROR_STOP=1 < 01_audit_partitions.sql
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
\timing on
|
||||||
|
|
||||||
|
-- Vue d'ensemble
|
||||||
|
SELECT c.relkind,
|
||||||
|
count(*) AS nb,
|
||||||
|
CASE c.relkind WHEN 'r' THEN 'RANGE simple (cible)'
|
||||||
|
WHEN 'p' THEN 'encore sous-partitionnée HASH' END AS etat
|
||||||
|
FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
|
||||||
|
WHERE i.inhparent = 'adsb.positions'::regclass
|
||||||
|
GROUP BY 1, 3
|
||||||
|
ORDER BY 1;
|
||||||
|
|
||||||
|
-- Comptage réel de chaque partition de type 'p'.
|
||||||
|
-- Sur ~127 partitions dont la plupart sont vides, l'opération est rapide :
|
||||||
|
-- un count(*) sur une partition vide ne lit aucune page.
|
||||||
|
CREATE TEMP TABLE audit_parts AS
|
||||||
|
SELECT c.relname::text AS partition,
|
||||||
|
pg_get_expr(c.relpartbound, c.oid) AS bornes,
|
||||||
|
0::bigint AS lignes
|
||||||
|
FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
|
||||||
|
WHERE i.inhparent = 'adsb.positions'::regclass
|
||||||
|
AND c.relkind = 'p';
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE r record; n bigint;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN SELECT partition FROM audit_parts LOOP
|
||||||
|
EXECUTE format('SELECT count(*) FROM adsb.%I', r.partition) INTO n;
|
||||||
|
UPDATE audit_parts SET lignes = n WHERE partition = r.partition;
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Synthèse
|
||||||
|
SELECT count(*) FILTER (WHERE lignes = 0) AS partitions_vides,
|
||||||
|
count(*) FILTER (WHERE lignes > 0) AS partitions_avec_donnees,
|
||||||
|
sum(lignes) AS lignes_totales_a_migrer
|
||||||
|
FROM audit_parts;
|
||||||
|
|
||||||
|
-- Détail des partitions contenant des données (à migrer avec copie)
|
||||||
|
SELECT partition, bornes, lignes
|
||||||
|
FROM audit_parts
|
||||||
|
WHERE lignes > 0
|
||||||
|
ORDER BY bornes;
|
||||||
|
|
||||||
|
-- Bornes extrêmes des partitions vides (à recréer sans copie)
|
||||||
|
SELECT min(bornes) AS premiere_vide, max(bornes) AS derniere_vide
|
||||||
|
FROM audit_parts WHERE lignes = 0;
|
||||||
|
|
||||||
|
\timing off
|
||||||
98
sql/02_convert_partitions_vides.sql
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- 02_convert_partitions_vides.sql — supprime les partitions HASH VIDES et les
|
||||||
|
-- recrée en RANGE simple, avec leurs index.
|
||||||
|
--
|
||||||
|
-- Sans risque de perte : chaque partition est recomptée juste avant sa
|
||||||
|
-- suppression, et toute partition non vide est IGNORÉE (jamais supprimée).
|
||||||
|
-- C'est le garde-fou principal de ce script.
|
||||||
|
--
|
||||||
|
-- Gain attendu : ~2000 sous-partitions HASH en moins, donc un temps de
|
||||||
|
-- planification qui retombe de ~1500 ms à quelques millisecondes. L'exécution
|
||||||
|
-- des requêtes, elle, était déjà à ~5 ms — c'est bien la planification qui
|
||||||
|
-- coûte, car le planner doit ouvrir toutes les partitions pour les élaguer.
|
||||||
|
--
|
||||||
|
-- Usage OBLIGATOIRE avec ON_ERROR_STOP : sans ce flag, psql poursuit après une
|
||||||
|
-- erreur et pourrait exécuter un DROP alors que le contrôle a échoué.
|
||||||
|
-- docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \
|
||||||
|
-- -v ON_ERROR_STOP=1 < 02_convert_partitions_vides.sql
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
\timing on
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
r record;
|
||||||
|
n bigint;
|
||||||
|
d0 date;
|
||||||
|
d1 date;
|
||||||
|
tbl text;
|
||||||
|
converties int := 0;
|
||||||
|
ignorees int := 0;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN
|
||||||
|
SELECT c.oid, c.relname::text AS partition,
|
||||||
|
pg_get_expr(c.relpartbound, c.oid) AS bornes
|
||||||
|
FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
|
||||||
|
WHERE i.inhparent = 'adsb.positions'::regclass
|
||||||
|
AND c.relkind = 'p'
|
||||||
|
ORDER BY pg_get_expr(c.relpartbound, c.oid)
|
||||||
|
LOOP
|
||||||
|
-- GARDE-FOU : comptage réel immédiatement avant toute action destructive.
|
||||||
|
EXECUTE format('SELECT count(*) FROM adsb.%I', r.partition) INTO n;
|
||||||
|
|
||||||
|
IF n > 0 THEN
|
||||||
|
RAISE NOTICE 'IGNORÉE % : % ligne(s) — migration avec copie requise (script 03)',
|
||||||
|
r.partition, n;
|
||||||
|
ignorees := ignorees + 1;
|
||||||
|
CONTINUE;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Extraction des bornes réelles depuis relpartbound : on ne se fie pas au
|
||||||
|
-- nom, qui peut être erroné (bug EXTRACT(YEAR) vs ISOYEAR).
|
||||||
|
d0 := (regexp_match(r.bornes, 'FROM \(''([0-9-]+)'))[1]::date;
|
||||||
|
d1 := (regexp_match(r.bornes, 'TO \(''([0-9-]+)'))[1]::date;
|
||||||
|
tbl := r.partition;
|
||||||
|
|
||||||
|
EXECUTE format('DROP TABLE adsb.%I', tbl);
|
||||||
|
|
||||||
|
EXECUTE format(
|
||||||
|
'CREATE TABLE adsb.%I PARTITION OF adsb.positions FOR VALUES FROM (%L) TO (%L)',
|
||||||
|
tbl, d0, d1);
|
||||||
|
|
||||||
|
-- Index identiques à ceux posés par adsb.create_week_partition
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I USING GIST (geom)', tbl||'_geom_gix', tbl);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (ts DESC)', tbl||'_ts_idx', tbl);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (icao, ts DESC)', tbl||'_icao_ts_idx', tbl);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (inserted_at DESC)', tbl||'_inserted_at_idx', tbl);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (callsign) WHERE callsign IS NOT NULL',
|
||||||
|
tbl||'_callsign_idx', tbl);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (snap_id)', tbl||'_snap_id_idx', tbl);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (receiver_id)', tbl||'_receiver_id_idx', tbl);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (aircraft_wtc)', tbl||'_aircraft_wtc_idx', tbl);
|
||||||
|
EXECUTE format(
|
||||||
|
'ALTER TABLE adsb.%I ADD CONSTRAINT %I FOREIGN KEY (receiver_id) '
|
||||||
|
'REFERENCES adsb.receivers(id) ON DELETE SET NULL',
|
||||||
|
tbl, tbl||'_receiver_fk');
|
||||||
|
|
||||||
|
converties := converties + 1;
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
RAISE NOTICE '─────────────────────────────────────────────';
|
||||||
|
RAISE NOTICE '% partition(s) converties en RANGE simple', converties;
|
||||||
|
RAISE NOTICE '% partition(s) ignorées (contiennent des données)', ignorees;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Résultat
|
||||||
|
SELECT c.relkind, count(*) AS nb
|
||||||
|
FROM pg_inherits i JOIN pg_class c ON c.oid = i.inhrelid
|
||||||
|
WHERE i.inhparent = 'adsb.positions'::regclass
|
||||||
|
GROUP BY 1 ORDER BY 1;
|
||||||
|
|
||||||
|
SELECT count(*) AS total_partitions_et_sous_partitions
|
||||||
|
FROM pg_class c JOIN pg_inherits i ON i.inhrelid = c.oid
|
||||||
|
WHERE i.inhparent = 'adsb.positions'::regclass
|
||||||
|
OR i.inhparent IN (SELECT inhrelid FROM pg_inherits
|
||||||
|
WHERE inhparent = 'adsb.positions'::regclass);
|
||||||
|
|
||||||
|
\timing off
|
||||||
141
sql/03_convert_partition_avec_donnees.sql
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- 03_convert_partition_avec_donnees.sql — migre UNE partition contenant des
|
||||||
|
-- données, de HASH×16 vers RANGE simple, sans perte.
|
||||||
|
--
|
||||||
|
-- À lancer une partition à la fois, en passant son nom :
|
||||||
|
-- docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \
|
||||||
|
-- -v ON_ERROR_STOP=1 -v part=positions_2026_w31 \
|
||||||
|
-- < 03_convert_partition_avec_donnees.sql
|
||||||
|
--
|
||||||
|
-- PRÉALABLE OBLIGATOIRE — sauvegarde de la partition concernée :
|
||||||
|
-- docker exec wordpress-postgres pg_dump -U pgz_admin -d wpz_postgres \
|
||||||
|
-- -t "adsb.<partition>*" -Fc -f /tmp/<partition>.dump
|
||||||
|
-- docker cp wordpress-postgres:/tmp/<partition>.dump /data/adsb/backups/
|
||||||
|
--
|
||||||
|
-- Principe :
|
||||||
|
-- 1. DETACH de l'ancienne partition (les données restent, hors du parent)
|
||||||
|
-- 2. CREATE de la nouvelle partition RANGE simple, attachée
|
||||||
|
-- 3. INSERT ... SELECT depuis l'ancienne vers le parent
|
||||||
|
-- 4. Contrôle des comptes AVANT/APRÈS — DROP uniquement si identiques
|
||||||
|
--
|
||||||
|
-- Le tout dans UNE transaction : en cas de problème, ROLLBACK ramène l'état
|
||||||
|
-- initial. Pendant l'opération, adsb2pg peut continuer d'insérer : ses lignes
|
||||||
|
-- arrivent dans la nouvelle partition, déjà attachée.
|
||||||
|
--
|
||||||
|
-- NOTE : sur la partition COURANTE, faire l'opération à faible trafic. Les
|
||||||
|
-- lignes insérées entre le DETACH et le CREATE (quelques millisecondes)
|
||||||
|
-- provoqueraient une erreur « no partition of relation found » côté adsb2pg,
|
||||||
|
-- qui les réessaiera au cycle suivant.
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
\timing on
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
|
\if :{?part}
|
||||||
|
\else
|
||||||
|
\echo 'ERREUR : passer -v part=<nom_partition>'
|
||||||
|
\quit 1
|
||||||
|
\endif
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Verrou explicite : empêche toute modification concurrente de la structure
|
||||||
|
LOCK TABLE adsb.positions IN SHARE UPDATE EXCLUSIVE MODE;
|
||||||
|
|
||||||
|
-- Transmission du nom de partition au bloc PL/pgSQL (les variables psql ne
|
||||||
|
-- sont pas visibles depuis un DO $$ : il faut passer par un paramètre de
|
||||||
|
-- session).
|
||||||
|
SET LOCAL my.part = :'part';
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
p_old text := current_setting('my.part');
|
||||||
|
p_tmp text := current_setting('my.part') || '_old';
|
||||||
|
bornes text;
|
||||||
|
d0 date;
|
||||||
|
d1 date;
|
||||||
|
n_avant bigint;
|
||||||
|
n_apres bigint;
|
||||||
|
cols text;
|
||||||
|
BEGIN
|
||||||
|
SELECT pg_get_expr(c.relpartbound, c.oid) INTO bornes
|
||||||
|
FROM pg_class c JOIN pg_namespace ns ON ns.oid = c.relnamespace
|
||||||
|
WHERE ns.nspname = 'adsb' AND c.relname = p_old;
|
||||||
|
|
||||||
|
IF bornes IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'Partition adsb.% introuvable ou non attachée', p_old;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
d0 := (regexp_match(bornes, 'FROM \(''([0-9-]+)'))[1]::date;
|
||||||
|
d1 := (regexp_match(bornes, 'TO \(''([0-9-]+)'))[1]::date;
|
||||||
|
|
||||||
|
EXECUTE format('SELECT count(*) FROM adsb.%I', p_old) INTO n_avant;
|
||||||
|
RAISE NOTICE 'Partition % : % ligne(s) à migrer (bornes % → %)', p_old, n_avant, d0, d1;
|
||||||
|
|
||||||
|
-- 1. Détacher et renommer l'ancienne
|
||||||
|
EXECUTE format('ALTER TABLE adsb.positions DETACH PARTITION adsb.%I', p_old);
|
||||||
|
EXECUTE format('ALTER TABLE adsb.%I RENAME TO %I', p_old, p_tmp);
|
||||||
|
|
||||||
|
-- 2. Créer la nouvelle, en RANGE simple
|
||||||
|
EXECUTE format(
|
||||||
|
'CREATE TABLE adsb.%I PARTITION OF adsb.positions FOR VALUES FROM (%L) TO (%L)',
|
||||||
|
p_old, d0, d1);
|
||||||
|
|
||||||
|
-- 3. Recopier les données.
|
||||||
|
-- geom est GENERATED ALWAYS : PostgreSQL refuse toute valeur explicite
|
||||||
|
-- (« cannot insert a non-DEFAULT value into column geom »). Un SELECT *
|
||||||
|
-- la remonterait, il faut donc énumérer les colonnes réelles et l'exclure.
|
||||||
|
-- Elle sera recalculée automatiquement depuis lat/lon à l'insertion.
|
||||||
|
SELECT string_agg(quote_ident(a.attname), ', ' ORDER BY a.attnum)
|
||||||
|
INTO cols
|
||||||
|
FROM pg_attribute a
|
||||||
|
WHERE a.attrelid = format('adsb.%I', p_tmp)::regclass
|
||||||
|
AND a.attnum > 0
|
||||||
|
AND NOT a.attisdropped
|
||||||
|
AND a.attgenerated = ''; -- exclut les colonnes générées
|
||||||
|
|
||||||
|
IF cols IS NULL OR cols = '' THEN
|
||||||
|
RAISE EXCEPTION 'Impossible de déterminer les colonnes de adsb.%', p_tmp;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Colonnes copiées (geom exclue, recalculée) : %', cols;
|
||||||
|
|
||||||
|
EXECUTE format('INSERT INTO adsb.%I (%s) SELECT %s FROM adsb.%I',
|
||||||
|
p_old, cols, cols, p_tmp);
|
||||||
|
|
||||||
|
EXECUTE format('SELECT count(*) FROM adsb.%I', p_old) INTO n_apres;
|
||||||
|
|
||||||
|
-- 4. Contrôle : on ne supprime QUE si le compte correspond exactement.
|
||||||
|
IF n_apres <> n_avant THEN
|
||||||
|
RAISE EXCEPTION 'ÉCART DE COMPTE : % avant, % après — transaction annulée, aucune donnée perdue',
|
||||||
|
n_avant, n_apres;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
RAISE NOTICE 'Copie vérifiée : % ligne(s) — suppression de l''ancienne structure', n_apres;
|
||||||
|
EXECUTE format('DROP TABLE adsb.%I', p_tmp);
|
||||||
|
|
||||||
|
-- 5. Index sur la nouvelle partition
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I USING GIST (geom)', p_old||'_geom_gix', p_old);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (ts DESC)', p_old||'_ts_idx', p_old);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (icao, ts DESC)', p_old||'_icao_ts_idx', p_old);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (inserted_at DESC)', p_old||'_inserted_at_idx', p_old);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (callsign) WHERE callsign IS NOT NULL',
|
||||||
|
p_old||'_callsign_idx', p_old);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (snap_id)', p_old||'_snap_id_idx', p_old);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (receiver_id)', p_old||'_receiver_id_idx', p_old);
|
||||||
|
EXECUTE format('CREATE INDEX %I ON adsb.%I (aircraft_wtc)', p_old||'_aircraft_wtc_idx', p_old);
|
||||||
|
EXECUTE format(
|
||||||
|
'ALTER TABLE adsb.%I ADD CONSTRAINT %I FOREIGN KEY (receiver_id) '
|
||||||
|
'REFERENCES adsb.receivers(id) ON DELETE SET NULL',
|
||||||
|
p_old, p_old||'_receiver_fk');
|
||||||
|
|
||||||
|
RAISE NOTICE 'Partition % convertie en RANGE simple avec ses index', p_old;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- ANALYZE hors transaction : indispensable, la nouvelle partition n'a aucune
|
||||||
|
-- statistique et le planner ferait de mauvais choix.
|
||||||
|
ANALYZE adsb.positions;
|
||||||
|
|
||||||
|
\timing off
|
||||||
88
sql/04_airports.sql
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- 04_airports.sql — table de référence des aérodromes d'Île-de-France
|
||||||
|
--
|
||||||
|
-- Sert de liste d'exclusion à l'onglet « Départs hors aérodromes » : tout point
|
||||||
|
-- de décollage situé dans le rayon d'un aérodrome ACTIF est écarté des
|
||||||
|
-- résultats. Chaque site a son propre rayon, ajustable indépendamment.
|
||||||
|
--
|
||||||
|
-- Le rayon n'est pas la taille de l'aérodrome mais la zone dans laquelle un
|
||||||
|
-- avion en montée initiale est encore attribuable à ce terrain. Il est donc
|
||||||
|
-- plus large pour les grandes plateformes (trajectoires de départ étendues)
|
||||||
|
-- que pour un héliport.
|
||||||
|
--
|
||||||
|
-- Usage :
|
||||||
|
-- docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \
|
||||||
|
-- -v ON_ERROR_STOP=1 < 04_airports.sql
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS adsb.airports (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
code text NOT NULL UNIQUE, -- code OACI (LFPG, LFPO, ...)
|
||||||
|
nom text NOT NULL,
|
||||||
|
lat double precision NOT NULL,
|
||||||
|
lon double precision NOT NULL,
|
||||||
|
rayon_km double precision NOT NULL DEFAULT 8,
|
||||||
|
type text NOT NULL DEFAULT 'aerodrome', -- aeroport|aerodrome|heliport|base
|
||||||
|
actif boolean NOT NULL DEFAULT true,
|
||||||
|
commentaire text
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE adsb.airports IS
|
||||||
|
'Aérodromes connus, utilisés comme zones d''exclusion pour la détection des '
|
||||||
|
'départs atypiques. actif=false désactive l''exclusion sans perdre la fiche.';
|
||||||
|
COMMENT ON COLUMN adsb.airports.rayon_km IS
|
||||||
|
'Rayon d''exclusion en km : zone dans laquelle une montée initiale est '
|
||||||
|
'considérée comme issue de ce terrain.';
|
||||||
|
|
||||||
|
-- Colonne géographique générée, pour un ST_DWithin indexable
|
||||||
|
ALTER TABLE adsb.airports
|
||||||
|
ADD COLUMN IF NOT EXISTS geom geography(Point,4326)
|
||||||
|
GENERATED ALWAYS AS (ST_SetSRID(ST_MakePoint(lon, lat), 4326)::geography) STORED;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS airports_geom_gix ON adsb.airports USING GIST (geom);
|
||||||
|
CREATE INDEX IF NOT EXISTS airports_actif_idx ON adsb.airports (actif) WHERE actif;
|
||||||
|
|
||||||
|
-- ── Données de référence ─────────────────────────────────────────────────────
|
||||||
|
-- Rayons de départ, à ajuster selon les résultats observés.
|
||||||
|
INSERT INTO adsb.airports (code, nom, lat, lon, rayon_km, type, commentaire) VALUES
|
||||||
|
('LFPG', 'Paris-Charles de Gaulle', 49.0097, 2.5479, 15, 'aeroport',
|
||||||
|
'Doublets de pistes, départs très étalés — rayon large'),
|
||||||
|
('LFPO', 'Paris-Orly', 48.7233, 2.3794, 12, 'aeroport',
|
||||||
|
'Départs vers le sud et l''ouest'),
|
||||||
|
('LFPB', 'Paris-Le Bourget', 48.9694, 2.4414, 10, 'aeroport',
|
||||||
|
'Aviation d''affaires, trafic dense en jets'),
|
||||||
|
('LFPV', 'Vélizy-Villacoublay', 48.7744, 2.2014, 8, 'base',
|
||||||
|
'Base aérienne 107 — à ~6 km de la station, très bien captée'),
|
||||||
|
('LFPN', 'Toussus-le-Noble', 48.7519, 2.1062, 8, 'aerodrome',
|
||||||
|
'Aviation légère et écoles — fort volume de tours de piste'),
|
||||||
|
('LFPZ', 'Saint-Cyr-l''École', 48.8081, 2.0736, 6, 'aerodrome',
|
||||||
|
'Aviation légère, planeurs'),
|
||||||
|
('LFPI', 'Paris-Issy-les-Moulineaux', 48.8331, 2.2725, 4, 'heliport',
|
||||||
|
'Héliport de Paris — à ~1 km de la station, couverture excellente'),
|
||||||
|
('LFPM', 'Melun-Villaroche', 48.6047, 2.6711, 8, 'aerodrome',
|
||||||
|
'Essais en vol et aviation générale'),
|
||||||
|
('LFPT', 'Pontoise-Cormeilles', 49.0966, 2.0408, 8, 'aerodrome',
|
||||||
|
'Aviation légère, école'),
|
||||||
|
('LFPL', 'Lognes-Émerainville', 48.8228, 2.6261, 6, 'aerodrome',
|
||||||
|
'Aviation légère'),
|
||||||
|
('LFPK', 'Coulommiers-Voisins', 48.8375, 3.0161, 8, 'aerodrome',
|
||||||
|
'Limite est de la couverture'),
|
||||||
|
('LFXU', 'Les Mureaux', 48.9983, 1.9433, 6, 'aerodrome',
|
||||||
|
'Aérodrome industriel'),
|
||||||
|
('LFPU', 'Moret-Épisy', 48.3422, 2.7986, 6, 'aerodrome',
|
||||||
|
'Planeurs — souvent hors de portée'),
|
||||||
|
('LFAI', 'Nangis-Les Loges', 48.5961, 3.0067, 6, 'aerodrome',
|
||||||
|
'Aviation légère, parachutisme'),
|
||||||
|
('LFOP', 'Rouen-Vallée de Seine', 49.3842, 1.1758, 10, 'aeroport',
|
||||||
|
'Hors couverture nominale — désactivable'),
|
||||||
|
('LFOB', 'Beauvais-Tillé', 49.4544, 2.1128, 12, 'aeroport',
|
||||||
|
'Limite nord de la couverture')
|
||||||
|
ON CONFLICT (code) DO NOTHING;
|
||||||
|
|
||||||
|
-- Sites au-delà de la portée utile : renseignés mais désactivés par défaut,
|
||||||
|
-- pour éviter d'exclure inutilement des zones où l'on ne capte rien.
|
||||||
|
UPDATE adsb.airports SET actif = false WHERE code IN ('LFOP', 'LFPU');
|
||||||
|
|
||||||
|
SELECT code, nom, rayon_km, type, actif FROM adsb.airports ORDER BY actif DESC, code;
|
||||||
76
sql/05_airports_zones.sql
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- 05_airports_zones.sql — gestion manuelle des zones d'exclusion
|
||||||
|
--
|
||||||
|
-- Étend adsb.airports pour accueillir, à côté des aérodromes de référence, des
|
||||||
|
-- zones ajoutées à la main depuis l'interface (typiquement une zone découverte
|
||||||
|
-- par l'analyse et que l'on souhaite écarter des recherches suivantes).
|
||||||
|
--
|
||||||
|
-- Idempotent : peut être relancé sans risque.
|
||||||
|
--
|
||||||
|
-- Usage :
|
||||||
|
-- docker exec -i wordpress-postgres psql -U pgz_admin -d wpz_postgres \
|
||||||
|
-- -v ON_ERROR_STOP=1 < 05_airports_zones.sql
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
|
-- Origine de la fiche : distingue ce qui vient du référentiel de ce que
|
||||||
|
-- l'utilisateur a ajouté. Permet de réinitialiser les aérodromes sans perdre
|
||||||
|
-- les zones personnelles, et inversement.
|
||||||
|
ALTER TABLE adsb.airports
|
||||||
|
ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'reference';
|
||||||
|
|
||||||
|
ALTER TABLE adsb.airports
|
||||||
|
ADD COLUMN IF NOT EXISTS cree_le timestamptz NOT NULL DEFAULT now();
|
||||||
|
|
||||||
|
ALTER TABLE adsb.airports
|
||||||
|
ADD COLUMN IF NOT EXISTS modifie_le timestamptz NOT NULL DEFAULT now();
|
||||||
|
|
||||||
|
-- Contrainte de valeurs, ajoutée seulement si absente
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'airports_source_chk') THEN
|
||||||
|
ALTER TABLE adsb.airports
|
||||||
|
ADD CONSTRAINT airports_source_chk
|
||||||
|
CHECK (source IN ('reference', 'manuelle'));
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'airports_rayon_chk') THEN
|
||||||
|
ALTER TABLE adsb.airports
|
||||||
|
ADD CONSTRAINT airports_rayon_chk
|
||||||
|
CHECK (rayon_km > 0 AND rayon_km <= 100);
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'airports_coord_chk') THEN
|
||||||
|
ALTER TABLE adsb.airports
|
||||||
|
ADD CONSTRAINT airports_coord_chk
|
||||||
|
CHECK (lat BETWEEN -90 AND 90 AND lon BETWEEN -180 AND 180);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN adsb.airports.source IS
|
||||||
|
'reference = aérodrome du référentiel livré ; manuelle = zone ajoutée depuis '
|
||||||
|
'l''interface (zone découverte, terrain privé, hélisurface…)';
|
||||||
|
|
||||||
|
-- Le type accepte désormais des valeurs propres aux zones manuelles.
|
||||||
|
-- Pas de contrainte CHECK sur `type` : la liste doit rester ouverte, on ne sait
|
||||||
|
-- pas d'avance ce que l'utilisateur va découvrir.
|
||||||
|
COMMENT ON COLUMN adsb.airports.type IS
|
||||||
|
'aeroport | aerodrome | heliport | base | ulm | helisurface | prive | inconnu | …';
|
||||||
|
|
||||||
|
-- Mise à jour automatique de modifie_le
|
||||||
|
CREATE OR REPLACE FUNCTION adsb.airports_touch() RETURNS trigger AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.modifie_le := now();
|
||||||
|
RETURN NEW;
|
||||||
|
END $$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS airports_touch_trg ON adsb.airports;
|
||||||
|
CREATE TRIGGER airports_touch_trg
|
||||||
|
BEFORE UPDATE ON adsb.airports
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION adsb.airports_touch();
|
||||||
|
|
||||||
|
-- Les fiches existantes viennent du référentiel
|
||||||
|
UPDATE adsb.airports SET source = 'reference' WHERE source IS NULL;
|
||||||
|
|
||||||
|
SELECT source, count(*) FILTER (WHERE actif) AS actives,
|
||||||
|
count(*) AS total
|
||||||
|
FROM adsb.airports GROUP BY source ORDER BY source;
|
||||||
20
start.sh
Executable file
@ -0,0 +1,20 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Trouve le device du dongle ADS-B par numéro de série et lance docker compose
|
||||||
|
|
||||||
|
DONGLE=$(lsusb | grep "0bda:2838")
|
||||||
|
if [ -z "$DONGLE" ]; then
|
||||||
|
echo "ERREUR: Dongle ADS-B non trouvé !"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extraire bus et device
|
||||||
|
BUS=$(echo "$DONGLE" | awk '{print $2}')
|
||||||
|
DEV=$(echo "$DONGLE" | awk '{print $4}' | tr -d ':')
|
||||||
|
DEVICE_PATH="/dev/bus/usb/$(printf '%03d' $BUS)/$(printf '%03d' $DEV)"
|
||||||
|
|
||||||
|
echo "Dongle trouvé : $DEVICE_PATH"
|
||||||
|
|
||||||
|
# Mettre à jour le docker-compose.yml dynamiquement
|
||||||
|
sed -i "s|/dev/bus/usb/[0-9]*/[0-9]*:/dev/adsb_dongle|${DEVICE_PATH}:/dev/adsb_dongle|" /data/adsb/docker-compose.yml
|
||||||
|
|
||||||
|
docker compose -f /data/adsb/docker-compose.yml up -d
|
||||||
518
update-adsb.sh
Executable file
@ -0,0 +1,518 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# update-adsb.sh — Mise à jour sécurisée du stack ADS-B
|
||||||
|
#
|
||||||
|
# Stratégie :
|
||||||
|
# - Images publiques : docker pull + recréation si nouvelle version
|
||||||
|
# - Images locales : rebuild --no-cache (nouvelles versions des paquets)
|
||||||
|
# - Données/config : jamais touchées (volumes nommés + fichiers sources)
|
||||||
|
# - Rollback : automatique si un conteneur ne démarre pas
|
||||||
|
#
|
||||||
|
# Usage :
|
||||||
|
# sudo ./update-adsb.sh [--check-only] [--service <nom>] [--force]
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
STACK_DIR="${STACK_DIR:-/data/adsb}"
|
||||||
|
LOG_FILE="/var/log/adsb-update.log"
|
||||||
|
BACKUP_DIR="/data/adsb/backups"
|
||||||
|
|
||||||
|
# ── Fichiers d'environnement (v4.2) ──────────────────────────────────────────
|
||||||
|
# Depuis le passage aux profiles Compose, docker compose doit recevoir DEUX
|
||||||
|
# --env-file : .env (secret PG_PASSWORD) et .env.receivers (RX<n>_SERIAL +
|
||||||
|
# COMPOSE_PROFILES, généré par gen_env.sh). Sans eux, Compose émet
|
||||||
|
# « The "RX1_SERIAL" variable is not set » et recréerait readsb avec un numéro
|
||||||
|
# de série vide — le conteneur démarrerait alors sur un dongle arbitraire.
|
||||||
|
ENV_FILE="${STACK_DIR}/.env"
|
||||||
|
ENV_RECEIVERS="${STACK_DIR}/.env.receivers"
|
||||||
|
GEN_ENV="${STACK_DIR}/scripts/gen_env.sh"
|
||||||
|
|
||||||
|
COMPOSE_ARGS=(-f "${STACK_DIR}/docker-compose.yml")
|
||||||
|
[[ -f "$ENV_FILE" ]] && COMPOSE_ARGS+=(--env-file "$ENV_FILE")
|
||||||
|
[[ -f "$ENV_RECEIVERS" ]] && COMPOSE_ARGS+=(--env-file "$ENV_RECEIVERS")
|
||||||
|
COMPOSE="docker compose ${COMPOSE_ARGS[*]}"
|
||||||
|
|
||||||
|
# ── Couleurs ─────────────────────────────────────────────────────────────────
|
||||||
|
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
|
||||||
|
info() { echo -e "${CYAN}[INFO]${NC} $*" | tee -a "$LOG_FILE"; }
|
||||||
|
success() { echo -e "${GREEN}[OK]${NC} $*" | tee -a "$LOG_FILE"; }
|
||||||
|
warn() { echo -e "${YELLOW}[WARN]${NC} $*" | tee -a "$LOG_FILE"; }
|
||||||
|
error() { echo -e "${RED}[ERROR]${NC} $*" | tee -a "$LOG_FILE" >&2; }
|
||||||
|
header() { echo -e "\n${BOLD}${BLUE}=== $* ===${NC}\n" | tee -a "$LOG_FILE"; }
|
||||||
|
die() { error "$*"; exit 1; }
|
||||||
|
|
||||||
|
# ── Arguments ────────────────────────────────────────────────────────────────
|
||||||
|
CHECK_ONLY=false
|
||||||
|
FORCE=false
|
||||||
|
TARGET_SERVICE=""
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
--check-only) CHECK_ONLY=true ; shift ;;
|
||||||
|
--force) FORCE=true ; shift ;;
|
||||||
|
--service) TARGET_SERVICE="$2" ; shift 2 ;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [--check-only] [--service <nom>] [--force]"
|
||||||
|
echo " --check-only Vérifie les mises à jour sans les appliquer"
|
||||||
|
echo " --service nom Met à jour uniquement ce service"
|
||||||
|
echo " --force Force le rebuild même sans nouvelle version"
|
||||||
|
exit 0 ;;
|
||||||
|
*) die "Argument inconnu : $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Prérequis ────────────────────────────────────────────────────────────────
|
||||||
|
[[ $EUID -eq 0 ]] || die "Exécuter en root"
|
||||||
|
command -v docker &>/dev/null || die "Docker non trouvé"
|
||||||
|
[[ -f "${STACK_DIR}/docker-compose.yml" ]] || die "docker-compose.yml introuvable dans ${STACK_DIR}"
|
||||||
|
|
||||||
|
# ── Prérequis v4.2 : environnement multi-récepteurs ──────────────────────────
|
||||||
|
[[ -f "$ENV_FILE" ]] || warn "$ENV_FILE absent — PG_PASSWORD ne sera pas injecté"
|
||||||
|
|
||||||
|
if [[ -x "$GEN_ENV" ]]; then
|
||||||
|
# Régénère .env.receivers depuis les dongles réellement branchés : évite de
|
||||||
|
# recréer un conteneur sur un slot dont le dongle a disparu depuis le
|
||||||
|
# dernier démarrage du stack.
|
||||||
|
if "$GEN_ENV" >/dev/null 2>&1; then
|
||||||
|
RX_COUNT=$(grep -c '^RX[0-9]*_SERIAL=..*' "$ENV_RECEIVERS" 2>/dev/null || echo 0)
|
||||||
|
PROFILES=$(sed -n 's/^COMPOSE_PROFILES=//p' "$ENV_RECEIVERS" 2>/dev/null)
|
||||||
|
success "Environnement régénéré : ${RX_COUNT} récepteur(s) — profiles=${PROFILES:-aucun}"
|
||||||
|
[[ -z "$PROFILES" ]] && warn "Aucun dongle RTL-SDR détecté — les services readsb/tar1090/adsb2pg ne seront pas recréés"
|
||||||
|
else
|
||||||
|
warn "gen_env.sh a échoué — utilisation de $ENV_RECEIVERS tel quel"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "$GEN_ENV absent — stack antérieur à la v4.2 ? Les profiles ne seront pas recalculés"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
echo "" >> "$LOG_FILE"
|
||||||
|
echo "═══════════════════════════════════════════════════" >> "$LOG_FILE"
|
||||||
|
echo " Mise à jour ADS-B — $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
|
||||||
|
echo "═══════════════════════════════════════════════════" >> "$LOG_FILE"
|
||||||
|
|
||||||
|
# ── Définition des services ───────────────────────────────────────────────────
|
||||||
|
# Format : "nom:type:image_ou_contexte"
|
||||||
|
# type = public (image Docker Hub/ghcr) | local (build local)
|
||||||
|
declare -A SERVICE_TYPE=(
|
||||||
|
[readsb]="public"
|
||||||
|
[tar1090]="public"
|
||||||
|
[adsb2pg]="local"
|
||||||
|
[webapp]="local"
|
||||||
|
# v4.2 : slots 2 et 3, présents seulement si un dongle les alimente
|
||||||
|
[readsb2]="public"
|
||||||
|
[tar10902]="public"
|
||||||
|
[adsb2pg2]="local"
|
||||||
|
[readsb3]="public"
|
||||||
|
[tar10903]="public"
|
||||||
|
[adsb2pg3]="local"
|
||||||
|
)
|
||||||
|
|
||||||
|
declare -A SERVICE_IMAGE=(
|
||||||
|
[readsb]="ghcr.io/sdr-enthusiasts/docker-readsb-protobuf:latest"
|
||||||
|
[tar1090]="ghcr.io/sdr-enthusiasts/docker-tar1090:latest"
|
||||||
|
[adsb2pg]=""
|
||||||
|
[webapp]=""
|
||||||
|
[readsb2]="ghcr.io/sdr-enthusiasts/docker-readsb-protobuf:latest"
|
||||||
|
[tar10902]="ghcr.io/sdr-enthusiasts/docker-tar1090:latest"
|
||||||
|
[adsb2pg2]=""
|
||||||
|
[readsb3]="ghcr.io/sdr-enthusiasts/docker-readsb-protobuf:latest"
|
||||||
|
[tar10903]="ghcr.io/sdr-enthusiasts/docker-tar1090:latest"
|
||||||
|
[adsb2pg3]=""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Nom du conteneur associé à un service (les slots 2/3 ne suivent pas la
|
||||||
|
# convention adsb-<service> : le service tar10902 donne adsb-tar10902).
|
||||||
|
service_container() {
|
||||||
|
case "$1" in
|
||||||
|
tar10902) echo "adsb-tar10902" ;;
|
||||||
|
tar10903) echo "adsb-tar10903" ;;
|
||||||
|
*) echo "adsb-$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Services réellement déployables : on ne traite un slot que si son conteneur
|
||||||
|
# TOURNE. docker inspect réussit aussi sur un conteneur arrêté — un slot dont
|
||||||
|
# le dongle a été retiré laisse un conteneur stoppé derrière lui, qu'il ne faut
|
||||||
|
# ni mettre à jour ni redémarrer (il serait relancé sans profile actif).
|
||||||
|
service_is_active() {
|
||||||
|
[[ "$(docker inspect --format '{{.State.Running}}' \
|
||||||
|
"$(service_container "$1")" 2>/dev/null)" == "true" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Services à traiter
|
||||||
|
if [[ -n "$TARGET_SERVICE" ]]; then
|
||||||
|
SERVICES=("$TARGET_SERVICE")
|
||||||
|
else
|
||||||
|
# webapp d'abord (toujours présent), puis les slots réellement déployés
|
||||||
|
SERVICES=("webapp")
|
||||||
|
for _svc in readsb tar1090 adsb2pg readsb2 tar10902 adsb2pg2 readsb3 tar10903 adsb2pg3; do
|
||||||
|
service_is_active "$_svc" && SERVICES+=("$_svc")
|
||||||
|
done
|
||||||
|
info "Services déployés à traiter : ${SERVICES[*]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Sauvegarde de la config avant mise à jour ─────────────────────────────────
|
||||||
|
backup_config() {
|
||||||
|
header "Sauvegarde de la configuration"
|
||||||
|
local backup="${BACKUP_DIR}/config_$(date +%Y%m%d_%H%M%S)"
|
||||||
|
mkdir -p "$backup"
|
||||||
|
|
||||||
|
# docker-compose.yml
|
||||||
|
cp "${STACK_DIR}/docker-compose.yml" "$backup/"
|
||||||
|
|
||||||
|
# Config adsb2pg
|
||||||
|
[[ -f "${STACK_DIR}/adsb2pg/adsb2pg.py" ]] && \
|
||||||
|
cp "${STACK_DIR}/adsb2pg/adsb2pg.py" "$backup/"
|
||||||
|
|
||||||
|
# Config webapp
|
||||||
|
[[ -d "${STACK_DIR}/webapp/src" ]] && \
|
||||||
|
cp -r "${STACK_DIR}/webapp/src" "$backup/webapp-src/"
|
||||||
|
|
||||||
|
# adsb.conf
|
||||||
|
[[ -f "${STACK_DIR}/adsb.conf" ]] && \
|
||||||
|
cp "${STACK_DIR}/adsb.conf" "$backup/"
|
||||||
|
|
||||||
|
success "Config sauvegardée dans $backup"
|
||||||
|
echo "$backup"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Sauvegarder l'ID de l'image actuelle (pour rollback) ─────────────────────
|
||||||
|
get_image_id() {
|
||||||
|
local service=$1
|
||||||
|
docker inspect --format='{{.Image}}' "$(service_container "$service")" 2>/dev/null || echo ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Vérifier si une nouvelle image est disponible ────────────────────────────
|
||||||
|
check_public_update() {
|
||||||
|
local service=$1
|
||||||
|
local image="${SERVICE_IMAGE[$service]}"
|
||||||
|
local current_id
|
||||||
|
current_id=$(get_image_id "$service")
|
||||||
|
|
||||||
|
info "Vérification de $service ($image)..."
|
||||||
|
|
||||||
|
# Puller la nouvelle image
|
||||||
|
if ! docker pull "$image" 2>&1 | tee -a "$LOG_FILE"; then
|
||||||
|
warn "Impossible de puller $image"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local new_id
|
||||||
|
new_id=$(docker inspect --format='{{.Id}}' "$image" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ "$current_id" == "$new_id" ]] && [[ "$FORCE" != "true" ]]; then
|
||||||
|
success "$service : déjà à jour (${new_id:7:12})"
|
||||||
|
return 1 # Pas de mise à jour nécessaire
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$current_id" != "$new_id" ]]; then
|
||||||
|
info "$service : nouvelle version disponible"
|
||||||
|
info " Avant : ${current_id:7:12}"
|
||||||
|
info " Après : ${new_id:7:12}"
|
||||||
|
else
|
||||||
|
info "$service : forçage du redémarrage"
|
||||||
|
fi
|
||||||
|
return 0 # Mise à jour disponible/forcée
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Vérifier les mises à jour des paquets dans une image locale ───────────────
|
||||||
|
check_local_update() {
|
||||||
|
local service=$1
|
||||||
|
info "Vérification des mises à jour de sécurité pour $service..."
|
||||||
|
|
||||||
|
# Simuler un build avec --no-cache pour voir s'il y a des paquets à mettre à jour
|
||||||
|
# On vérifie la date du dernier build
|
||||||
|
local built_at
|
||||||
|
built_at=$(docker inspect --format='{{.Created}}' "$(service_container "$service")" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
if [[ -z "$built_at" ]]; then
|
||||||
|
warn "$service : conteneur non trouvé, build nécessaire"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local built_epoch
|
||||||
|
built_epoch=$(date -d "$built_at" +%s 2>/dev/null || echo 0)
|
||||||
|
local now_epoch
|
||||||
|
now_epoch=$(date +%s)
|
||||||
|
local age_days=$(( (now_epoch - built_epoch) / 86400 ))
|
||||||
|
|
||||||
|
if [[ $age_days -ge 7 ]] || [[ "$FORCE" == "true" ]]; then
|
||||||
|
info "$service : image vieille de ${age_days} jours → rebuild recommandé"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
success "$service : image récente (${age_days} jours) — pas de rebuild nécessaire"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Mettre à jour un service public ──────────────────────────────────────────
|
||||||
|
update_public_service() {
|
||||||
|
local service=$1
|
||||||
|
local old_image_id
|
||||||
|
old_image_id=$(get_image_id "$service")
|
||||||
|
|
||||||
|
info "Mise à jour de $service..."
|
||||||
|
|
||||||
|
# Recréer le conteneur avec la nouvelle image
|
||||||
|
if ! $COMPOSE up -d --no-deps "$service" 2>&1 | tee -a "$LOG_FILE"; then
|
||||||
|
error "Échec du redémarrage de $service"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier que le conteneur est sain après 15s
|
||||||
|
sleep 15
|
||||||
|
if verify_service "$service"; then
|
||||||
|
success "$service mis à jour avec succès"
|
||||||
|
# Supprimer l'ancienne image si elle n'est plus utilisée
|
||||||
|
docker image prune -f 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
error "$service ne répond pas après mise à jour — rollback"
|
||||||
|
rollback_service "$service" "$old_image_id"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Rebuilder un service local ────────────────────────────────────────────────
|
||||||
|
update_local_service() {
|
||||||
|
local service=$1
|
||||||
|
|
||||||
|
info "Rebuild de $service (--no-cache pour forcer MAJ des paquets)..."
|
||||||
|
|
||||||
|
# Arrêter proprement
|
||||||
|
$COMPOSE stop "$service" 2>&1 | tee -a "$LOG_FILE" || true
|
||||||
|
|
||||||
|
# Rebuild sans cache
|
||||||
|
if ! $COMPOSE build --no-cache "$service" 2>&1 | tee -a "$LOG_FILE"; then
|
||||||
|
error "Échec du build de $service"
|
||||||
|
# Relancer avec l'ancienne image si elle existe encore
|
||||||
|
$COMPOSE up -d --no-deps "$service" 2>&1 | tee -a "$LOG_FILE" || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Relancer
|
||||||
|
if ! $COMPOSE up -d --no-deps "$service" 2>&1 | tee -a "$LOG_FILE"; then
|
||||||
|
error "Échec du démarrage de $service après rebuild"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Vérifier
|
||||||
|
sleep 20
|
||||||
|
if verify_service "$service"; then
|
||||||
|
success "$service rebuild et redémarré avec succès"
|
||||||
|
# Nettoyer les anciennes images
|
||||||
|
docker image prune -f 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
error "$service ne répond pas après rebuild"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Vérification qu'un service est opérationnel ───────────────────────────────
|
||||||
|
verify_service() {
|
||||||
|
local service=$1
|
||||||
|
local container; container=$(service_container "$service")
|
||||||
|
local max_attempts=6
|
||||||
|
local attempt=0
|
||||||
|
|
||||||
|
while [[ $attempt -lt $max_attempts ]]; do
|
||||||
|
local status
|
||||||
|
status=$(docker inspect --format='{{.State.Status}}' "$container" 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
case "$status" in
|
||||||
|
running)
|
||||||
|
# Vérification supplémentaire selon le service
|
||||||
|
case "$service" in
|
||||||
|
readsb)
|
||||||
|
# Vérifie que readsb décode des messages
|
||||||
|
if docker logs "$container" 2>&1 | \
|
||||||
|
grep -q "RTL\|tuner\|Found"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
tar1090)
|
||||||
|
# Vérifie que tar1090 a une connexion Beast
|
||||||
|
if docker logs "$container" 2>&1 | \
|
||||||
|
grep -q "Connection established\|successfully started"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
adsb2pg)
|
||||||
|
# Vérifie la connexion Postgres
|
||||||
|
if docker logs "$container" 2>&1 | \
|
||||||
|
grep -q "Connecté\|initialisé"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
webapp)
|
||||||
|
# Vérifie qu'Apache répond
|
||||||
|
if curl -s --max-time 3 \
|
||||||
|
"http://127.0.0.1:8080/" \
|
||||||
|
-o /dev/null -w "%{http_code}" 2>/dev/null \
|
||||||
|
| grep -q "200\|302"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
exited|dead)
|
||||||
|
error "$container est arrêté (status: $status)"
|
||||||
|
docker logs "$container" 2>&1 | tail -10 | tee -a "$LOG_FILE"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
((attempt++))
|
||||||
|
[[ $attempt -lt $max_attempts ]] && sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
warn "$service : timeout de vérification (peut encore démarrer)"
|
||||||
|
return 0 # On ne rollback pas sur timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Rollback vers une ancienne image ─────────────────────────────────────────
|
||||||
|
rollback_service() {
|
||||||
|
local service=$1
|
||||||
|
local old_image_id=$2
|
||||||
|
|
||||||
|
warn "Rollback de $service vers $old_image_id..."
|
||||||
|
|
||||||
|
if [[ -z "$old_image_id" ]]; then
|
||||||
|
error "Pas d'image précédente pour rollback"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Forcer l'utilisation de l'ancienne image
|
||||||
|
docker tag "$old_image_id" "$(service_container "$service"):rollback" 2>/dev/null || true
|
||||||
|
docker inspect "$(service_container "$service"):rollback" &>/dev/null && \
|
||||||
|
$COMPOSE up -d --no-deps "$service" 2>&1 | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
if verify_service "$service"; then
|
||||||
|
success "Rollback de $service réussi"
|
||||||
|
else
|
||||||
|
error "Rollback échoué — intervention manuelle nécessaire"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Nettoyer les images orphelines ────────────────────────────────────────────
|
||||||
|
cleanup_images() {
|
||||||
|
header "Nettoyage des images obsolètes"
|
||||||
|
local freed
|
||||||
|
freed=$(docker image prune -f 2>/dev/null | grep "reclaimed" || echo "")
|
||||||
|
[[ -n "$freed" ]] && info "Espace libéré : $freed" || info "Rien à nettoyer"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Afficher le statut final ──────────────────────────────────────────────────
|
||||||
|
show_status() {
|
||||||
|
header "Statut du stack après mise à jour"
|
||||||
|
$COMPOSE ps 2>&1 | tee -a "$LOG_FILE"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
info "Vérification du flux ADS-B :"
|
||||||
|
local count
|
||||||
|
count=$(curl -s --max-time 3 "http://127.0.0.1:8090/data/aircraft.json" \
|
||||||
|
2>/dev/null | python3 -c "
|
||||||
|
import json,sys
|
||||||
|
try:
|
||||||
|
d=json.load(sys.stdin)
|
||||||
|
ac=[a for a in d.get('aircraft',[]) if a.get('lat')]
|
||||||
|
print(f' Messages={d[\"messages\"]} Avions={len(ac)}')
|
||||||
|
except:
|
||||||
|
print(' tar1090 non disponible')
|
||||||
|
" 2>/dev/null || echo " tar1090 non disponible")
|
||||||
|
info "$count"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Mode check-only ───────────────────────────────────────────────────────────
|
||||||
|
if [[ "$CHECK_ONLY" == "true" ]]; then
|
||||||
|
header "Vérification des mises à jour disponibles (mode lecture seule)"
|
||||||
|
for service in "${SERVICES[@]}"; do
|
||||||
|
type="${SERVICE_TYPE[$service]}"
|
||||||
|
case "$type" in
|
||||||
|
public) check_public_update "$service" && \
|
||||||
|
echo " → Mise à jour disponible pour $service" || true ;;
|
||||||
|
local) check_local_update "$service" && \
|
||||||
|
echo " → Rebuild recommandé pour $service" || true ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
info "Lancez sans --check-only pour appliquer les mises à jour"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Mise à jour principale ────────────────────────────────────────────────────
|
||||||
|
header "Mise à jour du stack ADS-B — $(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
|
||||||
|
# 1. Sauvegarder la config
|
||||||
|
BACKUP_PATH=$(backup_config)
|
||||||
|
|
||||||
|
# 2. Traiter chaque service
|
||||||
|
UPDATED=()
|
||||||
|
FAILED=()
|
||||||
|
SKIPPED=()
|
||||||
|
|
||||||
|
for service in "${SERVICES[@]}"; do
|
||||||
|
echo ""
|
||||||
|
type="${SERVICE_TYPE[$service]:-}"
|
||||||
|
[[ -z "$type" ]] && { warn "Service inconnu : $service — ignoré"; continue; }
|
||||||
|
|
||||||
|
case "$type" in
|
||||||
|
public)
|
||||||
|
if check_public_update "$service"; then
|
||||||
|
if [[ "$CHECK_ONLY" != "true" ]]; then
|
||||||
|
if update_public_service "$service"; then
|
||||||
|
UPDATED+=("$service")
|
||||||
|
else
|
||||||
|
FAILED+=("$service")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
SKIPPED+=("$service")
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
local)
|
||||||
|
if check_local_update "$service"; then
|
||||||
|
if [[ "$CHECK_ONLY" != "true" ]]; then
|
||||||
|
if update_local_service "$service"; then
|
||||||
|
UPDATED+=("$service")
|
||||||
|
else
|
||||||
|
FAILED+=("$service")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
SKIPPED+=("$service")
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. Nettoyage
|
||||||
|
cleanup_images
|
||||||
|
|
||||||
|
# 4. Statut final
|
||||||
|
show_status
|
||||||
|
|
||||||
|
# 5. Résumé
|
||||||
|
header "Résumé"
|
||||||
|
[[ ${#UPDATED[@]} -gt 0 ]] && success "Mis à jour : ${UPDATED[*]}"
|
||||||
|
[[ ${#SKIPPED[@]} -gt 0 ]] && info "Pas de MAJ : ${SKIPPED[*]}"
|
||||||
|
[[ ${#FAILED[@]} -gt 0 ]] && error "Échecs : ${FAILED[*]}"
|
||||||
|
|
||||||
|
info "Log complet : $LOG_FILE"
|
||||||
|
info "Backup config : $BACKUP_PATH"
|
||||||
|
|
||||||
|
# 6. Rotation des backups (garder les 10 derniers)
|
||||||
|
ls -dt "${BACKUP_DIR}"/config_* 2>/dev/null | tail -n +11 | xargs rm -rf 2>/dev/null || true
|
||||||
|
|
||||||
|
# 7. Rotation des logs (garder 1 Mo max)
|
||||||
|
if [[ -f "$LOG_FILE" ]] && [[ $(stat -c%s "$LOG_FILE") -gt 1048576 ]]; then
|
||||||
|
mv "$LOG_FILE" "${LOG_FILE}.old"
|
||||||
|
info "Log archivé (${LOG_FILE}.old)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ ${#FAILED[@]} -gt 0 ]] && exit 1 || exit 0
|
||||||
263
update-webapp.sh
Executable file
@ -0,0 +1,263 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# update-webapp.sh — Rebuild et redéploiement de la webapp ADS-B
|
||||||
|
# Usage : ./update-webapp.sh [--no-cache] [--check-only]
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# ── Fichiers d'environnement (v4.2) ──────────────────────────────────────────
|
||||||
|
# Depuis les profiles Compose, un « docker compose » nu affiche
|
||||||
|
# « The "RX1_SERIAL" variable is not set » à chaque appel. Inoffensif pour la
|
||||||
|
# webapp (aucun profile), mais le bruit masque les vraies erreurs — et si
|
||||||
|
# PG_PASSWORD n'est pas injecté, le conteneur démarre sans accès à la base.
|
||||||
|
COMPOSE_ARGS=()
|
||||||
|
[[ -f "$SCRIPT_DIR/.env" ]] && COMPOSE_ARGS+=(--env-file "$SCRIPT_DIR/.env")
|
||||||
|
[[ -f "$SCRIPT_DIR/.env.receivers" ]] && COMPOSE_ARGS+=(--env-file "$SCRIPT_DIR/.env.receivers")
|
||||||
|
|
||||||
|
dc() { docker compose "${COMPOSE_ARGS[@]}" "$@"; }
|
||||||
|
|
||||||
|
# ── Couleurs ──────────────────────────────────────────────────────────────────
|
||||||
|
GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m'
|
||||||
|
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
|
||||||
|
|
||||||
|
ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
||||||
|
fail() { echo -e " ${RED}✗${NC} $1"; }
|
||||||
|
warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
|
||||||
|
info() { echo -e " ${CYAN}→${NC} $1"; }
|
||||||
|
hdr() { echo -e "\n${BOLD}$1${NC}"; }
|
||||||
|
|
||||||
|
NO_CACHE=""
|
||||||
|
CHECK_ONLY=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--no-cache) NO_CACHE="--no-cache" ;;
|
||||||
|
--check-only) CHECK_ONLY=1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ Déploiement webapp ADS-B Linux-25 ║${NC}"
|
||||||
|
echo -e "${BOLD}║ $(date '+%a %d/%m/%Y %H:%M:%S') ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
|
|
||||||
|
# ── 1. Prérequis ──────────────────────────────────────────────────────────────
|
||||||
|
hdr "1. Vérification des prérequis"
|
||||||
|
|
||||||
|
# Docker disponible ?
|
||||||
|
if ! docker info &>/dev/null; then
|
||||||
|
fail "Docker non disponible — arrêt"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "Docker disponible ($(docker version --format '{{.Server.Version}}' 2>/dev/null))"
|
||||||
|
|
||||||
|
# Dockerfile présent ?
|
||||||
|
if [ ! -f "$SCRIPT_DIR/webapp/Dockerfile" ]; then
|
||||||
|
fail "webapp/Dockerfile introuvable dans $SCRIPT_DIR"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "Dockerfile présent : $SCRIPT_DIR/webapp/Dockerfile"
|
||||||
|
|
||||||
|
# Sources présentes ?
|
||||||
|
SRC_COUNT=$(find "$SCRIPT_DIR/webapp/src" -type f 2>/dev/null | wc -l)
|
||||||
|
if [ "$SRC_COUNT" -eq 0 ]; then
|
||||||
|
fail "Aucun fichier source dans webapp/src/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
ok "$SRC_COUNT fichiers sources dans webapp/src/"
|
||||||
|
|
||||||
|
# ── 2. Accès internet ─────────────────────────────────────────────────────────
|
||||||
|
hdr "2. Vérification accès internet"
|
||||||
|
|
||||||
|
INTERNET=0
|
||||||
|
URLS_TO_CHECK=(
|
||||||
|
"https://cdn.jsdelivr.net/"
|
||||||
|
"https://unpkg.com/"
|
||||||
|
)
|
||||||
|
|
||||||
|
for url in "${URLS_TO_CHECK[@]}"; do
|
||||||
|
CODE=$(curl -s --max-time 5 -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "000")
|
||||||
|
if [ "$CODE" != "000" ] && [ "$CODE" != "599" ]; then
|
||||||
|
ok "Accès internet OK ($url → HTTP $CODE)"
|
||||||
|
INTERNET=1
|
||||||
|
break
|
||||||
|
else
|
||||||
|
warn "Pas de réponse : $url"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$INTERNET" -eq 0 ]; then
|
||||||
|
warn "Aucun accès internet détecté"
|
||||||
|
# Vérifier si les assets sont déjà dans l'image existante
|
||||||
|
EXISTING_ASSETS=$(docker exec adsb-webapp ls /var/www/html/assets/ 2>/dev/null | wc -l || echo 0)
|
||||||
|
if [ "$EXISTING_ASSETS" -gt 0 ]; then
|
||||||
|
warn "Assets déjà présents dans le conteneur ($EXISTING_ASSETS fichiers)"
|
||||||
|
warn "Le build va échouer si l'image doit être recréée depuis zéro"
|
||||||
|
warn "Option : copier uniquement les sources sans rebuild"
|
||||||
|
echo ""
|
||||||
|
echo -e " ${YELLOW}Que voulez-vous faire ?${NC}"
|
||||||
|
echo " [1] Tenter le rebuild quand même (peut échouer sans internet)"
|
||||||
|
echo " [2] Copier uniquement les sources dans le conteneur existant"
|
||||||
|
echo " [3] Annuler"
|
||||||
|
read -rp " Choix [1/2/3] : " CHOIX
|
||||||
|
case "$CHOIX" in
|
||||||
|
2)
|
||||||
|
hdr "Copie des sources dans le conteneur existant"
|
||||||
|
for f in "$SCRIPT_DIR/webapp/src/"*; do
|
||||||
|
fname=$(basename "$f")
|
||||||
|
if [ -d "$f" ]; then
|
||||||
|
docker cp "$f" "adsb-webapp:/var/www/html/$fname" && \
|
||||||
|
ok "Copié : $fname/" || warn "Échec : $fname/"
|
||||||
|
else
|
||||||
|
docker cp "$f" "adsb-webapp:/var/www/html/$fname" && \
|
||||||
|
ok "Copié : $fname" || warn "Échec : $fname"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
docker exec adsb-webapp chown -R www-data:www-data /var/www/html/ 2>/dev/null || true
|
||||||
|
ok "Sources mises à jour sans rebuild"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
info "Annulé"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
warn "Tentative de rebuild sans garantie de succès"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
fail "Pas d'internet et aucun conteneur existant — impossible de builder"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$CHECK_ONLY" -eq 1 ] && { info "Mode --check-only : arrêt ici"; exit 0; }
|
||||||
|
|
||||||
|
# ── 3. Build de l'image ───────────────────────────────────────────────────────
|
||||||
|
hdr "3. Build de l'image webapp"
|
||||||
|
|
||||||
|
info "Lancement du build${NO_CACHE:+ (--no-cache)}..."
|
||||||
|
BUILD_START=$(date +%s)
|
||||||
|
|
||||||
|
if dc build $NO_CACHE webapp 2>&1 | tee /tmp/webapp-build.log | \
|
||||||
|
grep -E "^(Step|#[0-9]|Successfully|ERROR|error)" ; then
|
||||||
|
BUILD_END=$(date +%s)
|
||||||
|
ok "Build réussi en $((BUILD_END - BUILD_START))s"
|
||||||
|
else
|
||||||
|
BUILD_END=$(date +%s)
|
||||||
|
fail "Build échoué après $((BUILD_END - BUILD_START))s"
|
||||||
|
echo ""
|
||||||
|
warn "Dernières lignes du log :"
|
||||||
|
tail -20 /tmp/webapp-build.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 4. Redémarrage du conteneur ───────────────────────────────────────────────
|
||||||
|
hdr "4. Redémarrage du conteneur"
|
||||||
|
|
||||||
|
info "Arrêt de adsb-webapp..."
|
||||||
|
dc stop webapp
|
||||||
|
dc rm -f webapp
|
||||||
|
info "Démarrage avec la nouvelle image..."
|
||||||
|
dc up -d webapp
|
||||||
|
sleep 8
|
||||||
|
|
||||||
|
# ── 5. Vérification post-déploiement ─────────────────────────────────────────
|
||||||
|
hdr "5. Vérification post-déploiement"
|
||||||
|
|
||||||
|
# Conteneur running ?
|
||||||
|
STATUS=$(docker inspect --format '{{.State.Status}}' adsb-webapp 2>/dev/null || echo "absent")
|
||||||
|
[ "$STATUS" = "running" ] && ok "Conteneur adsb-webapp : running" \
|
||||||
|
|| fail "Conteneur adsb-webapp : $STATUS"
|
||||||
|
|
||||||
|
# Port 8080 en écoute ?
|
||||||
|
ss -tlnp 2>/dev/null | grep -q ":8080" \
|
||||||
|
&& ok "Port 8080 en écoute" \
|
||||||
|
|| fail "Port 8080 absent"
|
||||||
|
|
||||||
|
# HTTP 200 ?
|
||||||
|
HTTP=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "http://127.0.0.1:8080/" 2>/dev/null)
|
||||||
|
[ "$HTTP" = "200" ] && ok "HTTP 200 sur http://127.0.0.1:8080/" \
|
||||||
|
|| fail "HTTP $HTTP sur http://127.0.0.1:8080/"
|
||||||
|
|
||||||
|
# Nombre d'onglets ?
|
||||||
|
TABS=$(docker exec adsb-webapp grep -c 'href="#tab' /var/www/html/index.php 2>/dev/null || echo 0)
|
||||||
|
[ "$TABS" -ge 5 ] && ok "$TABS onglets détectés dans index.php" \
|
||||||
|
|| warn "$TABS onglets détectés (< 5 attendus)"
|
||||||
|
|
||||||
|
# Taille app.js ?
|
||||||
|
JS_SZ=$(docker exec adsb-webapp wc -c /var/www/html/js/app.js 2>/dev/null | awk '{print $1}')
|
||||||
|
info "app.js : ${JS_SZ:-?} octets"
|
||||||
|
|
||||||
|
# ── Contrôles fonctionnels de l'API (v4.2) ───────────────────────────────────
|
||||||
|
# Le fuseau doit être ancré : sans date_default_timezone_set, PHP retombe sur
|
||||||
|
# UTC et la carte historique interroge une fenêtre décalée de 2 h — elle
|
||||||
|
# revient alors systématiquement vide, sans la moindre erreur HTTP.
|
||||||
|
if docker exec adsb-webapp grep -q "date_default_timezone_set" /var/www/html/api.php 2>/dev/null; then
|
||||||
|
ok "api.php : fuseau horaire ancré (date_default_timezone_set)"
|
||||||
|
else
|
||||||
|
warn "api.php : date_default_timezone_set absent — carte historique probablement vide (décalage UTC/CEST)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for act in live kpi trajectories; do
|
||||||
|
BODY=$(curl -s --max-time 10 "http://127.0.0.1:8080/api.php?action=${act}" 2>/dev/null)
|
||||||
|
if echo "$BODY" | head -c 1 | grep -q '{'; then
|
||||||
|
ok "api.php?action=${act} : JSON valide"
|
||||||
|
else
|
||||||
|
warn "api.php?action=${act} : réponse inattendue"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# La carte historique est le seul endpoint sensible au fuseau : on l'interroge
|
||||||
|
# sur une fenêtre récente, qui doit contenir des données si le stack reçoit.
|
||||||
|
HIST_FROM=$(date -d '-40 minutes' '+%Y-%m-%d %H:%M:%S')
|
||||||
|
HIST=$(curl -s --max-time 20 --get \
|
||||||
|
--data-urlencode "action=trajectories_history" \
|
||||||
|
--data-urlencode "from=${HIST_FROM}" \
|
||||||
|
--data-urlencode "window=30" \
|
||||||
|
"http://127.0.0.1:8080/api.php" 2>/dev/null)
|
||||||
|
HIST_COUNT=$(echo "$HIST" | grep -oP '"count"\s*:\s*\K[0-9]+' | head -1)
|
||||||
|
if [ "${HIST_COUNT:-0}" -gt 0 ]; then
|
||||||
|
ok "Carte historique : ${HIST_COUNT} avion(s) sur les 30 dernières minutes"
|
||||||
|
else
|
||||||
|
warn "Carte historique : 0 avion sur une fenêtre récente — vérifier le fuseau (api.php) ou l'ingestion adsb2pg"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Assets présents ?
|
||||||
|
ASSETS=$(docker exec adsb-webapp ls /var/www/html/assets/ 2>/dev/null | wc -l)
|
||||||
|
ok "$ASSETS fichiers assets dans /var/www/html/assets/"
|
||||||
|
|
||||||
|
# Drapeaux : sans eux, tous les pays s'affichent avec le pictogramme « ? ».
|
||||||
|
# Le Dockerfile les télécharge, mais un build sans réseau les laisse absents.
|
||||||
|
FLAG_N=$(docker exec adsb-webapp sh -c 'ls /var/www/html/assets/flags/*.png 2>/dev/null | wc -l' 2>/dev/null)
|
||||||
|
if [ "${FLAG_N:-0}" -gt 50 ]; then
|
||||||
|
ok "$FLAG_N drapeau(x) disponible(s)"
|
||||||
|
else
|
||||||
|
warn "Seulement ${FLAG_N:-0} drapeau(x) — les pays afficheront un pictogramme générique"
|
||||||
|
warn "Correctif : /data/adsb/scripts/fetch_flags.sh puis relancer ce script"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Endpoints d'analyse des départs
|
||||||
|
for act in airports departures_offfield_zones; do
|
||||||
|
BODY=$(curl -s --max-time 25 "http://127.0.0.1:8080/api.php?action=${act}&window=1440" 2>/dev/null)
|
||||||
|
if echo "$BODY" | grep -q '"error"'; then
|
||||||
|
warn "api.php?action=${act} : $(echo "$BODY" | head -c 110)"
|
||||||
|
elif echo "$BODY" | head -c 1 | grep -q '{'; then
|
||||||
|
ok "api.php?action=${act} : JSON valide"
|
||||||
|
else
|
||||||
|
warn "api.php?action=${act} : réponse inattendue"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# IP pour le lien final
|
||||||
|
IP=$(ip -4 addr show 2>/dev/null | grep -oP '(?<=inet )192\.[0-9.]+' | head -1)
|
||||||
|
[ -z "$IP" ] && IP=$(hostname -I | awk '{print $1}')
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${NC}"
|
||||||
|
echo -e "${BOLD}║ ✓ Déploiement terminé ║${NC}"
|
||||||
|
echo -e "${BOLD}║ Dashboard : http://${IP}:8080${NC}${BOLD} ║${NC}"
|
||||||
|
echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${NC}"
|
||||||
71
watchlist_schema.sql
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
-- Listes de surveillance d'aéronefs
|
||||||
|
--
|
||||||
|
-- Deux tables plutôt qu'une colonne « liste » libre : une liste est un objet
|
||||||
|
-- qu'on renomme, colore et supprime, et sa suppression doit emporter ses
|
||||||
|
-- membres. Une chaîne répétée sur chaque ligne rendrait le renommage fragile.
|
||||||
|
--
|
||||||
|
-- La clé de surveillance est l'adresse ICAO 24 bits, stable pour une cellule
|
||||||
|
-- donnée, et non l'indicatif, qui change à chaque vol et manque souvent.
|
||||||
|
--
|
||||||
|
-- À jouer avec ON_ERROR_STOP=1.
|
||||||
|
-- ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS adsb.watchlists (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
nom varchar(60) NOT NULL UNIQUE,
|
||||||
|
couleur varchar(9) NOT NULL DEFAULT '#d29922',
|
||||||
|
description text,
|
||||||
|
actif boolean NOT NULL DEFAULT true,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
COMMENT ON TABLE adsb.watchlists IS
|
||||||
|
'Listes de surveillance nommées (ex. Militaires, Hélicoptères).';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS adsb.watchlist_items (
|
||||||
|
id serial PRIMARY KEY,
|
||||||
|
watchlist_id integer NOT NULL
|
||||||
|
REFERENCES adsb.watchlists(id) ON DELETE CASCADE,
|
||||||
|
icao varchar(6) NOT NULL,
|
||||||
|
motif text,
|
||||||
|
-- Instantané des métadonnées au moment de la mise sous surveillance.
|
||||||
|
-- Redondant avec aircraft_history, et c'est voulu : il garde une trace de
|
||||||
|
-- ce qui avait motivé l'ajout, même si la fiche de l'appareil change
|
||||||
|
-- ensuite, et évite une jointure à chaque affichage de la liste.
|
||||||
|
callsign varchar(12),
|
||||||
|
aircraft_type varchar(8),
|
||||||
|
aircraft_desc varchar(60),
|
||||||
|
aircraft_wtc varchar(4),
|
||||||
|
operator_name varchar(120),
|
||||||
|
country varchar(60),
|
||||||
|
added_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT watchlist_items_uniq UNIQUE (watchlist_id, icao),
|
||||||
|
-- Normalisation imposée en base : l'API majuscule déjà, mais une insertion
|
||||||
|
-- manuelle en minuscules créerait un doublon invisible.
|
||||||
|
CONSTRAINT watchlist_items_icao_upper CHECK (icao = upper(icao))
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Recherche « cet appareil est-il surveillé ? » lors du rendu des tableaux
|
||||||
|
CREATE INDEX IF NOT EXISTS watchlist_items_icao_idx
|
||||||
|
ON adsb.watchlist_items (icao);
|
||||||
|
|
||||||
|
COMMENT ON TABLE adsb.watchlist_items IS
|
||||||
|
'Aéronefs surveillés, identifiés par leur adresse ICAO 24 bits.';
|
||||||
|
|
||||||
|
-- Liste par défaut, pour que le bouton « Surveiller » ait une cible dès le
|
||||||
|
-- premier clic sans imposer la création préalable d'une liste.
|
||||||
|
INSERT INTO adsb.watchlists (nom, couleur, description)
|
||||||
|
SELECT 'Surveillance', '#d29922', 'Liste par défaut'
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM adsb.watchlists);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
\echo '--- Tables créées ---'
|
||||||
|
SELECT w.id, w.nom, w.couleur, w.actif, count(i.id) AS appareils
|
||||||
|
FROM adsb.watchlists w
|
||||||
|
LEFT JOIN adsb.watchlist_items i ON i.watchlist_id = w.id
|
||||||
|
GROUP BY w.id, w.nom, w.couleur, w.actif
|
||||||
|
ORDER BY w.id;
|
||||||
71
webapp/Dockerfile
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
FROM php:8.4-apache
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libpq-dev curl \
|
||||||
|
&& docker-php-ext-install pgsql pdo pdo_pgsql \
|
||||||
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN a2enmod rewrite headers
|
||||||
|
|
||||||
|
RUN sed -i 's/Listen 80$/Listen 8080/' /etc/apache2/ports.conf && \
|
||||||
|
sed -i 's/<VirtualHost \*:80>/<VirtualHost *:8080>/' \
|
||||||
|
/etc/apache2/sites-enabled/000-default.conf && \
|
||||||
|
echo "ServerName localhost" >> /etc/apache2/apache2.conf
|
||||||
|
|
||||||
|
WORKDIR /var/www/html/assets
|
||||||
|
|
||||||
|
RUN curl -fsSL "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" \
|
||||||
|
-o bootstrap.min.css && \
|
||||||
|
curl -fsSL "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" \
|
||||||
|
-o bootstrap.bundle.min.js && \
|
||||||
|
curl -fsSL "https://cdn.jsdelivr.net/npm/chart.js@4.4.4/dist/chart.umd.min.js" \
|
||||||
|
-o chart.umd.min.js && \
|
||||||
|
curl -fsSL "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" \
|
||||||
|
-o bootstrap-icons.min.css && \
|
||||||
|
mkdir -p fonts && \
|
||||||
|
curl -fsSL "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/fonts/bootstrap-icons.woff2" \
|
||||||
|
-o fonts/bootstrap-icons.woff2 && \
|
||||||
|
curl -fsSL "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" \
|
||||||
|
-o leaflet.js && \
|
||||||
|
curl -fsSL "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" \
|
||||||
|
-o leaflet.css && \
|
||||||
|
mkdir -p leaflet && \
|
||||||
|
curl -fsSL "https://unpkg.com/leaflet@1.9.4/dist/images/marker-icon.png" \
|
||||||
|
-o leaflet/marker-icon.png && \
|
||||||
|
curl -fsSL "https://unpkg.com/leaflet@1.9.4/dist/images/marker-shadow.png" \
|
||||||
|
-o leaflet/marker-shadow.png
|
||||||
|
|
||||||
|
# ── Drapeaux nationaux (onglets Départs / Zones) ──────────────────────────────
|
||||||
|
# Source flagcdn.com : domaine public, libre d'usage sans attribution. Les
|
||||||
|
# drapeaux d'État ne sont pas couverts par le droit d'auteur.
|
||||||
|
#
|
||||||
|
# Téléchargés ici plutôt que seulement copiés depuis src/, pour qu'un build sur
|
||||||
|
# une machine neuve fonctionne sans étape préalable. Le COPY src/ qui suit
|
||||||
|
# fusionne avec ce dossier : des drapeaux déposés à la main dans
|
||||||
|
# src/assets/flags/ restent donc pris en compte et écrasent ceux-ci.
|
||||||
|
#
|
||||||
|
# Le `|| true` final est délibéré : un drapeau manquant retombe sur le
|
||||||
|
# pictogramme « pays inconnu » côté JS, ce n'est pas un motif d'échec du build.
|
||||||
|
RUN mkdir -p flags && cd flags && \
|
||||||
|
for c in fr de es it pt be nl lu ch at gb ie dk no se fi is pl cz sk hu ro \
|
||||||
|
bg gr hr si rs ba al mt cy ee lv lt ua by ru tr us ca mx br ar cl \
|
||||||
|
co pe ve cn jp kr in pk th vn sg my id ph au nz il sa qa ae kw bh \
|
||||||
|
om jo lb eg ma dz tn ly za ng ke et gh sn ci cm ga mc sm ad li im \
|
||||||
|
gg je bm ky aw bs pa cr do cu jm kz uz az ge am md me mk ir iq sy \
|
||||||
|
af bd lk np mm kh la mn tw hk mo ; do \
|
||||||
|
curl -fsSL --max-time 10 "https://flagcdn.com/24x18/${c}.png" \
|
||||||
|
-o "${c}.png" || rm -f "${c}.png" ; \
|
||||||
|
done ; \
|
||||||
|
echo "$(ls -1 *.png 2>/dev/null | wc -l) drapeau(x) embarqué(s)" ; \
|
||||||
|
true
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
COPY src/ /var/www/html/
|
||||||
|
|
||||||
|
RUN sed -i 's|AllowOverride None|AllowOverride All|g' \
|
||||||
|
/etc/apache2/apache2.conf
|
||||||
|
|
||||||
|
RUN chown -R www-data:www-data /var/www/html
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["apache2-foreground"]
|
||||||
18
webapp/src/.htaccess
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
Options -Indexes
|
||||||
|
RewriteEngine On
|
||||||
|
|
||||||
|
# Toutes les requêtes non-fichiers → index.php
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteRule ^(.*)$ index.php [QSA,L]
|
||||||
|
|
||||||
|
# Sécurité
|
||||||
|
Header always set X-Content-Type-Options "nosniff"
|
||||||
|
Header always set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header always set X-XSS-Protection "1; mode=block"
|
||||||
|
Header always set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
|
||||||
|
# Cache assets statiques
|
||||||
|
<FilesMatch "\.(css|js|woff2|png|ico)$">
|
||||||
|
Header set Cache-Control "max-age=86400, public"
|
||||||
|
</FilesMatch>
|
||||||
1914
webapp/src/api.php
Normal file
BIN
webapp/src/assets/flags/ad.png
Normal file
|
After Width: | Height: | Size: 644 B |
BIN
webapp/src/assets/flags/ae.png
Normal file
|
After Width: | Height: | Size: 677 B |
BIN
webapp/src/assets/flags/af.png
Normal file
|
After Width: | Height: | Size: 631 B |
BIN
webapp/src/assets/flags/al.png
Normal file
|
After Width: | Height: | Size: 551 B |
BIN
webapp/src/assets/flags/am.png
Normal file
|
After Width: | Height: | Size: 601 B |
BIN
webapp/src/assets/flags/ar.png
Normal file
|
After Width: | Height: | Size: 593 B |
BIN
webapp/src/assets/flags/at.png
Normal file
|
After Width: | Height: | Size: 613 B |
BIN
webapp/src/assets/flags/au.png
Normal file
|
After Width: | Height: | Size: 648 B |
BIN
webapp/src/assets/flags/aw.png
Normal file
|
After Width: | Height: | Size: 696 B |
BIN
webapp/src/assets/flags/az.png
Normal file
|
After Width: | Height: | Size: 673 B |
BIN
webapp/src/assets/flags/ba.png
Normal file
|
After Width: | Height: | Size: 734 B |
BIN
webapp/src/assets/flags/bd.png
Normal file
|
After Width: | Height: | Size: 692 B |
BIN
webapp/src/assets/flags/be.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
webapp/src/assets/flags/bg.png
Normal file
|
After Width: | Height: | Size: 561 B |
BIN
webapp/src/assets/flags/bh.png
Normal file
|
After Width: | Height: | Size: 662 B |
BIN
webapp/src/assets/flags/bm.png
Normal file
|
After Width: | Height: | Size: 751 B |
BIN
webapp/src/assets/flags/br.png
Normal file
|
After Width: | Height: | Size: 727 B |
BIN
webapp/src/assets/flags/bs.png
Normal file
|
After Width: | Height: | Size: 609 B |
BIN
webapp/src/assets/flags/by.png
Normal file
|
After Width: | Height: | Size: 621 B |
BIN
webapp/src/assets/flags/ca.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
webapp/src/assets/flags/ch.png
Normal file
|
After Width: | Height: | Size: 436 B |
BIN
webapp/src/assets/flags/ci.png
Normal file
|
After Width: | Height: | Size: 636 B |
BIN
webapp/src/assets/flags/cl.png
Normal file
|
After Width: | Height: | Size: 670 B |
BIN
webapp/src/assets/flags/cm.png
Normal file
|
After Width: | Height: | Size: 590 B |
BIN
webapp/src/assets/flags/cn.png
Normal file
|
After Width: | Height: | Size: 653 B |
BIN
webapp/src/assets/flags/co.png
Normal file
|
After Width: | Height: | Size: 569 B |
BIN
webapp/src/assets/flags/cr.png
Normal file
|
After Width: | Height: | Size: 685 B |
BIN
webapp/src/assets/flags/cu.png
Normal file
|
After Width: | Height: | Size: 720 B |
BIN
webapp/src/assets/flags/cy.png
Normal file
|
After Width: | Height: | Size: 620 B |
BIN
webapp/src/assets/flags/cz.png
Normal file
|
After Width: | Height: | Size: 679 B |
BIN
webapp/src/assets/flags/de.png
Normal file
|
After Width: | Height: | Size: 576 B |
BIN
webapp/src/assets/flags/dk.png
Normal file
|
After Width: | Height: | Size: 675 B |
BIN
webapp/src/assets/flags/do.png
Normal file
|
After Width: | Height: | Size: 618 B |
BIN
webapp/src/assets/flags/dz.png
Normal file
|
After Width: | Height: | Size: 731 B |
BIN
webapp/src/assets/flags/ee.png
Normal file
|
After Width: | Height: | Size: 647 B |
BIN
webapp/src/assets/flags/eg.png
Normal file
|
After Width: | Height: | Size: 660 B |
BIN
webapp/src/assets/flags/es.png
Normal file
|
After Width: | Height: | Size: 675 B |
BIN
webapp/src/assets/flags/et.png
Normal file
|
After Width: | Height: | Size: 769 B |
BIN
webapp/src/assets/flags/fi.png
Normal file
|
After Width: | Height: | Size: 663 B |
BIN
webapp/src/assets/flags/fr.png
Normal file
|
After Width: | Height: | Size: 643 B |
BIN
webapp/src/assets/flags/ga.png
Normal file
|
After Width: | Height: | Size: 709 B |
BIN
webapp/src/assets/flags/gb.png
Normal file
|
After Width: | Height: | Size: 815 B |
BIN
webapp/src/assets/flags/ge.png
Normal file
|
After Width: | Height: | Size: 701 B |
BIN
webapp/src/assets/flags/gg.png
Normal file
|
After Width: | Height: | Size: 741 B |
BIN
webapp/src/assets/flags/gh.png
Normal file
|
After Width: | Height: | Size: 633 B |
BIN
webapp/src/assets/flags/gr.png
Normal file
|
After Width: | Height: | Size: 713 B |
BIN
webapp/src/assets/flags/hk.png
Normal file
|
After Width: | Height: | Size: 501 B |
BIN
webapp/src/assets/flags/hr.png
Normal file
|
After Width: | Height: | Size: 658 B |
BIN
webapp/src/assets/flags/hu.png
Normal file
|
After Width: | Height: | Size: 560 B |
BIN
webapp/src/assets/flags/id.png
Normal file
|
After Width: | Height: | Size: 533 B |
BIN
webapp/src/assets/flags/ie.png
Normal file
|
After Width: | Height: | Size: 693 B |
BIN
webapp/src/assets/flags/il.png
Normal file
|
After Width: | Height: | Size: 644 B |
BIN
webapp/src/assets/flags/im.png
Normal file
|
After Width: | Height: | Size: 536 B |
BIN
webapp/src/assets/flags/in.png
Normal file
|
After Width: | Height: | Size: 673 B |
BIN
webapp/src/assets/flags/iq.png
Normal file
|
After Width: | Height: | Size: 694 B |
BIN
webapp/src/assets/flags/ir.png
Normal file
|
After Width: | Height: | Size: 685 B |
BIN
webapp/src/assets/flags/is.png
Normal file
|
After Width: | Height: | Size: 751 B |
BIN
webapp/src/assets/flags/it.png
Normal file
|
After Width: | Height: | Size: 493 B |