Full verification of the skill against Cascades (live): - All 19 scripts syntax-clean. - Controller-side read-only validated live: sites, audit-site, switch-audit, live-stats, model-rank, optimize-radios, monitor-run, gw-audit. Dry-run apply paths validated: apply-radio, apply-wlan, client-control, device-control. AP-side mechanism validated: SSH auth + /proc/ui_neighbor read on a sample AP; full neighbor-collect (74-AP SNR sweep) -> channel-plan end-to-end produced a 1/6/11 plan. Fixes: - optimize-radios.sh: the `for(k in prof)` loop's numeric completion value was REPL-echoed by the legacy mongo shell (stray "94.56..." line in output). Terminated the loop body with `void 0` to suppress it. - ASCII-clean printed output (CLAUDE.md no-non-ASCII): replaced em-dashes / Unicode arrows / § that reached stdout and rendered as `?`/mojibake on the Windows console, across optimize-radios, neighbor-collect, survey-collect, dfs-check, audit-site, sites, monitor-run, apply-radio, apply-wlan, pfsense-backend. (Comment-only non-ASCII left as-is; never printed.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
3.5 KiB
Bash
59 lines
3.5 KiB
Bash
#!/usr/bin/env bash
|
|
# audit-site.sh — Plane-1 (config + interference) WiFi audit for a UniFi site on the UOS.
|
|
# Reads the controller Mongo (ace) via .claude/scripts/uos-mongo.sh; no live-stats plane needed.
|
|
#
|
|
# Usage:
|
|
# bash .claude/skills/unifi-wifi/scripts/audit-site.sh <site-name|site_id>
|
|
# bash .claude/skills/unifi-wifi/scripts/audit-site.sh cascades
|
|
#
|
|
# Output: 2.4/5/6 config summary, the per-channel neighbor-density (interference) map, and
|
|
# flagged issues (min_rssi off, 40MHz on 2.4, off-1/6/11 channels, high power). Pair with
|
|
# references/methodology.md to turn flags into changes; validate with live stats (Plane 2).
|
|
set -euo pipefail
|
|
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
|
UOS="$REPO/.claude/scripts/uos-mongo.sh"
|
|
arg="${1:?usage: audit-site.sh <site-name|site_id>}"
|
|
|
|
if [[ "$arg" =~ ^[0-9a-f]{24}$ ]]; then
|
|
SITE="$arg"
|
|
else
|
|
SITE="$(bash "$UOS" --sites 2>/dev/null | grep -vi 'pq.html' | grep -i "$arg" | awk '{print $1}' | head -1)"
|
|
[ -n "$SITE" ] || { echo "[ERROR] no site matching '$arg' (try: uos-mongo.sh --sites)"; exit 1; }
|
|
fi
|
|
echo "[INFO] auditing site_id=$SITE"
|
|
|
|
cat <<JS | bash "$UOS" 2>&1 | grep -viE 'pq.html|post-quantum|store now|server may need'
|
|
var SITE='$SITE';
|
|
var aps=db.device.find({site_id:SITE,type:'uap'},{name:1,radio_table:1}).toArray();
|
|
function tally(o,k){o[k]=(o[k]||0)+1;}
|
|
var ng_ch={}, ng_w={}, ng_pwr={}, na_w={}, na_used=0, sixe_used=0, ngOffRssi=0, flags=[];
|
|
aps.forEach(function(a){
|
|
(a.radio_table||[]).forEach(function(r){
|
|
if(r.radio=='ng'){
|
|
tally(ng_ch,r.channel); tally(ng_w,r.ht); tally(ng_pwr,r.tx_power_mode);
|
|
if(!r.min_rssi_enabled){ ngOffRssi++; flags.push("2.4 min_rssi OFF: "+a.name); }
|
|
if(String(r.ht)!='20') flags.push("2.4 width "+r.ht+"MHz (want 20): "+a.name);
|
|
if([1,6,11].indexOf(r.channel)<0) flags.push("2.4 off-plan ch"+r.channel+" (want 1/6/11): "+a.name);
|
|
if(/high/i.test(String(r.tx_power_mode))) flags.push("2.4 power=high (want low/medium): "+a.name);
|
|
} else if(r.radio=='na'){ na_used++; tally(na_w,r.ht);
|
|
} else { sixe_used++; }
|
|
});
|
|
});
|
|
print("==== CONFIG SUMMARY ("+aps.length+" APs) ====");
|
|
print(" 2.4GHz channels: "+JSON.stringify(ng_ch)+" (want only 1/6/11)");
|
|
print(" 2.4GHz widths: "+JSON.stringify(ng_w)+" (want only 20)");
|
|
print(" 2.4GHz power: "+JSON.stringify(ng_pwr)+" (want low/medium/custom in density)");
|
|
print(" 2.4GHz min_rssi OFF on "+ngOffRssi+" radios");
|
|
print(" 5GHz radios: "+na_used+" widths: "+JSON.stringify(na_w)+" (want 40 in density, not 80/160)");
|
|
print(" 6GHz radios active: "+sixe_used+" (steer 6E-capable clients here - usually the clean band)");
|
|
print("\n==== NEIGHBOR-DENSITY MAP (rogue = co-channel interference) ====");
|
|
print(" 2.4GHz:");
|
|
db.rogue.aggregate([{\$match:{site_id:SITE,band:'ng'}},{\$group:{_id:'\$channel',n:{\$sum:1}}},{\$sort:{n:-1}},{\$limit:6}]).forEach(function(d){print(" ch"+d._id+": "+d.n+" neighbor BSSIDs")});
|
|
print(" 5GHz (top):");
|
|
db.rogue.aggregate([{\$match:{site_id:SITE,band:'na'}},{\$group:{_id:'\$channel',n:{\$sum:1}}},{\$sort:{n:-1}},{\$limit:6}]).forEach(function(d){print(" ch"+d._id+": "+d.n)});
|
|
print("\n==== FLAGS ("+flags.length+") ====");
|
|
if(flags.length==0) print(" (none from config plane)"); else flags.forEach(function(f){print(" [!] "+f)});
|
|
print("\n[next] map flags -> changes via references/methodology.md (prune 2.4, shrink cells, steer to 6GHz, manual 1/6/11).");
|
|
print("[next] validate cu_total / satisfaction / tx_retries before+after via the live Network API (Plane 2, not yet wired).");
|
|
JS
|