165 lines
5.5 KiB
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),
|
|
]);
|
|
}
|
|
}
|