diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index 53a63cc6ee..6085320080 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -7,6 +7,7 @@ use App\Enums\TrafficIpMode; use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Log; use OpenApi\Attributes as OA; @@ -450,4 +451,27 @@ class ServerSetting extends Model } ); } + + /** + * Summary of the saved patch check, or null when the server was never checked. + * Results saved before security and reboot detection count as 0 and false. + * + * @return array{updates: int, security: int, reboot_required: bool, error: ?string, checked_at: Carbon}|null + */ + public function patchStatus(): ?array + { + if ($this->patch_checked_at === null) { + return null; + } + + $result = $this->patch_check_result ?? []; + + return [ + 'updates' => (int) ($result['total_updates'] ?? 0), + 'security' => (int) ($result['security_updates'] ?? 0), + 'reboot_required' => ($result['reboot_required'] ?? null) === true, + 'error' => filled($this->patch_check_error) ? $this->patch_check_error : null, + 'checked_at' => $this->patch_checked_at, + ]; + } } diff --git a/resources/views/livewire/server/index.blade.php b/resources/views/livewire/server/index.blade.php index efa6a365ee..c6905cdde2 100644 --- a/resources/views/livewire/server/index.blade.php +++ b/resources/views/livewire/server/index.blade.php @@ -42,6 +42,20 @@ default => 'error', }; + $patchStatus = auth()->user()?->can('viewSecurity', $server) ? $server->settings->patchStatus() : null; + $patchBadges = []; + if ($patchStatus !== null) { + $patchBadges[] = match (true) { + $patchStatus['error'] !== null => ['label' => 'Patch check failed', 'type' => 'error'], + $patchStatus['security'] > 0 => ['label' => $patchStatus['security'].' security '.str('update')->plural($patchStatus['security']), 'type' => 'error'], + $patchStatus['updates'] > 0 => ['label' => $patchStatus['updates'].' '.str('update')->plural($patchStatus['updates']), 'type' => 'warning'], + default => ['label' => 'Up to date', 'type' => 'success'], + }; + if ($patchStatus['error'] === null && $patchStatus['reboot_required']) { + $patchBadges[] = ['label' => 'Reboot required', 'type' => 'warning']; + } + } + return [ 'uuid' => $server->uuid, 'name' => $server->name, @@ -50,8 +64,29 @@ 'status' => $status, 'statusType' => $statusType, 'resourceCount' => $server->definedResources()->count(), + 'patchStatus' => $patchStatus === null ? null : [ + 'failed' => $patchStatus['error'] !== null, + 'updates' => $patchStatus['error'] === null ? $patchStatus['updates'] : 0, + 'security' => $patchStatus['error'] === null ? $patchStatus['security'] : 0, + 'rebootRequired' => $patchStatus['error'] === null && $patchStatus['reboot_required'], + ], + 'patchBadges' => $patchBadges, + 'patchCheckedAt' => $patchStatus === null ? null : 'Patches checked '.$patchStatus['checked_at']->diffForHumans(), ]; })->values(); + + $patchStatuses = $serverRows->pluck('patchStatus')->filter(); + $showPatches = $patchStatuses->isNotEmpty(); + $withUpdates = $patchStatuses->where('updates', '>', 0)->count(); + $withSecurityUpdates = $patchStatuses->where('security', '>', 0)->count(); + $needReboot = $patchStatuses->where('rebootRequired', true)->count(); + $failedChecks = $patchStatuses->where('failed', true)->count(); + $patchSummary = collect([ + $withUpdates > 0 ? $withUpdates.' '.str('server')->plural($withUpdates).' with updates' : null, + $withSecurityUpdates > 0 ? $withSecurityUpdates.' with security updates' : null, + $needReboot > 0 ? $needReboot.' '.($needReboot === 1 ? 'needs' : 'need').' a reboot' : null, + $failedChecks > 0 ? $failedChecks.' '.str('check')->plural($failedChecks).' failed' : null, + ])->filter()->join(' · '); @endphp
+Patches: {{ $patchSummary }}
+ @endif +