b2in/resources/views/livewire/admin/monitoring.blade.php
Kevin Adametz 061f004013 Admin: Monitoring-Seite (Systemstatus) und Navigation neu sortiert
- /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>
2026-10-09 10:27:04 +00:00

196 lines
9.4 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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>