uuid → counts */ public array $counts = []; /** @var array uuid → last patch result */ public array $results = []; public bool $ready = false; public function mount(): void { abort_unless(Auth::user()?->can('operate'), 403); } public function title(): string { return __('patch.title'); } private function gate(): void { abort_unless(Auth::user()?->can('operate'), 403); } public function scan(MaintenanceService $maint): void { $this->gate(); $this->counts = []; foreach (Server::withActiveCredential()->orderBy('name')->get() as $server) { try { $this->counts[$server->uuid] = $maint->updateCounts($server); } catch (Throwable $e) { $this->counts[$server->uuid] = ['error' => $e->getMessage()]; } } $this->ready = true; } public function patch(string $uuid): void { $this->gate(); $server = Server::withActiveCredential()->where('uuid', $uuid)->firstOrFail(); $c = $this->counts[$uuid] ?? []; $pending = $c['pending'] ?? '?'; $this->dispatch('openModal', component: 'modals.confirm-action', arguments: [ 'heading' => __('patch.confirm_heading'), 'body' => __('patch.confirm_body', ['server' => $server->name, 'count' => $pending]), 'confirmLabel' => __('patch.apply'), 'danger' => true, 'icon' => 'rotate', 'notify' => '', // the handler reports the outcome 'token' => ConfirmToken::issue('patchApply', ['uuid' => $server->uuid], 'patch.apply', $server->name, $server->id), ], ); } #[On('patchApply')] public function applyPatch(string $confirmToken, MaintenanceService $maint): void { $this->gate(); try { $payload = ConfirmToken::consume($confirmToken, 'patchApply'); } catch (InvalidConfirmToken) { return; // forged / replayed — the modal already audited patch.apply } $server = Server::where('uuid', $payload['params']['uuid'])->first(); if (! $server) { return; } $res = $maint->applyUpgrades($server); $res['output'] = mb_scrub($res['output'], 'UTF-8'); // remote output into a Livewire prop → scrub invalid bytes $this->results[$server->uuid] = $res; // Refresh this server's counts so the badge reflects the post-patch state. try { $this->counts[$server->uuid] = $maint->updateCounts($server); } catch (Throwable) { // keep the old counts if the re-scan fails } $this->dispatch('notify', message: $res['ok'] ? __('patch.patched', ['server' => $server->name]) : __('patch.patch_failed', ['error' => $res['output']])); } public function render(): View { // Fleet roll-up (only over successfully-scanned servers). $scored = array_filter($this->counts, fn ($c) => isset($c['pending']) && $c['pending'] !== null); $totalPending = array_sum(array_map(fn ($c) => $c['pending'], $scored)); $totalSecurity = array_sum(array_map(fn ($c) => $c['security'] ?? 0, $scored)); return view('livewire.patch.index', [ 'servers' => Server::withActiveCredential()->orderBy('name')->get(['uuid', 'name']), 'totalPending' => (int) $totalPending, 'totalSecurity' => (int) $totalSecurity, ])->title($this->title()); } }