#!/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+="✓$1OK"; } h_fail() { HTML_ROWS+="✗$1ERREUR"; } h_warn() { HTML_ROWS+="⚠$1ATTENTION"; } 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

✈ ADS-B Linux-25 — Status

Généré le $NOW_DISP — actualisation auto toutes les 60s
${BADGE_TXT}
IP : ${IP}
Erreurs : ${ERRORS}   Avert. : ${WARNS}
/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
${DB_SIZE:-?}
Base adsb
${CUR_GAIN:-?} dB
Gain SDR
${HTML_ROWS}
ÉtatDétailStatut
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