Files
claudetools/clients/cascades-tucson/session-logs/2026-06/2026-06-15-howard-cascades-wifi-rf-audit.md
Howard Enos 8f602b3e17 sync: auto-sync from HOWARD-HOME at 2026-06-17 16:01:19
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-17 16:01:19
2026-06-17 16:01:28 -07:00

821 lines
59 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## User
- **User:** Howard Enos (howard)
- **Machine:** Howard-Home
- **Role:** tech
## Session Summary
Resumed the Cascades wireless investigation that had stalled on 2026-05-16 (that session was read-only via the cloud API and blocked from per-AP RF data). The day's earlier syncs delivered the unblock: Mike (GURU-5070) vaulted `infrastructure/uos-server-ssh-key` + `clients/cascades-tucson/unifi-ap-ssh` and shipped a purpose-built `unifi-wifi` skill (audit/model-rank/optimize/apply/watch scripts + methodology references). Controller access via the vaulted key was verified, then the full live audit ran against the Cascades site (`685f39068e65331c46ef6dd2`) on the UOS controller (172.16.3.29).
The audit confirmed and quantified the 05-16 hypothesis with real controller data: 77 U7-Pro APs, all running 2.4GHz at auto (~full) power, 20MHz. 2.4 airtime (`cu_total`) is 7494% busy on 75 radios with 6181% of that being pure interference, serving ~1 client each, at TX-retry rates of 4065% and single-digit AP satisfaction on the worst (209 sat=1/retr=65%, 139 sat=2/retr=49%, CC Bridge retr=48%). Neighbor-BSSID density is catastrophic on 2.4 (ch6=33,370, ch1=19,274, ch11=16,580). 5GHz is on 80MHz width on 76/77 APs (kills spatial reuse), biased to the busy upper channels (149/157). 6GHz is active on 75 radios but nearly empty of clients. 6 APs have 2.4 min-RSSI OFF (615, 608, 505, 517, 622, salon); 4 are off the 1/6/11 plan on auto (128, 108, 108U7-Pro, salon).
Diagnosis of "bad for SOME users": experience splits by band. Clients on 5/6GHz are fine; clients that land or stick on 2.4GHz (legacy phones, medical/IoT, poor band-steerers, or anything held by a min-RSSI-OFF AP from across the building) hit the saturated 2.4 radios with 4065% retransmits and near-zero satisfaction.
Late in the session Howard raised the **military-base / DFS** factor — Cascades is in Tucson near Davis-Monthan AFB (+ TUS airport radar). This reverses the earlier "bias 5GHz toward DFS" recommendation: DFS channels (UNII-2/2e, ch52144) will see frequent radar-detection events forcing channel-vacate + CAC silence, producing exactly the intermittent per-area dropouts reported. Revised plan uses non-DFS only (UNII-1 3648 + UNII-3 149161), which makes 40MHz width and 6GHz steering more important.
## Key Decisions
- Used the `unifi-wifi` skill end-to-end (audit-site, model-rank, optimize-radios, apply-radio dry-run) rather than ad-hoc Mongo queries — it encodes the multi-model methodology and coverage-safe model.
- **5GHz channel plan revised to AVOID DFS** due to proximity to Davis-Monthan AFB. Non-DFS UNII-1 + UNII-3 only; verify empirically against the controller's radar-detection event history before reconsidering DFS.
- No changes applied — writes are intentionally gated until a read-WRITE controller admin is vaulted (`infrastructure/uos-server-network-api-rw`) and `--apply` is passed. Confirmed the apply path works via dry-run (Floor 3: 17 radios auto->low with rollback values captured).
- Rollout will be per-zone (one floor at a time) with live before/after validation, never site-wide at once.
## Problems Encountered
- **Controller SSH key not on Howard-Home and not in vault (earlier in day).** Tested both vaulted keys (gururmm-server-physical, openclaw-fleet) against root@172.16.3.29 — both denied; OC-5070/OC-Mac unreachable over Tailscale, fleet key denied on OC-Beast. Resolved by coord-requesting Mike to vault the UOS key; he did (`infrastructure/uos-server-ssh-key`), picked up on sync.
- **Cloud Site Manager API insufficient (re-confirmed).** Device objects carry no site field and no RF/channel/power/uplink data — only online/offline + firmware. Cannot drive RF tuning. The Mongo-via-SSH path (now available) is required.
## Configuration Changes
- None to Cascades infra. Read-only audit + dry-run only.
- No repo files edited beyond this session log.
## Credentials & Secrets
- **`infrastructure/uos-server-ssh-key`** (vaulted by Mike this day) — root SSH key for the UOS controller 172.16.3.29; used by `uos-mongo.sh` / unifi-wifi scripts. This is the DATA plane (read), not an API write session.
- **`clients/cascades-tucson/unifi-ap-ssh`** (vaulted by Mike this day) — device-auth cred for SSHing directly into Cascades APs (used by `watch-ap.sh`; needs site VPN for L3 reach to 192.168.2.x/3.x).
- **Needed, not yet created:** `infrastructure/uos-server-network-api-rw` (read-write controller admin) to apply radio changes; `infrastructure/uos-server-network-api` (read-only admin) to wire live-stats Plane 2 validation.
## Infrastructure & Servers
- **UOS controller:** 172.16.3.29 (Rocky 9 VM "Unifi" on Jupiter 172.16.3.20); UniFi-OS HTTPS on **11443** (not 8443). Mongo `ace` on 127.0.0.1:27117 inside rootless podman `uosserver`. Cascades site_id `685f39068e65331c46ef6dd2`.
- **Cascades wireless:** 77 U7-Pro APs, ~550 clients. Firewall = pfSense 192.168.0.1 (site VPN endpoint; `.ovpn` comes from pfSense OpenVPN Client Export, NOT UniFi). APs on 192.168.2.x/3.x.
- **Location/RF:** Tucson, near Davis-Monthan AFB + TUS radar → DFS unreliable.
## Commands & Outputs
```bash
bash .claude/scripts/uos-mongo.sh --sites | grep -i casc # 685f39068e65331c46ef6dd2 Cascades (access OK)
bash .claude/skills/unifi-wifi/scripts/audit-site.sh cascades # config + neighbor-density + flags
bash .claude/skills/unifi-wifi/scripts/model-rank.sh cascades 7 ng
bash .claude/skills/unifi-wifi/scripts/optimize-radios.sh cascades 14 ng # power-down 74, disable 0, keep 1
bash .claude/skills/unifi-wifi/scripts/apply-radio.sh cascades ng power low --zone "Floor 3" # DRY-RUN: 17 radios auto->low
```
Key audit output: 2.4 cu_total 7494% / interf 6181% / ~1 client; retry 4065%; ch6=33,370 neighbors; 5GHz 80MHz on 76/77; 6GHz active 75 but empty; min-RSSI OFF on 615/608/505/517/622/salon.
## Pending / Incomplete Tasks
- [ ] Vault read-WRITE controller admin `infrastructure/uos-server-network-api-rw` (blocks applying any radio change). Candidate: coord-request Mike.
- [ ] Vault read-only `infrastructure/uos-server-network-api` to wire live-stats Plane 2 (before/after cu_total/satisfaction validation).
- [ ] Apply Phase A (2.4 power-down to Low) per-zone with live validation, once RW cred exists.
- [ ] 5GHz: 80->40MHz width; non-DFS channel plan (UNII-1 3648 + UNII-3 149161); 6GHz steering for capable clients.
- [ ] Min data rates (kill 111Mbps, 2.4 floor 12/24Mbps); set 2.4 min-RSSI -75/-76 on the 6 OFF APs; pin 4 off-plan APs to 1/6/11.
- [ ] Pull controller radar-detection event history to empirically confirm DFS unusability.
- [ ] Secondary: fix the non-working ".ovpn / Download configuration" — likely pfSense OpenVPN Client Export (192.168.0.1), not UniFi. Needed for watch-ap.sh live validation.
- [ ] AP 108 offline — KNOWN, needs a new cable run (per Howard); ignore for now. Also a stale duplicate controller object (108 vs 108U7 Pro) to clean up later.
## Reference Information
- Coord message to GURU-5070 re UOS key: id `a4b385ad-4fbb-4097-a066-099622080055`; backstop todo `3bd12a14-2b51-4c11-8f76-3f835b07e8dc` (--user mike).
- unifi-wifi skill: `.claude/skills/unifi-wifi/` (methodology.md, data-access.md, interference-model.md).
- Prior wireless log: `clients/cascades-tucson/session-logs/2026-05-16-howard-wireless-diagnostic.md`.
- UOS system wiki: `wiki/systems/uos-server.md`.
---
## Update: 20:40 PT — RW cred arrived, live (Plane 2) re-look, live-stats.sh accuracy fixes
Mike vaulted the RW controller admin (`infrastructure/uos-server-network-api-rw`) within ~30 min
of the request, so the live Network API (Plane 2) became available. Re-audited Cascades live and,
in doing so, found + fixed accuracy bugs in `unifi-wifi/scripts/live-stats.sh` that had skewed the
earlier read.
### live-stats.sh bugs fixed (held a coord lock; messaged Mike e8be889f)
1. `stat/device` output hard-capped at `head -60` -> only ~15 of 77 APs were shown (we'd been
judging the whole site from a 15-AP sample). Removed the cap; verified 77/77 now.
2. `satisfaction` was read per-radio (always `-1` on this controller). DEVICE-level satisfaction
IS populated -> switched to `d['satisfaction']`.
3. `tx_retries` was the raw cumulative counter (scales with traffic, misleading). Switched to
`radio_table_stats.tx_retries_pct` (a true rate).
4. `--clients` was `[:40]` unsorted (hid 90% of 574 clients). Now sorts worst-by-satisfaction and
prints signal/noise/retry%/`satisfaction_reason`.
5. RF-neighbor table left as a documented TODO in the header (not a single API field; must be built
from the `rogue` BSSID cross-ref vs each AP's `vap_table`). It's what unlocks confident radio
DISABLES; until then power-down/channel/width are the safe levers.
### Corrected diagnosis (the data fix changed a conclusion)
- Accurate avg retry RATE: **2.4GHz 11.2%** > 5GHz clear 9.0% ~= 5GHz DFS 8.4%. My mid-session
claim that "5GHz/DFS is now the #1 problem" was an ARTIFACT of the raw counter + 15-AP sample and
is WITHDRAWN. On the rate, DFS is NOT retrying worse than clear channels.
- **2.4GHz is the primary pain band** (highest retry; 27 of the 40 worst clients are on 2.4, retry
11-42%, mostly IoT/legacy: Ring cams, robotic cleaner, smart plugs, EPSON printer, Poly phone,
handheld scanners, Watch). The original 2.4 power-down/prune plan stands as #1.
- **DFS = resilience risk, not throughput killer.** 55/77 5GHz radios on DFS near Davis-Monthan =
radar-vacate exposure (client drops), worth moving off DFS for stability, but not urgent for
performance.
- **6GHz still dead (1 client of 574)** — top untapped clean/non-DFS capacity; steering remains a
key opportunity.
- **AP-level satisfaction 95-100 across the fleet** — network is healthy on average; pain is in the
client tail = consistent with "bad for SOME users."
### Cascades live snapshot (2026-06-15 ~20:30 PT)
- 77/77 APs reporting; 574 wireless clients. Band split: na (5GHz) ~87 / ng (2.4) ~21 sampled per
pull / 6e ~1.
- 2.4 cu_total 69-94% live (saturation confirmed). 5GHz cu_total mostly <40%.
- Worst clients: RingStickupCam (sat 60), Galaxy-A32-5G (60), DIRECTV (71), Samsung on ch1 (76,
retry 42%).
### Updated next steps
- [ ] Build Floor-pilot DRY-RUN: 2.4 power-down to Low on one floor (e.g. Floor 4), validate live
cu_total/retry% before+after via the fixed live-stats.sh. Get explicit go before `--apply`.
- [ ] Implement the AP-to-AP RF-neighbor table (rogue BSSID x vap_table) to enable safe DISABLEs.
- [ ] 6GHz steering plan; 5GHz 80->40MHz + non-DFS channel plan (resilience).
- [ ] Coord msgs this update: RW-cred request 6b98282f (+todo cbb355ef); live-stats fix e8be889f.
- [ ] pfSense `.ovpn` (Howard handling) — needed for per-AP watch-ap.sh live stream.
---
## Update: 22:05 PT — site VPN up (split-tunnel), direct-AP cross-validation, watch-ap.sh fixed
Howard got the Cascades OpenVPN running. The pushed config is **full-tunnel** (pfSense
`redirect-gateway`) which killed his local internet + Tailscale (and my tool calls, since they run
on his machine). Fixed with a **split-tunnel client profile**:
`C:\Users\Howard\Documents\ComputerGuru Connect v2\Files\cascades-splittunnel.ovpn` — copy of the
working profile + `pull-filter ignore "redirect-gateway"`, `pull-filter ignore "dhcp-option DNS"`,
`route 192.168.0.0 255.255.252.0`, and the VPN creds embedded inline (`<auth-user-pass>` =
vault `clients/cascades-tucson/pfsense-openvpn-howard`, user `Howard`). After import+connect:
internet + Tailscale + direct AP reach all work simultaneously.
### Rescan + ground-truth verification (Howard: "test until sure there are no issues")
- Controller rescan: **77/77 APs**, config identical to prior pull — no drift, scripts stable.
- **Cross-validated controller data against AP ground truth** (direct SSH to APs, `mca-dump` +
`iw survey`) on TWO APs (132 @192.168.2.34, 335 @192.168.3.46): controller cu_total/clients
match the radios' own numbers within polling jitter (AP132 2.4: controller 81% / mca 87% / iw
survey 92% busy, -95dBm noise; AP335: 2.4 controller 81% vs mca 79%, 5GHz 10%=10% exact,
clients 2=2 exact). **Conclusion: the controller is accurate; the diagnosis stands on solid data.**
- New concrete fact: U7-Pro 2.4 radios report `min_txpower 6 / max 23 dBm` → "Low" = 6 dBm
(aggressive cell shrink — good for the power-down plan).
### watch-ap.sh fixed (2 bugs found by testing; validated live; Mike notified b0cf24a5)
1. Hard `sshpass` dependency → failed on Windows (no sshpass). Added **SSH_ASKPASS fallback**
(temp askpass helper + `SSH_ASKPASS_REQUIRE=force`); uses sshpass if present. Fallback needs
`ssh` from PATH = MSYS ssh on Windows (system OpenSSH can't exec a shell askpass, CreateProcessW
err 193); Linux/macOS system ssh is fine.
2. Radio list included virtual APs (`wifi0ap0..`) + `mld-wifi0` → duplicate lines AND
`sh: malformed ?: operator` (hyphen in `mld-wifi0` broke `${ACT_mld-wifi0:-0}`). Filter now
`grep -E '^(wifi[0-9]+|ath[0-9]+)$'`. Validated: clean 3-line/tick (wifi0 ch6 92%, wifi1 8%,
wifi2 0%).
- Minor (not fixed, interactive use is fine): script's `grep|head` block-buffers when piped
non-interactively; would need `stdbuf -oL` to be pipeable.
### Issues found this round (none in the data; environment/VPN only)
- **192.168.0.0/24 home-LAN overlap:** Howard's home LAN is 192.168.0.x, so Cascades devices on
192.168.0.x (pfSense `192.168.0.1`, cascadesDS `192.168.0.120`) are **shadowed/unreachable over
the VPN** from Howard-Home. APs on 192.168.2.x/3.x are unaffected. Use Tailscale for controller;
for pfSense/NAS over VPN would need a home renumber or a more specific route trick.
- **OpenVPN Connect DCO↔TAP failover** intermittently drops the 192.168.2.x route mid-session
(caused two transient watch-ap timeouts). Community **OpenVPN GUI** is the more stable client if
it recurs.
### State / next
- live-stats.sh + watch-ap.sh both fixed + validated; controller data verified accurate.
- NEXT (per Howard): one more clean retest pass, then build the **AP-to-AP RF-neighbor table**
(now feasible via direct AP `iw scan`/scan_radio_table) to unlock safe radio DISABLEs; then the
Floor-4 2.4 power-down pilot with live before/after validation.
- Coord this update: watch-ap.sh fix b0cf24a5.
---
## Update: 23:03 PT — RF-neighbor table DEFINITIVELY unobtainable; survey-dump is the usable substitute
Exhaustively investigated whether an AP-to-AP RF-neighbor table (which of OUR managed APs each AP
hears + RSSI) is obtainable, to enable data-driven radio DISABLE decisions. Cross-checked against
two rounds of UniFi's own AI guidance. **Conclusion: NOT obtainable on this deployment by any
safe/supported method.** Stop chasing it.
### Why it's impossible here (all paths tested)
- `rogue` collection AND Network API `stat/rogueap` (7,354 records): both FILTER OUT our managed
APs — only foreign BSSIDs (our OUI 0c:ea:14 = 2 hits, CSCNet = 0, is_ubnt = 2). By design.
- AP `iw dev wifiX scan`: **firmware-blocked — "Operation not permitted (-1)" even as uid=0(root)**,
on all radios incl. an idle 6GHz radio. UniFi locks raw scanning on operating radios.
- `iw scan dump` (cached): empty. `device.scan_radio_table`: empty []. No spectrum/neighbor
collection exists. `hostapd_cli show_neighbor` (802.11k): FAIL on every VAP.
- `mca-cli-op`: interactive CLI (bare invocation spews prompts — feed `</dev/null`). Subcommand
`info` works (firmware 8.6.11.18870, U7-Pro) but exposes NO neighbor/RF data; wireless/radio/
status return nothing useful.
- Channel AI / `channelplan`: stale (last run 2026-04-06); exposes only resulting channel lists,
not the neighbor graph. UniFi consumes neighbor data internally but exposes it nowhere.
- Roam graph (`ace_stat.wifi_connectivity_event`): too sparse (50 clients, 6 edges) — Cascades
devices are largely stationary (senior living), so roams can't prove coverage redundancy.
- A disruptive controller spectrum-scan would only return per-channel RF ENERGY, not a managed-AP
BSSID list — so even disruptive testing won't yield the neighbor table. NOT worth running.
NET: automated radio DISABLES are not data-supportable at Cascades. `optimize-radios.sh`
"disable=0" is correct. Any disables must be MANUAL (UniFi UI RF Environment view + on-site).
### The usable WIN: non-disruptive per-AP per-channel RF environment via `iw survey dump`
`iw dev wifiX survey dump` returns, per AP, **per-channel noise floor + airtime busy%** across the
WHOLE band (not just in-use) — already populated by background scanning, zero disruption. This is
real measured RF data that supports a DATA-DRIVEN CHANNEL PLAN. Sample AP 132 (Rec Room):
- 2.4 GHz: ch1 89%, ch6 95% (noise -65dBm), ch11 96% — every channel saturated.
- 5 GHz: UNII-1/low (36-48) 1-20% = cleanest; ch157 31% = congested → confirms bias 5GHz LOW.
- 6 GHz: ~0% across nearly all channels → wide-open lane, confirms 6GHz steering is the big win.
### Decisions / state
- Do NOT pursue the AP-neighbor table or disruptive spectrum-scan further (definitively closed).
- Channel plan can be MEASURED from `survey dump` (non-disruptive) instead of inferred.
- Two safe high-value moves remain: (1) 2.4 GHz power-down per-zone; (2) measured non-DFS channel
plan + 6GHz steering.
### Next steps
- [ ] Build `survey-collect.sh` (unifi-wifi skill): sweep all 77 APs' `iw survey dump`
(non-disruptive) → fleet-wide measured per-channel busy%/noise map → drive the channel plan.
- [ ] Floor-4 2.4 GHz power-down pilot with live before/after (`watch-ap.sh` + `live-stats.sh`).
- [ ] Record the neighbor-data dead-end + survey-dump method in skill docs (interference-model.md,
watch-ap.sh/live-stats.sh headers) so it is never re-chased.
- [ ] (Howard) has more information to add — pending.
- Confirmed env: firmware 8.6.11.18870 on U7-Pro; AP device-auth SSH = uid 0 (root); split-tunnel
VPN gives AP reach on 192.168.2.x/3.x (192.168.0.x shadowed by home LAN).
---
## Update: 23:24 PT — BREAKTHROUGH: AP-to-AP SNR matrix IS obtainable; neighbor-collect.sh built; disables now data-supportable
**REVERSES the earlier "AP-neighbor table unobtainable / disables not data-supportable" conclusion.**
The data DOES exist — not via any controller API/DB (all of which filter our managed APs) but on
each AP in `/proc`, populated NON-DISRUPTIVELY by UniFi's background RRM scanning:
- `/proc/ui_neighbor/ess_ap_list` — full list of managed APs this AP hears (serial/band/channel)
- `/proc/ui_neighbor/ssid/<n>` — same neighbors WITH SNR (per scan-vap, per band)
Discovered via `strings $(which mcad) | grep neighbor``/proc/ui_neighbor/ess_ap_list` +
`load_ssid_neighbor_cache`. `/proc/ui_neighbor` exists on every UniFi AP → fleet-generic.
### NEW: .claude/skills/unifi-wifi/scripts/neighbor-collect.sh (built + validated, Mike notified 3dbe2437)
Logs into controller for the mac/bssid->AP-name map, SSHes each online AP (sshpass OR SSH_ASKPASS
fallback), reads `/proc/ui_neighbor`, maps neighbors to AP names, emits the AP-to-AP SNR adjacency
matrix + a redundancy summary (APs heard by >=2 neighbors at SNR>=N = power-down/disable candidates).
Usage: `neighbor-collect.sh <site> [ap-ssh-vault-path] [snr_min]`. Run FOREGROUND.
**Validated on Cascades: 74/74 APs reporting.** Physically sensible (115<->116 SNR 63, 121<->221
SNR 65 = adjacent). Redundancy: **73/74 APs have >=2 strong (SNR>=20) 2.4GHz neighbors**, 72/74 on
5GHz, 65/74 on 6GHz → nearly every AP is coverage-redundant on 2.4 → aggressive 2.4 pruning is
data-supported, and individual radio DISABLES are now decidable from data.
### Three bugs found+fixed building it (apply to any loop-over-AP script)
1. Python wrote the AP-list temp file in Windows TEXT mode -> CRLF -> bash `read` left `\r` on the
IP -> ssh "hostname contains invalid characters" on EVERY AP (the "reachable 0" symptom). Fix:
`open(...,newline='\n')` + `ip="${ip%$'\r'}"` strip in the read loop.
2. Must run FOREGROUND — a fully detached background process can't spawn the SSH_ASKPASS helper
(both background runs got reachable 0; foreground after the CRLF fix got 74).
3. `ssh` in a `while read` loop needs `</dev/null` or it consumes the loop's stdin.
### Skill capability assessment (Howard: "must work for ALL clients / any unifi issue")
For WiFi/RF, the skill now gathers essentially everything obtainable, all site-parameterized
(works for any of the 49 UOS sites): config (uos-mongo), live per-AP+per-client (live-stats),
interference (audit-site + neighbor-collect), per-AP ground truth (watch-ap: mca-dump/iw survey),
the AP-to-AP SNR matrix (neighbor-collect), and gated apply (apply-radio). Per-client gap:
watch-ap/neighbor-collect default the AP device-auth cred to cascades — other clients need their
own `clients/<x>/unifi-ap-ssh` vaulted (overridable via arg). Out of current scope (it's
unifi-WIFI): switch/PoE, gateway/WAN/firewall, adoption — reachable via the same access layer
(uos-mongo/controller API/device SSH) but not yet wrapped as scripts.
### Pending follow-ups
- [ ] Wire neighbor-collect redundancy output into optimize-radios.sh (data-backed disables).
- [ ] Fold survey-dump (per-channel busy/noise) + dmesg DFS-radar recipes into the skill as
reusable collectors (the two ad-hoc datasets from this session).
- [ ] Per-client AP device-auth creds for other clients when extending beyond Cascades.
- [ ] Floor-4 2.4 power-down pilot (still the next live change; nothing applied yet).
- Coord this update: neighbor-collect.sh announce 3dbe2437.
---
## Update: 23:50 PT — gap 1 + gap 2 closed: survey-collect + dfs-check built; SKILL marked wifi-working / switches-WIP
GAP 1 (fold ad-hoc recipes into reusable collectors) — DONE, both validated on Cascades:
- **survey-collect.sh** `<site> [ap-ssh-vault-path]` — per-AP per-channel busy%/noise from
`iw survey dump` -> in-use busy% + cleanest non-DFS channels per band. 72 APs reported.
Result: 2.4 saturated 94-97% on every channel; 5GHz cleanest non-DFS = ch36/48/153/161; 6GHz ~0%.
- **dfs-check.sh** `<site> [ap-ssh-vault-path]` — per-AP `dmesg` radar history, PRECISE patterns
(excludes the "cached ifindex" false-positive). Result: **55/75 APs on DFS, ZERO real radar
events fleet-wide** -> DFS empirically low-risk at Cascades despite Davis-Monthan proximity
(confirms the retry-rate finding; the DFS concern was theoretical).
GAP 2 (scope) — DONE via SKILL.md Status block: WiFi monitoring+tuning = **WORKING** for every UOS
site we monitor (audit-site, live-stats, model-rank, optimize-radios, survey-collect, dfs-check,
neighbor-collect, watch-ap, apply-radio — all site-parameterized). Switches/PoE, gateway/WAN/
firewall, adoption = **WIP** (access layer reaches them; need dedicated collectors). Per-client:
vault each client's own `clients/<x>/unifi-ap-ssh`, pass as the script arg.
CROSS-PLATFORM LESSON (baked into both new scripts): pass temp paths to python via ARGV — MSYS
translates POSIX->Windows for python.exe; `$TMP` embedded in a `python -c` string is NOT translated
and fails ("No such file or directory") on Windows. Use `python - "$TMP/x" <<'PY' ... sys.argv[1]`.
Coord: collectors+status announce c3ccaa07. Next: wire neighbor-collect redundancy into
optimize-radios.sh; Floor-4 2.4 power-down pilot (still nothing applied to live radios).
---
## Update: 00:05 PT (06-16) — wired neighbor SNR matrix into optimize-radios.sh (disables now data-backed)
Closed the last follow-up: the AP-to-AP SNR matrix now DRIVES optimize-radios.sh disable decisions.
- neighbor-collect.sh: `NBR_JSON=<path>` env now also emits machine-readable adjacency
`{ap:{band:{nbr:snr}}}` (74 APs, ~21KB).
- optimize-radios.sh: `NEIGHBOR_JSON=<path>` env now uses the measured bidirectional SNR overlap
(A hears B AND B hears A, SNR>=NBR_SNR_MIN default 20, any band) as the coverage-adjacency source,
replacing the sparse roam graph. Roam graph remains the fallback when NEIGHBOR_JSON unset.
Workflow:
NBR_JSON=.claude/tmp/cascades-nbr.json bash .../neighbor-collect.sh cascades
NEIGHBOR_JSON=.claude/tmp/cascades-nbr.json bash .../optimize-radios.sh cascades 14 ng
VALIDATED on Cascades 2.4GHz: roam-graph -> POWER-DOWN 74 / DISABLE 0; neighbor-SNR -> POWER-DOWN 65
/ **DISABLE 9** / KEEP 1, est. 621% interference-airtime removed, each disable listing its covering
neighbors (127->128; 229->128; 248->348; 330->128; 445->347/348/247; 428->128; 622->505/615/517;
Kitchen->Memcare TV room; salon->128). The disable capability we concluded was impossible is now
data-backed for any site with a neighbor-collect run.
IMPL NOTE (bug fixed): injecting the full matrix as a JS object literal CRASHED the old mongo shell
(SpiderMonkey GC-during-compile on a ~21KB literal). Fix: precompute bidirectional strong NAME-pairs
in python, inject a compact ';'-joined flat STRING, parse in JS. (Pattern for any large data ->
mongo-shell injection: flat string, not object literal.)
Still gated: apply-radio.sh has NO disable action (power/channel/width only); disables remain a
reviewed, per-zone, live-validated MANUAL step. Coord: wire announce 68cae757.
Remaining open: Floor-4 2.4 power-down pilot (still nothing applied to live radios).
---
## Update: 2026-06-16 00:03 PT — skill-build focus: apply-radio generalized + write path validated + ROADMAP
Per Howard: pause the Cascades production fix; build the unifi-wifi skill into a fully working tool
for any client. Used Cascades **0-client radios as the safe sandbox** (6 GHz is ~0-client fleetwide;
no whole-AP had 0 clients).
ADDED references/ROADMAP.md — full A/B/C/D plan: (A) apply side, (B) multi-client enablement,
(C) switches/gateway WIP, (D) robustness. Cross-platform gotchas captured for future scripts.
GENERALIZED apply-radio.sh from power-only to 4 radio_table actions (same gated REST mechanism,
field-generic via a compact FIELDS JSON shared by the dry-run preview and the apply python; rollback
auto-saved):
power low|medium|high|auto|<dBm> -> tx_power_mode (+tx_power)
width 20|40|80|160 -> ht
channel <number>|auto -> channel
minrssi off|on|-<NN> -> min_rssi_enabled (+min_rssi)
**WRITE PATH VALIDATED** end-to-end (first real --apply): on Floor 6 0-client 6e radios (608, 622)
applied ht 160->80, controller confirmed ht=80, then reverted to 160 (state restored). Dry-run
previews for width/minrssi verified correct (incl. 615 U6-Pro correctly has no 6e radio).
radio_table field names confirmed: channel, ht (width), tx_power_mode/tx_power, min_rssi +
min_rssi_enabled. NO radio enable/disable field -> DISABLE not implemented (ROADMAP A; discover via
UI-toggle + device-JSON diff). min-data-rate/band-steering are wlanconf-level -> separate path.
SKILL.md apply section updated with all 4 actions + the validated-write-path note. Coord: 6aac1298.
Next per roadmap: A (disable mechanism, wlanconf knobs, channel-plan apply) then B (per-client creds/VPN).
---
## Update: 2026-06-16 00:24 PT — radio DISABLE mechanism found; apply-radio disable/enable + --ap (validated)
Howard toggled 6GHz off on AP 622 in the UI; diffed the device-JSON before/after. **Disable mechanism:
`radio_table[].tx_power_mode = "disabled"`** (radio stays in table, channel/ht untouched). Re-enable =
`tx_power_mode "auto"`. Same field as power. (Other diffs were timestamps/cfgversion + JSON int/str noise.)
apply-radio.sh now supports actions: power | width | channel | minrssi | **disable | enable**, plus a
new **--ap "<name>"** single-AP filter (correct scope for disables). Arg parsing reworked so
disable/enable take no value. VALIDATED full enable->disable->enable cycle on 622's 0-client 6GHz via
--apply; 622 restored to ON (auto). Workflow: optimize-radios (NEIGHBOR_JSON) disable list ->
`apply-radio <site> <band> disable --ap <name> --apply` one AP at a time, watch-ap validated.
SKILL.md + ROADMAP.md updated (disable now done). apply-side now covers all radio_table knobs;
only wlanconf-level (min-data-rate, band-steering) remains -> future apply-wlan.sh. Coord: 6aac1298-> this: msg sent.
---
## Update: 2026-06-16 00:40 PT — multi-client enablement + WLAN-level apply (wlanconf); both validated
MULTI-CLIENT (B): NEW scripts/sites.sh lists all ~49 UOS sites (APs/SW/GW) + per-client AP-cred
readiness — the entry point for other clients. Controller-side scripts validated on **other clients**
(Glabman 6 APs, Sonoran Glass 5 APs) with zero setup. AP-side scripts now print the exact vault
command when a client's clients/<slug>/unifi-ap-ssh is missing (controller-side still works). SKILL.md
Multi-client section added. (Per-client AP creds vaulted as clients come online.)
WLAN-LEVEL APPLY (A2): NEW scripts/apply-wlan.sh (wlanconf, gated like apply-radio, --wlan filter):
minrate ng|na auto|off|<Mbps> (kill 1-11Mbps: minrate ng 12)
steer on|off (5GHz roaming_assistant)
GOTCHA found+fixed: manual min rate only applies when minrate_setting_preference=manual (else controller
auto-manages + silently reverts — first PUT looked [ok] but value stayed 1000). Script now sets the
preference; 'minrate ... auto' restores controller management. WRITE PATH VALIDATED on a 0-client WLAN
(Green Valley Computer Club): minrate ng 12 -> persisted (pref=manual, 12000) -> restored to auto.
apply-radio (radio_table: power all-states/width/channel/minrssi/disable/enable + --ap/--zone) and
apply-wlan (wlanconf: minrate/steer) now cover the WiFi apply surface, all gated + rollback + validated
on 0-client sandboxes. ROADMAP updated. Coord: this msg. Remaining: classic band-steering field, per-client
creds/VPN, switches/gateway collectors (C).
---
## Update: 2026-06-16 00:58 PT — band-steering field FOUND + wired into apply-wlan (validated)
Modern UniFi (Network 8.x) has NO classic 'bandsteering_mode'. Located its replacements by inspecting
wlanconf + the setting collection (no UI toggle needed this time):
no2ghz_oui -> the band-steering toggle (keep 5GHz-capable OUIs off 2.4)
wlan_bands (["2g","5g"]) -> per-WLAN band membership (set ["5g"]/["5g","6e"] to force clients up)
bss_transition -> 802.11v BSS-TM
roaming_assistant_na_* -> 5GHz sticky-client kicker
fast_roaming_enabled -> 802.11r (left out — risky for IoT/medical)
Wired into apply-wlan.sh as actions: bandsteer (no2ghz_oui) | bands (wlan_bands) | steer
(roaming_assistant) | bsstm (bss_transition), alongside minrate. Validated on the 0-client Green Valley
WLAN: bandsteer on->verify->off, bsstm toggle, bands 5g dry-run; restored to original (bss_transition=true).
Also surfaced setting.radio_ai (Channel AI: channels_na includes DFS, ht_modes_na=20/40, enabled=false)
+ setting.connectivity — possible future apply targets (pin the channel plan).
WiFi APPLY SURFACE NOW COMPLETE: apply-radio (power all-states/width/channel/minrssi/disable/enable,
--zone/--ap) + apply-wlan (minrate/bandsteer/bands/steer/bsstm, --wlan), all gated + rollback +
validated on 0-client sandboxes. Coord: this msg. Remaining (ROADMAP C): switch/PoE + gateway collectors.
---
## Update: 2026-06-16 01:09 PT — WiFi control surface complete: full wlanconf knobs + device-lock + client controls
(a) DEVICE LOCK / CONTROL:
- apply-wlan aps <ap-names|all> -> broadcasting_aps (resolves AP NAMES->MACs; restricts SSID to APs =
the "lock device to AP" lever, since UniFi has NO native per-client AP pin). Validated dry-run:
aps 622 --wlan CSCNet -> broadcasting_aps=[0c:ea:14:3e:65:a2].
- apply-wlan macfilter off|allow|deny <macs> -> mac_filter_*. Dry-run validated.
- NEW client-control.sh: block|unblock|kick a MAC via cmd/stamgr (gated). Validated block+unblock on
dummy 00:00:00:00:00:01 (rc:ok), cleaned up.
(b) REMAINING wlanconf TUNING KNOBS added to apply-wlan: wlan(enable/disable), dtim <band> <n>
(dtim_mode=custom gate), mcast(mcastenhance), bcfilter, rrm(802.11k), ftroam(802.11r, warns),
isolation(l2_isolation), hidessid. dtim apply-validated on 0-client Green Valley (mode default->custom,
reverted). Others via the proven gated REST path.
apply-wlan restructured: SITE resolved before the action case (so aps can map names->macs).
WiFi APPLY SURFACE COMPLETE: apply-radio (radio_table) + apply-wlan (wlanconf, 15 actions) +
client-control (cmd/stamgr), all gated + rollback + validated on 0-client sandboxes / dummy MAC.
SKILL.md + ROADMAP updated. Coord: this msg. NEXT: switches/PoE collector (ROADMAP C).
---
## Update: 2026-06-16 01:13 PT — switches: switch-audit.sh shipped (first non-WiFi collector, ROADMAP C)
NEW scripts/switch-audit.sh <site> [--all-ports] (controller-side, read-only, any site): per switch ->
PoE budget used/total, ports up/total, FLAGS: UNDERSPEED (gig-capable port linked 10/100 = FastEthernet
uplink problem), DROPS (rate-based tx_dropped/tx_packets>2%), ERRORS (>1000), POE-FAULT (poe_enable &
!poe_good), POE-BUDGET (>85%), + OFFLINE switches. port_table fields: speed/speed_caps/poe_*/rx_tx_errors.
VALIDATED on Cascades (12 switches): found ~25 ports linked at 100M but gig-capable (systematic
cabling/NIC issue = the long-suspected FastEthernet uplink problem, now fleet-visible), offline switches
(2nd Floor #2, 4th Floor #2, USW Pro Max 16), PoE budgets healthy, one real 4.1% tx-drop port (p38 on
1st Floor USW). Refined DROPS to rate-based (cumulative counters were noisy).
SKILL coverage now: WiFi (monitor+tune+full apply incl. device lock/client controls) + switch/PoE audit,
all controller-side multi-client + AP-side where cred vaulted. ROADMAP remaining: gateway/WAN/firewall +
adoption collectors. Coord: this msg.
---
## Update: 2026-06-16 01:20 PT — gateway/WAN + site-health collector (gw-audit.sh); ROADMAP C largely done
NEW scripts/gw-audit.sh <site> (controller-side, any site, read-only) via stat/health + gateway device:
WAN status/IP/uplink/duplex + gw CPU/mem/uptime; internet(www) status/latency/drops/last-speedtest;
health rollup (APs/switches/gw adopted vs disconnected vs pending, client user/guest/iot, firmware-
upgradable); flags WAN/internet not-ok, latency>80/drops>5, WAN down, disconnected/pending, gw cpu>85/
mem>90. Third-party-firewall sites (num_gw=0) handled. Fixed: gw_name fallback to device name; drops
threshold >5 (was >0, too sensitive).
Validated: Sonoran Glass (USG 3P [UGW3]) -> all healthy (cpu2%/mem21%, latency 14-24ms); Cascades
(pfSense) -> "no UniFi gateway" + correctly caught 2 disconnected APs + 3 disconnected switches.
SKILL now end-to-end for any client: WiFi (monitor + tune + full apply incl device-lock/client-control),
switch/PoE audit, gateway/WAN/site-health audit, sites.sh discovery. ROADMAP remaining: deeper
firewall/VPN policy + adoption REMEDIATION (adopt/restart), channel-plan apply, per-client AP creds/VPN.
Coord: this msg.
---
## Update: 2026-06-16 07:26 PT — adoption remediation (device-control.sh) + channel-plan apply (channel-plan.sh)
NEW device-control.sh <site> adopt|restart|provision|locate|unlocate|upgrade <mac> [--apply] (cmd/devmgr,
gated, controller-side) — remediates what gw-audit flags. Validated locate+unlocate on AP 622 (rc:ok).
NEW channel-plan.sh <site> ng|na [--apply] (NEIGHBOR_JSON + SURVEY_JSON):
ng: graph-color 1/6/11 via the neighbor matrix (strong neighbors differ).
na: per-AP lowest-cost NON-DFS channel = measured busy% (survey) + neighbor-collision penalty.
Reports co-channel pairs before/after; gated apply per-AP rest/device + rollback.
DRY-RUN validated on Cascades: ng 92->35 co-channel pairs (51 APs change); na 20->0 + ALL moved off
DFS onto non-DFS (radar-safe) (71 APs). Not applied (production paused); PUT mechanism proven.
Added SURVEY_JSON=<path> emit to survey-collect.sh ({ap:{band:{channel:busy%}}}).
SKILL now: WiFi (monitor+tune+full apply+device-lock+client/device control+channel-plan) + switch/PoE
audit + gateway/WAN/site-health + multi-client. ROADMAP nearly clear (deeper firewall/VPN policy +
per-client AP creds/VPN remain). Coord: this msg.
---
## Update: 2026-06-16 07:44 PT — robustness (ROADMAP D): monitor-run.sh + per-AP retry; gw-audit pfSense fix
Synced first to pick up Mike's gw-control.sh (eb87710, firewall/port-forward router actions — the
"deeper firewall/VPN policy" item; no dup with my robustness work).
NEW scripts/monitor-run.sh <site|all> — cron-friendly controller-side read-only fleet health digest:
per site -> gateway/WAN flags + switch/PoE flags + WiFi config flag count. Validated Sonoran (healthy)
+ Cascades (flags 2 disc APs / 3 disc switches / underspeed / firmware). Cron 'all' nightly.
VPN-flap resilience: neighbor-collect / survey-collect / dfs-check now RETRY per AP (3x, capture-to-var
so a failed attempt never appends partial data; dfs-check distinguishes UNREACHABLE vs no-events).
Validated neighbor-collect end-to-end (reachable 74/74, redundancy 73/74, JSON 74 APs - identical).
Fix: gw-audit no longer false-flags internet status=unknown on third-party-firewall sites (gated on num_gw).
SKILL.md + ROADMAP updated (D items done). Skill is feature-complete for monitoring+tuning+apply across
WiFi/switch/gateway, multi-client, with scheduling + flap resilience. Remaining: per-client AP creds/VPN,
read-only cred (Mike to create UI admin), gateway VPN-server/DHCP-DNS (Mike). Coord: this msg.
---
## Update: 2026-06-16 13:12 PT — FIRST PRODUCTION CHANGE: Floor-4 2.4 power-down pilot + 445 outage/recovery learning
Baseline captured (Floor-4 2.4: 15 radios, avg cu_total 86%, avg retry 13.2%; AP 434 ground-truth 94%
busy / hears 48 managed APs on 2.4). Applied: `apply-radio cascades ng power low --zone "Floor 4" --apply`.
RESULT: 14/15 radios -> 6dBm (from ~23), clients retained (118 vs ~119) - no coverage loss. Rollback saved.
INCIDENT: AP 445 lagged (config=low but radio stayed 23dBm). Tried device-control 'provision'
(cmd/devmgr force-provision) to nudge -> HTTP 400 AND it took 445 fully OFFLINE; Howard physically
power-cycled the switch port to recover. 445 back online (booted to 30dBm; config still low, not converged
- left alone, harmless).
FIX (Howard's directive - power-cycle the port is the recovery):
- REMOVED 'provision'/force-provision from device-control.sh (unsafe on U7-Pro; refuses now).
- ADDED 'poe-cycle <ap-name|mac>' - resolves AP uplink_mac + uplink_remote_port, POSTs cmd/devmgr
power-cycle {mac:switch, port_idx} = remote PoE port cycle (the controller equivalent of re-seating the
cable). Dry-run validated (445->Switch 4th Floor p7, 434->p5). errorlog.md updated. Coord -> Mike.
EXPECTATION SET: 2.4 cu_total won't plummet (channel is foreign-dominated, ~33k BSSIDs); power-down's win
is cell-shrink + reduced SELF-interference + better client SNR, seen in retry%/consolidation over ~10-15
min, and it sets up the Phase-C disables. Next: settle ~15 min, re-snapshot Floor-4 retry% for before/after;
decide on 445 (poe-cycle to apply low, or leave). Disables (445/428) still HELD.
---
## Update: 2026-06-16 13:30 PT — Floor-4 pilot RESULT (15-min settle) + pfSense-access review + VPN flap
FLOOR-4 power-down before/after (controller live-stats, via Tailscale — unaffected by the Cascades VPN flap):
avg 2.4 retry%: 13.2 -> 9.5 (~28% fewer retransmits); avg cu_total 86 -> 83% (small, channel is
foreign-dominated as expected); clients RETAINED (no coverage loss). Big per-AP retry wins: 407 21->0,
4th Floor Atrium 27->0, 434 22->7; a few bounced up (403/428/421) = instantaneous-snapshot jitter as
clients re-associate. Caveat: single 15-min snapshot is noisy; direction is positive + safe. Pilot = success.
(445 still at 30dBm not converged — left alone.)
PFSENSE-ACCESS REVIEW (Howard asked, given pfSense+UniFi access): KEY GAP — Cascades' gateway/WAN/DHCP/
DNS/firewall is 100% pfSense; gw-audit/gw-control are UniFi-only (num_gw=0) so we've audited NONE of it.
We have pfSense creds but NO path from Howard-Home: Cascades 192.168.0.0/24 (pfSense .0.1, NAS .0.120) is
shadowed by Howard's HOME UniFi network (also 192.168.0.0/24; .0.1 = home UniFi console cert unifi.local).
APs/CS-SERVER (.2.x/.3.x) work via the /22 tunnel route. Likely hidden non-RF contributors to "bad for
some users": DHCP pool exhaustion (per-room /28 VLANs + 587 clients), DNS latency, dual-WAN (Cox Fiber+Coax)
health/failover, inter-VLAN firewall. RF plan stays valid but don't declare "fixed" on RF alone.
THE MISS (skill): gateway verbs are UniFi-REST only; our real topology is UniFi-behind-pfSense (Cascades,
office, several clients) = Mike's ROADMAP §E pfSense compat layer = highest-value next build.
/32 ROUTE: added 192.168.0.1/32 via tunnel (192.168.10.1) to reach Cascades pfSense; REMOVED it again
when the VPN started flapping (don't tie home gateway IP to an unstable tunnel). pfSense untouched throughout.
VPN FLAP: OpenVPN Connect DCO instability (DCO adapter Disconnected, TAP up, 4 OpenVPNConnect procs) -
the same DCO/TAP issue seen earlier. Fix: disable DCO in OpenVPN Connect (force TAP datapath) OR switch to
community OpenVPN GUI; also check duplicate-CN (cert connected from 2 devices = connect/disconnect loop).
## Update: 13:42 PT (2026-06-16) — pfSense compat layer: started, backed out, aligned to Mike's design
Picked up "start the pfSense compat layer." Began a REST-API-backed `pfsense-audit.sh` **sibling**
scaffold + a `references/pfsense-driver.md` design doc. On re-reading the repo, corrected course:
- **What Mike actually built vs proposed.** `gw-control.sh` (commit `eb87710`) is **UniFi** gateway
control — it calls `/proxy/network/api/...` (the UniFi controller REST), validated on Grabb's
**USG-3P**. It does NOT touch pfSense. Commit `6998719` is a **roadmap entry** — ROADMAP §E
"pfSense gateway compatibility layer," a design with explicit **OPEN DECISIONS**, not built code.
So Mike *designed* the pfSense layer (with questions to resolve first); he did not build it.
- **I had pre-empted his open decisions.** §E leaves two open: (1) backend = REST API package vs
stock SSH `easyrule`/`pfSsh.php`; (2) where the abstraction lives = dispatch **inside**
`gw-audit.sh`/`gw-control.sh` (keyed on `num_gw=0`) vs sibling `pf-*` scripts — Mike leans "inside."
My scaffold unilaterally picked REST + a sibling script (the opposite structure).
- **Backed it out.** Removed `pfsense-audit.sh` + `pfsense-driver.md` (never committed/pushed) rather
than fork Mike's design. Released coord lock `a34360aa`. Messaged Mike (GURU-5070, id `e402e783`)
with the two open decisions and my lean.
- **Howard's decision:** "let us try what mike wanted and go that way unless we run into a real
roadblock we can't work past." → **REST API package backend, dispatched INSIDE
`gw-audit.sh`/`gw-control.sh`** (Mike's option 1 + his structural lean). Build to that next.
**Net for this update:** no code shipped; design alignment + coordination only. The WiFi/RF work
(Floor-4 pilot applied + held, Phase-C disables held, steering/min-rate/channel-plan dry-run-validated)
is unchanged from earlier in this log.
### Next steps (pfSense layer, per Howard + Mike)
1. Per-site setup helper: install `pfSense-pkg-RESTAPI` + create a read-only key + vault at
`clients/<slug>/pfsense-api` (url+apikey). Cascades + ACG office have pfSense web creds vaulted
already (`clients/cascades-tucson/pfsense-firewall`, `infrastructure/pfsense-firewall`) — need the
API key added.
2. Add a pfSense backend **inside** `gw-audit.sh` (dispatch on `num_gw=0`): WAN/gateway status via
`/api/v2/status/gateways`, system, DHCP pool pressure. Then `gw-control.sh` verbs (pf-list/disable/
enable/set-ports, fw-disable/enable, block-ips) onto `firewall/nat/port_forward` + `firewall/rule`
+ `easyrule`-equivalent.
3. Live validation pending a reachable pfSense (stable site VPN; mind the home-LAN .0.x shadow that
currently masks Cascades pfSense from Howard-Home). Coordinate the build with Mike (his §E).
## Update: 14:30 PT (2026-06-16) — pfSense compat layer built, then put ON HOLD (pfSense too old)
Built the pfSense gateway compatibility layer per Howard's decision (REST API package backend, dispatch
INSIDE the existing gateway verbs — Mike's §E lean):
- NEW `scripts/pfsense-backend.sh` — pfSense REST API driver (`pfSense-pkg-RESTAPI` v2, `X-API-Key`),
same verbs as gw-control (audit, pf-list/disable/enable/delete/set-ports, fw-list/disable/enable,
block-ips) + a `setup` helper. Writes `--apply`-gated, per-object rollback to `.claude/tmp/`, calls
`firewall/apply`.
- `gw-audit.sh` + `gw-control.sh` auto-dispatch to it when `num_gw=0` AND a `clients/<slug>/pfsense-api`
cred is vaulted (or `--pfsense <slug>`). Minimal diff to Mike's files (he edits them concurrently).
- SKILL.md `[PROPOSED]`->`[SCAFFOLDED]`; ROADMAP §E open decisions marked resolved. Committed `1118594`,
lock released, Mike notified (msg `f0c442cf`).
- Tested: syntax + setup/BLOCKED/no-cred paths + live `gw-audit cascades` (num_gw=0 -> pfSense hint,
gating correct). NOT live-tested: the actual REST calls.
**BLOCKER / ON HOLD (Howard):** the RESTAPI package is third-party and the **Cascades pfSense is too old
to install it** — it needs a **firmware upgrade first**. Live work is ON HOLD until the pfSense is upgraded.
Tried to vault the API key but none exists yet (package not installed); pfSense host = `192.168.0.1`
(web cred already at `clients/cascades-tucson/pfsense-firewall`). Also note `192.168.0.1` is shadowed from
Howard-Home by the home UniFi `.0.0/24` overlap, so first live validation must run from/through Cascades.
Filed coord todo (project unifi-wifi): upgrade Cascades pfSense -> install RESTAPI -> mint key -> vault
`clients/cascades-tucson/pfsense-api` -> first live `gw-audit cascades`. Resume trigger + steps in ROADMAP §E.
## Update: 17:02 PT (2026-06-16) — unifi-wifi skill health pass + 2.4 usage/coverage tools + tonight's runbook
### Skill health pass
Verified all 19 unifi-wifi scripts. Controller-side read-only (sites, audit-site, switch-audit, live-stats,
model-rank, optimize-radios, monitor-run, gw-audit) validated LIVE on Cascades; dry-run apply (apply-radio,
apply-wlan, client-control, device-control) correct; AP-side mechanism (SSH + /proc/ui_neighbor) confirmed +
full neighbor-collect (74-AP SNR sweep) -> channel-plan end-to-end. Fixes: optimize-radios stray mongo REPL
echo (`for(k in prof)` numeric completion -> `void 0`); ASCII-cleaned printed output (em-dash/arrow/§ that
rendered as mojibake on Windows console) across 10 scripts. Commit e1031ae.
### Question chain: "can we see 2.4 device history -> which 2.4 radios to disable"
1. radio-usage.sh (NEW, commit 659c3a2): per-AP 2.4 client history from ace_stat.stat_daily (~77d) — time-avg
concurrent users (`<radio>-num_sta_avg`) + peak snapshot (`ng-num_sta`). Finding: only AP 108 (offline)
has peak==0 over 77d; every other 2.4 radio takes real bursts at low avg -> confirms POWER-DOWN not disable.
NEIGHBOR_JSON adds a safe-to-disable shortlist.
2. radio-usage.sh --ap mode (commit 80384a7): per-DEVICE — devices on an AP's 2.4 (live stat/sta + recent
assoc events) tagged steerable (dual-band / randomized MAC) vs legacy/IoT (fixed OUI). Data finding:
wifi_connectivity_event retention on the UOS is ~1 DAY (current+recent, not long history); MAC-randomization
heuristic fills the gap.
3. coverage-thin.sh (NEW, commit f936224 + hint fix + mesh fix): the real deliverable — "which 2.4 radios to
disable given over-coverage, by AP proximity." Greedy dominating-set on the 2.4 SNR layer; only ACTIVE-2.4
neighbors count as coverage. Exposed + avoided an optimize-radios flaw (band-AGNOSTIC adjacency counted the
ng-DISABLED AP 128 as 'coverer' for 127/229/330/428 -> would be 2.4 holes; flagged to Mike).
### Howard caught two hazards (pre-execution review) -> fixes
- MESH: 2nd Floor Atrium is the wireless-mesh PARENT for CC Bridge + salon (backhaul ch36/5GHz); 206 U7 Pro
carries 108. coverage-thin was mesh-blind and had listed 2nd Floor Atrium/CC Bridge/206 as disable targets.
FIX: coverage-thin now fetches live uplink topology and auto-excludes wireless-mesh APs + their parents
(108, 206 U7 Pro, 2nd Floor Atrium, CC Bridge, salon). resilient plan 34->33.
- SSID/device pinning: verified NONE — CSC ENT/CSCNet/Guest broadcast on all APs (broadcasting_aps off), and
UniFi doesn't lock clients to an AP -> disabling a radio orphans nothing; clients roam. No pinning risk.
- Also fixed coverage-thin apply HINT: was emitting `apply-radio ng disable --zone "<floor>"` (would disable
a WHOLE floor) -> now per `--ap "<name>"`.
### Tonight's runbook (committed 8c048c3)
clients/cascades-tucson/reports/2026-06-16-2.4ghz-remediation-runbook.md. Phases: 0 pre-flight+baseline;
1 power-down all non-mesh 2.4 -> Low (per-zone clean floors, per-AP on mesh-containing Floor 2/misc; re-disable
128 after Floor 1); 2 settle+re-measure (valid stop point); 3 disable 25 mesh-safe low-client radios per floor
with gates (F1:7 F2:8 F3:5 F4:5); 4 wrap. Excluded: 7 high-client redundant (power-down instead: 3rd/4th
Atrium,318,303,406-408,505,Dining Room); Memcare TV room (memcare hold); companion levers (min-rate/band-steer/
min-RSSI) deferred. Recovery: device-control poe-cycle (never force-provision). Rollback JSONs in .claude/tmp.
### Open decision (awaiting Howard)
Scope for tonight: recommended = power-down-all + pilot ONE floor's disables, validate ~15min, expand if clean
(vs all 25 at once). And whether I stay live during execution to run the per-floor validation gates.
### Note: mesh data is from .claude/tmp/cascades-nbr.json (fresh tonight, gitignored) — regenerate via
neighbor-collect before execution if stale.
## Update: 19:28 PT (2026-06-16) — home LAN renumber fixes VPN shadow; pfSense ruled out as WiFi cause; SSH backend
### Home-LAN shadow RESOLVED (unblocked Cascades pfSense access)
Diagnosed why the Cascades VPN couldn't reach 192.168.0.x: Howard-Home was 192.168.0.0/24 with a UniFi
gateway at 192.168.0.1 (cert unifi.local — NOT pfSense), colliding with Cascades' own 192.168.0.0/24
(pfSense .0.1). OS prefers the directly-connected local /24, so 192.168.0.x never crossed the tunnel
(APs on 192.168.2.x/3.x worked). A /32 route can't fix it (.0.1 was Howard's own gateway). **Howard
renumbered Howard-Home to 10.137.42.0/24** (gw 10.137.42.1). Verified: ping 192.168.0.1 now replies over
the tunnel (TTL 64, ~15ms); cert = CN=pfSense-685f277aa6886 / "Netgate pfSense Plus" (the REAL Cascades
pfSense); NAS .120 reachable; controller-side (172.16.3.29) + AP reach (192.168.2.79) intact. Memory:
[[howard-home-lan-shadow]] updated to RESOLVED.
### pfSense investigated — NOT a WiFi factor (gateway ruled out)
SSH into Cascades pfSense (admin, shell works directly — no menu gotcha; pfSense Plus 25.07-RELEASE).
Findings: DHCP NOT exhausted (0 "no free leases"; WiFi/AP pool 192.168.0.0/22 range .2.2-.3.254 cap ~507
at 270 active ~53%; 199 subnets total, mostly per-unit /28s; active backend ISC, Kea dormant); unbound DNS
up; dual-WAN (WAN1 184.191.143.62/30, WAN2 72.211.21.217/27) both full-duplex, no gateway loss/down events;
PF states 28-31k/790k; load 0.6; 10-day uptime. Minor: igc3/WAN2 1707 Ierrs+Coll = Intel I225/226 2.5G
counter quirk (2.5G full-duplex active, no loss), not a fault. CONCLUSION: gateway/DHCP/DNS/WAN are not
bottlenecking WiFi — the 2.4 GHz RF remediation is the sole fix. Added "pfSense health check (2026-06-16)"
section to clients/cascades-tucson/reports/2026-06-16-unifi-full-audit.md.
### pfSense compat layer: pivot to SSH backend, OFF HOLD (Mike's call)
Mike (relayed by Howard): "we don't need the RESTAPI — VPN + SSH gets the same data and makes changes."
The earlier "pfSense too old for RESTAPI" premise was WRONG (it's 25.07, current). So the upgrade/package
blocker is moot; layer off hold. Built scripts/pfsense-ssh.sh (audit | dhcp | run "<cmd>"); cred from
clients/<slug>/pfsense-firewall; system OpenSSH via askpass; validated live on Cascades (the health data
above came from it). ROADMAP §E + SKILL.md updated to the SSH decision; REST pfsense-backend.sh left
dormant/optional. Closed obsolete coord todo afbe6a22 (upgrade-for-RESTAPI). Remaining = named gated
CONTROL verbs over SSH (easyrule block-ips, pf/fw toggles) — Howard: "leave that up to Mike" (his §E).
Commits: 58ecc5a (pfSense-ssh + health + off-hold), e42ad8f (memory).
### Still open (the actual WiFi win)
Tonight's 2.4 remediation per the runbook (reports/2026-06-16-2.4ghz-remediation-runbook.md): scope
(power-down-all + pilot one floor vs all 25) + whether Claude runs validation gates live during execution.
## Update: 23:53 PT (2026-06-16) — 2.4 remediation EXECUTED (power-down + partial disables); PAUSED before Floor 2
Howard rebooted the Cascades pfSense, then gave the go to push the 2.4 changes. (Changes go through the
UOS controller 172.16.3.29, NOT the pfSense/VPN — reboot didn't block them. Verified fleet healthy
post-reboot: 77 adopted / 2 disconnected, unchanged.)
### Phase 1 — 2.4 power-down to Low: COMPLETE (65 radios)
Applied auto->low across Floors 1,2,3 + misc (Floor 4 was already done). Floor 3 by-zone (17); Floors 1/2
+ misc PER-AP to skip mesh + offline + 128. Final ng state: low=65, auto=11 (correctly left: mesh parents
2nd Floor Atrium/206 U7 Pro, mesh children CC Bridge/salon, offline 108/108U7 Pro, EXCLUDED Floors 5/6 =
505/517/608/615/622), disabled=1 (128). 0 failures, no AP knocked offline.
Initially missed 247 (Floor 2) and 4 Floor-1 stragglers (121/122/109/102) — see Problems; all fixed.
### Phase 2 — re-measure (15-min settle): the cumulative WIN
Site-wide 2.4 averages (live-stats, ~74 radios):
PRE power-down: cu_total 77% cu_interf 64% retry 12.2% clients 134
POST settle: cu_total 68% cu_interf 53% retry 13.8% clients 111
=> cu_interf -11 pts, cu_total -9 pts site-wide (vs only ~4 pts from the Floor-4-only test earlier ->
confirms the benefit is cumulative/site-wide). Retry +1.6 is midnight client-mix noise (fewer clients).
### Phase 3 — disables: 15 of 23 done, PAUSED before Floor 2
Refreshed coverage-thin (MINCOV=2, mesh-aware) -> conservative set = 23 (low-client <2 avg, >=2 active
coverers, non-mesh, non-memcare, Floors 1-4). Disabled (apply-radio ng disable --ap, spaced):
Floor 4: 445 442 407 450 Floor 3: 342 317 304 336 Floor 1: 139 115 102 145 127 122 116
REMAINING (Howard said "lets pause"): **Floor 2: 240 229 241 209 203 248 204 236** (8 APs).
Fleet after disables: 77 adopted / 2 disconnected — disabling 2.4 just drops that radio; AP stays up on
5/6 GHz, every disabled radio's area keeps >=2 active 2.4 coverers. The force-provision hazard does NOT
apply to `disable` (clean radio-off, unlike the 445 force-provision incident).
### Other this turn
- apply-wlan.sh BUG FIXED: wlan_bands token was "6e" but THIS controller stores "6g" (verified live on
Guest SSID) -> 6 GHz enablement would have failed. Fixed (5g6g/6g/all). Commit 5ad25d1.
- Runbook: folded in Phase 5 (5 GHz: 80->40 width on 76 radios + channel plan with DFS decision flagged --
DFS empirically CLEAN here so including clean-DFS gives ~20 ch vs ~5 non-DFS-only) and Phase 6 (6 GHz:
ROOT CAUSE 6 GHz empty = production SSID CSCNet not on 6 GHz [bands 2g,5g]; add 6g + bss-transition).
- Floors 5 & 6 EXCLUDED from the whole plan (Howard).
### RESUME / next
1. Finish Phase 3: disable Floor 2 (240 229 241 209 203 248 204 236), spaced, validate.
2. Proper site-wide re-measure after settle (compare cu_interf again).
3. Phase 5 (5 GHz width+channel) + Phase 6 (6 GHz CSCNet) when ready.
ROLLBACK: re-enable a radio = apply-radio cascades ng enable --ap "<name>" --apply; restore power =
apply-radio cascades ng power auto --zone "Floor X" --apply. (apply-radio rollback JSON is per-call in
.claude/tmp, overwritten each call -> for power-down revert just set power auto.)
### Friction logged
apply-radio per-AP --apply loop: each call re-logs-in to the controller; rapid succession throttles and
the write silently skips (no [ok]). Fix: space calls (sleep ~4) or add multi-AP/one-login support.
## Update: 00:30 PT (2026-06-17) — Phase 3 disables COMPLETE (all 23 + 128); daytime re-check scheduled
Finished Floor 2 disables (240 229 241 209 203 248 204 236). Final 2.4 state: **disabled=24, low=42, auto=11**
(auto = mesh parents/children 2nd Floor Atrium/206/CC Bridge/salon + offline 108/108U7Pro + EXCLUDED Floors
5/6 = 505/517/608/615/622). Fleet 77 adopted / 2 disconnected — no AP offline.
**Experienced improvement so far (midnight, low load — directional):** active-radio-only site-wide 2.4
cu_interf **64% -> 48%** (-16 pts, ~25% rel), cu_total 77% -> 66%. (NOTE: averaging ALL ng radios incl.
the 24 disabled-reporting-0 misleadingly showed cu 45%/interf 33% -> always exclude disabled for a fair
read.) Hourly time-of-day-controlled (same hr last night): retry 27.8->26.0, sat ~flat. Real client-
experience delta needs BUSINESS-HOURS load -> daytime re-check scheduled.
Daytime re-check: CronCreate one-shot 9:33 AM 6/17 (reported session-only — NOT durable) + coord todo
(reliable backstop) filed for Howard. Remaining: Phase 5 (5GHz 80->40 + channel) + Phase 6 (6GHz: add 6g
to CSCNet + bss-tm). apply-wlan 6e->6g fix already committed (5ad25d1).
## Update: 16:00 PT (2026-06-17) — daytime re-checks: interference DOWN but client experience WORSE (over-thinning) -> Low->Medium recommended. (Howard continuing in another session.)
Two loaded-network re-checks (09:33 + 11:27, time-of-day controlled vs same hours yesterday at full power)
both say the SAME thing: the 2.4 remediation cleaned too aggressive.
metric (2.4 site-wide, hourly) yesterday FULL now POST-change
interference cu_interf 65-66% 32% <- WIN (down ~half)
total cu_total 79% 44% <- WIN
retry% 17-18.5% 23-32% <- WORSE (roughly doubled)
satisfaction 39 30-31 <- WORSE
Active-radio-only snapshot: cu_interf ~48% (vs 64 pre), retry ~22%. Fleet: 77 adopted / 2 disconnected,
NO AP offline.
DIAGNOSIS: OVER-THINNING. Power to Low (~6 dBm) is a 17 dB cut from auto(~23); combined with 24 disabled
radios, some edge 2.4 clients now reach a farther/weaker AP -> retries up, satisfaction down EVEN THOUGH
the air is much cleaner. The interference goal is met; the lever overshot on coverage.
>>> CURRENT DECISION POINT (next action, NOT yet done):
1. Bump the 42 KEPT 2.4 radios Low -> MEDIUM (~12-15 dBm): restores edge signal, cells still tighter than
full, and the 24 disables STAY OFF so most of the interference win is kept. Command per floor (skip 5/6,
skip mesh/offline like the power-down was):
bash .claude/skills/unifi-wifi/scripts/apply-radio.sh cascades ng power medium --zone "Floor 3" --apply
(Floor 1/2/misc per-AP, spaced ~4s, to skip mesh parents + 128/108 + login throttle)
2. Re-measure retry/satisfaction (same time-of-day) after the bump to confirm recovery.
3. If still high, re-enable a few of the 24 disabled radios in the weakest spots.
STATE RECAP for the other session: Phase1 power-down DONE (was Low on 65; recommend ->Medium on the kept
42). Phase3 disables DONE (24 off: 102 115 116 122 127 128 139 145 203 204 209 229 236 240 241 248 304 317
336 342 407 442 445 450). Floors 5/6 + mesh (2nd Floor Atrium/206/CC Bridge/salon) + offline 108/108U7Pro
UNTOUCHED. Phase 5 (5GHz 80->40 + channel) and Phase 6 (6GHz: add 6g to CSCNet + bss-tm; apply-wlan 6e->6g
already fixed) still PENDING. Runbook: reports/2026-06-16-2.4ghz-remediation-runbook.md. Rollback: ng power
auto / ng enable --ap. Friction logged: per-AP apply re-logins -> space calls.