sync: auto-sync from HOWARD-HOME at 2026-06-16 00:40:03
Author: Howard Enos Machine: HOWARD-HOME Timestamp: 2026-06-16 00:40:03
This commit is contained in:
@@ -10,6 +10,15 @@ performance + stability for connected devices in congested environments by analy
|
|||||||
controller knows and making prioritized, validated changes. Built for any site; **Cascades**
|
controller knows and making prioritized, validated changes. Built for any site; **Cascades**
|
||||||
(77 APs, ~550 clients, brutal 2.4GHz) is the reference hard case.
|
(77 APs, ~550 clients, brutal 2.4GHz) is the reference hard case.
|
||||||
|
|
||||||
|
## Multi-client (any UOS site)
|
||||||
|
**`scripts/sites.sh`** lists all ~49 sites (APs/switches/gateways) + per-client AP-cred readiness — the
|
||||||
|
entry point for pointing the skill at another client. **Controller-side** scripts (audit-site,
|
||||||
|
live-stats, model-rank, optimize-radios, apply-radio) work on ANY site with **zero per-client setup**
|
||||||
|
(shared controller creds) — pass the site name/desc/id. **AP-side** collectors (neighbor/survey/dfs/
|
||||||
|
watch-ap) additionally need that client's `clients/<slug>/unifi-ap-ssh` vaulted + L3 reach (site VPN);
|
||||||
|
if missing they print the exact vault command and exit (controller-side still works). Default AP-cred
|
||||||
|
path is Cascades — override with the script's vault-path arg per client.
|
||||||
|
|
||||||
## Status (2026-06-15)
|
## Status (2026-06-15)
|
||||||
- **[WORKING] WiFi monitoring + RF tuning** — complete data-gathering for any UOS site/client:
|
- **[WORKING] WiFi monitoring + RF tuning** — complete data-gathering for any UOS site/client:
|
||||||
config + interference (`audit-site`), live per-AP + per-client (`live-stats`), airtime history
|
config + interference (`audit-site`), live per-AP + per-client (`live-stats`), airtime history
|
||||||
@@ -133,6 +142,16 @@ disable IS implemented** = `tx_power_mode:"disabled"` (confirmed via UI-toggle +
|
|||||||
— separate future apply path (see references/ROADMAP.md).
|
— separate future apply path (see references/ROADMAP.md).
|
||||||
Get explicit go before any write. Full roadmap: **references/ROADMAP.md**.
|
Get explicit go before any write. Full roadmap: **references/ROADMAP.md**.
|
||||||
|
|
||||||
|
**WLAN-level knobs — `scripts/apply-wlan.sh`** (wlanconf, not radio_table; affects every AP on the WLAN
|
||||||
|
— target with `--wlan`). Same gated REST path (`rest/wlanconf`), dry-run default, rollback saved:
|
||||||
|
```bash
|
||||||
|
bash .../apply-wlan.sh <site> minrate ng|na auto|off|<Mbps> [--wlan NAME] [--apply] # kill 1-11Mbps: minrate ng 12
|
||||||
|
bash .../apply-wlan.sh <site> steer on|off [--wlan NAME] [--apply] # 5GHz roaming-assistant
|
||||||
|
```
|
||||||
|
GOTCHA (handled): a manual min rate is only honored when `minrate_setting_preference=manual` — the
|
||||||
|
script sets it; `minrate ... auto` hands rate management back to the controller. Write path validated
|
||||||
|
2026-06-16 on a 0-client WLAN (Green Valley Computer Club) — apply->verify->restore.
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
- **Phase 1 (done):** config + interference audit, flags, methodology. Read-only.
|
- **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 →
|
- **Phase 2:** wire the live Network API (Plane 2) for `cu_total`/satisfaction/per-client RF →
|
||||||
|
|||||||
@@ -26,17 +26,19 @@ side, multi-client enablement, and non-WiFi scope. Build/validate new apply acti
|
|||||||
2026-06-16 via UI-toggle + device-JSON diff); enable = `auto`. Validated full cycle on a 0-client
|
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
|
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.
|
(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
|
- [x] **min data rates** (kill 1–11 Mbps; 2.4 floor 12/24) — DONE in `apply-wlan.sh`
|
||||||
live in **`wlanconf`** (WLAN object), NOT `radio_table`; they affect every AP on the WLAN. Separate
|
(`minrate ng|na auto|off|<Mbps>`). Requires `minrate_setting_preference=manual` (handled). Validated
|
||||||
apply path (`apply-wlan.sh`), more blast radius — design carefully.
|
on a 0-client WLAN. **band-steering / 6 GHz steer**: partial — `steer on|off` toggles the 5 GHz
|
||||||
|
`roaming_assistant`; classic prefer-5G band-steering field not yet located (per-AP-group?) — TODO.
|
||||||
- [ ] **channel-plan apply** — feed `survey-collect` cleanest-channel output into a per-AP channel set.
|
- [ ] **channel-plan apply** — feed `survey-collect` cleanest-channel output into a per-AP channel set.
|
||||||
|
|
||||||
## B. Multi-client enablement (use on any client we manage)
|
## B. Multi-client enablement (use on any client we manage)
|
||||||
- [ ] Per-client AP device-auth cred: vault `clients/<x>/unifi-ap-ssh`, pass as the script arg (only
|
- [ ] Per-client AP device-auth cred: vault `clients/<x>/unifi-ap-ssh`, pass as the script arg (only
|
||||||
Cascades exists today). Keys vaulted per-client as needed.
|
Cascades exists today). Keys vaulted per-client as needed.
|
||||||
- [ ] Per-client L3 reach to APs (site VPN / route) for the AP-side collectors (Cascades split-tunnel done).
|
- [ ] Per-client L3 reach to APs (site VPN / route) for the AP-side collectors (Cascades split-tunnel done).
|
||||||
- [ ] Controller-only degraded mode is already usable (audit/live-stats/model-rank need no AP reach) —
|
- [x] Controller-only mode documented + discoverable — `scripts/sites.sh` lists all sites + AP-cred
|
||||||
document it so a client with no VPN still gets the bulk of the value.
|
readiness; controller-side scripts validated on other clients (Glabman, Sonoran Glass). AP-side
|
||||||
|
scripts print the exact vault command when a client's cred is missing (and note controller-side works).
|
||||||
|
|
||||||
## C. Non-WiFi UniFi (currently WIP / out of scope)
|
## C. Non-WiFi UniFi (currently WIP / out of scope)
|
||||||
- [ ] **Switch/PoE collector** — port up/down, PoE budget + per-port draw, errors, **uplink negotiated
|
- [ ] **Switch/PoE collector** — port up/down, PoE budget + per-port draw, errors, **uplink negotiated
|
||||||
|
|||||||
106
.claude/skills/unifi-wifi/scripts/apply-wlan.sh
Normal file
106
.claude/skills/unifi-wifi/scripts/apply-wlan.sh
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# apply-wlan.sh — WLAN-level (wlanconf) config changes that aren't on the radio: minimum data rates
|
||||||
|
# (and, extensibly, band-steering / roaming-assistant). These live on the WLAN object, so a change
|
||||||
|
# affects EVERY AP broadcasting that WLAN — bigger blast radius than apply-radio; target with --wlan.
|
||||||
|
#
|
||||||
|
# DRY-RUN by default; --apply gated behind infrastructure/uos-server-network-api-rw. Same controller
|
||||||
|
# REST write path as apply-radio (login -> GET/modify/PUT rest/wlanconf/<id>), rollback auto-saved.
|
||||||
|
#
|
||||||
|
# Actions:
|
||||||
|
# minrate <ng|na> off|<Mbps> -> minrate_<band>_enabled (+ minrate_<band>_data_rate_kbps)
|
||||||
|
# kill 1-11Mbps legacy basic rates: set 2.4 floor to 12 or 24. e.g. minrate ng 12
|
||||||
|
# steer <on|off> -> roaming_assistant_na_enabled (5GHz client steering / "roaming assistant")
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# bash .../apply-wlan.sh <site> minrate ng 12 [--wlan "CSCNet"] [--apply]
|
||||||
|
# bash .../apply-wlan.sh <site> steer on --wlan "CSC ENT" --apply
|
||||||
|
set -uo 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-wlan.sh <site> <minrate|steer> ... [--wlan NAME] [--apply]}"
|
||||||
|
ACT="${2:?action: minrate|steer}"; shift 2
|
||||||
|
WLAN=""; APPLY=0
|
||||||
|
# action-specific positional args
|
||||||
|
case "$ACT" in
|
||||||
|
minrate) RBAND="${1:?minrate <ng|na> <auto|off|Mbps>}"; RVAL="${2:?minrate <ng|na> <auto|off|Mbps>}"; shift 2
|
||||||
|
case "$RBAND" in ng|na) ;; *) echo "minrate band must be ng|na"; exit 1;; esac
|
||||||
|
# NOTE: a manual min rate is ONLY honored when minrate_setting_preference=manual (else the
|
||||||
|
# controller auto-manages and ignores the kbps). 'auto' hands it back to the controller.
|
||||||
|
if [ "$RVAL" = auto ]; then FIELDS="{\"minrate_setting_preference\":\"auto\"}";
|
||||||
|
elif [ "$RVAL" = off ]; then FIELDS="{\"minrate_setting_preference\":\"manual\",\"minrate_${RBAND}_enabled\":false}";
|
||||||
|
elif [[ "$RVAL" =~ ^[0-9]+$ ]]; then FIELDS="{\"minrate_setting_preference\":\"manual\",\"minrate_${RBAND}_enabled\":true,\"minrate_${RBAND}_data_rate_kbps\":$((RVAL*1000))}";
|
||||||
|
else echo "minrate value: auto|off|<Mbps>"; exit 1; fi ;;
|
||||||
|
steer) SVAL="${1:?steer <on|off>}"; shift
|
||||||
|
case "$SVAL" in on) FIELDS="{\"roaming_assistant_na_enabled\":true}";; off) FIELDS="{\"roaming_assistant_na_enabled\":false}";; *) echo "steer: on|off"; exit 1;; esac ;;
|
||||||
|
*) echo "action must be minrate|steer"; exit 1;;
|
||||||
|
esac
|
||||||
|
while [ $# -gt 0 ]; do case "$1" in --wlan) WLAN="$2"; shift 2;; --apply) APPLY=1; shift;; *) shift;; esac; done
|
||||||
|
|
||||||
|
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 set $FIELDS${WLAN:+ wlan='$WLAN'} mode=$([ $APPLY = 1 ] && echo APPLY || echo DRY-RUN)"
|
||||||
|
[ -z "$WLAN" ] && echo "[WARNING] no --wlan filter: this targets ALL WLANs at the site (incl. Guest). Consider --wlan."
|
||||||
|
|
||||||
|
# ---- DRY-RUN preview against current wlanconf (Mongo) ----
|
||||||
|
cat <<JS | bash "$UOS" 2>&1 | grep -viE 'pq.html|post-quantum|store now|server may need'
|
||||||
|
var SITE='$SITE',FIELDS=$FIELDS,WLAN='$WLAN';
|
||||||
|
var n=0,skip=0;
|
||||||
|
db.wlanconf.find({site_id:SITE}).forEach(function(w){
|
||||||
|
if(WLAN && w.name!==WLAN) return;
|
||||||
|
var change=false,roll={};
|
||||||
|
for(var f in FIELDS){ roll[f]=(w[f]!==undefined?w[f]:null); if(String(w[f])!==String(FIELDS[f])) change=true; }
|
||||||
|
if(!change){ skip++; return; }
|
||||||
|
n++; print("CHANGE wlan='"+w.name+"' "+JSON.stringify(roll)+" -> "+JSON.stringify(FIELDS));
|
||||||
|
});
|
||||||
|
print("\nSUMMARY: "+n+" WLAN(s) would change, "+skip+" already at target.");
|
||||||
|
JS
|
||||||
|
|
||||||
|
if [ "$APPLY" != "1" ]; then
|
||||||
|
echo; echo "[dry-run] no changes made. Add --apply to write (needs the RW admin vaulted)."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---- WRITE PATH (controller REST: login -> GET/modify/PUT rest/wlanconf/<id>, rollback) ----
|
||||||
|
RWP="infrastructure/uos-server-network-api-rw"
|
||||||
|
export RW_U="$(bash "$VAULT" get-field "$RWP" credentials.username 2>/dev/null || true)"
|
||||||
|
export RW_P="$(bash "$VAULT" get-field "$RWP" credentials.password 2>/dev/null || true)"
|
||||||
|
if [ -z "$RW_U" ] || [ -z "$RW_P" ]; then
|
||||||
|
echo "[BLOCKED] --apply needs the RW controller admin vaulted at: $RWP"; exit 2; fi
|
||||||
|
export AW_SITE="$SITE" AW_FIELDS="$FIELDS" AW_WLAN="$WLAN" REPO
|
||||||
|
python - <<'PY'
|
||||||
|
import os,sys,json,ssl,urllib.request,http.cookiejar
|
||||||
|
H="172.16.3.29";PORT=11443;base=f"https://{H}:{PORT}"
|
||||||
|
ctx=ssl.create_default_context();ctx.check_hostname=False;ctx.verify_mode=ssl.CERT_NONE
|
||||||
|
cj=http.cookiejar.CookieJar();op=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cj),urllib.request.HTTPSHandler(context=ctx))
|
||||||
|
def call(method,path,body=None,csrf=None,want_headers=False):
|
||||||
|
data=json.dumps(body).encode() if body is not None else None
|
||||||
|
r=urllib.request.Request(base+path,data=data,method=method); r.add_header('Content-Type','application/json')
|
||||||
|
if csrf:r.add_header('X-CSRF-Token',csrf)
|
||||||
|
resp=op.open(r,timeout=20);hdr=resp.headers;txt=resp.read().decode('utf-8','replace')
|
||||||
|
return (txt,hdr) if want_headers else txt
|
||||||
|
try:_,hd=call('POST','/api/auth/login',{'username':os.environ['RW_U'],'password':os.environ['RW_P']},want_headers=True)
|
||||||
|
except Exception as e:print("[ERROR] login failed:",e);sys.exit(1)
|
||||||
|
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['AW_SITE']),None)
|
||||||
|
if not short:print("[ERROR] site resolve failed");sys.exit(1)
|
||||||
|
fields=json.loads(os.environ['AW_FIELDS']);wlan=os.environ['AW_WLAN']
|
||||||
|
wl=json.loads(call('GET',f'/proxy/network/api/s/{short}/rest/wlanconf')).get('data',[])
|
||||||
|
roll=[];done=0;fail=0
|
||||||
|
for w in wl:
|
||||||
|
if wlan and w.get('name')!=wlan:continue
|
||||||
|
if all(str(w.get(f))==str(v) for f,v in fields.items()):continue
|
||||||
|
old={f:w.get(f) for f in fields}
|
||||||
|
for f,v in fields.items():w[f]=v
|
||||||
|
try:
|
||||||
|
call('PUT',f"/proxy/network/api/s/{short}/rest/wlanconf/{w['_id']}",w,csrf=csrf)
|
||||||
|
roll.append({'id':w['_id'],'name':w.get('name'),'old':old});done+=1;print(f" [ok] wlan '{w.get('name')}' -> {fields}")
|
||||||
|
except Exception as e:
|
||||||
|
fail+=1;print(f" [FAIL] wlan '{w.get('name')}': {e}")
|
||||||
|
rp=os.path.join(os.environ.get('REPO','.'),'.claude','tmp',f"apply-wlan-rollback-{short}.json")
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(rp),exist_ok=True);open(rp,'w').write(json.dumps(roll,indent=1))
|
||||||
|
print(f"\n[APPLY] {done} changed, {fail} failed. Rollback saved: {rp}")
|
||||||
|
except Exception as e:print("[APPLY] done; rollback save failed:",e)
|
||||||
|
PY
|
||||||
@@ -53,7 +53,7 @@ PY
|
|||||||
|
|
||||||
AU="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
AU="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
||||||
AP_PW="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"; export AP_PW
|
AP_PW="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"; export AP_PW
|
||||||
[ -n "$AU" ] && [ -n "$AP_PW" ] || { echo "[ERROR] no AP device-auth cred at vault:$VP"; exit 1; }
|
[ -n "$AU" ] && [ -n "$AP_PW" ] || { echo "[BLOCKED] no AP device-auth cred at vault:$VP"; echo " Controller-side scripts (audit-site/live-stats/model-rank/optimize-radios/apply-radio) still work for this site."; echo " For AP-side collectors, vault this client's cred then re-run:"; echo " bash .claude/skills/vault/scripts/vault-helper.sh new $VP --kind generic --name '<Client> UniFi AP device-auth SSH' --tag unifi --set username=<u> --set password=<pw>"; exit 2; }
|
||||||
SSH_OPTS=(-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
SSH_OPTS=(-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
||||||
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
|
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
|
||||||
if command -v sshpass >/dev/null 2>&1; then
|
if command -v sshpass >/dev/null 2>&1; then
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ PY
|
|||||||
# --- AP SSH auth: sshpass if present, else SSH_ASKPASS fallback ---
|
# --- AP SSH auth: sshpass if present, else SSH_ASKPASS fallback ---
|
||||||
AU="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
AU="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
||||||
AP_PW="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"; export AP_PW
|
AP_PW="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"; export AP_PW
|
||||||
[ -n "$AU" ] && [ -n "$AP_PW" ] || { echo "[ERROR] no AP device-auth cred at vault:$VP"; exit 1; }
|
[ -n "$AU" ] && [ -n "$AP_PW" ] || { echo "[BLOCKED] no AP device-auth cred at vault:$VP"; echo " Controller-side scripts (audit-site/live-stats/model-rank/optimize-radios/apply-radio) still work for this site."; echo " For AP-side collectors, vault this client's cred then re-run:"; echo " bash .claude/skills/vault/scripts/vault-helper.sh new $VP --kind generic --name '<Client> UniFi AP device-auth SSH' --tag unifi --set username=<u> --set password=<pw>"; exit 2; }
|
||||||
SSH_OPTS=(-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
SSH_OPTS=(-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
||||||
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
|
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
|
||||||
# NOTE: </dev/null on each ssh is REQUIRED — in the `while read` harvest loop, ssh would otherwise
|
# NOTE: </dev/null on each ssh is REQUIRED — in the `while read` harvest loop, ssh would otherwise
|
||||||
|
|||||||
52
.claude/skills/unifi-wifi/scripts/sites.sh
Normal file
52
.claude/skills/unifi-wifi/scripts/sites.sh
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# sites.sh — fleet overview of every UniFi site on the UOS controller + per-site skill readiness.
|
||||||
|
# The entry point for "what clients can I run this on". Controller-side scripts (audit-site,
|
||||||
|
# live-stats, model-rank, optimize-radios, apply-radio) work on ANY site with ZERO per-client setup
|
||||||
|
# (they use the shared controller creds). The AP-side collectors (neighbor-collect, survey-collect,
|
||||||
|
# dfs-check, watch-ap) additionally need that client's AP device-auth cred vaulted + L3 reach (VPN).
|
||||||
|
#
|
||||||
|
# Usage: bash .claude/skills/unifi-wifi/scripts/sites.sh [--ap-cred-glob 'clients/*/unifi-ap-ssh']
|
||||||
|
set -uo pipefail
|
||||||
|
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
|
||||||
|
UOS="$REPO/.claude/scripts/uos-mongo.sh"
|
||||||
|
VROOT="${VAULT_ROOT:-$(jq -r '.vault_path // empty' "$REPO/.claude/identity.json" 2>/dev/null)}"
|
||||||
|
[ -n "$VROOT" ] || VROOT="$REPO/../vault"
|
||||||
|
|
||||||
|
echo "[INFO] UniFi sites on the UOS controller (controller-side scripts work on ALL of these now):"
|
||||||
|
cat <<'JS' | bash "$UOS" 2>&1 | grep -viE 'WARNING|post-quantum|store now|upgraded|openssh'
|
||||||
|
var sites={};
|
||||||
|
db.site.find({},{name:1,desc:1}).forEach(function(s){ sites[s._id.str]={name:(s.desc||s.name),short:s.name,ap:0,sw:0,gw:0}; });
|
||||||
|
db.device.find({},{site_id:1,type:1}).forEach(function(d){ var s=sites[d.site_id]; if(!s)return;
|
||||||
|
if(d.type=='uap')s.ap++; else if(d.type=='usw')s.sw++; else if(d.type=='ugw'||d.type=='uxg'||d.type=='udm')s.gw++; });
|
||||||
|
function pad(v,n){var s=String(v);while(s.length<n)s=' '+s;return s;} // old mongo JS has no padStart
|
||||||
|
var rows=Object.keys(sites).map(function(id){var s=sites[id];return [id,s];}).sort(function(a,b){return b[1].ap-a[1].ap;});
|
||||||
|
print("");
|
||||||
|
print("site_id APs SW GW name");
|
||||||
|
print("------------------------- --- -- -- ----");
|
||||||
|
rows.forEach(function(r){ var s=r[1];
|
||||||
|
print(r[0]+" "+pad(s.ap,3)+" "+pad(s.sw,2)+" "+pad(s.gw,2)+" "+s.name);
|
||||||
|
});
|
||||||
|
print("\n# "+rows.length+" sites. Run any controller-side script with the site name/desc or id, e.g.:");
|
||||||
|
print("# bash .claude/skills/unifi-wifi/scripts/audit-site.sh <name|id>");
|
||||||
|
JS
|
||||||
|
|
||||||
|
# AP-side readiness: which clients have an AP device-auth cred vaulted (clients/<x>/unifi-ap-ssh)
|
||||||
|
echo ""
|
||||||
|
echo "[INFO] AP-side collectors (neighbor/survey/dfs/watch-ap) need a per-client AP device-auth cred."
|
||||||
|
echo " Vaulted AP creds found (clients/*/unifi-ap-ssh):"
|
||||||
|
if [ -d "$VROOT" ]; then
|
||||||
|
found=$(find "$VROOT/clients" -name 'unifi-ap-ssh.sops.yaml' 2>/dev/null | sed -E 's#.*/clients/([^/]+)/.*# [ready] clients/\1/unifi-ap-ssh#' | sort)
|
||||||
|
[ -n "$found" ] && echo "$found" || echo " (none yet — only the controller-side scripts will work until you vault one)"
|
||||||
|
else
|
||||||
|
echo " (vault not found at $VROOT — set VAULT_ROOT)"
|
||||||
|
fi
|
||||||
|
cat <<'EOF'
|
||||||
|
To enable AP-side collectors for a new client:
|
||||||
|
1) get that site's Device Authentication user/pass (UniFi OS -> Settings -> System ->
|
||||||
|
Device Authentication, per site) + L3 reach to its AP mgmt VLAN (site VPN/route).
|
||||||
|
2) bash .claude/skills/vault/scripts/vault-helper.sh new clients/<slug>/unifi-ap-ssh \
|
||||||
|
--kind generic --name '<Client> UniFi AP device-auth SSH' --tag unifi \
|
||||||
|
--set username=<u> --set password=<pw>
|
||||||
|
3) pass it as the script's vault-path arg, e.g.:
|
||||||
|
bash .../neighbor-collect.sh <site> clients/<slug>/unifi-ap-ssh
|
||||||
|
EOF
|
||||||
@@ -49,7 +49,7 @@ PY
|
|||||||
# --- AP SSH auth (sshpass or SSH_ASKPASS fallback) ---
|
# --- AP SSH auth (sshpass or SSH_ASKPASS fallback) ---
|
||||||
AU="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
AU="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
||||||
AP_PW="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"; export AP_PW
|
AP_PW="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"; export AP_PW
|
||||||
[ -n "$AU" ] && [ -n "$AP_PW" ] || { echo "[ERROR] no AP device-auth cred at vault:$VP"; exit 1; }
|
[ -n "$AU" ] && [ -n "$AP_PW" ] || { echo "[BLOCKED] no AP device-auth cred at vault:$VP"; echo " Controller-side scripts (audit-site/live-stats/model-rank/optimize-radios/apply-radio) still work for this site."; echo " For AP-side collectors, vault this client's cred then re-run:"; echo " bash .claude/skills/vault/scripts/vault-helper.sh new $VP --kind generic --name '<Client> UniFi AP device-auth SSH' --tag unifi --set username=<u> --set password=<pw>"; exit 2; }
|
||||||
SSH_OPTS=(-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
SSH_OPTS=(-o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
||||||
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
|
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
|
||||||
if command -v sshpass >/dev/null 2>&1; then
|
if command -v sshpass >/dev/null 2>&1; then
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ VAULT="$REPO/.claude/scripts/vault.sh"
|
|||||||
AP="${1:?usage: watch-ap.sh <ap-ip> [interval] [vault-path]}"; INT="${2:-2}"; VP="${3:-clients/cascades-tucson/unifi-ap-ssh}"
|
AP="${1:?usage: watch-ap.sh <ap-ip> [interval] [vault-path]}"; INT="${2:-2}"; VP="${3:-clients/cascades-tucson/unifi-ap-ssh}"
|
||||||
U="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
U="$(bash "$VAULT" get-field "$VP" credentials.username 2>/dev/null)"
|
||||||
P="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"
|
P="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"
|
||||||
[ -n "$U" ] && [ -n "$P" ] || { echo "[ERROR] no device-auth cred at vault:$VP"; exit 1; }
|
[ -n "$U" ] && [ -n "$P" ] || { echo "[BLOCKED] no AP device-auth cred at vault:$VP"; echo " Vault this client's cred: bash .claude/skills/vault/scripts/vault-helper.sh new $VP --kind generic --name '<Client> UniFi AP device-auth SSH' --tag unifi --set username=<u> --set password=<pw>"; exit 2; }
|
||||||
|
|
||||||
# Auth method: sshpass if available, else SSH_ASKPASS fallback (no sshpass needed).
|
# Auth method: sshpass if available, else SSH_ASKPASS fallback (no sshpass needed).
|
||||||
SSH_OPTS=(-o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
SSH_OPTS=(-o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
|
||||||
|
|||||||
@@ -378,3 +378,26 @@ disable/enable take no value. VALIDATED full enable->disable->enable cycle on 62
|
|||||||
|
|
||||||
SKILL.md + ROADMAP.md updated (disable now done). apply-side now covers all radio_table knobs;
|
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.
|
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).
|
||||||
|
|||||||
Reference in New Issue
Block a user