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.
+
+
+
+
> [!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());
+ }
+}