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,67 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// VLAN mapping rules: OU/group → VLAN + device limit + session duration
Schema::create('unifi_vlan_mappings', function (Blueprint $table) {
$table->id();
$table->string('group_name', 255); // Google OU or group name
$table->string('match_type', 20) // 'ou', 'group', 'email_domain', 'default'
->default('group');
$table->string('match_value', 255); // the value to match against
$table->unsignedSmallInteger('vlan_id'); // VLAN to assign
$table->unsignedTinyInteger('max_devices') // max concurrent devices
->default(1);
$table->unsignedInteger('session_minutes') // portal session duration
->default(720);
$table->unsignedSmallInteger('sort_order')
->default(0);
$table->timestamps();
});
// Known MAC addresses → direct VLAN assignment (no portal needed)
Schema::create('unifi_known_macs', function (Blueprint $table) {
$table->id();
$table->string('mac_address', 17)->unique(); // aa:bb:cc:dd:ee:ff
$table->string('device_name', 255)->nullable();
$table->string('device_type', 100)->nullable(); // chromebook, printer, phone, etc.
$table->string('owner', 255)->nullable(); // who it belongs to
$table->unsignedSmallInteger('vlan_id');
$table->text('notes')->nullable();
$table->timestamps();
});
// Active portal sessions (tracks who's connected via captive portal)
Schema::create('unifi_portal_sessions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('mac_address', 17);
$table->string('device_hostname', 255)->nullable();
$table->string('device_os', 100)->nullable(); // detected OS
$table->string('device_type', 100)->nullable(); // detected device category
$table->string('ssid', 100)->nullable();
$table->unsignedSmallInteger('vlan_id')->nullable();
$table->string('ap_mac', 17)->nullable();
$table->boolean('is_active')->default(true);
$table->timestamp('authorized_at');
$table->timestamp('expires_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'is_active']);
$table->index('mac_address');
});
}
public function down(): void
{
Schema::dropIfExists('unifi_portal_sessions');
Schema::dropIfExists('unifi_known_macs');
Schema::dropIfExists('unifi_vlan_mappings');
}
};

View File

@@ -0,0 +1,51 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('unifi_webhook_configs', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('url', 500);
$table->string('secret', 255)->nullable();
$table->boolean('is_active')->default(true);
$table->json('events'); // ['device_offline','wan_down',...]
$table->json('thresholds')->nullable(); // {"client_count":50,"cu_threshold":80,...}
$table->json('device_filter')->nullable(); // ["aa:bb:cc:dd:ee:ff",...]
$table->unsignedInteger('cooldown_minutes')->default(15);
$table->timestamps();
});
Schema::create('unifi_webhook_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('webhook_config_id')->constrained('unifi_webhook_configs')->cascadeOnDelete();
$table->string('event_type', 100);
$table->json('payload');
$table->unsignedSmallInteger('response_code')->nullable();
$table->text('response_body')->nullable();
$table->timestamp('fired_at');
$table->index(['webhook_config_id', 'event_type', 'fired_at']);
});
Schema::create('unifi_device_states', function (Blueprint $table) {
$table->id();
$table->string('device_mac', 17)->unique();
$table->string('device_name', 255)->nullable();
$table->boolean('was_online')->default(true);
$table->timestamp('last_seen_at')->nullable();
$table->timestamp('updated_at')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists('unifi_webhook_logs');
Schema::dropIfExists('unifi_webhook_configs');
Schema::dropIfExists('unifi_device_states');
}
};

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('unifi_ap_snapshots', function (Blueprint $table) {
$table->id();
$table->string('ap_mac', 17)->index();
$table->string('ap_name', 255)->nullable();
$table->unsignedSmallInteger('num_sta')->default(0);
$table->unsignedBigInteger('rx_bytes')->default(0);
$table->unsignedBigInteger('tx_bytes')->default(0);
$table->unsignedInteger('tx_retries')->default(0);
$table->unsignedInteger('tx_dropped')->default(0);
$table->unsignedInteger('rx_dropped')->default(0);
$table->unsignedInteger('tx_errors')->default(0);
$table->unsignedInteger('rx_errors')->default(0);
$table->unsignedTinyInteger('satisfaction')->nullable();
$table->unsignedTinyInteger('cu_2g')->nullable();
$table->unsignedTinyInteger('cu_5g')->nullable();
$table->unsignedTinyInteger('cu_6g')->nullable();
$table->timestamp('captured_at')->index();
});
}
public function down(): void
{
Schema::dropIfExists('unifi_ap_snapshots');
}
};

View File

@@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('unifi_client_snapshots', function (Blueprint $table) {
$table->id();
$table->string('mac', 17)->index();
$table->string('name', 255)->nullable();
$table->string('dev_cat', 64)->nullable()->index();
$table->string('os_name', 64)->nullable()->index();
$table->boolean('is_wired')->default(false);
$table->unsignedBigInteger('rx_bytes')->default(0);
$table->unsignedBigInteger('tx_bytes')->default(0);
$table->unsignedTinyInteger('satisfaction')->nullable();
$table->timestamp('captured_at')->index();
});
}
public function down(): void
{
Schema::dropIfExists('unifi_client_snapshots');
}
};

View File

@@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('unifi_client_snapshots', function (Blueprint $table) {
// Unifi reports signal strength in dBm (typically -30 to -95, negative).
// SMALLINT so a nullable signed value fits comfortably.
$table->smallInteger('signal')->nullable()->after('satisfaction');
});
}
public function down(): void
{
Schema::table('unifi_client_snapshots', function (Blueprint $table) {
$table->dropColumn('signal');
});
}
};