The sibling-update path on prod failed with "Embedded PPSK not found
by current passphrase" because the DB-stored x_passphrase on the
unedited band was stale — earlier manual edits (pre-1.8.1) only
touched one band, leaving the other band's row out of sync. When
rotation then tried to use that stale passphrase to find the entry,
no match.
updateEmbeddedPpsk now takes an optional $name parameter and tries it
first. PPSK names within a WLAN are unique, so name-matching survives
any passphrase drift caused by historical out-of-band edits.
Passphrase matching stays as a fallback for callers that don't have
a name (none currently — both rotation and the manual modal pass it).
v1.9.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three bugs reported from prod after a PPSK rotation:
1. unifi.password_rotation.last_password was only saved after a
whole-SSID rotation. PPSK-only setups (the typical guest-WiFi case)
ran a successful rotation but the setting stayed empty, so the
Settings → Tasks UI never showed the current password and the
/api/unifi/wifi/current-password endpoint returned 404
"no rotated password recorded yet". The PPSK loop now writes
last_password on every successful PPSK rotation.
2. When an SSID is "banded" (band-steering disabled), UniFi splits it
into one wlanconf per band — 2.4GHz and 5GHz each get their own _id
and their own embedded PPSK array. Rotating the PPSK on one band
left the other band with the old password. New
UnifiApiClient::getWlanSiblings($wlanId) finds all wlanconfs that
share an SSID name; both rotation and the manual modal edit now
call updateEmbeddedPpsk on each sibling and update the matching
UnifiPpsk DB rows.
3. The manual WiFi modal edit had the same band-blindness as #2 —
editing the GUEST PPSK on the 2.4GHz half left the 5GHz half stale.
WifiController::ppskUpdate now walks siblings the same way.
v1.8.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Embedded PPSKs live inside the parent WLAN's private_preshared_keys
array — they have no controller-side _id and the synthetic emb_<hash>
we generate locally isn't a real REST id. Hitting /rest/ppsk/emb_xxx
returns HTTP 400/503, which is what the GUEST PPSK rotation was
failing on at the scheduled 3pm run.
* New UnifiApiClient::updateEmbeddedPpsk($wlanId, $oldPass, $newPass):
GETs /rest/wlanconf/{wlanId}, finds the matching entry in
private_preshared_keys by current passphrase, swaps the value while
preserving whichever field name the controller uses (x_passphrase /
password / passphrase), and PUTs the whole WLAN object back.
* RotatePasswords detects emb_-prefixed unifi_ids and routes through
the embedded path. The synthetic id is rederived from the new
passphrase so the DB row stays addressable.
* WifiController::ppskUpdate (manual modal save) does the same — this
is why manual edits sometimes appeared to succeed but the controller
side actually rejected them.
Verified live against the GUEST PPSK on 10.81.0.1.
v1.5.5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UniFi's /rest/apgroup endpoints (and per-SSID ap_group_ids writes via
/rest/wlanconf) require session-cookie auth — they don't accept the
X-API-Key header. The Integration API doesn't expose AP groups at all.
So with the current deployment running on API-key auth, every AP-group
operation returned 400 api.err.InvalidObject. Removing the dead code
rather than carrying a feature that can't function.
* Deleted ApGroupController, ApGroups.vue, the /ap-groups/* routes,
and getApGroups/createApGroup/updateApGroup/deleteApGroup from
UnifiApiClient.
* Removed the per-SSID AP-group assignment from Wifi.vue + the
updateApGroups action + /wifi/{wlanId}/ap-groups route + the
ap_group_ids field from the mapWlan output.
* Removed the AP Groups nav entry from composer.json.
If a future deploy adds local-admin username+password auth, AP groups
can be reintroduced — the UnifiApiClient::buildRequest() session-cookie
path is intact.
v1.3.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /api/self/sites and /proxy/network/api/self/sites endpoints belong
to the legacy session-cookie API — they don't accept X-API-Key auth and
return 401 for keys generated in UniFi OS → Control Plane → Integrations.
Adds /proxy/network/integration/v1/sites as the first endpoint tried,
which is the actual home of API keys. Integration response rows look
like { id, internalReference, name }; getSites normalizes them to the
legacy { name, desc } shape using internalReference as the slug so
downstream URLs (which build paths from $this->site) keep working.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add password rotation: RotatePasswords console command + migration + service updates
- Add PPSK management: UnifiPpsk model, migration, SyncPpskSchedules console
- Add VLAN groups and AP groups: VlanGroupController, ApGroupController, model, migration
- Add RebootAllAps console command
- Add in_alert column to device states
- Wire new features through service provider, routes, and existing controllers/services
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>