feat: multi-server switcher + fleet context
- WithFleetContext concern: resolves the active server (session, default first online) + the fleet. - ServerSwitcher Livewire component in the persistent sidebar: dropdown of the fleet; selecting persists active_server_id in the session and re-navigates so panels reflect the choice. - Dashboard reacts: resource rings + panel subtitles follow the active server. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
cd5f7c107e
commit
db0a1de92f
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Concerns;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared fleet context for pages: the active server (from session, default
|
||||||
|
* first) plus the whole fleet. Real metrics come from the SSH layer later.
|
||||||
|
*/
|
||||||
|
trait WithFleetContext
|
||||||
|
{
|
||||||
|
public function fleet(): Collection
|
||||||
|
{
|
||||||
|
return Server::orderBy('name')->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activeServer(): ?Server
|
||||||
|
{
|
||||||
|
$fleet = $this->fleet();
|
||||||
|
|
||||||
|
return $fleet->firstWhere('id', session('active_server_id'))
|
||||||
|
?? $fleet->firstWhere('status', 'online')
|
||||||
|
?? $fleet->first();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Livewire\Concerns\WithFleetContext;
|
||||||
use Livewire\Attributes\Layout;
|
use Livewire\Attributes\Layout;
|
||||||
use Livewire\Attributes\Title;
|
use Livewire\Attributes\Title;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -10,6 +11,8 @@ use Livewire\Component;
|
||||||
#[Title('Clusev — Dashboard')]
|
#[Title('Clusev — Dashboard')]
|
||||||
class Dashboard extends Component
|
class Dashboard extends Component
|
||||||
{
|
{
|
||||||
|
use WithFleetContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mock fleet data. Replaced by the SSH layer (phpseclib) + Reverb later.
|
* Mock fleet data. Replaced by the SSH layer (phpseclib) + Reverb later.
|
||||||
* Shapes mirror what MetricsPoller/FleetService will return.
|
* Shapes mirror what MetricsPoller/FleetService will return.
|
||||||
|
|
@ -52,6 +55,6 @@ class Dashboard extends Component
|
||||||
|
|
||||||
public function render()
|
public function render()
|
||||||
{
|
{
|
||||||
return view('livewire.dashboard');
|
return view('livewire.dashboard', ['active' => $this->activeServer()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use App\Models\Server;
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sidebar fleet switcher. Lives in the persistent layout, so it survives
|
||||||
|
* wire:navigate. Selecting a server stores it in the session and reloads the
|
||||||
|
* current page so every panel reflects the new active server.
|
||||||
|
*/
|
||||||
|
class ServerSwitcher extends Component
|
||||||
|
{
|
||||||
|
public function select(string $uuid): void
|
||||||
|
{
|
||||||
|
$server = Server::where('uuid', $uuid)->firstOrFail();
|
||||||
|
|
||||||
|
session(['active_server_id' => $server->id]);
|
||||||
|
|
||||||
|
$this->redirect(request()->header('Referer') ?: route('dashboard'), navigate: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
$servers = Server::orderBy('name')->get();
|
||||||
|
$active = $servers->firstWhere('id', session('active_server_id'))
|
||||||
|
?? $servers->firstWhere('status', 'online')
|
||||||
|
?? $servers->first();
|
||||||
|
|
||||||
|
return view('livewire.server-switcher', [
|
||||||
|
'servers' => $servers,
|
||||||
|
'active' => $active,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,17 +16,9 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Server switcher (mock) --}}
|
{{-- Server switcher --}}
|
||||||
<div class="shrink-0 border-b border-line p-3">
|
<div class="shrink-0 border-b border-line p-3">
|
||||||
<button type="button"
|
<livewire:server-switcher />
|
||||||
class="flex min-h-11 w-full items-center gap-2.5 rounded-md border border-line bg-inset px-3 py-2 text-left transition-colors hover:border-line-strong">
|
|
||||||
<x-status-dot status="online" :ping="true" />
|
|
||||||
<span class="min-w-0 flex-1">
|
|
||||||
<span class="block truncate text-sm text-ink">web-01</span>
|
|
||||||
<span class="block truncate font-mono text-[11px] text-ink-3">10.10.90.136</span>
|
|
||||||
</span>
|
|
||||||
<x-icon name="switcher" class="h-4 w-4 shrink-0 text-ink-3" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Nav --}}
|
{{-- Nav --}}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
{{-- Live chart (2/3) + resource rings (1/3) --}}
|
{{-- Live chart (2/3) + resource rings (1/3) --}}
|
||||||
<div class="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||||
<x-panel title="Live-Auslastung" subtitle="web-01 · CPU %" class="lg:col-span-2">
|
<x-panel title="Live-Auslastung" :subtitle="($active?->name ?? '—') . ' · CPU %'" class="lg:col-span-2">
|
||||||
<x-slot:actions>
|
<x-slot:actions>
|
||||||
<x-badge tone="cyan">Reverb</x-badge>
|
<x-badge tone="cyan">Reverb</x-badge>
|
||||||
</x-slot:actions>
|
</x-slot:actions>
|
||||||
|
|
@ -57,11 +57,11 @@
|
||||||
</div>
|
</div>
|
||||||
</x-panel>
|
</x-panel>
|
||||||
|
|
||||||
<x-panel title="Ressourcen" subtitle="web-01">
|
<x-panel title="Ressourcen" :subtitle="$active?->name ?? '—'">
|
||||||
<div class="grid grid-cols-3 gap-2">
|
<div class="grid grid-cols-3 gap-2">
|
||||||
<x-ring :value="$web['cpu']" label="CPU" :tone="$ringTone($web['cpu'])" />
|
<x-ring :value="$active?->cpu ?? 0" label="CPU" :tone="$ringTone($active?->cpu ?? 0)" />
|
||||||
<x-ring :value="$web['mem']" label="RAM" :tone="$ringTone($web['mem'])" />
|
<x-ring :value="$active?->mem ?? 0" label="RAM" :tone="$ringTone($active?->mem ?? 0)" />
|
||||||
<x-ring :value="$disk" label="Disk" :tone="$ringTone($disk)" />
|
<x-ring :value="$active?->disk ?? 0" label="Disk" :tone="$ringTone($active?->disk ?? 0)" />
|
||||||
</div>
|
</div>
|
||||||
</x-panel>
|
</x-panel>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
</div>
|
</div>
|
||||||
</x-panel>
|
</x-panel>
|
||||||
|
|
||||||
<x-panel title="systemd-Dienste" subtitle="web-01" :padded="false">
|
<x-panel title="systemd-Dienste" :subtitle="$active?->name" :padded="false">
|
||||||
<div class="divide-y divide-line">
|
<div class="divide-y divide-line">
|
||||||
@foreach ($services as $svc)
|
@foreach ($services as $svc)
|
||||||
<div class="flex items-center gap-3 px-4 py-2.5 sm:px-5">
|
<div class="flex items-center gap-3 px-4 py-2.5 sm:px-5">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
<div x-data="{ open: false }" @click.outside="open = false" @keydown.escape="open = false" class="relative">
|
||||||
|
<button type="button" @click="open = ! open"
|
||||||
|
class="flex min-h-11 w-full items-center gap-2.5 rounded-md border border-line bg-inset px-3 py-2 text-left transition-colors hover:border-line-strong"
|
||||||
|
:aria-expanded="open" aria-haspopup="listbox">
|
||||||
|
@if ($active)
|
||||||
|
<x-status-dot :status="$active->status" :ping="$active->status === 'online'" />
|
||||||
|
<span class="min-w-0 flex-1">
|
||||||
|
<span class="block truncate font-mono text-sm text-ink">{{ $active->name }}</span>
|
||||||
|
<span class="block truncate font-mono text-[11px] text-ink-3">{{ $active->ip }}</span>
|
||||||
|
</span>
|
||||||
|
@else
|
||||||
|
<span class="min-w-0 flex-1 text-sm text-ink-3">Kein Server</span>
|
||||||
|
@endif
|
||||||
|
<x-icon name="switcher" class="h-4 w-4 shrink-0 text-ink-3" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div x-show="open" x-cloak x-transition.origin.top
|
||||||
|
class="absolute inset-x-0 top-full z-50 mt-1 overflow-hidden rounded-md border border-line bg-raised shadow-pop"
|
||||||
|
role="listbox">
|
||||||
|
<div class="max-h-72 overflow-y-auto p-1">
|
||||||
|
@foreach ($servers as $server)
|
||||||
|
<button type="button" wire:click="select('{{ $server->uuid }}')" @click="open = false"
|
||||||
|
@class([
|
||||||
|
'flex min-h-11 w-full items-center gap-2.5 rounded-sm px-2.5 py-1.5 text-left transition-colors hover:bg-surface',
|
||||||
|
'bg-accent/10' => $active && $server->id === $active->id,
|
||||||
|
])
|
||||||
|
role="option" :aria-selected="{{ $active && $server->id === $active->id ? 'true' : 'false' }}">
|
||||||
|
<x-status-dot :status="$server->status" :ping="$server->status === 'online'" />
|
||||||
|
<span class="min-w-0 flex-1">
|
||||||
|
<span class="block truncate font-mono text-sm text-ink">{{ $server->name }}</span>
|
||||||
|
<span class="block truncate font-mono text-[11px] text-ink-3">{{ $server->ip }} · {{ $server->os }}</span>
|
||||||
|
</span>
|
||||||
|
@if ($active && $server->id === $active->id)
|
||||||
|
<x-icon name="activity" class="h-3.5 w-3.5 shrink-0 text-accent" />
|
||||||
|
@endif
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Reference in New Issue