2 Commits

Author SHA1 Message Date
e59f193ffc feat(nav): surface AP Groups page; always-fresh device data on edit pages
The AP Groups page (ApGroups.vue + ApGroupController + UnifiApiClient
CRUD methods) has been built but never declared in composer.json's
pages list, so it was hidden from the menu. Added it at sort_order=6,
between WiFi Networks and Portal.

The WiFi Networks page already has per-SSID AP-group assignment via
the existing updateApGroups route — that wires into UniFi's standard
ap_group_ids field on wlanconf. (UniFi doesn't expose per-AP-only
assignment separately; the convention is "make a one-AP group for
this AP and assign the SSID to it.")

For the "always pull from UniFi on load" guarantee:
- getWlans() and getApGroups() are already uncached — fresh on every
  page load
- getDevices() (feeds the AP picker for group membership) is cached
  for unifi.cache_ttl seconds; both ApGroupController::index and
  WifiController::index now Cache::forget('unifi:devices') before
  reading so the device list is always fresh

v1.3.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 16:06:49 -04:00
f7672771e0 refactor: read timezone from shell-level site_timezone
Drops unifi.timezone from the settings form (now lives in
Admin → Settings on the shell). Schedulers (PPSK sync, password
rotation) now read \App\Support\Timezone::current() — same fallback
chain as the rest of the platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 15:26:50 -04:00
6 changed files with 18 additions and 8 deletions

View File

@@ -1,7 +1,7 @@
{ {
"name": "dashboard/unifi", "name": "dashboard/unifi",
"description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform", "description": "UniFi network management, WiFi stats, and captive portal authentication for the Dashboard platform",
"version": "1.0.0", "version": "1.3.0",
"type": "library", "type": "library",
"license": "MIT", "license": "MIT",
"autoload": { "autoload": {
@@ -27,8 +27,9 @@
{ "label": "Devices", "route_name": "unifi.devices", "icon": "cpu-chip", "permission": "unifi.stats", "sort_order": 3 }, { "label": "Devices", "route_name": "unifi.devices", "icon": "cpu-chip", "permission": "unifi.stats", "sort_order": 3 },
{ "label": "Clients", "route_name": "unifi.clients", "icon": "users", "permission": "unifi.stats", "sort_order": 4 }, { "label": "Clients", "route_name": "unifi.clients", "icon": "users", "permission": "unifi.stats", "sort_order": 4 },
{ "label": "WiFi Networks", "route_name": "unifi.wifi", "icon": "wifi", "permission": "unifi.manage", "sort_order": 5 }, { "label": "WiFi Networks", "route_name": "unifi.wifi", "icon": "wifi", "permission": "unifi.manage", "sort_order": 5 },
{ "label": "Portal", "route_name": "unifi.portal.settings", "icon": "shield-check", "permission": "unifi.auth", "sort_order": 6 }, { "label": "AP Groups", "route_name": "unifi.ap-groups.index", "icon": "rectangle-stack", "permission": "unifi.manage", "sort_order": 6 },
{ "label": "Webhooks", "route_name": "unifi.webhooks.index", "icon": "bell-alert", "permission": "unifi.settings", "sort_order": 7 }, { "label": "Portal", "route_name": "unifi.portal.settings", "icon": "shield-check", "permission": "unifi.auth", "sort_order": 7 },
{ "label": "Webhooks", "route_name": "unifi.webhooks.index", "icon": "bell-alert", "permission": "unifi.settings", "sort_order": 8 },
{ "label": "Settings", "route_name": "unifi.settings", "icon": "cog-6-tooth", "permission": "unifi.settings", "sort_order": 99 } { "label": "Settings", "route_name": "unifi.settings", "icon": "cog-6-tooth", "permission": "unifi.settings", "sort_order": 99 }
], ],
"permissions": [ "permissions": [

View File

@@ -81,7 +81,7 @@ class RotatePasswords extends Command
$hour = (int) Setting::get('unifi.password_rotation.hour', 2); $hour = (int) Setting::get('unifi.password_rotation.hour', 2);
$minute = (int) Setting::get('unifi.password_rotation.minute', 0); $minute = (int) Setting::get('unifi.password_rotation.minute', 0);
$dow = (int) Setting::get('unifi.password_rotation.day_of_week', 0); $dow = (int) Setting::get('unifi.password_rotation.day_of_week', 0);
$tz = Setting::get('unifi.timezone', 'UTC'); $tz = \App\Support\Timezone::current();
$now = now($tz); $now = now($tz);
if ($now->hour !== $hour || $now->minute !== $minute) { if ($now->hour !== $hour || $now->minute !== $minute) {

View File

@@ -18,7 +18,7 @@ class SyncPpskSchedules extends Command
return self::SUCCESS; return self::SUCCESS;
} }
$tz = Setting::get('unifi.timezone', 'UTC'); $tz = \App\Support\Timezone::current();
$now = now($tz); $now = now($tz);
$day = $now->dayOfWeek; // 0=Sun … 6=Sat $day = $now->dayOfWeek; // 0=Sun … 6=Sat
$slot = $now->hour * 2 + ($now->minute >= 30 ? 1 : 0); // 047 $slot = $now->hour * 2 + ($now->minute >= 30 ? 1 : 0); // 047

View File

@@ -5,12 +5,19 @@ namespace Dashboard\Unifi\Http\Controllers;
use Dashboard\Unifi\Services\UnifiApiClient; use Dashboard\Unifi\Services\UnifiApiClient;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Routing\Controller; use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Cache;
use Inertia\Inertia; use Inertia\Inertia;
class ApGroupController extends Controller class ApGroupController extends Controller
{ {
public function index(UnifiApiClient $unifi) public function index(UnifiApiClient $unifi)
{ {
// Always pull fresh from the controller on this page so the
// operator never edits against a stale snapshot. getApGroups()
// and getWlans() aren't cached, but getDevices() (which feeds
// the AP picker) is — bust it explicitly.
Cache::forget('unifi:devices');
try { try {
$groups = collect($unifi->getApGroups())->map(fn ($g) => [ $groups = collect($unifi->getApGroups())->map(fn ($g) => [
'id' => $g['_id'], 'id' => $g['_id'],

View File

@@ -19,7 +19,6 @@ class UnifiSettingsController extends Controller
'pollInterval' => (int) Setting::get('unifi.poll_interval', 30), 'pollInterval' => (int) Setting::get('unifi.poll_interval', 30),
'cacheTtl' => (int) Setting::get('unifi.cache_ttl', 30), 'cacheTtl' => (int) Setting::get('unifi.cache_ttl', 30),
'retentionDays' => (int) Setting::get('unifi.retention_days', 30), 'retentionDays' => (int) Setting::get('unifi.retention_days', 30),
'timezone' => Setting::get('unifi.timezone', 'UTC'),
'autoRebootEnabled' => (bool) Setting::get('unifi.auto_reboot.enabled', false), 'autoRebootEnabled' => (bool) Setting::get('unifi.auto_reboot.enabled', false),
'autoRebootFrequency' => Setting::get('unifi.auto_reboot.frequency', 'daily'), 'autoRebootFrequency' => Setting::get('unifi.auto_reboot.frequency', 'daily'),
'autoRebootDow' => (int) Setting::get('unifi.auto_reboot.day_of_week', 0), 'autoRebootDow' => (int) Setting::get('unifi.auto_reboot.day_of_week', 0),
@@ -45,7 +44,6 @@ class UnifiSettingsController extends Controller
'poll_interval' => 'nullable|integer|min:5|max:300', 'poll_interval' => 'nullable|integer|min:5|max:300',
'cache_ttl' => 'nullable|integer|min:5|max:300', 'cache_ttl' => 'nullable|integer|min:5|max:300',
'retention_days' => 'nullable|integer|min:1|max:365', 'retention_days' => 'nullable|integer|min:1|max:365',
'timezone' => 'nullable|string|timezone',
'auto_reboot_enabled' => 'boolean', 'auto_reboot_enabled' => 'boolean',
'auto_reboot_frequency' => 'in:daily,weekly', 'auto_reboot_frequency' => 'in:daily,weekly',
'auto_reboot_dow' => 'nullable|integer|min:0|max:6', 'auto_reboot_dow' => 'nullable|integer|min:0|max:6',
@@ -70,7 +68,6 @@ class UnifiSettingsController extends Controller
if ($request->has('poll_interval')) Setting::set('unifi.poll_interval', $request->poll_interval ?? 30); if ($request->has('poll_interval')) Setting::set('unifi.poll_interval', $request->poll_interval ?? 30);
if ($request->has('cache_ttl')) Setting::set('unifi.cache_ttl', $request->cache_ttl ?? 30); if ($request->has('cache_ttl')) Setting::set('unifi.cache_ttl', $request->cache_ttl ?? 30);
if ($request->has('retention_days')) Setting::set('unifi.retention_days', $request->retention_days ?? 30); if ($request->has('retention_days')) Setting::set('unifi.retention_days', $request->retention_days ?? 30);
if ($request->has('timezone')) Setting::set('unifi.timezone', $request->timezone ?? 'UTC');
Setting::set('unifi.auto_reboot.enabled', $request->boolean('auto_reboot_enabled') ? '1' : ''); Setting::set('unifi.auto_reboot.enabled', $request->boolean('auto_reboot_enabled') ? '1' : '');
Setting::set('unifi.auto_reboot.frequency', $request->input('auto_reboot_frequency', 'daily')); Setting::set('unifi.auto_reboot.frequency', $request->input('auto_reboot_frequency', 'daily'));

View File

@@ -13,6 +13,11 @@ class WifiController extends Controller
{ {
public function index(UnifiApiClient $unifi) public function index(UnifiApiClient $unifi)
{ {
// Always pull fresh device data on this page so AP-group / SSID
// edits never go out against a stale snapshot. getWlans() and
// getApGroups() aren't cached, but getDevices() is.
\Illuminate\Support\Facades\Cache::forget('unifi:devices');
try { try {
$wlans = collect($unifi->getWlans())->map(fn ($w) => $this->mapWlan($w))->values(); $wlans = collect($unifi->getWlans())->map(fn ($w) => $this->mapWlan($w))->values();