feat: initial commit — UniFi snap-in package

Full UniFi dashboard snap-in including:
- WiFi/client/device stats with time-series snapshots
- Client Dashboard with traffic, satisfaction, signal, download charts
- Webhook alerting with debounced offline/online detection
- AP snapshot collection, client snapshot collection
- Device classification (type and OS) from OUI/hostname heuristics
- Webhook cooldown, templates, and multi-platform delivery

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Joel Wedemire
2026-04-12 23:00:05 -07:00
commit ce3217d8f4
29 changed files with 2972 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
<?php
namespace Dashboard\Unifi\Http\Controllers;
use App\Models\Setting;
use Dashboard\Unifi\Services\UnifiApiClient;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Inertia\Inertia;
class WifiController extends Controller
{
public function index(UnifiApiClient $unifi)
{
try {
$wlans = collect($unifi->getWlans())->map(fn ($w) => [
'id' => $w['_id'],
'name' => $w['name'],
'enabled' => $w['enabled'] ?? true,
'security' => $w['security'] ?? 'open',
'wpa_mode' => $w['wpa_mode'] ?? '',
'is_guest' => $w['is_guest'] ?? false,
'vlan_enabled' => $w['vlan_enabled'] ?? false,
'vlan' => $w['vlan'] ?? null,
'hide_ssid' => $w['hide_ssid'] ?? false,
'passphrase' => $w['x_passphrase'] ?? '',
'band' => $this->detectBand($w),
])->values();
// Load saved groups: { "Staff": ["id1", "id2"], ... }
$raw = Setting::get('unifi.ssid_groups', '{}');
$groups = json_decode($raw, true);
if (! is_array($groups) || array_is_list($groups)) $groups = [];
// Force object cast so Vue gets {} not []
$groups = (object) $groups;
return Inertia::render('Unifi/Wifi', [
'wlans' => $wlans,
'groups' => $groups,
]);
} catch (\Throwable $e) {
return Inertia::render('Unifi/Wifi', ['wlans' => [], 'groups' => [], 'error' => $e->getMessage()]);
}
}
public function update(Request $request, string $wlanId, UnifiApiClient $unifi)
{
$data = $request->validate([
'name' => 'sometimes|string|max:255',
'enabled' => 'sometimes|boolean',
'x_passphrase' => 'sometimes|string|min:8|max:63',
'hide_ssid' => 'sometimes|boolean',
]);
try {
// If this WLAN is in a group, apply the same change to all grouped WLANs
$groups = json_decode(Setting::get('unifi.ssid_groups', '{}'), true) ?: [];
$groupedIds = collect($groups)->first(fn ($ids) => in_array($wlanId, $ids));
if ($groupedIds) {
foreach ($groupedIds as $id) {
$unifi->updateWlan($id, $data);
}
} else {
$unifi->updateWlan($wlanId, $data);
}
return back()->with('success', 'WiFi network updated.');
} catch (\Throwable $e) {
return back()->withErrors(['error' => $e->getMessage()]);
}
}
public function toggle(Request $request, string $wlanId, UnifiApiClient $unifi)
{
$request->validate(['enabled' => 'required|boolean']);
try {
$groups = json_decode(Setting::get('unifi.ssid_groups', '{}'), true) ?: [];
$groupedIds = collect($groups)->first(fn ($ids) => in_array($wlanId, $ids));
$enabled = $request->boolean('enabled');
if ($groupedIds) {
foreach ($groupedIds as $id) {
$unifi->updateWlan($id, ['enabled' => $enabled]);
}
} else {
$unifi->updateWlan($wlanId, ['enabled' => $enabled]);
}
return back()->with('success', 'WiFi network ' . ($enabled ? 'enabled' : 'disabled') . '.');
} catch (\Throwable $e) {
return back()->withErrors(['error' => $e->getMessage()]);
}
}
/**
* Group/ungroup SSIDs. Groups are stored as { "GroupName": ["wlan_id_1", "wlan_id_2"] }
*/
public function saveGroups(Request $request)
{
$request->validate(['groups' => 'present']);
$groups = $request->input('groups', []);
if (is_array($groups) && array_is_list($groups)) $groups = (object) [];
Setting::set('unifi.ssid_groups', json_encode($groups ?: (object) []));
return back()->with('success', 'SSID groups saved.');
}
private function detectBand(array $w): string
{
// UniFi stores band info in wlan_band or in the radio settings
$band = $w['wlan_band'] ?? null;
if ($band === 'ng' || $band === '2g') return '2.4 GHz';
if ($band === 'na' || $band === '5g') return '5 GHz';
if ($band === 'a6' || $band === '6g' || $band === '6e') return '6 GHz';
if ($band === 'both' || $band === null) return 'All bands';
// Try to detect from SSID name as fallback
$name = strtolower($w['name'] ?? '');
if (preg_match('/2\.?4\s*g/i', $name)) return '2.4 GHz';
if (preg_match('/5\s*g/i', $name)) return '5 GHz';
if (preg_match('/6\s*g/i', $name)) return '6 GHz';
return 'All bands';
}
}