adsb-1090/check_adsb_claude_ok_v2.sh

400 lines
22 KiB
Bash
Executable File

#!/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