sync: auto-sync from HOWARD-HOME at 2026-06-15 23:43:51

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-15 23:43:51
This commit is contained in:
2026-06-15 23:44:00 -07:00
parent e5f07afd90
commit a62bd65be7
4 changed files with 84 additions and 2 deletions

View File

@@ -81,7 +81,11 @@ controller knows and making prioritized, validated changes. Built for any site;
These read each AP directly (controller hides this data). All take `<site> [ap-ssh-vault-path]`,
use sshpass-or-SSH_ASKPASS auth, and must run in the **foreground**:
```bash
# AP-to-AP SNR neighbor matrix (/proc/ui_neighbor) -> redundancy = data-backed disable candidates
# AP-to-AP SNR neighbor matrix (/proc/ui_neighbor) -> redundancy = data-backed disable candidates.
# Set NBR_JSON to also emit a machine-readable adjacency, then feed it to optimize-radios.sh:
NBR_JSON=.claude/tmp/<site>-nbr.json bash .claude/skills/unifi-wifi/scripts/neighbor-collect.sh cascades
NEIGHBOR_JSON=.claude/tmp/<site>-nbr.json bash .claude/skills/unifi-wifi/scripts/optimize-radios.sh cascades 14 ng
# ^ optimize-radios uses the measured SNR overlap (not the sparse roam graph) -> real DISABLE list.
bash .claude/skills/unifi-wifi/scripts/neighbor-collect.sh cascades [vault-path] [snr_min=20]
# measured per-channel busy%/noise per AP -> cleanest-channel plan (iw survey dump)
bash .claude/skills/unifi-wifi/scripts/survey-collect.sh cascades [vault-path]

View File

@@ -26,6 +26,8 @@ VAULT="$REPO/.claude/scripts/vault.sh"; UOS="$REPO/.claude/scripts/uos-mongo.sh"
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: neighbor-collect.sh <site-name|id> [ap-ssh-vault-path] [snr_min]}"
VP="${2:-clients/cascades-tucson/unifi-ap-ssh}"; SNR_MIN="${3:-20}"
NBR_JSON="${NBR_JSON:-}" # if set, also write a machine-readable adjacency {ap:{band:{nbr:snr}}} here
# (consumed by optimize-radios.sh via NEIGHBOR_JSON for data-backed disables)
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
# --- controller creds + login (RW admin reads fine) ---
@@ -97,10 +99,11 @@ done < "$TMP/aps.tsv"
echo "" >&2
# --- parse + map + emit adjacency matrix + redundancy summary ---
python - "$RAW" "$TMP/macmap.json" "$TMP/bssmap.json" "$SNR_MIN" <<'PY'
python - "$RAW" "$TMP/macmap.json" "$TMP/bssmap.json" "$SNR_MIN" "${NBR_JSON:-NONE}" <<'PY'
import json,re,sys
raw=open(sys.argv[1],encoding='utf-8',errors='replace').read().splitlines()
macmap=json.load(open(sys.argv[2])); bssmap=json.load(open(sys.argv[3])); SNR_MIN=int(sys.argv[4])
OUTJSON=sys.argv[5] if len(sys.argv)>5 else 'NONE'
BN={'0':'2.4','1':'5','2':'6'}; EB={'2.4ghz':'2.4','5ghz':'5','6ghz':'6'}
# edges[(src, band)] = {neighbor_name: best_snr}; presence[(src,band)] = set(names) from ess_ap_list
edges={}; presence={}; cur=None; mode=None; band=None
@@ -153,6 +156,14 @@ for b in ('2.4','5','6'):
print(f"\n-- {b}GHz: {len(redund)}/{len(rows)} APs have >=2 strong neighbors (disable/power-down candidates) --")
for cnt,ap,strong in sorted(redund,reverse=True)[:12]:
print(f" {ap}: {cnt} strong ({', '.join(strong[:4])}{'...' if len(strong)>4 else ''})")
# machine-readable adjacency for optimize-radios.sh: {ap_name: {band: {neighbor_name: snr}}}
if OUTJSON!='NONE':
adj={}
for (src,b),nbrs in edges.items():
adj.setdefault(src,{})[b]=nbrs
json.dump(adj, open(OUTJSON,'w'))
print(f"\n[INFO] wrote adjacency JSON -> {OUTJSON} ({len(adj)} APs) — feed to optimize-radios.sh via NEIGHBOR_JSON")
PY
echo ""
echo "[next] feed the redundancy list to optimize-radios.sh; validate per-zone with watch-ap.sh before any --apply."

View File

@@ -29,10 +29,35 @@ if [[ "$arg" =~ ^[0-9a-f]{24}$ ]]; then SITE="$arg"; else
[ -n "$SITE" ] || { echo "[ERROR] no site matching '$arg'"; exit 1; }
fi
case "$BAND" in ng) RSSI_OK=-68; REDUN=$REDUN_NG;; na) RSSI_OK=-72; REDUN=$REDUN_OTHER;; 6e) RSSI_OK=-75; REDUN=$REDUN_OTHER;; *) echo "band must be ng|na|6e"; exit 1;; esac
# Coverage-adjacency source: the measured AP-to-AP SNR matrix from neighbor-collect.sh (NEIGHBOR_JSON)
# is preferred — it's dense + accurate. The roam graph is the fallback but is sparse in sites with
# stationary clients (e.g. senior living), which is why disables came back 0 there. NBR_SNR_MIN = the
# SNR (dB) at which a managed-AP neighbor counts as real overlap (neighbor-collect default 20).
NBR_SNR_MIN="${NBR_SNR_MIN:-20}"; STRONG_RAW=""; ADJ_SRC="roam-graph (sparse w/ stationary clients)"
if [ -n "${NEIGHBOR_JSON:-}" ] && [ -f "${NEIGHBOR_JSON:-}" ]; then
# Precompute bidirectional strong-overlap NAME pairs HERE (not inside mongo JS — a large embedded
# object literal crashes the old mongo shell). Emit a compact ';'-joined, tab-internal flat string.
STRONG_RAW="$(python - "$NEIGHBOR_JSON" "$NBR_SNR_MIN" <<'PY'
import json,sys
nbr=json.load(open(sys.argv[1])); MIN=int(sys.argv[2])
def ms(a,b): # max SNR a->b over any band
o=nbr.get(a,{}); return max([o.get(bd,{}).get(b,-1) for bd in ('2.4','5','6')]+[-1])
aps=list(nbr); out=[]
for i,a in enumerate(aps):
for b in aps[i+1:]:
if ms(a,b)>=MIN and ms(b,a)>=MIN: # bidirectional overlap
out.append(a.replace('"','').replace('\\','')+'\t'+b.replace('"','').replace('\\',''))
print(';'.join(out))
PY
)"
ADJ_SRC="neighbor SNR matrix /proc/ui_neighbor (SNR>=$NBR_SNR_MIN)"
fi
echo "[INFO] site=$SITE band=$BAND window=${DAYS}d rssi>=$RSSI_OK roam>=$ROAM_MIN cap=${CAP}% need_neighbors=$REDUN zone_cap=${ZPCT}%"
echo "[INFO] coverage adjacency source: $ADJ_SRC"
cat <<JS | bash "$UOS" 2>&1 | grep -viE 'pq.html|post-quantum|store now|server may need'
var SITE='$SITE',DAYS=$DAYS,BAND='$BAND',RSSI_OK=$RSSI_OK,ROAM_MIN=$ROAM_MIN,CAP=$CAP,ZPCT=$ZPCT,REDUN=$REDUN;
var STRONG_RAW="$STRONG_RAW"; // ';'-joined "nameA<tab>nameB" bidirectional strong-overlap pairs, or ""
var ace=db.getSiblingDB('ace'),st=db.getSiblingDB('ace_stat');
var since=new Date().getTime()-DAYS*86400000;
// identity + zone
@@ -73,6 +98,18 @@ Object.keys(prof).forEach(function(A){ strong[A]={};
if(Math.min(ab,ba)>=ROAM_MIN && p25((rs[A+'>'+B]||[]).concat(rs[B+'>'+A]||[]))>=RSSI_OK) strong[A][B]=1;
});
});
// PREFERRED: rebuild adjacency from the measured AP-to-AP SNR matrix (neighbor-collect / /proc/ui_neighbor)
// when provided — denser + accurate. Overlap is bidirectional: A hears B AND B hears A, both >= NBR_SNR_MIN
// on any band (physical overlap is band-independent). This is what makes DISABLE decisions data-backed in
// sites where the roam graph is too sparse (stationary clients).
if(STRONG_RAW){
var nm2mac={}; for(var m in name){ nm2mac[name[m]]=m; }
Object.keys(prof).forEach(function(A){ strong[A]={}; });
STRONG_RAW.split(';').forEach(function(pr){ if(!pr)return; var t=pr.split('\t');
var A=nm2mac[t[0]], B=nm2mac[t[1]];
if(A&&B&&prof[A]&&prof[B]){ strong[A][B]=1; strong[B][A]=1; } // bidirectional overlap proven by SNR
});
}
// greedy capacity-aware disable
var aps=Object.keys(prof), active={}; aps.forEach(function(a){active[a]=true;});
var projCu={}; aps.forEach(function(a){projCu[a]=prof[a].cu;}); // projected utilization (grows as we shift load)