diff --git a/app/resources/css/app.css b/app/resources/css/app.css index a0fef1f..f36fd09 100644 --- a/app/resources/css/app.css +++ b/app/resources/css/app.css @@ -1001,4 +1001,32 @@ transition: background .15s, color .15s; } .clu-row-actions button:hover { background: rgba(255,255,255,0.7); color: var(--color-fg); } + + /* Server card (Server.html) */ + .clu-server-card { + background: var(--color-glass-strong); + border: 1px solid var(--color-glass-border); + border-radius: var(--radius); + padding: 18px; + box-shadow: var(--shadow-glass), var(--shadow-glass-inset); + backdrop-filter: blur(20px) saturate(170%); + -webkit-backdrop-filter: blur(20px) saturate(170%); + display: flex; flex-direction: column; gap: 14px; + } + .clu-server-head { display: flex; align-items: center; gap: 12px; } + .clu-server-flag { font-size: 22px; line-height: 1; } + .clu-server-name { font-weight: 600; font-size: 14px; } + .clu-server-meta { font-size: 11.5px; color: var(--color-muted); font-family: var(--font-mono); } + .clu-gauge-row { display: flex; flex-direction: column; gap: 9px; } + .clu-gauge { + display: flex; align-items: center; gap: 10px; + } + .clu-gauge-label { font-size: 11px; color: var(--color-muted); width: 38px; flex: none; font-family: var(--font-mono); text-transform: uppercase; letter-spacing: 0.05em; } + .clu-gauge-bar { flex: 1; height: 6px; border-radius: 4px; background: rgba(30,35,60,0.10); overflow: hidden; } + .clu-gauge-bar > span { display: block; height: 100%; border-radius: 4px; background: var(--color-accent); } + .clu-gauge-bar > span.warn { background: var(--color-warning); } + .clu-gauge-bar > span.bad { background: var(--color-danger); } + .clu-gauge-val { font-family: var(--font-mono); font-size: 11px; color: var(--color-fg); width: 36px; text-align: right; } + .clu-server-spec { display: flex; gap: 8px; font-family: var(--font-mono); font-size: 11px; color: var(--color-muted); } + .clu-server-foot { display: flex; align-items: center; justify-content: space-between; padding-top: 10px; border-top: 1px solid var(--color-hairline); } } diff --git a/app/resources/views/livewire/pages/servers/index.blade.php b/app/resources/views/livewire/pages/servers/index.blade.php new file mode 100644 index 0000000..438dda7 --- /dev/null +++ b/app/resources/views/livewire/pages/servers/index.blade.php @@ -0,0 +1,174 @@ +envFilter = $env; + } + + public function with(): array + { + $query = Server::query()->withCount('sites'); + + if ($this->envFilter !== 'all') { + $query->where('env', $this->envFilter); + } + if ($this->regionFilter !== '') { + $query->where('region', $this->regionFilter); + } + + return [ + 'servers' => $query->orderBy('name')->get(), + 'totalServers' => Server::count(), + 'okCount' => Server::where('status', 'ok')->count(), + 'avgCpu' => (int) Server::avg('cpu_load_pct'), + 'avgRam' => (int) Server::avg('ram_used_pct'), + 'maintenance' => Server::where('status', 'maintenance')->count(), + 'regions' => Server::query()->select('region')->distinct()->pluck('region'), + 'prodCount' => Server::where('env', 'prod')->count(), + 'edgeCount' => Server::where('env', 'edge')->count(), + 'stagingCount' => Server::where('env', 'staging')->count(), + ]; + } +}; ?> + +
+ {{-- ───── Topbar ───── --}} +
+
+

Server

+
{{ $totalServers }} nodes · cluster acme
+
+ {{ $okCount }}/{{ $totalServers }} aktiv +
+ +
+ + {{-- ───── Metric tiles ───── --}} +
+
+
+ Aktive Nodes + +
+
{{ $okCount }}/{{ $totalServers }}
+
{{ $maintenance }} im Maintenance
+
+
+
+ Ø CPU Load + +
+
{{ $avgCpu }}%
+
across {{ $totalServers }} nodes
+
+
+
+ Ø RAM + +
+
{{ $avgRam }}%
+
used
+
+
+
+ Maintenance + +
+
{{ $maintenance }}
+
geplant
+
+
+ + {{-- ───── Toolbar ───── --}} +
+
+
+ + + + +
+
+ +
+
+
+ + {{-- ───── Server cards grid ───── --}} +
+ @forelse ($servers as $server) +
+
+
{{ $server->country_flag }}
+
+
{{ $server->name }}
+
{{ $server->region }} · {{ $server->env }}
+
+ {{ strtoupper($server->status) }} +
+ +
+ {{ $server->cpu_cores }} vCPU + · + {{ $server->ram_gb }} GB RAM + · + {{ $server->disk_gb }} GB SSD +
+ +
+
+ CPU +
+ {{ $server->cpu_load_pct }}% +
+
+ RAM +
+ {{ $server->ram_used_pct }}% +
+
+ DISK +
+ {{ $server->disk_used_pct }}% +
+
+ +
+ {{ $server->sites_count }} sites +
+ + +
+
+
+ @empty +
Keine Server gefunden.
+ @endforelse +
+
diff --git a/app/routes/web.php b/app/routes/web.php index 90b953c..eebbc24 100644 --- a/app/routes/web.php +++ b/app/routes/web.php @@ -15,8 +15,8 @@ Route::view('profile', 'profile') /* ───── Placeholder named routes for sidebar links (real Volt pages land later). ───── */ Route::middleware(['auth'])->group(function () { - Volt::route('sites', 'pages.sites.index')->name('sites.index'); - Route::view('servers', 'placeholder', ['title' => 'Server'])->name('servers.index'); + Volt::route('sites', 'pages.sites.index')->name('sites.index'); + Volt::route('servers', 'pages.servers.index')->name('servers.index'); Route::view('backups', 'placeholder', ['title' => 'Backups'])->name('backups.index'); Route::view('security', 'placeholder', ['title' => 'Sicherheit'])->name('security.index'); Route::view('team', 'placeholder', ['title' => 'Team'])->name('team.index'); diff --git a/app/tests/Feature/Pages/ServersPageTest.php b/app/tests/Feature/Pages/ServersPageTest.php new file mode 100644 index 0000000..e6c96f1 --- /dev/null +++ b/app/tests/Feature/Pages/ServersPageTest.php @@ -0,0 +1,39 @@ +assertRedirect(route('login')); +}); + +it('renders servers chrome + grid', function () { + $user = User::factory()->create(['email_verified_at' => now()]); + $s = Server::factory()->create(['name' => 'eu-prod-77', 'env' => 'prod']); + Site::factory()->count(4)->create(['server_id' => $s->id]); + + actingAs($user)->get(route('servers.index')) + ->assertStatus(200) + ->assertSeeText('Server') + ->assertSeeText('Aktive Nodes') + ->assertSeeText('Ø CPU Load') + ->assertSeeText('Ø RAM') + ->assertSeeText('eu-prod-77') + ->assertSeeText('4 sites') + ->assertSee('clu-server-card', false) + ->assertSee('clu-gauge-bar', false); +}); + +it('filters by env segment', function () { + Server::factory()->create(['env' => 'prod', 'name' => 'p-1']); + Server::factory()->create(['env' => 'edge', 'name' => 'e-1']); + + livewire('pages.servers.index') + ->call('setEnv', 'edge') + ->assertSee('e-1') + ->assertDontSee('p-1'); +});