diff --git a/README.md b/README.md index cd7e9b5..794ad62 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,10 @@ It uses [Oxidized 0.37.0](https://github.com/ytti/oxidized) as its unmodified collection engine and adds inventory, authentication, audit logs, permanent Git history, notifications, and encrypted redundancy. +

+ NetKeep dashboard in dark mode with fictional example network data +

+ > [!IMPORTANT] > In safe mode, NetKeep collects, compares, and exports configurations without > applying changes to equipment. This guarantee is conditional on reviewed diff --git a/config/netkeep.php b/config/netkeep.php index 3c16ec5..99a9b9e 100644 --- a/config/netkeep.php +++ b/config/netkeep.php @@ -8,6 +8,9 @@ 'version' => env('NETKEEP_VERSION', 'dev'), 'source_url' => env('NETKEEP_SOURCE_URL', 'https://github.com/lrqnet/NetKeep'), + 'readme_preview' => [ + 'password' => env('NETKEEP_README_DEMO_PASSWORD'), + ], 'oxidized' => [ 'url' => env('OXIDIZED_URL', 'http://oxidized:8888'), 'token' => env('OXIDIZED_INTERNAL_TOKEN'), diff --git a/database/seeders/ReadmeDashboardSeeder.php b/database/seeders/ReadmeDashboardSeeder.php new file mode 100644 index 0000000..c933cb4 --- /dev/null +++ b/database/seeders/ReadmeDashboardSeeder.php @@ -0,0 +1,149 @@ +updateOrCreate( + ['email' => 'dashboard@example.test'], + [ + 'name' => 'Documentation Owner', + 'password' => Hash::make($password), + 'role' => UserRole::Owner, + 'locale' => 'en', + 'is_active' => true, + 'email_verified_at' => now(), + ], + ); + + Organization::query()->updateOrCreate( + ['slug' => 'example-network-operations'], + [ + 'name' => 'Example Network Operations', + 'locale' => 'en', + 'timezone' => 'UTC', + 'settings' => [ + 'default_backup_interval' => 3600, + 'default_timeout' => 20, + 'full_backup_retention_days' => 30, + ], + 'setup_completed_at' => now(), + ], + ); + + $core = DeviceGroup::query()->updateOrCreate( + ['name' => 'core'], + ['description' => 'Example core network devices'], + ); + $edge = DeviceGroup::query()->updateOrCreate( + ['name' => 'edge'], + ['description' => 'Example edge network devices'], + ); + $primary = Site::query()->updateOrCreate( + ['name' => 'Example Datacenter One'], + ['location' => 'Example City'], + ); + $secondary = Site::query()->updateOrCreate( + ['name' => 'Example Branch West'], + ['location' => 'Example Region'], + ); + + $this->seedDevice($owner, $primary, $core, 'Example-Core-01', '192.0.2.10', 'ios', DeviceStatus::Healthy, 12); + $this->seedDevice($owner, $primary, $core, 'Example-Core-02', '192.0.2.11', 'junos', DeviceStatus::Healthy, 18); + $this->seedDevice($owner, $secondary, $edge, 'Example-Edge-01', '192.0.2.12', 'routeros', DeviceStatus::Healthy, 24); + $this->seedDevice($owner, $secondary, $edge, 'Example-Edge-02', '192.0.2.13', 'routeros', DeviceStatus::Healthy, 31); + $this->seedDevice($owner, $primary, $core, 'Example-Firewall-01', '192.0.2.14', 'fortios', DeviceStatus::Healthy, 42); + $this->seedDevice($owner, $secondary, $edge, 'Example-Access-01', '192.0.2.15', 'iosxe', DeviceStatus::Healthy, 55); + $this->seedDevice($owner, $secondary, $edge, 'Example-Edge-03', '192.0.2.16', 'routeros', DeviceStatus::Failing, 125); + $this->seedDevice($owner, $primary, $edge, 'Example-Access-02', '192.0.2.17', 'ios', DeviceStatus::Pending, 30); + + Device::query() + ->whereIn('name', [ + 'Example-Core-01', + 'Example-Core-02', + 'Example-Edge-01', + 'Example-Firewall-01', + 'Example-Access-01', + ]) + ->get() + ->each(function (Device $device): void { + BackupRun::query()->updateOrCreate( + [ + 'device_id' => $device->id, + 'git_commit' => str_pad((string) $device->id, 40, '0', STR_PAD_LEFT), + ], + [ + 'status' => 'success', + 'started_at' => now()->subMinutes(20), + 'finished_at' => now()->subMinutes(10), + 'changed' => true, + ], + ); + }); + } + + private function seedDevice( + User $owner, + Site $site, + DeviceGroup $group, + string $name, + string $address, + string $model, + DeviceStatus $status, + int $minutesAgo, + ): void { + $device = Device::query()->updateOrCreate( + ['name' => $name], + [ + 'ip_address' => $address, + 'port' => 22, + 'transport' => 'ssh', + 'oxidized_model' => $model, + 'site_id' => $site->id, + 'device_group_id' => $group->id, + 'backup_interval' => 3600, + 'timeout' => 20, + 'enabled' => true, + 'status' => $status, + 'approval_status' => DeviceApprovalStatus::Approved, + 'approved_by' => $owner->id, + 'approved_at' => now()->subDays(7), + 'approved_resolved_addresses' => [$address], + 'ssh_host_key' => 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDocumentationOnlyKey', + 'ssh_host_key_fingerprint' => 'SHA256:documentation-only-key', + 'last_backup_at' => now()->subMinutes($minutesAgo), + 'last_success_at' => $status === DeviceStatus::Healthy + ? now()->subMinutes($minutesAgo) + : null, + 'next_collection_at' => now()->addDay(), + 'consecutive_failures' => $status === DeviceStatus::Failing ? 1 : 0, + ], + ); + + $device->update([ + 'approval_fingerprint' => app(DeviceApprovalService::class)->fingerprint($device), + ]); + } +} diff --git a/docs/assets/netkeep-dashboard-preview.webp b/docs/assets/netkeep-dashboard-preview.webp new file mode 100644 index 0000000..a4be91a Binary files /dev/null and b/docs/assets/netkeep-dashboard-preview.webp differ diff --git a/tests/Feature/ReadmeDashboardSeederTest.php b/tests/Feature/ReadmeDashboardSeederTest.php new file mode 100644 index 0000000..df07a69 --- /dev/null +++ b/tests/Feature/ReadmeDashboardSeederTest.php @@ -0,0 +1,54 @@ +set('netkeep.readme_preview.password', null); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('NETKEEP_README_DEMO_PASSWORD is required.'); + + $this->seed(ReadmeDashboardSeeder::class); + } + + public function test_it_seeds_only_fictional_dashboard_data(): void + { + config()->set('netkeep.readme_preview.password', 'documentation-demo-password'); + + $this->seed(ReadmeDashboardSeeder::class); + + $this->assertDatabaseCount('devices', 8); + $this->assertDatabaseCount('backup_runs', 5); + $this->assertDatabaseHas('organizations', [ + 'slug' => 'example-network-operations', + ]); + $this->assertDatabaseHas('users', [ + 'email' => 'dashboard@example.test', + ]); + $this->assertSame(0, Device::query() + ->whereNot('ip_address', 'like', '192.0.2.%') + ->count()); + $this->assertSame(8, Device::query() + ->where('approval_status', DeviceApprovalStatus::Approved) + ->where('enabled', true) + ->count()); + $this->assertSame(1, User::query() + ->where('email', 'dashboard@example.test') + ->count()); + $this->assertSame(5, BackupRun::query()->count()); + } +}