Add superadmin ops health dashboard
This commit is contained in:
@@ -95,7 +95,7 @@
|
||||
{"id":"fotospiel-app-swb","title":"Security review: replace public asset URLs with signed routes","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:04:05.610098299+01:00","created_by":"soeren","updated_at":"2026-01-01T16:04:11.215921463+01:00","closed_at":"2026-01-01T16:04:11.215921463+01:00","close_reason":"Completed in codebase (verified)"}
|
||||
{"id":"fotospiel-app-tqg","title":"Tenant admin onboarding: staging E2E validation","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-01T16:08:57.448899354+01:00","created_by":"soeren","updated_at":"2026-01-01T16:08:57.448899354+01:00"}
|
||||
{"id":"fotospiel-app-ty9","title":"Security review: data classes \u0026 retention baseline","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:03:09.595870306+01:00","created_by":"soeren","updated_at":"2026-01-01T16:03:15.211042718+01:00","closed_at":"2026-01-01T16:03:15.211042718+01:00","close_reason":"Completed in codebase (verified)"}
|
||||
{"id":"fotospiel-app-tym","title":"Ops health dashboard (queues, storage, upload pipeline)","description":"Superadmin ops dashboard showing queue backlog, failed jobs, storage thresholds, and upload pipeline health.","status":"open","priority":2,"issue_type":"feature","created_at":"2026-01-01T14:20:04.991351193+01:00","updated_at":"2026-01-01T14:20:04.991351193+01:00"}
|
||||
{"id":"fotospiel-app-tym","title":"Ops health dashboard (queues, storage, upload pipeline)","description":"Superadmin ops dashboard showing queue backlog, failed jobs, storage thresholds, and upload pipeline health.","notes":"Implemented Ops Health dashboard with storage+queue widgets, new translations, and navigation wiring.","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-01-01T14:20:04.991351193+01:00","updated_at":"2026-01-01T21:07:06.982356199+01:00","closed_at":"2026-01-01T21:07:06.982356199+01:00","close_reason":"completed"}
|
||||
{"id":"fotospiel-app-ugk","title":"Paddle catalog sync: feature test for artisan command","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T16:01:33.309716868+01:00","created_by":"soeren","updated_at":"2026-01-01T16:01:38.940407157+01:00","closed_at":"2026-01-01T16:01:38.940407157+01:00","close_reason":"Completed in codebase (verified)"}
|
||||
{"id":"fotospiel-app-umr","title":"Paddle migration: build Paddle API service layer","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T15:57:38.529187953+01:00","created_by":"soeren","updated_at":"2026-01-01T15:57:44.178675076+01:00","closed_at":"2026-01-01T15:57:44.178675076+01:00","close_reason":"Completed in codebase (verified)"}
|
||||
{"id":"fotospiel-app-v31","title":"SEC-MS-01 AV + EXIF scrubber worker integration","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-01T15:52:52.476048623+01:00","created_by":"soeren","updated_at":"2026-01-01T15:52:58.118336529+01:00","closed_at":"2026-01-01T15:52:58.118336529+01:00","close_reason":"Completed in codebase (verified)"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
fotospiel-app-arp
|
||||
fotospiel-app-tym
|
||||
|
||||
67
app/Filament/SuperAdmin/Pages/OpsHealthDashboard.php
Normal file
67
app/Filament/SuperAdmin/Pages/OpsHealthDashboard.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\SuperAdmin\Pages;
|
||||
|
||||
use App\Filament\Clusters\DailyOps\DailyOpsCluster;
|
||||
use App\Filament\Widgets\QueueHealthWidget;
|
||||
use App\Filament\Widgets\StorageCapacityWidget;
|
||||
use App\Filament\Widgets\UploadPipelineHealthWidget;
|
||||
use BackedEnum;
|
||||
use Filament\Pages\Page;
|
||||
use UnitEnum;
|
||||
|
||||
class OpsHealthDashboard extends Page
|
||||
{
|
||||
protected string $view = 'filament.super-admin.pages.ops-health-dashboard';
|
||||
|
||||
protected static ?string $cluster = DailyOpsCluster::class;
|
||||
|
||||
protected static null|string|BackedEnum $navigationIcon = 'heroicon-o-signal';
|
||||
|
||||
protected static null|string|UnitEnum $navigationGroup = null;
|
||||
|
||||
protected static ?int $navigationSort = 5;
|
||||
|
||||
public static function getNavigationGroup(): UnitEnum|string|null
|
||||
{
|
||||
return __('admin.nav.infrastructure');
|
||||
}
|
||||
|
||||
public static function getNavigationLabel(): string
|
||||
{
|
||||
return __('admin.ops_health.navigation.label');
|
||||
}
|
||||
|
||||
public function getHeading(): string
|
||||
{
|
||||
return __('admin.ops_health.heading');
|
||||
}
|
||||
|
||||
public function getSubheading(): ?string
|
||||
{
|
||||
return __('admin.ops_health.subheading');
|
||||
}
|
||||
|
||||
protected function getHeaderWidgets(): array
|
||||
{
|
||||
return [
|
||||
StorageCapacityWidget::class,
|
||||
UploadPipelineHealthWidget::class,
|
||||
];
|
||||
}
|
||||
|
||||
protected function getFooterWidgets(): array
|
||||
{
|
||||
return [
|
||||
QueueHealthWidget::class,
|
||||
];
|
||||
}
|
||||
|
||||
public function getHeaderWidgetsColumns(): int|array
|
||||
{
|
||||
return [
|
||||
'md' => 2,
|
||||
'xl' => 2,
|
||||
];
|
||||
}
|
||||
}
|
||||
103
app/Filament/Widgets/QueueHealthWidget.php
Normal file
103
app/Filament/Widgets/QueueHealthWidget.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use Filament\Widgets\Widget;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class QueueHealthWidget extends Widget
|
||||
{
|
||||
protected string $view = 'filament.widgets.queue-health';
|
||||
|
||||
protected ?string $pollingInterval = '60s';
|
||||
|
||||
protected function getViewData(): array
|
||||
{
|
||||
$snapshot = Cache::get('storage:queue-health:last');
|
||||
$stalledMinutes = (int) config('storage-monitor.queue_health.stalled_minutes', 10);
|
||||
|
||||
if (! is_array($snapshot)) {
|
||||
return [
|
||||
'snapshotMissing' => true,
|
||||
'connection' => (string) config('queue.default'),
|
||||
'snapshotLabel' => __('admin.ops_health.snapshot_missing'),
|
||||
'queues' => [],
|
||||
'alerts' => [],
|
||||
'alertCount' => 0,
|
||||
'stalledAssets' => 0,
|
||||
'stalledMinutes' => $stalledMinutes,
|
||||
];
|
||||
}
|
||||
|
||||
$queues = collect($snapshot['queues'] ?? [])
|
||||
->map(fn (array $queue) => $this->formatQueue($queue))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$alerts = collect($snapshot['alerts'] ?? [])
|
||||
->map(fn (array $alert) => $this->formatAlert($alert))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$snapshotAge = $this->formatSnapshotAge($snapshot['generated_at'] ?? null);
|
||||
$snapshotLabel = $snapshotAge
|
||||
? __('admin.ops_health.snapshot_age', ['age' => $snapshotAge])
|
||||
: __('admin.ops_health.snapshot_missing');
|
||||
|
||||
return [
|
||||
'snapshotMissing' => false,
|
||||
'connection' => (string) ($snapshot['connection'] ?? config('queue.default')),
|
||||
'snapshotLabel' => $snapshotLabel,
|
||||
'queues' => $queues,
|
||||
'alerts' => $alerts,
|
||||
'alertCount' => count($alerts),
|
||||
'stalledAssets' => (int) ($snapshot['stalled_assets'] ?? 0),
|
||||
'stalledMinutes' => $stalledMinutes,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatQueue(array $queue): array
|
||||
{
|
||||
$size = (int) ($queue['size'] ?? 0);
|
||||
$failed = (int) ($queue['failed'] ?? 0);
|
||||
$limits = $queue['limits'] ?? [];
|
||||
|
||||
return [
|
||||
'name' => (string) ($queue['queue'] ?? 'default'),
|
||||
'size' => $size,
|
||||
'size_label' => $size < 0 ? '-' : number_format($size),
|
||||
'failed' => $failed,
|
||||
'failed_label' => number_format($failed),
|
||||
'severity' => (string) ($queue['severity'] ?? 'unknown'),
|
||||
'warning' => (int) ($limits['warning'] ?? 0),
|
||||
'critical' => (int) ($limits['critical'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
private function formatAlert(array $alert): array
|
||||
{
|
||||
return [
|
||||
'queue' => $alert['queue'] ?? null,
|
||||
'type' => (string) ($alert['type'] ?? 'unknown'),
|
||||
'severity' => (string) ($alert['severity'] ?? 'warning'),
|
||||
'size' => $alert['size'] ?? null,
|
||||
'failed' => $alert['failed'] ?? null,
|
||||
'count' => $alert['count'] ?? null,
|
||||
'older_than_minutes' => $alert['older_than_minutes'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
private function formatSnapshotAge(?string $timestamp): ?string
|
||||
{
|
||||
if (! $timestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($timestamp)->diffForHumans();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
86
app/Filament/Widgets/UploadPipelineHealthWidget.php
Normal file
86
app/Filament/Widgets/UploadPipelineHealthWidget.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Widgets;
|
||||
|
||||
use Filament\Widgets\StatsOverviewWidget;
|
||||
use Filament\Widgets\StatsOverviewWidget\Stat;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class UploadPipelineHealthWidget extends StatsOverviewWidget
|
||||
{
|
||||
protected static ?int $sort = 2;
|
||||
|
||||
protected ?string $pollingInterval = '60s';
|
||||
|
||||
protected function getStats(): array
|
||||
{
|
||||
$snapshot = Cache::get('storage:monitor:last');
|
||||
|
||||
if (! is_array($snapshot)) {
|
||||
return [
|
||||
Stat::make(__('admin.ops_health.pipeline.label'), __('admin.ops_health.pipeline.no_snapshot'))
|
||||
->description(__('admin.ops_health.pipeline.no_snapshot_desc'))
|
||||
->color('warning'),
|
||||
];
|
||||
}
|
||||
|
||||
$totals = $this->summarizeAssets($snapshot['targets'] ?? []);
|
||||
$alerts = $snapshot['alerts'] ?? [];
|
||||
$snapshotAge = $this->formatSnapshotAge($snapshot['generated_at'] ?? null);
|
||||
$snapshotLabel = $snapshotAge
|
||||
? __('admin.ops_health.snapshot_age', ['age' => $snapshotAge])
|
||||
: __('admin.ops_health.snapshot_missing');
|
||||
|
||||
return [
|
||||
Stat::make(__('admin.ops_health.pipeline.total'), number_format($totals['total']))
|
||||
->description(__('admin.ops_health.pipeline.hot_hint', ['count' => number_format($totals['hot'])]))
|
||||
->color('primary'),
|
||||
Stat::make(__('admin.ops_health.pipeline.pending'), number_format($totals['pending']))
|
||||
->description($snapshotLabel)
|
||||
->descriptionIcon('heroicon-m-clock')
|
||||
->color($totals['pending'] > 0 ? 'warning' : 'success'),
|
||||
Stat::make(__('admin.ops_health.pipeline.failed'), number_format($totals['failed']))
|
||||
->description(__('admin.ops_health.pipeline.archived_hint', ['count' => number_format($totals['archived'])]))
|
||||
->color($totals['failed'] > 0 ? 'danger' : 'success'),
|
||||
Stat::make(__('admin.ops_health.pipeline.alerts'), number_format(count($alerts)))
|
||||
->color(count($alerts) > 0 ? 'danger' : 'success'),
|
||||
];
|
||||
}
|
||||
|
||||
private function summarizeAssets(array $targets): array
|
||||
{
|
||||
$totals = [
|
||||
'total' => 0,
|
||||
'pending' => 0,
|
||||
'failed' => 0,
|
||||
'hot' => 0,
|
||||
'archived' => 0,
|
||||
];
|
||||
|
||||
foreach ($targets as $target) {
|
||||
$assets = $target['assets'] ?? [];
|
||||
$totals['total'] += (int) ($assets['total'] ?? 0);
|
||||
|
||||
$byStatus = $assets['by_status'] ?? [];
|
||||
foreach (['pending', 'failed', 'hot', 'archived'] as $status) {
|
||||
$totals[$status] += (int) ($byStatus[$status]['count'] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
return $totals;
|
||||
}
|
||||
|
||||
private function formatSnapshotAge(?string $timestamp): ?string
|
||||
{
|
||||
if (! $timestamp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return Carbon::parse($timestamp)->diffForHumans();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -112,6 +112,7 @@ class SuperAdminPanelProvider extends PanelProvider
|
||||
Pages\Dashboard::class,
|
||||
\App\Filament\SuperAdmin\Pages\WatermarkSettingsPage::class,
|
||||
\App\Filament\SuperAdmin\Pages\GuestPolicySettingsPage::class,
|
||||
\App\Filament\SuperAdmin\Pages\OpsHealthDashboard::class,
|
||||
])
|
||||
->authGuard('super_admin');
|
||||
|
||||
|
||||
@@ -219,6 +219,56 @@ return [
|
||||
'saved' => 'Gast-Richtlinien aktualisiert.',
|
||||
],
|
||||
],
|
||||
'ops_health' => [
|
||||
'navigation' => [
|
||||
'label' => 'Ops-Health',
|
||||
],
|
||||
'heading' => 'Ops-Health',
|
||||
'subheading' => 'Storage- und Queue-Snapshots für die Upload-Pipeline.',
|
||||
'help' => 'Snapshots werden über die Scheduled Commands storage:monitor und storage:check-upload-queues erzeugt.',
|
||||
'snapshot_age' => 'Aktualisiert :age',
|
||||
'snapshot_missing' => 'Snapshot fehlt',
|
||||
'pipeline' => [
|
||||
'label' => 'Upload-Pipeline',
|
||||
'total' => 'Assets gesamt',
|
||||
'pending' => 'Ausstehende Assets',
|
||||
'failed' => 'Fehlgeschlagene Assets',
|
||||
'hot' => 'Hot-Assets',
|
||||
'archived' => 'Archivierte Assets',
|
||||
'alerts' => 'Alarme',
|
||||
'hot_hint' => 'Hot: :count',
|
||||
'archived_hint' => 'Archiviert: :count',
|
||||
'no_snapshot' => 'Snapshot fehlt',
|
||||
'no_snapshot_desc' => 'storage:monitor ausführen, um einen Snapshot zu erzeugen.',
|
||||
],
|
||||
'queue' => [
|
||||
'heading' => 'Queue-Health',
|
||||
'description' => 'Upload-Queues, fehlgeschlagene Jobs und hängende Assets.',
|
||||
'connection' => 'Verbindung',
|
||||
'snapshot' => 'Snapshot',
|
||||
'stalled_assets' => 'Ausstehende Assets > :minutes Min',
|
||||
'no_snapshot' => 'Kein Queue-Snapshot vorhanden. storage:check-upload-queues ausführen.',
|
||||
'no_queues' => 'Keine Queues konfiguriert.',
|
||||
'alerts_heading' => 'Alarme',
|
||||
'thresholds' => 'Grenzwerte',
|
||||
'size' => 'Größe',
|
||||
'failed' => 'Fehler',
|
||||
'queue' => 'Queue',
|
||||
],
|
||||
'alert_types' => [
|
||||
'size' => 'Queue-Größe über Grenzwert (:size)',
|
||||
'failed_jobs' => 'Fehlgeschlagene Jobs erkannt (:failed)',
|
||||
'pending_assets' => ':count Assets seit mehr als :minutes Min in der Warteschlange',
|
||||
],
|
||||
'severity' => [
|
||||
'ok' => 'OK',
|
||||
'warning' => 'Warnung',
|
||||
'critical' => 'Kritisch',
|
||||
'unknown' => 'Unbekannt',
|
||||
'error' => 'Fehler',
|
||||
'unavailable' => 'Nicht verfügbar',
|
||||
],
|
||||
],
|
||||
|
||||
'events' => [
|
||||
'fields' => [
|
||||
|
||||
@@ -219,6 +219,56 @@ return [
|
||||
'saved' => 'Guest policy updated.',
|
||||
],
|
||||
],
|
||||
'ops_health' => [
|
||||
'navigation' => [
|
||||
'label' => 'Ops health',
|
||||
],
|
||||
'heading' => 'Ops health',
|
||||
'subheading' => 'Storage and queue snapshots for the upload pipeline.',
|
||||
'help' => 'Snapshots are generated by scheduled commands: storage:monitor and storage:check-upload-queues.',
|
||||
'snapshot_age' => 'Updated :age',
|
||||
'snapshot_missing' => 'Snapshot missing',
|
||||
'pipeline' => [
|
||||
'label' => 'Upload pipeline',
|
||||
'total' => 'Total assets',
|
||||
'pending' => 'Pending assets',
|
||||
'failed' => 'Failed assets',
|
||||
'hot' => 'Hot assets',
|
||||
'archived' => 'Archived assets',
|
||||
'alerts' => 'Alerts',
|
||||
'hot_hint' => 'Hot: :count',
|
||||
'archived_hint' => 'Archived: :count',
|
||||
'no_snapshot' => 'Snapshot missing',
|
||||
'no_snapshot_desc' => 'Run storage:monitor to generate a snapshot.',
|
||||
],
|
||||
'queue' => [
|
||||
'heading' => 'Queue health',
|
||||
'description' => 'Upload queue sizes, failed jobs, and stalled assets.',
|
||||
'connection' => 'Connection',
|
||||
'snapshot' => 'Snapshot',
|
||||
'stalled_assets' => 'Pending assets > :minutes min',
|
||||
'no_snapshot' => 'No queue snapshot available. Run storage:check-upload-queues.',
|
||||
'no_queues' => 'No queues configured.',
|
||||
'alerts_heading' => 'Alerts',
|
||||
'thresholds' => 'Thresholds',
|
||||
'size' => 'Size',
|
||||
'failed' => 'Failed',
|
||||
'queue' => 'Queue',
|
||||
],
|
||||
'alert_types' => [
|
||||
'size' => 'Queue size above threshold (:size)',
|
||||
'failed_jobs' => 'Failed jobs detected (:failed)',
|
||||
'pending_assets' => ':count assets pending for more than :minutes min',
|
||||
],
|
||||
'severity' => [
|
||||
'ok' => 'OK',
|
||||
'warning' => 'Warning',
|
||||
'critical' => 'Critical',
|
||||
'unknown' => 'Unknown',
|
||||
'error' => 'Error',
|
||||
'unavailable' => 'Unavailable',
|
||||
],
|
||||
],
|
||||
|
||||
'events' => [
|
||||
'fields' => [
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6">
|
||||
<x-filament::section>
|
||||
<p class="text-sm text-slate-600 dark:text-slate-300">
|
||||
{{ __('admin.ops_health.help') }}
|
||||
</p>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
115
resources/views/filament/widgets/queue-health.blade.php
Normal file
115
resources/views/filament/widgets/queue-health.blade.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<x-filament-widgets::widget>
|
||||
<x-filament::section
|
||||
:heading="__('admin.ops_health.queue.heading')"
|
||||
:description="__('admin.ops_health.queue.description')"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 text-xs text-slate-500 dark:text-slate-300">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<span>
|
||||
{{ __('admin.ops_health.queue.connection') }}:
|
||||
<span class="font-semibold text-slate-700 dark:text-slate-100">{{ $connection }}</span>
|
||||
</span>
|
||||
<span>{{ $snapshotLabel }}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-700 dark:bg-slate-800 dark:text-slate-200">
|
||||
{{ __('admin.ops_health.queue.alerts_heading') }}: {{ number_format($alertCount) }}
|
||||
</span>
|
||||
@if($stalledAssets > 0)
|
||||
<span class="rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-800 dark:bg-amber-400/20 dark:text-amber-200">
|
||||
{{ __('admin.ops_health.queue.stalled_assets', ['minutes' => $stalledMinutes]) }}:
|
||||
{{ number_format($stalledAssets) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($snapshotMissing)
|
||||
<div class="mt-4 rounded-xl border border-dashed border-slate-200 bg-slate-50 p-4 text-sm text-slate-600 dark:border-white/10 dark:bg-slate-900/40 dark:text-slate-300">
|
||||
{{ __('admin.ops_health.queue.no_snapshot') }}
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-4 grid gap-3">
|
||||
@forelse($queues as $queue)
|
||||
<div class="rounded-2xl border border-slate-200/70 bg-white/80 p-4 shadow-sm dark:border-white/10 dark:bg-slate-900/60">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-slate-700 dark:text-slate-100">{{ $queue['name'] }}</p>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||
{{ __('admin.ops_health.queue.thresholds') }}:
|
||||
@if($queue['warning'] || $queue['critical'])
|
||||
W {{ number_format($queue['warning']) }} / C {{ number_format($queue['critical']) }}
|
||||
@else
|
||||
-
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs">
|
||||
<span class="text-slate-500 dark:text-slate-400">
|
||||
{{ __('admin.ops_health.queue.size') }}:
|
||||
<span class="font-semibold text-slate-700 dark:text-slate-100">{{ $queue['size_label'] }}</span>
|
||||
</span>
|
||||
<span class="text-slate-500 dark:text-slate-400">
|
||||
{{ __('admin.ops_health.queue.failed') }}:
|
||||
<span class="font-semibold text-slate-700 dark:text-slate-100">{{ $queue['failed_label'] }}</span>
|
||||
</span>
|
||||
<span @class([
|
||||
'rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
||||
'bg-emerald-100 text-emerald-800 dark:bg-emerald-400/20 dark:text-emerald-200' => $queue['severity'] === 'ok',
|
||||
'bg-amber-100 text-amber-800 dark:bg-amber-400/20 dark:text-amber-200' => $queue['severity'] === 'warning',
|
||||
'bg-rose-100 text-rose-800 dark:bg-rose-400/20 dark:text-rose-200' => $queue['severity'] === 'critical',
|
||||
'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-200' => ! in_array($queue['severity'], ['ok', 'warning', 'critical'], true),
|
||||
])>
|
||||
{{ __('admin.ops_health.severity.'.$queue['severity']) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<p class="text-sm text-slate-500 dark:text-slate-300">{{ __('admin.ops_health.queue.no_queues') }}</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@if($alertCount > 0)
|
||||
<div class="mt-4 grid gap-2">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400">
|
||||
{{ __('admin.ops_health.queue.alerts_heading') }}
|
||||
</p>
|
||||
@foreach($alerts as $alert)
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-slate-200/70 bg-white/70 px-3 py-2 text-xs text-slate-600 dark:border-white/10 dark:bg-slate-900/50 dark:text-slate-200">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@if($alert['queue'])
|
||||
<span class="font-semibold text-slate-700 dark:text-slate-100">{{ $alert['queue'] }}</span>
|
||||
@endif
|
||||
<span class="text-slate-500 dark:text-slate-400">
|
||||
@switch($alert['type'])
|
||||
@case('size')
|
||||
{{ __('admin.ops_health.alert_types.size', ['size' => number_format($alert['size'] ?? 0)]) }}
|
||||
@break
|
||||
@case('failed_jobs')
|
||||
{{ __('admin.ops_health.alert_types.failed_jobs', ['failed' => number_format($alert['failed'] ?? 0)]) }}
|
||||
@break
|
||||
@case('pending_assets')
|
||||
{{ __('admin.ops_health.alert_types.pending_assets', ['count' => number_format($alert['count'] ?? 0), 'minutes' => $alert['older_than_minutes'] ?? $stalledMinutes]) }}
|
||||
@break
|
||||
@default
|
||||
{{ __('admin.ops_health.snapshot_missing') }}
|
||||
@endswitch
|
||||
</span>
|
||||
</div>
|
||||
<span @class([
|
||||
'rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide',
|
||||
'bg-emerald-100 text-emerald-800 dark:bg-emerald-400/20 dark:text-emerald-200' => $alert['severity'] === 'ok',
|
||||
'bg-amber-100 text-amber-800 dark:bg-amber-400/20 dark:text-amber-200' => $alert['severity'] === 'warning',
|
||||
'bg-rose-100 text-rose-800 dark:bg-rose-400/20 dark:text-rose-200' => $alert['severity'] === 'critical',
|
||||
'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-200' => ! in_array($alert['severity'], ['ok', 'warning', 'critical'], true),
|
||||
])>
|
||||
{{ __('admin.ops_health.severity.'.$alert['severity']) }}
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</x-filament::section>
|
||||
</x-filament-widgets::widget>
|
||||
@@ -41,6 +41,7 @@ class SuperAdminNavigationGroupsTest extends TestCase
|
||||
\App\Filament\Resources\InfrastructureActionLogs\InfrastructureActionLogResource::class => 'admin.nav.infrastructure',
|
||||
\App\Filament\SuperAdmin\Pages\WatermarkSettingsPage::class => 'admin.nav.branding',
|
||||
\App\Filament\SuperAdmin\Pages\DokployDeployments::class => 'admin.nav.infrastructure',
|
||||
\App\Filament\SuperAdmin\Pages\OpsHealthDashboard::class => 'admin.nav.infrastructure',
|
||||
];
|
||||
|
||||
foreach ($expectations as $resourceClass => $key) {
|
||||
@@ -53,6 +54,7 @@ class SuperAdminNavigationGroupsTest extends TestCase
|
||||
\App\Filament\Resources\TenantResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Resources\PurchaseResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\Resources\TenantFeedbackResource::class => DailyOpsCluster::class,
|
||||
\App\Filament\SuperAdmin\Pages\OpsHealthDashboard::class => DailyOpsCluster::class,
|
||||
\App\Filament\Resources\TaskResource::class => WeeklyOpsCluster::class,
|
||||
\App\Filament\Resources\EmotionResource::class => WeeklyOpsCluster::class,
|
||||
\App\Filament\Resources\EventTypeResource::class => WeeklyOpsCluster::class,
|
||||
|
||||
Reference in New Issue
Block a user