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>
This commit is contained in:
Kevin Adametz 2026-10-09 10:27:04 +00:00
parent 8ddc85422e
commit 061f004013
6 changed files with 525 additions and 42 deletions

View file

@ -117,48 +117,6 @@
@endhasrole
@hasrole('Super-Admin|Admin')
<flux:navlist.group :heading="__('Admin')" class="grid mb-4">
<flux:navlist.group expandable
:expanded="request()->routeIs(['admin.users', 'admin.users.permissions', 'admin.partners.invite', 'admin.partners.registration-codes'])"
heading="Partner" class="grid">
<flux:navlist.item icon="user-group" :href="route('admin.users')"
:current="request()->routeIs('admin.users')" wire:navigate>{{ __('Benutzer') }}
</flux:navlist.item>
<flux:navlist.item icon="envelope" :href="route('admin.partners.invite')"
:current="request()->routeIs('admin.partners.invite')" wire:navigate>
{{ __('Partner einladen') }}</flux:navlist.item>
<flux:navlist.item icon="key" :href="route('admin.partners.registration-codes')"
:current="request()->routeIs('admin.partners.registration-codes')" wire:navigate>
{{ __('Registrierungscodes') }}</flux:navlist.item>
<flux:navlist.item icon="shield-check" :href="route('admin.users.permissions')"
:current="request()->routeIs('admin.users.permissions')" wire:navigate>{{ __('Permissions') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group expandable :expanded="request()->routeIs('testing')" heading="Testing"
class="hidden lg:grid mt-2">
<flux:navlist.item icon="user-group" :href="route('testing.landing')"
:current="request()->routeIs('testing.landing')" wire:navigate>{{ __('Register') }}
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist.group>
<flux:navlist.group :heading="__('Produkte')" class="grid mb-4">
<flux:navlist.item icon="clipboard-document-check" :href="route('admin.products.index')"
:current="request()->routeIs('admin.products.*')" wire:navigate>
{{ __('Produkt-Verwaltung') }}
</flux:navlist.item>
<flux:navlist.item icon="cube" :href="route('products.index')"
:current="request()->routeIs('products.index')" wire:navigate>
{{ __('Alle Produkte') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Regionen')" class="grid mb-4">
<flux:navlist.item icon="map" :href="route('admin.hubs.index')"
:current="request()->routeIs('admin.hubs.*')" wire:navigate>{{ __('Hub-Verwaltung') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('CMS')" class="grid mb-4">
<flux:navlist.group expandable
:expanded="request()->routeIs(['cms.dashboard', 'cms.content.index', 'cms.references.index', 'cms.brands.index', 'cms.articles.index', 'cms.media.index'])"
@ -206,6 +164,48 @@
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist.group>
<flux:navlist.group :heading="__('Produkte')" class="grid mb-4">
<flux:navlist.item icon="clipboard-document-check" :href="route('admin.products.index')"
:current="request()->routeIs('admin.products.*')" wire:navigate>
{{ __('Produkt-Verwaltung') }}
</flux:navlist.item>
<flux:navlist.item icon="cube" :href="route('products.index')"
:current="request()->routeIs('products.index')" wire:navigate>
{{ __('Alle Produkte') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Regionen')" class="grid mb-4">
<flux:navlist.item icon="map" :href="route('admin.hubs.index')"
:current="request()->routeIs('admin.hubs.*')" wire:navigate>{{ __('Hub-Verwaltung') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group :heading="__('Admin')" class="grid mb-4">
<flux:navlist.group expandable
:expanded="request()->routeIs(['admin.users', 'admin.users.permissions', 'admin.partners.invite', 'admin.partners.registration-codes'])"
heading="Partner" class="grid">
<flux:navlist.item icon="user-group" :href="route('admin.users')"
:current="request()->routeIs('admin.users')" wire:navigate>{{ __('Benutzer') }}
</flux:navlist.item>
<flux:navlist.item icon="envelope" :href="route('admin.partners.invite')"
:current="request()->routeIs('admin.partners.invite')" wire:navigate>
{{ __('Partner einladen') }}</flux:navlist.item>
<flux:navlist.item icon="key" :href="route('admin.partners.registration-codes')"
:current="request()->routeIs('admin.partners.registration-codes')" wire:navigate>
{{ __('Registrierungscodes') }}</flux:navlist.item>
<flux:navlist.item icon="shield-check" :href="route('admin.users.permissions')"
:current="request()->routeIs('admin.users.permissions')" wire:navigate>{{ __('Permissions') }}
</flux:navlist.item>
</flux:navlist.group>
<flux:navlist.group expandable :expanded="request()->routeIs('testing')" heading="Testing"
class="hidden lg:grid mt-2">
<flux:navlist.item icon="user-group" :href="route('testing.landing')"
:current="request()->routeIs('testing.landing')" wire:navigate>{{ __('Register') }}
</flux:navlist.item>
</flux:navlist.group>
</flux:navlist.group>
@endhasrole
@hasrole('Super-Admin')
@ -214,6 +214,15 @@
:current="request()->routeIs('dashboard')" wire:navigate>{{ __('Dashboard') }}</flux:navlist.item>
</flux:navlist.group>
@endhasrole
{{-- Monitoring ganz unten in der Navigation (09.10.2026) --}}
@hasrole('Super-Admin|Admin')
<flux:navlist.group :heading="__('Monitoring')" class="grid mb-4">
<flux:navlist.item icon="heart" :href="route('admin.monitoring')"
:current="request()->routeIs('admin.monitoring')" wire:navigate>{{ __('Systemstatus') }}
</flux:navlist.item>
</flux:navlist.group>
@endhasrole
</flux:navlist>
<flux:spacer />

View file

@ -0,0 +1,196 @@
<?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>