From ae3d760c3702e5d599400e017875aa49963f13ff Mon Sep 17 00:00:00 2001 From: Howard Enos Date: Tue, 16 Jun 2026 00:24:27 -0700 Subject: [PATCH] sync: auto-sync from HOWARD-HOME at 2026-06-16 00:24:18 Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-16 00:24:18 --- .claude/skills/unifi-wifi/SKILL.md | 11 +++++--- .../skills/unifi-wifi/references/ROADMAP.md | 8 +++--- .../skills/unifi-wifi/scripts/apply-radio.sh | 27 ++++++++++++------- ...026-06-15-howard-cascades-wifi-rf-audit.md | 17 ++++++++++++ 4 files changed, 47 insertions(+), 16 deletions(-) diff --git a/.claude/skills/unifi-wifi/SKILL.md b/.claude/skills/unifi-wifi/SKILL.md index 67262f7..c6b5f9a 100644 --- a/.claude/skills/unifi-wifi/SKILL.md +++ b/.claude/skills/unifi-wifi/SKILL.md @@ -117,15 +117,20 @@ bash .../apply-radio.sh power low|medium|high|auto| [- bash .../apply-radio.sh width 20|40|80|160 [--zone Z] [--apply] bash .../apply-radio.sh channel |auto [--zone Z] [--apply] bash .../apply-radio.sh minrssi off|on|- [--zone Z] [--apply] +bash .../apply-radio.sh disable # radio OFF (tx_power_mode=disabled) [--ap NAME] [--apply] +bash .../apply-radio.sh enable # radio ON (tx_power_mode=auto) [--ap NAME] [--apply] ``` +`--ap ""` targets a single AP (the right scope for disables — execute optimize-radios' disable +list one AP at a time: `optimize-radios ... ` -> for each, `apply-radio ng disable --ap --apply`). Dry-run (default) prints per-AP before->after + rollback values + the REST payload. **Writes are GATED OFF** until (1) `infrastructure/uos-server-network-api-rw` is vaulted (the root SSH key is the data plane, NOT an API write session) and (2) `--apply` is passed. Even then: rollback is auto-saved to `.claude/tmp/apply-rollback-*.json`, go **one `--zone` at a time**, validate live with `watch-ap.sh` **before and after**, and never auto channel-optimize in ultra-dense sites. WRITE PATH VALIDATED -2026-06-16 (apply->verify->revert on 0-client 6 GHz radios). **`disable` a radio is NOT implemented** -— there is no `radio_table` enable field; the mechanism is unconfirmed (see references/ROADMAP.md A). -**min-data-rate / band-steering** live in `wlanconf` (not radio_table) — separate future apply path. +2026-06-16 (apply->verify->revert + full disable/enable cycle on 0-client 6 GHz radios). **Radio +disable IS implemented** = `tx_power_mode:"disabled"` (confirmed via UI-toggle + device-JSON diff); +`enable` sets it back to `auto`. **min-data-rate / band-steering** live in `wlanconf` (not radio_table) +— separate future apply path (see references/ROADMAP.md). Get explicit go before any write. Full roadmap: **references/ROADMAP.md**. ## Roadmap diff --git a/.claude/skills/unifi-wifi/references/ROADMAP.md b/.claude/skills/unifi-wifi/references/ROADMAP.md index cd0d527..27ac9b2 100644 --- a/.claude/skills/unifi-wifi/references/ROADMAP.md +++ b/.claude/skills/unifi-wifi/references/ROADMAP.md @@ -22,10 +22,10 @@ side, multi-client enablement, and non-WiFi scope. Build/validate new apply acti - [x] apply-radio: width (ht 20/40/80/160) - [x] apply-radio: channel (manual channel assignment) - [x] apply-radio: min-RSSI (min_rssi_enabled + min_rssi) -- [ ] **apply-radio: disable a radio** — NO `enabled` field in `radio_table`; the disable mechanism is - unconfirmed (likely an AP-level/`vap` setting or a field that only appears once set). Discover by - toggling a radio in the UI and diffing the device JSON before/after, then implement. Highest-risk - action (coverage holes) — keep gated + per-zone + on-site validation. +- [x] **apply-radio: disable/enable a radio** — DONE. Disable = `tx_power_mode:"disabled"` (confirmed + 2026-06-16 via UI-toggle + device-JSON diff); enable = `auto`. Validated full cycle on a 0-client + 6 GHz radio. Use `--ap ` to target one AP (the right scope for disables). Still highest-risk + (coverage holes) — gate + validate per AP with watch-ap; pair with optimize-radios' disable list. - [ ] **min data rates** (kill 1–11 Mbps; 2.4 floor 12/24) and **band-steering / 6 GHz steer** — these live in **`wlanconf`** (WLAN object), NOT `radio_table`; they affect every AP on the WLAN. Separate apply path (`apply-wlan.sh`), more blast radius — design carefully. diff --git a/.claude/skills/unifi-wifi/scripts/apply-radio.sh b/.claude/skills/unifi-wifi/scripts/apply-radio.sh index ed6a156..1dbe1f9 100644 --- a/.claude/skills/unifi-wifi/scripts/apply-radio.sh +++ b/.claude/skills/unifi-wifi/scripts/apply-radio.sh @@ -15,7 +15,9 @@ # width 20|40|80|160 -> ht (channel width) # channel |auto -> channel # minrssi off|on|- -> min_rssi_enabled (+ min_rssi if a dBm number) -# (disable a radio is NOT here — no radio_table enable field; see references/ROADMAP.md) +# disable -> tx_power_mode=disabled (turn the radio OFF) +# enable -> tx_power_mode=auto (turn it back ON) +# (radio disable == tx_power_mode "disabled"; confirmed 2026-06-16 by UI-toggle + device-JSON diff) # Examples: # apply-radio.sh cascades na width 40 # preview: 5GHz -> 40MHz everywhere # apply-radio.sh cascades na width 40 --zone "Floor 4" --apply @@ -24,10 +26,13 @@ set -euo pipefail REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)" UOS="$REPO/.claude/scripts/uos-mongo.sh"; VAULT="$REPO/.claude/scripts/vault.sh" -SITEARG="${1:?usage: apply-radio.sh [--zone Z] [--apply]}" -BAND="${2:?band ng|na|6e}"; ACT="${3:?action: power|width|channel|minrssi}"; VAL="${4:?value}" -ZONE=""; APPLY=0; shift 4 || true -while [ $# -gt 0 ]; do case "$1" in --zone) ZONE="$2"; shift 2;; --apply) APPLY=1; shift;; *) shift;; esac; done +SITEARG="${1:?usage: apply-radio.sh [value] [--zone Z] [--ap NAME] [--apply]}" +BAND="${2:?band ng|na|6e}"; ACT="${3:?action: power|width|channel|minrssi|disable|enable}" +shift 3 +VAL="" # disable/enable take no value; the others consume the next positional as the value +case "$ACT" in power|width|channel|minrssi) VAL="${1:-}"; shift || true;; esac +ZONE=""; APN=""; APPLY=0 +while [ $# -gt 0 ]; do case "$1" in --zone) ZONE="$2"; shift 2;; --ap) APN="$2"; shift 2;; --apply) APPLY=1; shift;; *) shift;; esac; done case "$BAND" in ng|na|6e) ;; *) echo "band must be ng|na|6e"; exit 1;; esac # action+value -> the radio_table fields to set (compact JSON, used by both the preview JS and apply python) @@ -42,13 +47,15 @@ case "$ACT" in on) FIELDS="{\"min_rssi_enabled\":true}";; -[0-9]*) FIELDS="{\"min_rssi_enabled\":true,\"min_rssi\":$VAL}";; *) echo "minrssi: off|on|-"; exit 1;; esac ;; - *) echo "action must be power|width|channel|minrssi"; exit 1;; + disable) FIELDS="{\"tx_power_mode\":\"disabled\"}";; # turn the radio OFF (confirmed via UI diff 2026-06-16) + enable) FIELDS="{\"tx_power_mode\":\"auto\"}";; # turn it back ON (auto power) + *) echo "action must be power|width|channel|minrssi|disable|enable"; exit 1;; esac if [[ "$SITEARG" =~ ^[0-9a-f]{24}$ ]]; then SITE="$SITEARG"; else SITE="$(bash "$UOS" --sites 2>/dev/null | grep -vi 'pq.html' | grep -i "$SITEARG" | awk '{print $1}' | head -1)"; fi [ -n "$SITE" ] || { echo "[ERROR] site not found"; exit 1; } -echo "[INFO] site=$SITE band=$BAND set $FIELDS${ZONE:+ zone='$ZONE'} mode=$([ $APPLY = 1 ] && echo APPLY || echo DRY-RUN)" +echo "[INFO] site=$SITE band=$BAND set $FIELDS${ZONE:+ zone='$ZONE'}${APN:+ ap='$APN'} mode=$([ $APPLY = 1 ] && echo APPLY || echo DRY-RUN)" # ---- DRY-RUN preview: compare target fields vs current radio_table (Mongo) ---- cat <&1 | grep -viE 'pq.html|post-quantum|store now|server may need' @@ -57,6 +64,7 @@ function zoneOf(n){var fz=String(n||'').match(/(\d)(?:st|nd|rd|th)\s*floor/i),rm var n=0,skip=0; db.device.find({site_id:SITE,type:'uap'},{name:1,radio_table:1}).forEach(function(a){ if('$ZONE' && zoneOf(a.name)!=='$ZONE') return; + if('$APN' && (a.name||'')!=='$APN') return; (a.radio_table||[]).forEach(function(r){ if(r.radio!==BAND) return; var change=false,roll={}; for(var f in FIELDS){ roll[f]=(r[f]!==undefined?r[f]:null); if(String(r[f])!==String(FIELDS[f])) change=true; } @@ -88,7 +96,7 @@ EOF exit 2 fi -export AR_SITE="$SITE" AR_BAND="$BAND" AR_FIELDS="$FIELDS" AR_ZONE="$ZONE" REPO +export AR_SITE="$SITE" AR_BAND="$BAND" AR_FIELDS="$FIELDS" AR_ZONE="$ZONE" AR_AP="$APN" REPO python - <<'PY' import os,sys,json,ssl,urllib.request,http.cookiejar H="172.16.3.29";PORT=11443;base=f"https://{H}:{PORT}" @@ -106,7 +114,7 @@ csrf=hd.get('X-CSRF-Token') or hd.get('X-Updated-Csrf-Token') sites=json.loads(call('GET','/proxy/network/api/self/sites')).get('data',[]) short=next((s['name'] for s in sites if s.get('_id')==os.environ['AR_SITE']),None) if not short:print("[ERROR] site resolve failed");sys.exit(1) -band=os.environ['AR_BAND'];zone=os.environ['AR_ZONE'];fields=json.loads(os.environ['AR_FIELDS']) +band=os.environ['AR_BAND'];zone=os.environ['AR_ZONE'];apn=os.environ.get('AR_AP','');fields=json.loads(os.environ['AR_FIELDS']) def zof(n): import re;n=n or '' m=re.search(r'(\d)(?:st|nd|rd|th)\s*floor',n,re.I) or re.search(r'\b(\d)\d{2}\b',n) @@ -116,6 +124,7 @@ roll=[];done=0;fail=0 for d in devs: if d.get('type')!='uap':continue if zone and zof(d.get('name'))!=zone:continue + if apn and d.get('name')!=apn:continue rt=d.get('radio_table') or [];changed=False;old=[] for r in rt: if r.get('radio')!=band:continue diff --git a/clients/cascades-tucson/session-logs/2026-06/2026-06-15-howard-cascades-wifi-rf-audit.md b/clients/cascades-tucson/session-logs/2026-06/2026-06-15-howard-cascades-wifi-rf-audit.md index 823d315..40a2e88 100644 --- a/clients/cascades-tucson/session-logs/2026-06/2026-06-15-howard-cascades-wifi-rf-audit.md +++ b/clients/cascades-tucson/session-logs/2026-06/2026-06-15-howard-cascades-wifi-rf-audit.md @@ -361,3 +361,20 @@ UI-toggle + device-JSON diff). min-data-rate/band-steering are wlanconf-level -> 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 ""** 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 disable --ap --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.