CluPilotCloud/app/Livewire/Admin/HostDetail.php

165 lines
5.5 KiB
PHP

<?php
namespace App\Livewire\Admin;
use App\Models\Host;
use App\Models\ProvisioningRun;
use App\Provisioning\Jobs\AdvanceRunJob;
use App\Provisioning\Jobs\CollectHostLoad;
use App\Services\Proxmox\HostLoadSeries;
use App\Support\PveVersion;
use Illuminate\Support\Collection;
use Livewire\Attributes\Layout;
use Livewire\Attributes\On;
use Livewire\Component;
#[Layout('layouts.admin')]
class HostDetail extends Component
{
public Host $host;
public function mount(Host $host): void
{
$this->host = $host;
// Nothing collected yet? Ask for it now rather than leaving the tiles
// empty until the minutely collector next comes round. Opening the page
// is exactly the moment somebody wants these figures, and waiting up to
// a minute for the first ones reads as "broken", not as "not yet".
//
// The job is ShouldBeUnique, so a page opened twice does not queue two.
$worthAsking = Host::query()->collectable()->whereKey($host->id)->exists();
if ($worthAsking && ! app(HostLoadSeries::class)->forHost($host)['available']) {
CollectHostLoad::dispatch();
}
}
/** Live refresh whenever any run advances (admins-only channel). */
#[On('echo-private:admin.runs,StepAdvanced')]
public function onStepAdvanced(): void
{
$this->host->refresh();
}
/** Adjust the capacity reserve (% of storage kept free for headroom). */
public function saveReserve(int $reserve): void
{
$this->authorize('hosts.manage');
$reserve = max(0, min(90, $reserve));
$this->host->update(['reserve_pct' => $reserve]);
$this->dispatch('notify', message: __('hosts.detail.reserve_saved'));
}
/**
* Drain / return a host: toggle between active and disabled. Disabled takes
* it out of placement (maintenance) without purging it — distinct from the
* destructive "remove host". Never touches a host mid-onboarding.
*/
public function toggleMaintenance(): void
{
$this->authorize('hosts.manage');
if ($this->host->status === 'active') {
$this->host->update(['status' => 'disabled']);
} elseif ($this->host->status === 'disabled') {
$this->host->update(['status' => 'active']);
}
}
public function retry(): void
{
$this->authorize('hosts.manage');
$run = $this->currentRun();
if ($run !== null && $run->status === ProvisioningRun::STATUS_FAILED) {
$run->update([
'status' => ProvisioningRun::STATUS_RUNNING,
'attempt' => 0,
'next_attempt_at' => now(),
'started_at' => now(), // reset the step timer so it doesn't re-time-out instantly
'error' => null,
]);
$this->host->update(['status' => 'onboarding']);
AdvanceRunJob::dispatch($run->uuid);
}
}
private function currentRun(): ?ProvisioningRun
{
return $this->host->runs()->latest('id')->first();
}
/** @return array<int, array{label: string, state: string}> */
private function buildSteps(?ProvisioningRun $run): array
{
$pipeline = config('provisioning.pipelines.host', []);
$current = $run?->current_step ?? 0;
$status = $run?->status;
$steps = [];
foreach ($pipeline as $index => $class) {
if ($status === ProvisioningRun::STATUS_COMPLETED || $index < $current) {
$state = 'done';
} elseif ($index === $current) {
$state = $status === ProvisioningRun::STATUS_FAILED ? 'failed' : 'running';
} else {
$state = 'pending';
}
$steps[] = ['label' => __(app($class)->label()), 'state' => $state];
}
return $steps;
}
/**
* The most recent reading that is actually a reading.
*
* Walked from the back rather than taking the last element: Proxmox omits
* the fields for a sample it has no data for, so the newest entry is quite
* often a gap — and a tile that showed 0 % because of it would be stating
* something about the host that nobody measured.
*
* @param array<int, ?float> $series
*/
private function latest(array $series): ?float
{
for ($i = count($series) - 1; $i >= 0; $i--) {
if ($series[$i] !== null) {
return $series[$i];
}
}
return null;
}
public function render()
{
$run = $this->currentRun();
/** @var Collection $events */
$events = $run
? $run->events()->latest('id')->limit(30)->get()
: collect();
$load = app(HostLoadSeries::class)->forHost($this->host);
return view('livewire.admin.host-detail', [
'run' => $run,
'steps' => $this->buildSteps($run),
'events' => $events,
'instances' => $this->host->instances()->latest('id')->get(),
'health' => $this->host->healthState(),
'load' => $load,
// Eine Zahl je Kachel, aus derselben Reihe, die die Kachel zeichnet.
'now' => [
'cpu' => $this->latest($load['cpu']),
'ram' => $this->latest($load['ram']),
'netin' => $this->latest($load['netin']),
'netout' => $this->latest($load['netout']),
],
'version' => PveVersion::parse($this->host->pve_version),
]);
}
}