sync: auto-sync from GURU-5070 at 2026-06-15 17:58:51

Author: Mike Swanson
Machine: GURU-5070
Timestamp: 2026-06-15 17:58:51
This commit is contained in:
2026-06-15 17:59:06 -07:00
parent 4ef6a9a3b0
commit 236604924a
4 changed files with 289 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
---
name: unifi-wifi
description: "Analyze and tune UniFi WiFi for performance + stability, especially in dense/congested environments. Audits AP/radio config and the neighbor-interference map from the UOS controller, flags issues (2.4GHz over-provisioning, channel width, min-RSSI/sticky clients, channel plan), and recommends prioritized changes. Works for any UniFi site on the UOS (172.16.3.29); Cascades is the hard case. Triggers: unifi wifi tuning, RF/airtime/channel analysis, 2.4GHz congestion, AP channel plan, sticky clients, wireless performance."
---
# UniFi WiFi tuning (UOS sites)
Data-driven WiFi tuning for UniFi sites on the **UOS Server** (`172.16.3.29`). Goal: solid
performance + stability for connected devices in congested environments by analyzing what the
controller knows and making prioritized, validated changes. Built for any site; **Cascades**
(77 APs, ~550 clients, brutal 2.4GHz) is the reference hard case.
## First, load context
- **[references/data-access.md](references/data-access.md)** — what data the UOS exposes and how
to read it (the two planes: Mongo config/interference now, live Network API later).
- **[references/methodology.md](references/methodology.md)** — the prioritized tuning playbook
(synthesized from a multi-model pass + live recon). Read before recommending any change.
## What it does (current = Plane 1, read-only)
1. **Audit a site** — config + interference, no live plane needed:
```bash
bash .claude/skills/unifi-wifi/scripts/audit-site.sh <site-name|site_id>
bash .claude/skills/unifi-wifi/scripts/audit-site.sh cascades
```
Outputs 2.4/5/6 config summary, the per-channel neighbor-density (interference) map, and flagged
issues (2.4 over-provisioning, 40/80/160MHz width, off-1/6/11 channels, min-RSSI off, high power).
2. **Interpret** the flags against `methodology.md` (fix order: prune 2.4 -> shrink cells/power ->
min data rates -> manual 1/6/11 plan -> min-RSSI + roaming -> steer to 6GHz).
3. **Recommend** a prioritized, per-zone change plan. Roll out per zone, not site-wide at once.
Ad-hoc Mongo queries: `.claude/scripts/uos-mongo.sh` (recipes in data-access.md). Access is the
vaulted dedicated key `infrastructure/uos-server-ssh-key` (works from any fleet machine).
## The two data planes (know which you're using)
- **Plane 1 — Mongo `ace`** (available now, read-only via SSH): radio config (`radio_table`), the
`rogue` neighbor-interference map, `channelplan`, AP/client inventory. Enough for the full config
audit + channel/interference plan — covers the 2.4GHz "first problem".
- **Plane 2 — live Network API** (`stat/device`, `stat/sta`; NOT yet wired): live channel
utilization (`cu_total`), per-client RSSI/SNR/retries, AP satisfaction. Needed to **validate**
changes (before/after) and find the worst APs by live airtime. Wiring it needs a dedicated
read-only UniFi admin or integration API key on `.29`, vaulted as
`infrastructure/uos-server-network-api`. See data-access.md "Plane 2".
## Applying changes — IMPORTANT boundary
This skill is **audit + advisory** today. **Writing config to the controller is not wired** and is
high-stakes (a bad channel/power/disable push degrades a live facility). When change-application is
added it MUST: go through the controller API with an authed account; change **one zone at a time**;
capture live `cu_total`/satisfaction **before and after** each change; and never run nightly/auto
channel optimization in ultra-dense sites (pin a manual plan). Until then, hand the recommended
plan to a tech to apply in the UniFi UI, or get explicit go before any write path is built.
## Roadmap
- **Phase 1 (done):** config + interference audit, flags, methodology. Read-only.
- **Phase 2:** wire the live Network API (Plane 2) for `cu_total`/satisfaction/per-client RF →
before/after validation + "worst APs by airtime" ranking.
- **Phase 3:** assisted change application (per-zone, API-driven, with live before/after gating).
## Notes
- The methodology is independent-model guidance + config-plane recon — **validate against live
stats before trusting any single recommendation**, and roll out per zone.
- Multi-site: pass any site name/id; `uos-mongo.sh --sites` lists them. Cascades = `685f39068e65331c46ef6dd2`.

View File

@@ -0,0 +1,107 @@
# UniFi WiFi data access — what the UOS controller exposes and how to read it
This is the data-capability map for the `unifi-wifi` tuning skill, built from live recon of the
**UOS Server** (`172.16.3.29`, self-hosted UniFi OS / classic Network app, Mongo DB `ace`) using
the **Cascades** site (`site_id 685f39068e65331c46ef6dd2`) as the hard case (77 APs / 12 switches,
~550 concurrent wireless clients, severe 2.4GHz neighbor congestion). Access: `infrastructure/uos-server-ssh-key`
(vaulted) + `.claude/scripts/uos-mongo.sh`. See also [[uos-server]].
## Two data planes (the key finding)
| Plane | Source | Reach | Holds |
|---|---|---|---|
| **Config + history** | Mongo `ace` via `uos-mongo.sh` | root SSH, fully available now | radio config, the interference map, channel-plan settings, AP/client inventory |
| **Live RF/airtime** | Controller Network API (`stat/device`, `stat/sta`) | needs a session / integration key — NOT yet wired | current channel utilization, per-client RSSI/retries/tx-rate, AP satisfaction, num_sta |
The live per-AP utilization and per-client RF stats are **NOT persisted in Mongo** (the `device`
collection carries config but no `radio_table_stats`; the `user`/client collection only keeps
`last_radio`). So a first-pass audit + channel/interference plan comes entirely from Mongo; the
live "current airtime / who's unhappy right now" feedback loop needs the local Network API (see
"Live-stats gap" below).
## Plane 1 — Mongo `ace` (available now)
### `device` collection (per AP/switch; filter `type:'uap'` for APs)
Per-AP `radio_table[]` (the config we tune), one entry per radio:
- `radio`: `ng` = 2.4GHz, `na` = 5GHz, `6e` = 6GHz
- `channel`, `ht` (width: 20/40/80/160), `tx_power_mode` (auto/low/medium/high/custom)
- `min_rssi_enabled`, `min_rssi` (sticky-client / roaming floor, e.g. -77)
- plus `atf_enabled` (airtime fairness), `country_code`, `antenna_table`, `scan_radio_table`,
`support_wifi6e`, `wifi_caps`, model/firmware, `num_sta` is present at the device level (last reported).
Cascades is a **U7-Pro / WiFi-6E** fleet (tri-band ng/na/6e). Example AP: 2.4 ch11/20MHz,
5GHz ch153/80MHz, 6GHz ch145/160MHz, min_rssi -77 enabled.
### `rogue` collection — the interference map (586,688 docs fleet-wide; the gold)
Every neighbor/over-the-air BSSID an AP has seen, with `band`, `channel`, `ap_mac` (which of our
APs saw it), `bssid`, `essid`, `rssi`, `age`, `site_id`. Aggregate by channel to quantify
**co-channel congestion**. Cascades 2.4GHz is brutal:
| Band | Channel | Neighbor BSSIDs seen |
|---|---|---|
| 2.4 (ng) | **6** | 33,359 |
| 2.4 (ng) | **1** | 19,275 |
| 2.4 (ng) | **11** | 16,578 |
| 5 (na) | 149 | 8,889 |
| 5 (na) | 157 | 6,964 |
| 5 (na) | 44 | 5,477 |
| 6 (6e) | 69 | 86 |
Takeaways the skill uses: 2.4GHz is saturated on all three usable channels (so the fix is fewer
2.4 radios + tight power, not "find a clean channel"); 6GHz is nearly empty (steer capable
clients up); 5GHz upper band (149/157) is busier than the UNII-1/DFS lower band.
### `channelplan` collection
The controller's auto-channel-plan inputs/outputs per site: `channels_ng`/`channels_na`/`channels_6e`
(allowed channel lists), `ht_modes_ng/na/6e`, `method`, `optimize`, `exclude_devices`,
`high_priority_devices`, `date`. Lets the skill read/propose the channel plan the controller will honor.
### `user` collection — client inventory/history
~1,807 client records for Cascades. Holds identity + `last_radio` (band) but **not** live RF; use
for inventory/segmentation, not live signal.
### Config-audit signals already computable from Mongo
- 2.4GHz width != 20MHz (40MHz on 2.4 in density = self-inflicted overlap).
- `min_rssi_enabled=false` on 2.4 radios → sticky-client risk. (Cascades: **6 of 77** APs have 2.4 min_rssi disabled.)
- 2.4 channels not on the 1/6/11 plan; adjacent APs on the same channel.
- TX-power mode (auto/high) on 2.4 in dense clusters (should be low/medium).
- Per-AP radio enable: which dense-cluster APs should have their 2.4 radio disabled entirely.
## Plane 2 — Live RF/airtime (the gap to wire next)
Live data the tuner ideally also wants (current utilization, satisfaction, per-client RSSI/SNR/
retries, roam events) lives in the **classic Network API**, session-authenticated:
- `GET /proxy/network/api/s/<site_short>/stat/device` → per-AP `radio_table_stats[]`:
`cu_total` (channel utilization %), `cu_self_rx`/`cu_self_tx` (our own airtime), `num_sta`,
`tx_retries`, `satisfaction`, per radio.
- `GET /proxy/network/api/s/<site_short>/stat/sta` → per-client: `rssi`, `signal`, `noise`,
`tx_rate`/`rx_rate`, `tx_retries`, `satisfaction`, `radio`/`channel`, `nss`, anomalies.
Auth options (none wired yet): (a) a **dedicated read-only local UniFi admin** → login for a
session cookie; (b) a **Network integration API key** (`X-API-KEY` vs `/proxy/network/integration/v1/...`).
The cloud Site Manager key does NOT authenticate the local API (401); the existing local "Claude"
integration key's value is hashed/unrecoverable. **Action for phase 2:** create a dedicated
read-only admin or integration key on `.29`, vault it (`infrastructure/uos-server-network-api`),
and read live stats from it. Until then the skill runs config+interference analysis (Plane 1),
which already covers the 2.4GHz "first problem".
## Quick recipes
```bash
# Cascades site id
bash .claude/scripts/uos-mongo.sh --sites | grep -i casc # 685f39068e65331c46ef6dd2
# 2.4GHz neighbor congestion per channel for a site (the interference map)
cat <<'JS' | bash .claude/scripts/uos-mongo.sh
db.rogue.aggregate([{$match:{site_id:'685f39068e65331c46ef6dd2',band:'ng'}},
{$group:{_id:'$channel',neighbors:{$sum:1}}},{$sort:{neighbors:-1}}]).forEach(printjson)
JS
# Per-AP 2.4GHz config audit (channel, width, power, min_rssi)
cat <<'JS' | bash .claude/scripts/uos-mongo.sh
db.device.find({site_id:'685f39068e65331c46ef6dd2',type:'uap'},{name:1,radio_table:1}).forEach(function(a){
(a.radio_table||[]).forEach(function(r){ if(r.radio=='ng')
print(a.name+" ch="+r.channel+" ht="+r.ht+" pwr="+r.tx_power_mode+" min_rssi="+(r.min_rssi_enabled?r.min_rssi:'OFF')); });
});
JS
```

View File

@@ -0,0 +1,63 @@
# High-density UniFi RF tuning — methodology
Synthesized from a multi-model pass (Grok 4.3 + Gemini 3.1 Pro, which converged strongly) plus
live recon of Cascades. The governing principle in a congested environment: **conserve airtime
and shrink 2.4GHz participation** — you cannot out-tune ambient interference (Cascades sees ~33k
neighbor BSSIDs on 2.4 ch6), so the win is moving capable clients onto smaller 5/6GHz cells and
using 2.4 for legacy coverage only. Applies to any UOS site; thresholds below are starting points
to tune from each site's data.
## Fix order (do in this sequence — earlier steps de-risk later ones)
1. **Prune 2.4GHz radios.** Disable the 2.4 radio on ~4060% of APs in dense clusters; keep 2.4
only where needed for coverage/legacy (perimeter, stairwells, elevators, rooms with 2.4-only
medical pendants/tablets). 77 APs all on 2.4 in this density is catastrophic self-interference.
Target **≤1525 STAs per active 2.4 radio**.
2. **Power + width.** 2.4 → **Low / custom ~611 dBm** (smallest cells so a client hears 23
BSSIDs, not 20). 5GHz → **Medium / ~1215 dBm, 40MHz** (avoid 80/160 in density — wide channels
destroy spatial reuse). 6GHz → **80MHz, higher power ~1820 dBm** as a "staircase" that pulls
6E-capable clients up into the clean lane.
3. **Minimum data rates.** Disable 111 Mbps; set 2.4 minimum to **12 or 24 Mbps**. Kills the
management-frame overhead from distant/legacy stations and effectively shrinks cells.
4. **Channel plan (manual).** Strict **1/6/11, 20MHz** on 2.4; assign per-zone so adjacent APs
differ, weighting toward the channel where *our* APs dominate vs neighbor density. 5GHz: spread
across UNII-1 + **DFS** (UNII-2/2e) for more non-overlapping channels; U7-Pro DFS filtering is
good. 6GHz: **PSC channels**. **Do NOT use nightly/auto channel optimization in ultra-dense**
it can't model intermittent neighbor interference; pin a manual plan.
5. **min-RSSI + roaming.** 2.4 min-RSSI **75/76**, 5GHz **70/72** (start 72, tighten only if
satisfaction stays high). Enable **802.11r (fast roaming)** + **802.11v (BSS transition)**
but **test legacy medical/IoT devices first**; if pendants drop, scope 802.11r to a separate
SSID or disable. Senior users pause mid-hallway — don't over-aggress (65 causes drops).
6. **Steering.** Band steering **prefer 5GHz** globally; enable 6GHz for capable clients. Consider a
separate legacy SSID for fragile 2.4-only devices so the main SSID can be tuned aggressively.
7. **Monitor + iterate** (needs live-stats plane). Re-audit neighbor density monthly — external
interference won't go away; the job is to keep shrinking 2.4 participation and packing capable
clients onto small 5/6GHz cells.
## Metrics → action thresholds (drive every change from data, not vibes)
| Signal (source) | Threshold | Action |
|---|---|---|
| `cu_total` (channel utilization) | > 5060% sustained | reduce width, disable a 2.4 radio, or move AP off that band |
| `cu_self_rx`/`cu_self_tx` vs `cu_total` | self low but total high | neighbors eating airtime → raise min data rates, shrink cell, change channel |
| `tx_retries` per radio/client | > 1520% | co-channel/hidden-node or power too high or sticky client; try a DFS channel |
| `satisfaction` (AP/client) | < 8090% | investigate that AP/zone |
| `num_sta` per radio | > 40 on a 5GHz radio / > 25 on 2.4 | add steering pressure or another AP; prune nearby 2.4 |
| neighbor BSSIDs per channel (`rogue`) | >> 500 on a channel | don't site critical 2.4 there; consider disabling 2.4 in that wing |
| roam quality | edge RSSI 68..72, SNR ≥ 20 dB | healthy handoff target |
Plane 1 (Mongo) gives: config audit, the neighbor-density (`rogue`) map, channel plan, num_sta
(last-reported), min_rssi state. Plane 2 (live Network API, not yet wired) gives: live `cu_total`,
`tx_retries`, `satisfaction`, per-client RSSI/SNR. See [data-access.md](data-access.md).
## 30-day success criteria
Median client satisfaction > 90%; 2.4 `cu_total` < 40% on active radios; 5/6GHz carrying > 75% of
associations; tx-retry < 10% on the primary SSID.
## Cascades-specific (the hard case)
77 U7-Pro APs / ~550 clients / 6 of 77 APs currently have 2.4 min-RSSI OFF. 2.4 is saturated on
1/6/11 (16k33k neighbors each) → aggressive pruning is mandatory, not optional. 6GHz is nearly
empty (ch69: 86 neighbors) → the biggest untapped win is steering 6E-capable clients to 6GHz.
5GHz upper (149/157) is busier than UNII-1/DFS lower → bias the 5GHz plan toward 3648 + DFS.
> Caveat: these are independent-model recommendations + config-plane recon. Validate against live
> `cu_total`/satisfaction before/after each change (Plane 2), and roll out per-zone, not site-wide
> at once, so a bad assumption is contained.

View File

@@ -0,0 +1,58 @@
#!/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