diff --git a/app/Livewire/Admin/HostDetail.php b/app/Livewire/Admin/HostDetail.php index 19c753c..94defc2 100644 --- a/app/Livewire/Admin/HostDetail.php +++ b/app/Livewire/Admin/HostDetail.php @@ -85,6 +85,8 @@ class HostDetail extends Component 'run' => $run, 'steps' => $this->buildSteps($run), 'events' => $events, + 'instances' => $this->host->instances()->latest('id')->get(), + 'health' => $this->host->healthState(), ]); } } diff --git a/app/Livewire/Admin/Hosts.php b/app/Livewire/Admin/Hosts.php index e5abe12..62ce79b 100644 --- a/app/Livewire/Admin/Hosts.php +++ b/app/Livewire/Admin/Hosts.php @@ -2,17 +2,52 @@ namespace App\Livewire\Admin; +use App\Models\Datacenter; use App\Models\Host; use Livewire\Attributes\Layout; +use Livewire\Attributes\Url; use Livewire\Component; #[Layout('layouts.admin')] class Hosts extends Component { + #[Url(as: 'q')] + public string $search = ''; + + #[Url] + public string $datacenter = ''; + + #[Url] + public string $status = ''; + + public function updated(): void + { + // no-op; keeps the query string in sync as filters change + } + + public function clearFilters(): void + { + $this->reset('search', 'datacenter', 'status'); + } + public function render() { + $hosts = Host::query() + ->withCount('instances') + ->when($this->search !== '', function ($q) { + $term = '%'.$this->search.'%'; + $q->where(fn ($w) => $w->where('name', 'like', $term)->orWhere('public_ip', 'like', $term)); + }) + ->when($this->datacenter !== '', fn ($q) => $q->where('datacenter', $this->datacenter)) + ->when($this->status !== '', fn ($q) => $q->where('status', $this->status)) + ->orderBy('datacenter')->orderBy('name') + ->get(); + return view('livewire.admin.hosts', [ - 'hosts' => Host::query()->orderBy('datacenter')->orderBy('name')->get(), + 'hosts' => $hosts, + 'datacenters' => Datacenter::query()->orderBy('name')->get(), + 'statuses' => ['pending', 'onboarding', 'active', 'error', 'disabled'], + 'total' => Host::query()->count(), ]); } } diff --git a/app/Models/Host.php b/app/Models/Host.php index e5eb059..2a29f52 100644 --- a/app/Models/Host.php +++ b/app/Models/Host.php @@ -81,6 +81,32 @@ class Host extends Model implements ProvisioningSubject return max(0, $this->freeGb() - $this->committedGb()); } + /** Used-storage percentage (committed vs committable free), 0–100. */ + public function usedPct(): int + { + $free = $this->freeGb(); + + return $free > 0 ? min(100, (int) round($this->committedGb() / $free * 100)) : 0; + } + + /** + * Heartbeat health from last_seen_at: online (≤5 min), stale (≤30 min), + * offline (older or never seen). Drives the health dot in the console. + */ + public function healthState(): string + { + if ($this->last_seen_at === null) { + return 'offline'; + } + $minutes = $this->last_seen_at->diffInMinutes(now()); + + return match (true) { + $minutes <= 5 => 'online', + $minutes <= 30 => 'stale', + default => 'offline', + }; + } + /** * Placement (spec §1): first active host in the datacenter with enough free * committable storage. Cluster is ignored in v1.0. diff --git a/lang/de/hosts.php b/lang/de/hosts.php index d318246..93dfb4c 100644 --- a/lang/de/hosts.php +++ b/lang/de/hosts.php @@ -5,6 +5,25 @@ return [ 'subtitle' => 'Proxmox-Hosts und Kapazität.', 'add' => 'Host hinzufügen', 'empty' => 'Noch keine Hosts. Füge deinen ersten Server hinzu.', + 'no_matches' => 'Keine Hosts für diese Filter.', + 'search_placeholder' => 'Nach Name oder IP suchen …', + 'all_datacenters' => 'Alle Rechenzentren', + 'all_statuses' => 'Alle Status', + 'clear' => 'Zurücksetzen', + 'count' => ':shown von :total', + 'col' => [ + 'host' => 'Host', + 'datacenter' => 'RZ', + 'health' => 'Zustand', + 'instances' => 'Instanzen', + 'capacity' => 'Kapazität', + 'status' => 'Status', + ], + 'health' => [ + 'online' => 'Online', + 'stale' => 'Verzögert', + 'offline' => 'Offline', + ], 'create_title' => 'Host hinzufügen', 'create_sub' => 'Frischen Server (Debian, nur Root) automatisch als Proxmox-Host aufnehmen.', @@ -45,6 +64,28 @@ return [ 'disabled' => 'Deaktiviert', ], + 'detail' => [ + 'health' => 'Zustand', + 'storage' => 'Speicher', + 'compute' => 'Rechenleistung', + 'node' => 'Node', + 'instances' => 'Instanzen', + 'hosted' => 'Gehostete Instanzen', + 'no_instances' => 'Auf diesem Host laufen noch keine Instanzen.', + 'last_seen' => 'Zuletzt gesehen :time', + 'never_seen' => 'Noch nie gesehen', + 'committed' => ':gb GB belegt', + 'reserve' => ':pct % Reserve', + ], + + 'istatus' => [ + 'active' => 'Aktiv', + 'provisioning' => 'Bereitstellung', + 'failed' => 'Fehler', + 'suspended' => 'Ausgesetzt', + 'cancellation_scheduled' => 'Kündigung', + ], + 'progress' => 'Fortschritt', 'events' => 'Ereignisse', 'no_run' => 'Für diesen Host läuft keine Bereitstellung.', diff --git a/lang/en/hosts.php b/lang/en/hosts.php index 72f5a72..e0def10 100644 --- a/lang/en/hosts.php +++ b/lang/en/hosts.php @@ -5,6 +5,25 @@ return [ 'subtitle' => 'Proxmox hosts and capacity.', 'add' => 'Add host', 'empty' => 'No hosts yet. Add your first server.', + 'no_matches' => 'No hosts match these filters.', + 'search_placeholder' => 'Search by name or IP …', + 'all_datacenters' => 'All datacenters', + 'all_statuses' => 'All statuses', + 'clear' => 'Clear', + 'count' => ':shown of :total', + 'col' => [ + 'host' => 'Host', + 'datacenter' => 'DC', + 'health' => 'Health', + 'instances' => 'Instances', + 'capacity' => 'Capacity', + 'status' => 'Status', + ], + 'health' => [ + 'online' => 'Online', + 'stale' => 'Stale', + 'offline' => 'Offline', + ], 'create_title' => 'Add host', 'create_sub' => 'Onboard a fresh server (Debian, root only) as a Proxmox host automatically.', @@ -45,6 +64,28 @@ return [ 'disabled' => 'Disabled', ], + 'detail' => [ + 'health' => 'Health', + 'storage' => 'Storage', + 'compute' => 'Compute', + 'node' => 'Node', + 'instances' => 'Instances', + 'hosted' => 'Hosted instances', + 'no_instances' => 'No instances running on this host yet.', + 'last_seen' => 'Last seen :time', + 'never_seen' => 'Never seen', + 'committed' => ':gb GB committed', + 'reserve' => ':pct % reserve', + ], + + 'istatus' => [ + 'active' => 'Active', + 'provisioning' => 'Provisioning', + 'failed' => 'Failed', + 'suspended' => 'Suspended', + 'cancellation_scheduled' => 'Cancelling', + ], + 'progress' => 'Progress', 'events' => 'Events', 'no_run' => 'No provisioning run for this host.', diff --git a/resources/views/livewire/admin/host-detail.blade.php b/resources/views/livewire/admin/host-detail.blade.php index 6b20d0b..bdfd1c9 100644 --- a/resources/views/livewire/admin/host-detail.blade.php +++ b/resources/views/livewire/admin/host-detail.blade.php @@ -27,21 +27,93 @@ - @if ($host->status === 'active') -
{{ __('hosts.'.$key) }}
-{{ $value }}
-{{ __('hosts.detail.health') }}
++ + {{ __('hosts.health.'.$health) }} +
++ {{ $host->last_seen_at ? __('hosts.detail.last_seen', ['time' => $host->last_seen_at->diffForHumans()]) : __('hosts.detail.never_seen') }} +
{{ __('hosts.detail.storage') }}
+ @if ($host->total_gb) +{{ $host->availableGb() }} / {{ $host->freeGb() }} GB {{ __('hosts.free') }}
+{{ __('hosts.detail.committed', ['gb' => $host->committedGb()]) }} · {{ __('hosts.detail.reserve', ['pct' => $host->reserve_pct]) }}
+ @else +{{ __('hosts.unknown') }}
+ @endif +{{ __('hosts.detail.compute') }}
+{{ $host->cpu_cores ?? '—' }}
+{{ __('hosts.meta.cores') }}
+{{ $host->total_ram_mb ? round($host->total_ram_mb / 1024).' GB' : '—' }}
+{{ __('hosts.meta.ram') }}
+{{ __('hosts.'.$key) }}
+{{ $value }}
+{{ __('hosts.detail.no_instances') }}
+ @else +| {{ $inst->subdomain ?? '—' }} | +{{ __('billing.plan.'.$inst->plan) }} | +{{ $inst->disk_gb }} GB | +
{{ __('hosts.empty') }}
+{{ $total === 0 ? __('hosts.empty') : __('hosts.no_matches') }}
{{ $host->name }}
-{{ $host->public_ip }} · {{ $host->datacenter }}
-| {{ __('hosts.col.host') }} | +{{ __('hosts.col.datacenter') }} | +{{ __('hosts.col.health') }} | +{{ __('hosts.col.instances') }} | +{{ __('hosts.col.capacity') }} | +{{ __('hosts.col.status') }} | +
|---|---|---|---|---|---|
|
+
+ |
+ {{ $host->datacenter }} | ++ + {{ __('hosts.health.'.$health) }} + + | +{{ $host->instances_count }} | +
+ @if ($host->total_gb)
+
+
+ @else
+ {{ __('hosts.unknown') }}
+ @endif
+
+
+
+ {{ $host->availableGb() }} / {{ $host->freeGb() }} GB
+ |
+