Files
claudetools/.claude/skills/unifi-wifi/scripts/audit-site.sh
Howard Enos e1031ae91a unifi-wifi: skill health pass — fix optimize-radios stray REPL echo + ASCII-clean all output
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>
2026-06-16 15:08:35 -07:00

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