#!/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+="
| $1 |
"; }
h_ok() { HTML_ROWS+="| ✓ | $1 | OK |
"; }
h_fail() { HTML_ROWS+="| ✗ | $1 | ERREUR |
"; }
h_warn() { HTML_ROWS+="| ⚠ | $1 | ATTENTION |
"; }
h_info() { HTML_ROWS+="| → | $1 |
"; }
# ── 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" != "" ] && 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
ADS-B Linux-25 — Status
/dev/null && echo ok-kpi || echo warn-kpi)">
${AVIONS:-0}
Avions en vue
/dev/null && echo ok-kpi || echo warn-kpi)">
${WITH_POS:-0}
Avec GPS
${AV_TODAY:-0}
Avions aujourd'hui
${SESS_TODAY:-0}
Sessions aujourd'hui
${CUR_GAIN:-?} dB
Gain SDR
| État | Détail | Statut |
${HTML_ROWS}
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