Files
claudetools/.claude/skills/unifi-wifi/scripts/live-stats.sh
Mike Swanson 7eab5ed030 sync: auto-sync from GURU-5070 at 2026-06-15 19:38:20
Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-15 19:38:20
2026-06-15 19:38:35 -07:00

76 lines
4.2 KiB
Bash

#!/usr/bin/env bash
# live-stats.sh — Plane-2 live RF/airtime from the UOS Network API (classic session API).
# Gives CURRENT per-AP per-radio cu_total / cu_self / num_sta / satisfaction / tx_retries and the
# AP RF-neighbor table — for before/after validation of changes and (the neighbor table) the
# materials-aware AP-to-AP coverage graph that unlocks confident radio DISABLES.
#
# AUTH (provision once): the classic API needs a controller admin session. Create a dedicated
# READ-ONLY admin in the UniFi UI (OS Settings -> Admins -> add a Viewer), then vault it:
# bash .claude/skills/vault/scripts/vault-helper.sh new infrastructure/uos-server-network-api \
# --kind generic --name "UOS Network API (read-only admin)" --tag unifi \
# --set username=<viewer> --set password=<pw>
# (A UniFi Network Integration API key also works for /proxy/network/integration/v1, but the rich
# radio_table_stats + RF-neighbor data live in the classic /proxy/network/api path used here.)
#
# Usage: bash .claude/skills/unifi-wifi/scripts/live-stats.sh <site-name|short> [--clients]
set -euo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: live-stats.sh <site-name|short> [--clients]}"; WANT_CLIENTS="${2:-}"
# Prefer the read-only admin; fall back to the read-write one (a single admin covers both).
U="$(bash "$VAULT" get-field infrastructure/uos-server-network-api credentials.username 2>/dev/null || true)"
P="$(bash "$VAULT" get-field infrastructure/uos-server-network-api credentials.password 2>/dev/null || true)"
if [ -z "$U" ] || [ -z "$P" ]; then
U="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credentials.username 2>/dev/null || true)"
P="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credentials.password 2>/dev/null || true)"
fi
if [ -z "$U" ] || [ -z "$P" ]; then
echo "[BLOCKED] No controller credential vaulted yet. Provision a read-only admin and vault it:"
sed -n '8,14p' "$0"
exit 2
fi
CJ="$(mktemp)"; trap 'rm -f "$CJ"' EXIT
base="https://$HOST:$PORT"
# UniFi OS login -> session cookie
code=$(curl -sk -c "$CJ" -o /dev/null -w '%{http_code}' -X POST "$base/api/auth/login" \
-H 'Content-Type: application/json' --data-binary "$(python -c 'import json,os;print(json.dumps({"username":os.environ["U"],"password":os.environ["P"]}))' U="$U" P="$P")")
[ "$code" = "200" ] || { echo "[ERROR] login HTTP $code"; exit 1; }
# resolve site short name (classic API keys on the short name, not the _id)
SHORT="$SITEARG"
if [[ "$SITEARG" =~ ^[0-9a-f]{24}$ || ! "$SITEARG" =~ ^[a-z0-9]{8}$ ]]; then
SHORT="$(curl -sk -b "$CJ" "$base/proxy/network/api/self/sites" | python -c "
import sys,json
d=json.load(sys.stdin).get('data',[])
q='''$SITEARG'''.lower()
for s in d:
if s.get('_id')=='''$SITEARG''' or q in (s.get('desc','').lower()): print(s.get('name')); break
" 2>/dev/null)"
fi
[ -n "$SHORT" ] || { echo "[ERROR] could not resolve site '$SITEARG'"; exit 1; }
echo "[INFO] site short=$SHORT"
curl -sk -b "$CJ" "$base/proxy/network/api/s/$SHORT/stat/device" | python -c "
import sys,json
for d in json.load(sys.stdin).get('data',[]):
if d.get('type')!='uap': continue
print('AP',d.get('name'),'clients=',d.get('num_sta'))
for r in d.get('radio_table_stats',[]):
print(' ',r.get('radio'),'ch',r.get('channel'),'cu_total',r.get('cu_total'),'cu_self_rx',r.get('cu_self_rx'),'cu_self_tx',r.get('cu_self_tx'),'num_sta',r.get('num_sta'),'tx_retries',r.get('tx_retries'),'satisfaction',r.get('satisfaction'))
# RF neighbor table (materials-aware AP-to-AP visibility) if present
for n in (d.get('radio_table') or []):
pass
" 2>&1 | head -60
if [ "$WANT_CLIENTS" = "--clients" ]; then
echo "=== clients (rssi/rate/retries) ==="
curl -sk -b "$CJ" "$base/proxy/network/api/s/$SHORT/stat/sta" | python -c "
import sys,json
for c in json.load(sys.stdin).get('data',[])[:40]:
print(' ',c.get('hostname') or c.get('mac'),'ap',c.get('ap_mac'),'rssi',c.get('rssi'),'signal',c.get('signal'),'tx_rate',c.get('tx_rate'),'retries',c.get('tx_retries'),'sat',c.get('satisfaction'))
" 2>&1 | head -45
fi