diff --git a/composer.json b/composer.json index e3f26d7..f01253b 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "dashboard/unifi", "description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform", - "version": "1.10.1", + "version": "1.10.2", "type": "library", "license": "MIT", "autoload": { diff --git a/src/Http/Controllers/WifiController.php b/src/Http/Controllers/WifiController.php index fdd0beb..f326777 100644 --- a/src/Http/Controllers/WifiController.php +++ b/src/Http/Controllers/WifiController.php @@ -149,8 +149,18 @@ class WifiController extends Controller $name = $networksById[$nconfId]['name'] ?? null; } - // Match by unifi_id, or by passphrase for a held embedded record re-appearing + // Match in priority order: + // 1. by current unifi_id (already-synced row) + // 2. by name within this wlan (catches rotation: passphrase + // changed → synthetic id changed → row identity unchanged) + // 3. by passphrase among held rows (legacy fallback for + // cases where name wasn't ingested) $record = UnifiPpsk::where('unifi_id', $uid)->first() + ?? ($name + ? UnifiPpsk::where('wlan_id', $wlanId)->where('name', $name) + ->orderByRaw("FIELD(state, 'active', 'held')") + ->first() + : null) ?? UnifiPpsk::where('wlan_id', $wlanId) ->where('x_passphrase', $pass) ->where('state', 'held') @@ -174,8 +184,8 @@ class WifiController extends Controller } } - // Only mark as held when we have confirmed live IDs — - // never wipe on an empty API response (prevents false-holds on API failures) + // Mark non-matching active rows as held — but ONLY if there's no + // other active row with the same name we just reconnected. if (! empty($liveIds)) { UnifiPpsk::where('wlan_id', $wlanId) ->where('state', 'active') @@ -184,6 +194,47 @@ class WifiController extends Controller ->update(['state' => 'held', 'unifi_id' => null]); } + // For each active row, salvage any rotate_password / schedule + // settings from the held tombstones with the same name BEFORE + // we prune them. Otherwise a row that had rotate=on loses the + // flag every time a rotation changes its synthetic id. + $activeRows = UnifiPpsk::where('wlan_id', $wlanId) + ->where('state', 'active') + ->whereNotNull('name') + ->get(); + foreach ($activeRows as $active) { + $heldWithSettings = UnifiPpsk::where('wlan_id', $wlanId) + ->where('state', 'held') + ->where('name', $active->name) + ->where(fn ($q) => $q + ->where('rotate_password', true) + ->orWhereNotNull('schedule')) + ->orderByDesc('updated_at') + ->first(); + if (! $heldWithSettings) continue; + + $patch = []; + if ($heldWithSettings->rotate_password && ! $active->rotate_password) { + $patch['rotate_password'] = true; + } + if ($heldWithSettings->schedule && ! $active->schedule) { + $patch['schedule'] = $heldWithSettings->schedule; + } + if ($patch) $active->update($patch); + } + + // Prune obsolete held rows: any held row whose name matches an + // active row in the same wlan is a stale tombstone — its + // settings have been salvaged above, and its data has been + // superseded by the active one. + $activeNames = $activeRows->pluck('name')->filter()->unique(); + if ($activeNames->isNotEmpty()) { + UnifiPpsk::where('wlan_id', $wlanId) + ->where('state', 'held') + ->whereIn('name', $activeNames) + ->delete(); + } + $dbRecords = UnifiPpsk::where('wlan_id', $wlanId) ->orderByRaw("FIELD(state, 'active', 'held')") ->orderBy('name')