3 Commits

Author SHA1 Message Date
429cd44ac5 fix: register unifi pages with shell NavVisibilityRegistry; v1.12.1
The Access tab persists user/group grants in unifi_page_grants and the
existing RouteMatched listener honors them on the request path, but
NavItem::visibleTo() only consulted the page's required_permission —
so a granted user never saw the menu entry to reach the page. Register
the unifi.* prefix with the shell's NavVisibilityRegistry so the
sidebar lists exactly the pages the grant table allows.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 12:12:58 -04:00
274f210337 release: 1.12.0 — rolls up 1.11.1 (reboot suppression hardening)
Bundled stable cut for prod. Contents since 1.11.0:

* fix(reboot): suppress webhook alerts during a fleet reboot
  regardless of cache driver. RebootAllAps now stamps a Setting
  (unifi.reboot_suppression_until) for 20 minutes at the start of a
  fleet reboot, and WebhookCheckService consults that Setting in
  checkDeviceTransition + checkReboot in addition to the existing
  per-MAC cache keys. Database-backed Setting always crosses
  container/process/cache-driver boundaries so the suppression can
  no longer be defeated by config differences.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:09:08 -04:00
c0f12ce931 fix(reboot): suppress webhook alerts during a fleet reboot, regardless of cache driver
Existing Cache::has('unifi:planned_reboot:{mac}') per-MAC suppression
relies on the cache driver being shared across the scheduler and the
snapshot-capture containers. In environments where the cache is
backed by something process-local (or where the keys expire before a
slow reboot completes), webhook alerts fire even though the dashboard
itself initiated the reboots.

RebootAllAps now also stamps a single Setting
(unifi.reboot_suppression_until) at the start of a fleet reboot,
covering a 20-minute window. WebhookCheckService checks this Setting
in addition to the per-MAC cache key, in checkDeviceTransition and
checkReboot. Setting is database-backed so it's always visible across
containers regardless of cache configuration.

v1.11.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 22:07:58 -04:00
3 changed files with 51 additions and 2 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.11.0", "version": "1.12.1",
"type": "library", "type": "library",
"license": "MIT", "license": "MIT",
"autoload": { "autoload": {

View File

@@ -181,7 +181,11 @@ class WebhookCheckService
$prev = DeviceState::where('device_mac', $mac)->first(); $prev = DeviceState::where('device_mac', $mac)->first();
if (! $prev) continue; if (! $prev) continue;
// Skip planned reboots — these are intentional, not alerts // Skip planned reboots — these are intentional, not alerts.
// Two layers: a global suppression window set by RebootAllAps
// (Setting, survives any cache driver), plus the per-MAC
// cache keys for finer granularity.
if ($this->inGlobalRebootSuppression()) continue;
if (Cache::has('unifi:planned_reboot:' . strtolower($mac))) continue; if (Cache::has('unifi:planned_reboot:' . strtolower($mac))) continue;
if ($comingOnline) { if ($comingOnline) {
@@ -509,6 +513,8 @@ class WebhookCheckService
private function checkReboot($aps, array $filter): array private function checkReboot($aps, array $filter): array
{ {
$alerts = []; $alerts = [];
if ($this->inGlobalRebootSuppression()) return $alerts;
foreach ($aps as $ap) { foreach ($aps as $ap) {
if (! empty($filter) && ! in_array($ap['mac'], $filter)) continue; if (! empty($filter) && ! in_array($ap['mac'], $filter)) continue;
if (Cache::has('unifi:planned_reboot:' . strtolower($ap['mac']))) continue; if (Cache::has('unifi:planned_reboot:' . strtolower($ap['mac']))) continue;
@@ -584,6 +590,24 @@ class WebhookCheckService
return self::buildPlatformPayload($url, $message, $fullPayload); return self::buildPlatformPayload($url, $message, $fullPayload);
} }
/**
* Is a fleet reboot in progress right now? RebootAllAps stamps a
* suppression-until timestamp as a Setting; while that timestamp
* is in the future, we skip all device-offline / device-online /
* unexpected-reboot alerts to avoid flooding webhooks during the
* known maintenance window.
*/
private function inGlobalRebootSuppression(): bool
{
$until = \App\Models\Setting::get('unifi.reboot_suppression_until');
if (! $until) return false;
try {
return \Carbon\Carbon::parse($until)->isFuture();
} catch (\Throwable) {
return false;
}
}
/** /**
* Public/static helper so the test-webhook endpoint produces the * Public/static helper so the test-webhook endpoint produces the
* same per-platform payload shape that real events do. * same per-platform payload shape that real events do.

View File

@@ -25,6 +25,31 @@ class UnifiServiceProvider extends ServiceProvider
$this->loadRoutesFrom(__DIR__ . '/routes/unifi.php'); $this->loadRoutesFrom(__DIR__ . '/routes/unifi.php');
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); $this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
// Tell the shell's NavVisibilityRegistry which unifi nav items
// the user can see in the sidebar. Without this the sidebar
// would only follow legacy required_permission, hiding pages
// the user has been explicitly granted via the Access tab.
try {
app(\App\Support\NavVisibilityRegistry::class)->register(
'unifi.',
function (\App\Models\User $user) {
if (! \Illuminate\Support\Facades\Schema::hasTable('unifi_page_grants')) {
return collect();
}
$groupIds = $user->groups()->pluck('groups.id');
return UnifiPageGrant::query()
->where(function ($q) use ($user, $groupIds) {
$q->where(fn ($u) => $u->where('grantee_type', 'user')->where('grantee_id', $user->id))
->orWhere(fn ($g) => $g->where('grantee_type', 'group')->whereIn('grantee_id', $groupIds));
})
->pluck('nav_item_id');
}
);
} catch (\Throwable) {
// Shell may not have the registry yet (older shell version).
// Sidebar will fall back to legacy permission filter.
}
// Per-page access enforcement for unifi routes. If a unifi page has // Per-page access enforcement for unifi routes. If a unifi page has
// any UnifiPageGrant rows, only super-admins and granted users/ // any UnifiPageGrant rows, only super-admins and granted users/
// groups can hit it; otherwise (no grants) it's open per the existing // groups can hit it; otherwise (no grants) it's open per the existing