sync: auto-sync from HOWARD-HOME at 2026-06-21 12:25:00

Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-06-21 12:25:00
This commit is contained in:
2026-06-21 12:25:45 -07:00
parent ef0398bc6b
commit 1836bfd34d
26 changed files with 197 additions and 36 deletions

View File

@@ -186,6 +186,83 @@ PS
echo "[OK] uploaded $(basename "$remote") ($nch chunk(s)) -> $(echo "$r"|jq -r '.stdout'|tr -d '\r')"
}
# ---------------------------------------------------------------------------
# DETACHED, NO-CAP execution. A scanner must never be killed by an RMM command
# timeout - large drives can scan for hours. So we launch GuruScan as a
# scheduled task with ExecutionTimeLimit=0 (unlimited); the launch command
# returns immediately, the scan runs to completion on its own, and we poll a
# disk done-marker with NO overall cap.
# ---------------------------------------------------------------------------
# gs_launch_detached "<invoke-guruscan-args>" <tag>
gs_launch_detached() {
local gsargs="$1" tag="$2"
local wl="$WORK_DIR/wrapper_$tag.ps1"
cat > "$wl" <<PS
\$ErrorActionPreference='Continue'
\$marker='C:\\GuruScan\\_done_${tag}.txt'
\$log='C:\\GuruScan\\_log_${tag}.txt'
Remove-Item \$marker,\$log -Force -ErrorAction SilentlyContinue
try { & C:\\GuruScan\\Invoke-GuruScan.ps1 ${gsargs} *> \$log; \$rc=\$LASTEXITCODE }
catch { \$rc=-1; \$_ | Out-String | Add-Content \$log }
"DONE rc=\$rc at \$(Get-Date -Format o)" | Set-Content \$marker
PS
upload_file "$wl" "C:\\GuruScan\\_detached_${tag}.ps1" || return 1
local sf="$WORK_DIR/launch_$tag.ps1"
cat > "$sf" <<PS
\$ErrorActionPreference='Stop'
\$tn='GuruScan-${tag}'
try{ Unregister-ScheduledTask -TaskName \$tn -Confirm:\$false -ErrorAction SilentlyContinue }catch{}
\$a=New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NonInteractive -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\\GuruScan\\_detached_${tag}.ps1'
\$pr=New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
\$st=New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -MultipleInstances IgnoreNew
Register-ScheduledTask -TaskName \$tn -Action \$a -Principal \$pr -Settings \$st -Force | Out-Null
Start-ScheduledTask -TaskName \$tn
Start-Sleep -Seconds 2
Write-Output ('LAUNCHED ' + \$tn + ' state=' + ((Get-ScheduledTask -TaskName \$tn).State))
PS
run_ps "$sf" 90 24 "launch-$tag" || return 1
}
# gs_wait_detached <tag> <label> -- polls for the done-marker; NO cap (24h safety net)
gs_wait_detached() {
local tag="$1" label="$2" i=0 maxi=2880
local sf="$WORK_DIR/poll_$tag.ps1"
cat > "$sf" <<PS
\$ErrorActionPreference='Continue'
\$marker='C:\\GuruScan\\_done_${tag}.txt'
if(Test-Path \$marker){ Write-Output ('MARKER ' + ((Get-Content \$marker -Raw) -replace '\s+',' ').Trim()) }
else {
\$act=@(); foreach(\$n in @('a2cmd','HitmanPro_x64','rkill')){ \$p=Get-Process -Name \$n -ErrorAction SilentlyContinue; if(\$p){ \$act += (\$n+' CPU='+[math]::Round((\$p|Select-Object -First 1).CPU,0)+'s') } }
if(\$act.Count){ Write-Output ('RUNNING ' + (\$act -join ', ')) } else { Write-Output 'RUNNING (between scanners / starting)' }
}
PS
echo "[INFO] waiting for '$label' to finish (NO cap; checking every 30s)..."
while [ $i -lt $maxi ]; do
run_ps "$sf" 30 8 "poll-$tag" >/dev/null 2>&1 || true
local out; out="$(jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null | tr -d '\r')"
if printf '%s' "$out" | grep -q '^MARKER '; then
echo "[OK] '$label' finished -> $(printf '%s' "$out" | grep '^MARKER ')"
return 0
fi
i=$((i+1))
if [ $((i % 4)) -eq 0 ]; then echo " [$label] $((i/2)) min elapsed: $(printf '%s' "$out" | grep -E '^(RUNNING|MARKER)' | head -1)"; fi
sleep 30
done
echo "[WARN] '$label' exceeded 24h safety net"; return 1
}
# fetch the captured console log of a detached run (contains GURUSCAN_RESULT_JSON)
gs_fetch_detached_log() {
local tag="$1"
local sf="$WORK_DIR/getlog_$tag.ps1"
cat > "$sf" <<PS
\$p='C:\\GuruScan\\_log_${tag}.txt'
if(Test-Path \$p){ Get-Content \$p -Raw } else { Write-Output 'NO-LOG' }
PS
run_ps "$sf" 60 24 "fetchlog-$tag" >/dev/null 2>&1 || true
jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null
}
# ===========================================================================
phase_prep() {
echo ""; echo "=== PHASE: prep ==="
@@ -283,21 +360,14 @@ PS
# ===========================================================================
phase_scan() {
echo ""; echo "=== PHASE: scan (clean mode, full chain, headless - LONG) ==="
local sf="$WORK_DIR/scan.ps1"
cat > "$sf" <<'PS'
$ErrorActionPreference='Continue'
& C:\GuruScan\Invoke-GuruScan.ps1 -Headless
PS
# Emsisoft timeout_min=120, HitmanPro=60. Give the command 2.5h and poll up to 3h.
run_ps "$sf" 9000 2160 "guruscan-run" || { _logerr "GuruScan run failed" --context "host=$AGENT_HOST"; echo "[ERROR] scan phase failed"; return 1; }
# surface the structured result line
local out; out="$(jq -r '.stdout' "$WORK_DIR/last_result.json" 2>/dev/null)"
echo ""; echo "=== PHASE: scan (clean mode, full chain, headless, DETACHED - NO CAP) ==="
gs_launch_detached "-Headless" "scan" || { _logerr "detached scan launch failed" --context "host=$AGENT_HOST"; echo "[ERROR] launch failed"; return 1; }
gs_wait_detached "scan" "full-scan" || true
local out; out="$(gs_fetch_detached_log "scan")"
echo ""
echo "$out" | grep 'GURUSCAN_RESULT_JSON:' | sed 's/^GURUSCAN_RESULT_JSON://' | jq '.' 2>/dev/null \
|| echo "[WARN] no GURUSCAN_RESULT_JSON line in stdout (see full output above)"
local cmd_id; cmd_id="$(cat "$WORK_DIR/last_cmd_id" 2>/dev/null||echo ?)"
post_alert "[RMM] GuruScan test: scan finished on $AGENT_HOST -> cmd:${cmd_id:0:8}"
printf '%s\n' "$out" | grep 'GURUSCAN_RESULT_JSON:' | sed 's/^.*GURUSCAN_RESULT_JSON://' | jq '.' 2>/dev/null \
|| echo "[WARN] no GURUSCAN_RESULT_JSON in detached log"
post_alert "[RMM] GuruScan test: detached scan finished on $AGENT_HOST"
}
# ===========================================================================
@@ -412,12 +482,10 @@ PS
ve_seed_ps > "$WORK_DIR/ve_seed.ps1"
run_ps "$WORK_DIR/ve_seed.ps1" 60 24 "seed-for-$eng" || { echo "[ERROR] seed failed for $eng"; return 1; }
# 2) run ONLY this engine in clean mode (long; Emsisoft updates+scans C:\)
cat > "$WORK_DIR/ve_run.ps1" <<PS
\$ErrorActionPreference='Continue'
& C:\\GuruScan\\Invoke-GuruScan.ps1 -Scanners $eng -Headless
PS
run_ps "$WORK_DIR/ve_run.ps1" 2700 600 "run-$eng" || echo "[WARN] $eng run reported non-zero"
# 2) run ONLY this engine in clean mode, DETACHED with NO cap (Emsisoft
# updates+scans all of C:\ - can take far longer than any RMM timeout)
gs_launch_detached "-Scanners $eng -Headless" "ve_$eng" || { echo "[ERROR] launch failed for $eng"; return 1; }
gs_wait_detached "ve_$eng" "run-$eng" || true
# 3) check which seeded copies survived + read that run's result json
cat > "$WORK_DIR/ve_check.ps1" <<'PS'

View File

@@ -186,6 +186,12 @@ vaulted dedicated key `infrastructure/uos-server-ssh-key` (works from any fleet
`references/site-manager-api.md`. Use this for consoles NOT adopted into the UOS server.
## Applying changes — IMPORTANT boundary
**Error logging (mandatory):** on a GENUINE functional failure (controller/SSH login or connect fail,
unexpected REST/pfSense error, php fatal), the scripts log it via `.claude/scripts/log-skill-error.sh`
before surfacing — do NOT log handled conditions (missing cred, bad args, site-not-found, "no results").
The live scripts (`pfsense-ssh.sh`, `gw-control.sh`, `gw-audit.sh`) self-log; if you add a new script,
do the same at its failure branches.
Config changes are automated across many APs (no per-AP UI clicking) via the controller REST API
(`PUT .../rest/device/<id>` radio_table) — **`scripts/apply-radio.sh`**. Actions (all radio_table):
```bash

View File

@@ -26,6 +26,8 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/apply-radio" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
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
@@ -97,7 +99,7 @@ EOF
fi
export AR_SITE="$SITE" AR_BAND="$BAND" AR_FIELDS="$FIELDS" AR_ZONE="$ZONE" AR_AP="$APN" REPO
python - <<'PY'
rc=0; python - <<'PY' || rc=$?
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
@@ -145,3 +147,5 @@ try:
except Exception as e:print("[APPLY] done; rollback save failed:",e)
print("[validate] watch the target APs live: watch-ap.sh <ap-ip> (before/after). Roll out per --zone.")
PY
[ "$rc" -ne 0 ] && logerr "apply-radio REST write failed" "site=$SITE band=$BAND act=$ACT rc=$rc"
exit $rc

View File

@@ -21,6 +21,8 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/apply-wlan" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
SITEARG="${1:?usage: apply-wlan.sh <site> <minrate|steer> ... [--wlan NAME] [--apply]}"
ACT="${2:?action: minrate|bandsteer|bands|steer|bsstm|wlan|dtim|mcast|bcfilter|rrm|ftroam|isolation|hidessid|macfilter|aps}"; shift 2
WLAN=""; APPLY=0
@@ -154,3 +156,6 @@ try:
print(f"\n[APPLY] {done} changed, {fail} failed. Rollback saved: {rp}")
except Exception as e:print("[APPLY] done; rollback save failed:",e)
PY
rc=$?
[ "$rc" -ne 0 ] && logerr "apply-wlan REST write failed" "site=$SITE act=$ACT rc=$rc"
exit $rc

View File

@@ -12,6 +12,9 @@
set -euo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
UOS="$REPO/.claude/scripts/uos-mongo.sh"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
# (Pure Mongo read/analysis; no external call with an uninstrumented failure branch -> helper for consistency.)
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/audit-site" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
arg="${1:?usage: audit-site.sh <site-name|site_id>}"
if [[ "$arg" =~ ^[0-9a-f]{24}$ ]]; then

View File

@@ -13,6 +13,8 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/channel-plan" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: channel-plan.sh <site> <ng|na> [--apply]}"; BAND="${2:?band ng|na}"; APPLY=0
CHANS=""; DFSPOL=""
@@ -135,3 +137,6 @@ os.makedirs(os.path.dirname(rp),exist_ok=True); open(rp,'w').write(json.dumps(ro
print(f"\n[APPLY] {done} changed, {fail} failed. Rollback: {rp}")
print("[validate] re-run survey-collect / watch-ap after settle; roll out per-zone in practice.")
PY
rc=$?
[ "$rc" -ne 0 ] && logerr "channel-plan REST call failed" "site=$SITE band=$BAND rc=$rc"
exit $rc

View File

@@ -13,6 +13,8 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/client-control" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
SITEARG="${1:?usage: client-control.sh <site> <block|unblock|kick> <mac> [--apply]}"
ACT="${2:?action: block|unblock|kick}"; MAC="$(echo "${3:?mac required}" | tr 'A-Z' 'a-z')"; APPLY=0
shift 3; while [ $# -gt 0 ]; do case "$1" in --apply) APPLY=1; shift;; *) shift;; esac; done
@@ -51,3 +53,6 @@ try:
print(f" [{'ok' if meta.get('rc')=='ok' else 'FAIL'}] {os.environ['CC_CMD']} {os.environ['CC_MAC']} -> {meta}")
except Exception as e:print(" [FAIL]",e)
PY
rc=$?
[ "$rc" -ne 0 ] && logerr "client-control REST cmd failed" "site=$SITE act=$ACT cmd=$CMD rc=$rc"
exit $rc

View File

@@ -19,6 +19,9 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
# (Mongo read + local-JSON analysis; the controller fetch is best-effort/soft-degrading -> helper for consistency, no live failure branch.)
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/coverage-thin" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: coverage-thin.sh <site> [days=14] (NEIGHBOR_JSON=<matrix> required)}"; DAYS="${2:-14}"
NJ="${NEIGHBOR_JSON:-}"; [ -n "$NJ" ] && [ -f "$NJ" ] || { echo "[ERROR] NEIGHBOR_JSON=<matrix.json> required (run neighbor-collect.sh with NBR_JSON=...)"; exit 1; }

View File

@@ -18,6 +18,8 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/device-control" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
SITEARG="${1:?usage: device-control.sh <site> <action> <target> [--apply]}"
ACT="${2:?action: adopt|restart|locate|unlocate|upgrade|poe-cycle}"; TGT="${3:?target (mac, or AP name for poe-cycle)}"; APPLY=0
shift 3; while [ $# -gt 0 ]; do case "$1" in --apply) APPLY=1; shift;; *) shift;; esac; done
@@ -75,3 +77,6 @@ else:
meta=json.loads(r).get('meta',{}); print(f" [{'ok' if meta.get('rc')=='ok' else 'FAIL'}] {os.environ['DC_CMD']} {tgt} -> {meta}")
except Exception as e:print(" [FAIL]",e)
PY
rc=$?
[ "$rc" -ne 0 ] && logerr "device-control REST cmd failed" "site=$SITE act=$ACT rc=$rc"
exit $rc

View File

@@ -15,6 +15,8 @@
set -uo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/dfs-check" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: dfs-check.sh <site-name|id> [ap-ssh-vault-path]}"
VP="${2:-clients/cascades-tucson/unifi-ap-ssh}"
@@ -26,7 +28,7 @@ CP="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credentia
base="https://$HOST:$PORT"; CJ="$TMP/cj"
code=$(curl -sk -c "$CJ" -o /dev/null -w '%{http_code}' -X POST "$base/api/auth/login" -H 'Content-Type: application/json' \
--data-binary "$(python -c 'import json,sys;print(json.dumps({"username":sys.argv[1],"password":sys.argv[2]}))' "$CU" "$CP")")
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; exit 1; }
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; logerr "UOS controller login failed (HTTP $code)" "host=$HOST:$PORT site=$SITEARG"; exit 1; }
SHORT="$(curl -sk -b "$CJ" "$base/proxy/network/api/self/sites" | python -c "
import sys,json; d=json.load(sys.stdin).get('data',[]); q='''$SITEARG'''.lower()
for s in d:

View File

@@ -13,6 +13,8 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/gw-audit" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: gw-audit.sh <site-name|id> [--pfsense <slug>]}"; shift || true
PFARG="" # optional: pfSense client slug (or full vault path) when UOS site name != client slug
@@ -24,7 +26,7 @@ P="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credential
base="https://$HOST:$PORT"; CJ="$TMP/cj"
code=$(curl -sk -c "$CJ" -o /dev/null -w '%{http_code}' -X POST "$base/api/auth/login" -H 'Content-Type: application/json' \
--data-binary "$(python -c 'import json,sys;print(json.dumps({"username":sys.argv[1],"password":sys.argv[2]}))' "$U" "$P")")
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; exit 1; }
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; logerr "UOS controller login failed (HTTP $code)" "host=$HOST:$PORT site=$SITEARG"; exit 1; }
SHORT="$(curl -sk -b "$CJ" "$base/proxy/network/api/self/sites" | python -c "
import sys,json; d=json.load(sys.stdin).get('data',[]); q='''$SITEARG'''.lower()
for s in d:

View File

@@ -26,6 +26,8 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/gw-control" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
SITEARG="${1:?usage: gw-control.sh <site> <action> [args] [--apply]}"
ACT="${2:?action: pf-list|pf-disable|pf-enable|pf-delete|pf-set-ports|pf-set-src|fw-list|fw-disable|fw-enable|block-ips}"
shift 2
@@ -258,3 +260,6 @@ except urllib.error.HTTPError as e:
except Exception as e:
print("[FAIL]",e);sys.exit(1)
PY
rc=$?
[ "$rc" -ne 0 ] && logerr "gw-control REST write failed" "site=$SITE act=$ACT rc=$rc"
exit $rc

View File

@@ -41,6 +41,8 @@ PY="$(command -v py 2>/dev/null || command -v python 2>/dev/null || command -v p
[ -z "$PY" ] && { echo "[$SELF] python required" >&2; exit 1; }
REPO_ROOT="${CLAUDETOOLS_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "unifi-wifi/gw-sitemanager" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
# --- API key from vault (or env override) ---
KEY="${UNIFI_SM_KEY:-}"
@@ -54,7 +56,7 @@ api() { # $1 = path -> raw JSON on stdout; non-200 -> empty + stderr note
resp=$(curl -s -w "\n__C__%{http_code}" -H "X-API-KEY: $KEY" -H "Accept: application/json" "$BASE$path" 2>/dev/null)
code=$(printf '%s' "$resp" | sed -n 's/.*__C__//p')
body=$(printf '%s' "$resp" | sed 's/__C__[0-9]*$//')
if [ "$code" != "200" ]; then echo "[$SELF] GET $path -> HTTP $code" >&2; return 1; fi
if [ "$code" != "200" ]; then echo "[$SELF] GET $path -> HTTP $code" >&2; logerr "Site Manager API call failed (HTTP $code)" "path=$path"; return 1; fi
printf '%s' "$body"
}

View File

@@ -20,6 +20,8 @@
set -euo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/live-stats" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: live-stats.sh <site-name|short> [--clients]}"; WANT_CLIENTS="${2:-}"
@@ -41,7 +43,7 @@ base="https://$HOST:$PORT"
# UniFi OS login -> session cookie
code=$(curl -sk -c "$CJ" -o /dev/null -w '%{http_code}' -X POST "$base/api/auth/login" \
-H 'Content-Type: application/json' --data-binary "$(python -c 'import json,sys;print(json.dumps({"username":sys.argv[1],"password":sys.argv[2]}))' "$U" "$P")")
[ "$code" = "200" ] || { echo "[ERROR] login HTTP $code"; exit 1; }
[ "$code" = "200" ] || { echo "[ERROR] login HTTP $code"; logerr "UOS controller login failed (HTTP $code)" "host=$HOST:$PORT site=$SITEARG"; exit 1; }
# resolve site short name (classic API keys on the short name, not the _id or display name)
SHORT="$(curl -sk -b "$CJ" "$base/proxy/network/api/self/sites" | python -c "

View File

@@ -11,6 +11,9 @@
# Usage: bash .claude/skills/unifi-wifi/scripts/model-rank.sh <site-name|site_id> [days=7] [band=ng|na|6e|all]
set -euo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
# (Pure Mongo read/analysis; --console path execs rf-analyze.py -> helper for consistency, no live failure branch.)
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/model-rank" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
# Cloud connector path (non-UOS console): `model-rank.sh --console "<name>" [days] [band]` routes to
# rf-analyze.py (Site Manager API; see references/site-manager-api.md). The UOS Mongo path below is unchanged.
if [ "${1:-}" = "--console" ]; then

View File

@@ -12,6 +12,9 @@
set -uo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
UOS="$REPO/.claude/scripts/uos-mongo.sh"; D="$REPO/.claude/skills/unifi-wifi/scripts"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
# (Orchestrator -> the sub-scripts it calls log their own failures; no direct uninstrumented call here -> helper for consistency.)
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/monitor-run" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
ARG="${1:?usage: monitor-run.sh <site|all>}"
clean(){ grep -viE 'post-quantum|store now|upgraded|openssh.com|WARNING: connection'; }

View File

@@ -23,6 +23,8 @@
set -uo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"; UOS="$REPO/.claude/scripts/uos-mongo.sh"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/neighbor-collect" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
# Data source for the AP name/BSSID/IP map: UOS direct-login (default) OR the cloud CONNECTOR
# (`--console <name> [--site <short>]`) for non-UOS / remote consoles. The AP SSH harvest below is
@@ -69,7 +71,7 @@ else
base="https://$HOST:$PORT"; CJ="$TMP/cj"
code=$(curl -sk -c "$CJ" -o /dev/null -w '%{http_code}' -X POST "$base/api/auth/login" -H 'Content-Type: application/json' \
--data-binary "$(python -c 'import json,sys;print(json.dumps({"username":sys.argv[1],"password":sys.argv[2]}))' "$CU" "$CP")")
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; exit 1; }
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; logerr "UOS controller login failed (HTTP $code)" "host=$HOST:$PORT site=$SITEARG"; exit 1; }
SHORT="$(curl -sk -b "$CJ" "$base/proxy/network/api/self/sites" | python -c "
import sys,json; d=json.load(sys.stdin).get('data',[]); q='''$SITEARG'''.lower()
for s in d:

View File

@@ -20,6 +20,9 @@
# env: ROAM_MIN(4) CAP(85) ZONE_DISABLE_PCT(40) REDUN_NG(2) REDUN_OTHER(1)
set -euo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
# (Mongo read + local-JSON analysis; --console path execs rf-analyze.py -> helper for consistency, no live failure branch.)
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/optimize-radios" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
# Cloud connector path (non-UOS console): `optimize-radios.sh --console "<name>" [days] [band]` routes to
# rf-analyze.py (Site Manager API; see references/site-manager-api.md). UOS Mongo path below is unchanged.
# NEIGHBOR_JSON / ROAM_MIN / CAP / ZONE_DISABLE_PCT / REDUN_* env vars pass through to the analyzer.

View File

@@ -24,6 +24,8 @@
set -uo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/pfsense-backend" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
VP="${1:?usage: pfsense-backend.sh <vault-path> <action> [args] [--apply]}"
ACT="${2:?action: audit|pf-list|pf-disable|pf-enable|pf-delete|pf-set-ports|fw-list|fw-disable|fw-enable|block-ips|setup}"
shift 2
@@ -215,3 +217,6 @@ try:
except urllib.error.HTTPError as e: err(e)
except Exception as e: print("[FAIL]",e); sys.exit(1)
PY
rc=$?
[ "$rc" -ne 0 ] && logerr "pfsense-backend REST call failed" "vp=$VP act=$ACT rc=$rc"
exit $rc

View File

@@ -32,6 +32,11 @@ REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"
HERE="$(cd "$(dirname "$0")" && pwd)"
GWC_PHP="$HERE/pfsense-gwc.php"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only
# (SSH connect/auth fail, php fatal, easyrule non-success) — NOT handled conditions (missing cred,
# bad args, site not found). Soft-fails so it never breaks the caller.
SKILL_ID="unifi-wifi/pfsense-ssh"
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "$SKILL_ID" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
SLUG="${1:?usage: pfsense-ssh.sh <slug> <action> [args] [--apply]}"
ACT="${2:?action: audit|dhcp|pf-list|fw-list|pf-*|fw-*|block-ips|unblock|showblock|run|shell}"; shift 2 || true
APPLY=0; BLOCK_IF="wan"; PORT=""; POS=()
@@ -64,19 +69,26 @@ ASKP="$TMP/a.sh"; printf '#!/bin/sh\nprintf "%%s\\n" "$PP"\n' >"$ASKP"; chmod +x
pfssh(){ SSH_ASKPASS="$ASKP" SSH_ASKPASS_REQUIRE=force DISPLAY=:0 ssh -p "$PORT" \
-o ConnectTimeout=12 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1 \
"$U@$HOST" 'sh -s' 2>/dev/null; }
"$U@$HOST" 'sh -s' 2>/dev/null; local rc=$?
[ "$rc" = 255 ] && logerr "SSH connect/auth failed (rc=255)" "host=$HOST:$PORT slug=$SLUG act=$ACT"
return $rc; }
# shell-quote a value for safe embedding in the remote sh script
sq(){ printf "'%s'" "$(printf '%s' "${1:-}" | sed "s/'/'\\\\''/g")"; }
# ship pfsense-gwc.php to /tmp on the box (base64 over the wire) and run it with argv: action apply T V1 V2
run_gwc(){
[ -f "$GWC_PHP" ] || { echo "[ERROR] helper not found: $GWC_PHP"; exit 1; }
[ -f "$GWC_PHP" ] || { echo "[ERROR] helper not found: $GWC_PHP"; logerr "gwc helper missing" "path=$GWC_PHP"; exit 1; }
local b64; b64="$(base64 "$GWC_PHP" | tr -d '\n')"
{ printf 'A=%s; AP=%s; T=%s; V1=%s; V2=%s\n' "$(sq "$1")" "$(sq "$2")" "$(sq "$3")" "$(sq "$4")" "$(sq "$5")"
local out
out="$( { printf 'A=%s; AP=%s; T=%s; V1=%s; V2=%s\n' "$(sq "$1")" "$(sq "$2")" "$(sq "$3")" "$(sq "$4")" "$(sq "$5")"
printf 'printf %%s %s | openssl base64 -A -d > /tmp/pfsense-gwc.php\n' "$(sq "$b64")"
printf 'php /tmp/pfsense-gwc.php "$A" "$AP" "$T" "$V1" "$V2" 2>&1\n' # 2>&1: surface php fatals (display_errors is Off on pfSense)
printf 'rm -f /tmp/pfsense-gwc.php\n'
} | pfssh
} | pfssh )"
printf '%s\n' "$out"
case "$out" in
*"Fatal error"*|*"[FAIL]"*) logerr "pfsense-gwc functional error" "slug=$SLUG act=$1 host=$HOST:$PORT" ;;
esac
}
echo "[INFO] pfSense $ACT @ $U@$HOST:$PORT (vault:$VP)"
@@ -101,7 +113,8 @@ case "$ACT" in
echo " [dry-run] would run (per ip): easyrule $VERB $BLOCK_IF <ip>. Add --apply to write."; exit 0; fi
CMDS=""; IFS=',' read -ra ARR <<< "$IPS"
for ip in "${ARR[@]}"; do ip="$(printf '%s' "$ip" | tr -d ' ')"; [ -n "$ip" ] && CMDS+="echo \"-- $VERB $ip\"; easyrule $VERB $BLOCK_IF $ip; "; done
printf '%s\n' "$CMDS" | pfssh ;;
out="$(printf '%s\n' "$CMDS" | pfssh)"; printf '%s\n' "$out"
case "$out" in *[Ss]uccess*) ;; *) logerr "easyrule $VERB did not report success" "if=$BLOCK_IF ips=$IPS" ;; esac ;;
showblock)
printf 'easyrule showblock %s\n' "$BLOCK_IF" | pfssh ;;

View File

@@ -21,6 +21,8 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/radio-usage" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: radio-usage.sh <site> [band=ng|na|6e] [days=30] [--ap <name>]}"; shift
BAND="ng"; DAYS="30"; APNAME=""; POS=()
@@ -44,7 +46,7 @@ if [ -n "$APNAME" ]; then
base="https://$HOST:$PORT"; CJ="$TMP/cj"
code=$(curl -sk -c "$CJ" -o /dev/null -w '%{http_code}' -X POST "$base/api/auth/login" -H 'Content-Type: application/json' \
--data-binary "$(python -c 'import json,sys;print(json.dumps({"username":sys.argv[1],"password":sys.argv[2]}))' "$CU" "$CP")")
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; exit 1; }
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; logerr "UOS controller login failed (HTTP $code)" "host=$HOST:$PORT site=$SITE band=$BAND"; exit 1; }
SHORT="$(curl -sk -b "$CJ" "$base/proxy/network/api/self/sites" | python -c "import sys,json;[print(s['name']) for s in json.load(sys.stdin).get('data',[]) if s.get('_id')=='$SITE']")"
[ -n "$SHORT" ] || SHORT="$SITEARG"
curl -sk -b "$CJ" "$base/proxy/network/api/s/$SHORT/stat/device" -o "$TMP/dev.json"

View File

@@ -9,6 +9,9 @@
set -uo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
UOS="$REPO/.claude/scripts/uos-mongo.sh"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
# (Mongo read + vault-listing overview; no external call with an uninstrumented failure branch -> helper for consistency.)
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/sites" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
VROOT="${VAULT_ROOT:-$(jq -r '.vault_path // empty' "$REPO/.claude/identity.json" 2>/dev/null)}"
[ -n "$VROOT" ] || VROOT="$REPO/../vault"

View File

@@ -16,6 +16,8 @@
set -uo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/survey-collect" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: survey-collect.sh <site-name|id> [ap-ssh-vault-path]}"
VP="${2:-clients/cascades-tucson/unifi-ap-ssh}"
@@ -28,7 +30,7 @@ CP="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credentia
base="https://$HOST:$PORT"; CJ="$TMP/cj"
code=$(curl -sk -c "$CJ" -o /dev/null -w '%{http_code}' -X POST "$base/api/auth/login" -H 'Content-Type: application/json' \
--data-binary "$(python -c 'import json,sys;print(json.dumps({"username":sys.argv[1],"password":sys.argv[2]}))' "$CU" "$CP")")
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; exit 1; }
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; logerr "UOS controller login failed (HTTP $code)" "host=$HOST:$PORT site=$SITEARG"; exit 1; }
SHORT="$(curl -sk -b "$CJ" "$base/proxy/network/api/self/sites" | python -c "
import sys,json; d=json.load(sys.stdin).get('data',[]); q='''$SITEARG'''.lower()
for s in d:

View File

@@ -9,6 +9,8 @@
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"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only.
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/switch-audit" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
HOST="${UOS_HOST:-172.16.3.29}"; PORT="${UOS_HTTPS_PORT:-11443}"
SITEARG="${1:?usage: switch-audit.sh <site-name|id> [--all-ports]}"; ALL="${2:-}"
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
@@ -18,7 +20,7 @@ P="$(bash "$VAULT" get-field infrastructure/uos-server-network-api-rw credential
base="https://$HOST:$PORT"; CJ="$TMP/cj"
code=$(curl -sk -c "$CJ" -o /dev/null -w '%{http_code}' -X POST "$base/api/auth/login" -H 'Content-Type: application/json' \
--data-binary "$(python -c 'import json,sys;print(json.dumps({"username":sys.argv[1],"password":sys.argv[2]}))' "$U" "$P")")
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; exit 1; }
[ "$code" = "200" ] || { echo "[ERROR] controller login HTTP $code"; logerr "UOS controller login failed (HTTP $code)" "host=$HOST:$PORT site=$SITEARG"; exit 1; }
SHORT="$(curl -sk -b "$CJ" "$base/proxy/network/api/self/sites" | python -c "
import sys,json; d=json.load(sys.stdin).get('data',[]); q='''$SITEARG'''.lower()
for s in d:

View File

@@ -16,6 +16,8 @@
set -euo pipefail
REPO="$(git rev-parse --show-toplevel 2>/dev/null || echo .)"
VAULT="$REPO/.claude/scripts/vault.sh"
# Mandatory skill error logging (skill-creator rule): log GENUINE functional failures only (SSH connect/auth fail rc=255).
logerr(){ bash "$REPO/.claude/scripts/log-skill-error.sh" "unifi-wifi/watch-ap" "$1" --context "${2:-}" >/dev/null 2>&1 || true; }
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)"
P="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"
@@ -25,12 +27,12 @@ P="$(bash "$VAULT" get-field "$VP" credentials.password 2>/dev/null)"
SSH_OPTS=(-o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/dev/null \
-o PreferredAuthentications=password -o PubkeyAuthentication=no -o NumberOfPasswordPrompts=1)
if command -v sshpass >/dev/null 2>&1; then
run_ssh() { SSHPASS="$P" sshpass -e ssh "${SSH_OPTS[@]}" "$@"; }
run_ssh() { local rc; SSHPASS="$P" sshpass -e ssh "${SSH_OPTS[@]}" "$@" || { rc=$?; [ "$rc" = 255 ] && logerr "watch-ap SSH connect/auth failed (rc=255)" "ap=$AP vp=$VP"; return $rc; }; }
echo "[INFO] auth: sshpass"
else
ASKPASS="$(mktemp)"; printf '#!/bin/sh\nprintf "%%s\\n" "$WATCH_AP_PW"\n' > "$ASKPASS"; chmod +x "$ASKPASS"
trap 'rm -f "$ASKPASS"' EXIT
run_ssh() { WATCH_AP_PW="$P" SSH_ASKPASS="$ASKPASS" SSH_ASKPASS_REQUIRE=force DISPLAY="${DISPLAY:-:0}" ssh "${SSH_OPTS[@]}" "$@"; }
run_ssh() { local rc; WATCH_AP_PW="$P" SSH_ASKPASS="$ASKPASS" SSH_ASKPASS_REQUIRE=force DISPLAY="${DISPLAY:-:0}" ssh "${SSH_OPTS[@]}" "$@" || { rc=$?; [ "$rc" = 255 ] && logerr "watch-ap SSH connect/auth failed (rc=255)" "ap=$AP vp=$VP"; return $rc; }; }
echo "[INFO] auth: SSH_ASKPASS fallback (sshpass not installed)"
fi

View File

@@ -17,6 +17,10 @@ Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
<!-- Append entries below this line -->
2026-06-21 | Howard-Home | unifi-wifi/pfsense-ssh | SSH connect/auth failed (rc=255) [ctx: host=192.168.0.1:9999 slug=cascades-tucson act=showblock]
2026-06-21 | Howard-Home | unifi-wifi/pfsense-ssh | SSH connect/auth failed (rc=255) [ctx: host=192.168.0.1:22 slug=cascades-tucson act=showblock]
2026-06-21 | Howard-Home | guruscan/whitelist-design | [correction] over-engineered whitelist as dynamic service-discovery; correct approach is a simple static hard list of install folders (scanners that support exclude-lists ignore listed folders; RKill won't, accepted)
2026-06-21 | Howard-Home | bitdefender | GravityZone API error [policies.getPolicyDetails]: Invalid value for 'policyId' parameter. [ctx: cmd=policy]