Add superadmin ops health dashboard
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user