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:
parent
8ddc85422e
commit
061f004013
6 changed files with 525 additions and 42 deletions
|
|
@ -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 />
|
||||
|
|
|
|||
196
resources/views/livewire/admin/monitoring.blade.php
Normal file
196
resources/views/livewire/admin/monitoring.blade.php
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue