Ajout du code initial ADS-B

This commit is contained in:
azur_test3 2026-08-10 02:26:10 +02:00
commit 732d8be9b8
171 changed files with 16800 additions and 0 deletions

20
.gitignore vendored Normal file
View 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
View 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
View 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

Binary file not shown.

2
adsb2pg/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
requests==2.32.3
psycopg2-binary==2.9.9

97
apply_tuning.sh Executable file
View 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

File diff suppressed because it is too large Load Diff

399
check_adsb_claude_ok_v2.sh Executable file
View 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> &nbsp;
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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

90
migrate_to_postgis.sh Executable file
View 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
View 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
View File

@ -0,0 +1 @@
/var/snap/docker/common/var-lib-docker/volumes/pg-conf-std/_data

1
postgres/logs Symbolic link
View File

@ -0,0 +1 @@
/var/snap/docker/common/var-lib-docker/volumes/pg-logs-std/_data

65
queries.sql Normal file
View 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
View 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
View 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
View 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
View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 593 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 648 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 727 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 590 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 675 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 815 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 633 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 713 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 533 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 693 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 694 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 751 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 493 B

Some files were not shown because too many files have changed in this diff Show More