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
This commit is contained in:
2026-06-16 00:24:27 -07:00
parent 35a0cca2c6
commit ae3d760c37
4 changed files with 47 additions and 16 deletions

View File

@@ -117,15 +117,20 @@ bash .../apply-radio.sh <site> <ng|na|6e> power low|medium|high|auto|<dBm> [-
bash .../apply-radio.sh <site> <ng|na|6e> width 20|40|80|160 [--zone Z] [--apply]
bash .../apply-radio.sh <site> <ng|na|6e> channel <number>|auto [--zone Z] [--apply]
bash .../apply-radio.sh <site> <ng|na|6e> minrssi off|on|-<NN> [--zone Z] [--apply]
bash .../apply-radio.sh <site> <ng|na|6e> disable # radio OFF (tx_power_mode=disabled) [--ap NAME] [--apply]
bash .../apply-radio.sh <site> <ng|na|6e> enable # radio ON (tx_power_mode=auto) [--ap NAME] [--apply]
```
`--ap "<name>"` 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 <site> ng disable --ap <name> --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

View File

@@ -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 <name>` 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 111 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.

View File

@@ -15,7 +15,9 @@
# width 20|40|80|160 -> ht (channel width)
# channel <number>|auto -> channel
# minrssi off|on|-<NN> -> 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 <site> <band> <action> <value> [--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 <site> <band> <action> [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|-<NN>"; 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 <<JS | bash "$UOS" 2>&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