- /admin/monitoring (nur Admin): Prüfungen wie /health, Version und Deploy-Zeit, geplante Aufgaben mit nächstem Lauf, letzte Fehler aus dem Log (ohne E-Mail-Adressen), Alarme, fehlgeschlagene Jobs, Links zu GlitchTip/Uptime Kuma (MONITORING_STATUS_URL), Test-Alarm - Navigation: CMS, Cabinet, Produkte, Regionen, Admin, ganz unten Monitoring Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
196 lines
9.4 KiB
PHP
196 lines
9.4 KiB
PHP
<?php
|
||
|
||
use App\Services\Monitoring\HealthProbe;
|
||
use App\Services\Monitoring\SystemStatus;
|
||
use App\Support\OperationalAlert;
|
||
use Flux\Flux;
|
||
use function Livewire\Volt\{computed, layout, mount, title};
|
||
|
||
layout('components.layouts.app');
|
||
title('Monitoring');
|
||
|
||
mount(function () {
|
||
abort_unless(auth()->user()?->hasAnyRole(['Super-Admin', 'Admin']), 403);
|
||
});
|
||
|
||
$health = computed(fn () => app(SystemStatus::class)->health());
|
||
$application = computed(fn () => app(SystemStatus::class)->application());
|
||
$tasks = computed(fn () => app(SystemStatus::class)->scheduledTasks());
|
||
$failedJobs = computed(fn () => app(SystemStatus::class)->failedJobs());
|
||
$errors = computed(fn () => app(SystemStatus::class)->recentLogEntries('laravel', ['ERROR', 'CRITICAL', 'ALERT', 'EMERGENCY']));
|
||
$alerts = computed(fn () => app(SystemStatus::class)->recentLogEntries('monitoring', ['CRITICAL', 'ERROR', 'WARNING']));
|
||
$links = computed(fn () => app(SystemStatus::class)->links());
|
||
|
||
$refresh = function () {
|
||
unset($this->health, $this->tasks, $this->failedJobs, $this->errors, $this->alerts);
|
||
};
|
||
|
||
$sendTestAlert = function () {
|
||
OperationalAlert::send('Test-Alarm aus dem Admin (Monitoring)', ['ausgeloest_von' => auth()->id()]);
|
||
unset($this->alerts);
|
||
|
||
Flux::toast(variant: 'success', heading: 'Test-Alarm gesendet', text: 'Mail und Push gehen an die konfigurierten Empfänger; der Eintrag steht unten unter „Alarme“.');
|
||
};
|
||
|
||
?>
|
||
|
||
@php
|
||
$statusColor = fn (string $status) => match ($status) {
|
||
HealthProbe::StatusOk => 'green',
|
||
HealthProbe::StatusWarn => 'amber',
|
||
default => 'red',
|
||
};
|
||
$statusLabel = fn (string $status) => match ($status) {
|
||
HealthProbe::StatusOk => 'OK',
|
||
HealthProbe::StatusWarn => 'Warnung',
|
||
default => 'Kritisch',
|
||
};
|
||
@endphp
|
||
|
||
<div class="space-y-8">
|
||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||
<div>
|
||
<flux:heading size="xl">Monitoring</flux:heading>
|
||
<flux:text class="mt-1">Zustand von Website und Server – dieselben Prüfungen, die Uptime Kuma über /health abfragt.</flux:text>
|
||
</div>
|
||
<div class="flex flex-wrap gap-2">
|
||
@foreach ($this->links as $label => $url)
|
||
<flux:button size="sm" variant="ghost" icon-trailing="arrow-top-right-on-square" :href="$url" target="_blank">{{ $label }}</flux:button>
|
||
@endforeach
|
||
<flux:button size="sm" icon="bell-alert" wire:click="sendTestAlert" wire:confirm="Test-Alarm per Mail und Push senden?">Test-Alarm</flux:button>
|
||
<flux:button size="sm" variant="primary" icon="arrow-path" wire:click="refresh">Neu prüfen</flux:button>
|
||
</div>
|
||
</div>
|
||
|
||
{{-- Gesamtstatus und Prüfungen --}}
|
||
<div data-monitoring-health>
|
||
<div class="mb-3 flex items-center gap-3">
|
||
<flux:heading size="lg">Prüfungen</flux:heading>
|
||
<flux:badge :color="$statusColor($this->health['status'])">{{ $statusLabel($this->health['status']) }}</flux:badge>
|
||
</div>
|
||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
||
@foreach ($this->health['checks'] as $check)
|
||
<flux:card wire:key="check-{{ $check['key'] }}" class="space-y-2">
|
||
<div class="flex items-center justify-between gap-2">
|
||
<flux:heading>{{ $check['label'] }}</flux:heading>
|
||
<flux:badge size="sm" :color="$statusColor($check['status'])">{{ $statusLabel($check['status']) }}</flux:badge>
|
||
</div>
|
||
<flux:text size="sm">{{ $check['detail'] }}</flux:text>
|
||
</flux:card>
|
||
@endforeach
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||
{{-- Anwendung --}}
|
||
<flux:card class="space-y-3">
|
||
<flux:heading size="lg">Anwendung</flux:heading>
|
||
<dl class="grid grid-cols-[max-content_1fr] gap-x-6 gap-y-2 text-sm">
|
||
<dt class="text-zinc-500">Version</dt>
|
||
<dd class="font-medium">{{ $this->application['version'] }}</dd>
|
||
<dt class="text-zinc-500">Ausgeliefert</dt>
|
||
<dd>{{ $this->application['deployed_at']?->format('d.m.Y H:i') ?? '—' }}</dd>
|
||
<dt class="text-zinc-500">Umgebung</dt>
|
||
<dd>{{ $this->application['environment'] }}</dd>
|
||
<dt class="text-zinc-500">Debug-Modus</dt>
|
||
<dd>
|
||
@if ($this->application['debug'] && app()->environment('production'))
|
||
<flux:badge size="sm" color="red">an – auf Live ausschalten</flux:badge>
|
||
@else
|
||
{{ $this->application['debug'] ? 'an' : 'aus' }}
|
||
@endif
|
||
</dd>
|
||
<dt class="text-zinc-500">PHP / Laravel</dt>
|
||
<dd>{{ $this->application['php'] }} / {{ $this->application['laravel'] }}</dd>
|
||
</dl>
|
||
</flux:card>
|
||
|
||
{{-- Geplante Aufgaben --}}
|
||
<flux:card class="space-y-3">
|
||
<flux:heading size="lg">Geplante Aufgaben</flux:heading>
|
||
@if ($this->tasks === [])
|
||
<flux:text>Keine geplanten Aufgaben gefunden.</flux:text>
|
||
@else
|
||
<div class="divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||
@foreach ($this->tasks as $task)
|
||
<div wire:key="task-{{ $loop->index }}" class="flex items-start justify-between gap-4 py-2">
|
||
<div>
|
||
<div class="font-medium">{{ $task['command'] }}</div>
|
||
@if ($task['description'] !== '')
|
||
<div class="text-zinc-500">{{ $task['description'] }}</div>
|
||
@endif
|
||
</div>
|
||
<div class="shrink-0 text-right text-zinc-500">
|
||
<div class="font-mono text-xs">{{ $task['expression'] }}</div>
|
||
<div>nächster Lauf {{ $task['next']?->format('d.m. H:i') ?? '—' }}</div>
|
||
</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
</flux:card>
|
||
</div>
|
||
|
||
{{-- Fehler aus dem Log --}}
|
||
<flux:card class="space-y-3" data-monitoring-errors>
|
||
<div class="flex items-center justify-between gap-4">
|
||
<flux:heading size="lg">Letzte Fehler</flux:heading>
|
||
<flux:text size="sm">Aus storage/logs – Details und Häufigkeit in GlitchTip</flux:text>
|
||
</div>
|
||
@if ($this->errors === [])
|
||
<flux:text>Keine Fehler im aktuellen Log.</flux:text>
|
||
@else
|
||
<div class="divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||
@foreach ($this->errors as $entry)
|
||
<div wire:key="error-{{ $loop->index }}" class="flex gap-4 py-2">
|
||
<div class="w-28 shrink-0 text-zinc-500">{{ $entry['at']->format('d.m. H:i:s') }}</div>
|
||
<flux:badge size="sm" color="red" class="h-fit shrink-0">{{ $entry['level'] }}</flux:badge>
|
||
<div class="min-w-0 break-words">{{ $entry['message'] }}</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
</flux:card>
|
||
|
||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||
{{-- Alarme --}}
|
||
<flux:card class="space-y-3" data-monitoring-alerts>
|
||
<flux:heading size="lg">Alarme</flux:heading>
|
||
@if ($this->alerts === [])
|
||
<flux:text>Keine Alarme im aktuellen Monitoring-Log.</flux:text>
|
||
@else
|
||
<div class="divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||
@foreach ($this->alerts as $entry)
|
||
<div wire:key="alert-{{ $loop->index }}" class="flex gap-4 py-2">
|
||
<div class="w-28 shrink-0 text-zinc-500">{{ $entry['at']->format('d.m. H:i:s') }}</div>
|
||
<div class="min-w-0 break-words">{{ \Illuminate\Support\Str::after($entry['message'], '[monitoring] ') }}</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
</flux:card>
|
||
|
||
{{-- Fehlgeschlagene Jobs --}}
|
||
<flux:card class="space-y-3">
|
||
<div class="flex items-center gap-3">
|
||
<flux:heading size="lg">Fehlgeschlagene Jobs</flux:heading>
|
||
<flux:badge size="sm" :color="$this->failedJobs['count'] > 0 ? 'red' : 'zinc'">{{ $this->failedJobs['count'] }}</flux:badge>
|
||
</div>
|
||
@if ($this->failedJobs['latest'] === [])
|
||
<flux:text>Keine fehlgeschlagenen Jobs.</flux:text>
|
||
@else
|
||
<div class="divide-y divide-zinc-200 text-sm dark:divide-zinc-700">
|
||
@foreach ($this->failedJobs['latest'] as $job)
|
||
<div wire:key="job-{{ $loop->index }}" class="py-2">
|
||
<div class="flex justify-between gap-4">
|
||
<span class="font-medium">{{ $job['job'] }}</span>
|
||
<span class="text-zinc-500">{{ $job['failed_at']->format('d.m. H:i') }}</span>
|
||
</div>
|
||
<div class="text-zinc-500">{{ $job['error'] }}</div>
|
||
</div>
|
||
@endforeach
|
||
</div>
|
||
@endif
|
||
</flux:card>
|
||
</div>
|
||
</div>
|