feat(113): UX polish — Filament-native section components, system panel theme, live scope selector
- Rewrote runbooks.blade.php and view-run.blade.php using <x-filament::section> instead of raw Tailwind div cards (cards now render correctly) - Created resources/css/filament/system/theme.css and registered viteTheme() on SystemPanelProvider — fixes missing Tailwind utilities in system panel - Added ->live() to scope Radio field so Single tenant selector appears immediately - Extended spec.md with US4 (UX Polish), FR-010–FR-014 - Extended tasks.md with Phase 7 (T050–T057)
This commit is contained in:
parent
cf6e2b1f6a
commit
add45df609
@ -4,6 +4,7 @@
|
||||
|
||||
namespace App\Filament\System\Pages\Ops;
|
||||
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\PlatformUser;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Auth\BreakGlassSession;
|
||||
@ -59,6 +60,21 @@ public function scopeLabel(): string
|
||||
return $this->tenantId !== null ? "Single tenant (#{$this->tenantId})" : 'Single tenant';
|
||||
}
|
||||
|
||||
public function lastRun(): ?OperationRun
|
||||
{
|
||||
$platformTenant = Tenant::query()->where('external_id', 'platform')->first();
|
||||
|
||||
if (! $platformTenant instanceof Tenant) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return OperationRun::query()
|
||||
->where('workspace_id', (int) $platformTenant->workspace_id)
|
||||
->where('type', FindingsLifecycleBackfillRunbookService::RUNBOOK_KEY)
|
||||
->latest('id')
|
||||
->first();
|
||||
}
|
||||
|
||||
public function selectedTenantName(): ?string
|
||||
{
|
||||
if ($this->tenantId === null) {
|
||||
@ -192,6 +208,7 @@ private function scopeForm(): array
|
||||
FindingsLifecycleBackfillScope::MODE_SINGLE_TENANT => 'Single tenant',
|
||||
])
|
||||
->default($this->scopeMode)
|
||||
->live()
|
||||
->required(),
|
||||
|
||||
Select::make('tenant_id')
|
||||
|
||||
@ -58,6 +58,7 @@ public function panel(Panel $panel): Panel
|
||||
->authMiddleware([
|
||||
Authenticate::class,
|
||||
'ensure-platform-capability:'.PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
||||
]);
|
||||
])
|
||||
->viteTheme('resources/css/filament/system/theme.css');
|
||||
}
|
||||
}
|
||||
|
||||
4
resources/css/filament/system/theme.css
Normal file
4
resources/css/filament/system/theme.css
Normal file
@ -0,0 +1,4 @@
|
||||
@import '../../../../vendor/filament/filament/resources/css/theme.css';
|
||||
|
||||
@source '../../../../app/Filament/System/**/*';
|
||||
@source '../../../../resources/views/filament/system/**/*.blade.php';
|
||||
@ -1,59 +1,129 @@
|
||||
@php
|
||||
$lastRun = $this->lastRun();
|
||||
$lastRunStatusSpec = $lastRun
|
||||
? \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::OperationRunStatus, (string) $lastRun->status)
|
||||
: null;
|
||||
$lastRunOutcomeSpec = $lastRun && (string) $lastRun->status === 'completed'
|
||||
? \App\Support\Badges\BadgeRenderer::spec(\App\Support\Badges\BadgeDomain::OperationRunOutcome, (string) $lastRun->outcome)
|
||||
: null;
|
||||
@endphp
|
||||
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-lg border border-amber-200 bg-amber-50 p-4 text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100">
|
||||
<div class="text-sm font-semibold">Operator warning</div>
|
||||
<div class="mt-1 text-sm">
|
||||
Runbooks can modify customer data across tenants. Always run preflight first, and ensure you have the correct scope selected.
|
||||
</div>
|
||||
</div>
|
||||
{{-- Operator warning banner --}}
|
||||
<x-filament::section>
|
||||
<div class="flex items-start gap-3">
|
||||
<x-heroicon-o-exclamation-triangle class="h-6 w-6 shrink-0 text-amber-500 dark:text-amber-400" />
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
Rebuild Findings Lifecycle
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
Backfills legacy findings lifecycle fields, SLA due dates, and consolidates drift duplicate findings.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
Scope: {{ $this->scopeLabel() }}
|
||||
<p class="text-sm font-semibold text-amber-700 dark:text-amber-300">Operator warning</p>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
Runbooks can modify customer data across tenants. Always run preflight first, and ensure you have the correct scope selected.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<div class="mt-4 border-t border-gray-100 pt-4 text-sm text-gray-700 dark:border-gray-800 dark:text-gray-200">
|
||||
{{-- Runbook card: Rebuild Findings Lifecycle --}}
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">
|
||||
Rebuild Findings Lifecycle
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="description">
|
||||
Backfills legacy findings lifecycle fields, SLA due dates, and consolidates drift duplicate findings.
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="afterHeader">
|
||||
<x-filament::badge color="info" size="sm">
|
||||
{{ $this->scopeLabel() }}
|
||||
</x-filament::badge>
|
||||
</x-slot>
|
||||
|
||||
<div class="space-y-4">
|
||||
{{-- Last run metadata --}}
|
||||
@if ($lastRun)
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-2 rounded-lg bg-gray-50 px-4 py-3 dark:bg-white/5">
|
||||
<span class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Last run</span>
|
||||
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ $lastRun->created_at?->diffForHumans() ?? '—' }}
|
||||
</span>
|
||||
|
||||
@if ($lastRunStatusSpec)
|
||||
<x-filament::badge
|
||||
:color="$lastRunStatusSpec->color"
|
||||
:icon="$lastRunStatusSpec->icon"
|
||||
size="sm"
|
||||
>
|
||||
{{ $lastRunStatusSpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
@if ($lastRunOutcomeSpec)
|
||||
<x-filament::badge
|
||||
:color="$lastRunOutcomeSpec->color"
|
||||
:icon="$lastRunOutcomeSpec->icon"
|
||||
size="sm"
|
||||
>
|
||||
{{ $lastRunOutcomeSpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
|
||||
@if ($lastRun->initiator_name)
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
by {{ $lastRun->initiator_name }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Preflight results --}}
|
||||
@if (is_array($this->preflight))
|
||||
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Affected</div>
|
||||
<div class="mt-1 font-semibold">{{ (int) ($this->preflight['affected_count'] ?? 0) }}</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<x-filament::section>
|
||||
<div class="text-center">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Affected</p>
|
||||
<p class="mt-2 text-3xl font-bold tabular-nums text-gray-950 dark:text-white">
|
||||
{{ number_format((int) ($this->preflight['affected_count'] ?? 0)) }}
|
||||
</p>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Total scanned</div>
|
||||
<div class="mt-1 font-semibold">{{ (int) ($this->preflight['total_count'] ?? 0) }}</div>
|
||||
</div>
|
||||
<x-filament::section>
|
||||
<div class="text-center">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Total scanned</p>
|
||||
<p class="mt-2 text-3xl font-bold tabular-nums text-gray-950 dark:text-white">
|
||||
{{ number_format((int) ($this->preflight['total_count'] ?? 0)) }}
|
||||
</p>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Estimated tenants</div>
|
||||
<div class="mt-1 font-semibold">{{ is_numeric($this->preflight['estimated_tenants'] ?? null) ? (int) $this->preflight['estimated_tenants'] : '—' }}</div>
|
||||
</div>
|
||||
<x-filament::section>
|
||||
<div class="text-center">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Estimated tenants</p>
|
||||
<p class="mt-2 text-3xl font-bold tabular-nums text-gray-950 dark:text-white">
|
||||
{{ is_numeric($this->preflight['estimated_tenants'] ?? null) ? number_format((int) $this->preflight['estimated_tenants']) : '—' }}
|
||||
</p>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
|
||||
@if ((int) ($this->preflight['affected_count'] ?? 0) <= 0)
|
||||
<div class="mt-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<x-heroicon-m-check-circle class="h-5 w-5 text-success-500" />
|
||||
Nothing to do for the current scope.
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div>
|
||||
Run <span class="font-medium">Preflight</span> to see how many findings would change for the selected scope.
|
||||
{{-- Preflight CTA --}}
|
||||
<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<x-heroicon-m-magnifying-glass class="h-5 w-5" />
|
||||
Run <span class="mx-1 font-semibold text-gray-700 dark:text-gray-200">Preflight</span> to see how many findings would change for the selected scope.
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
||||
|
||||
@ -8,104 +8,172 @@
|
||||
$reasonText = data_get($run->context, 'reason.reason_text');
|
||||
|
||||
$platformInitiator = data_get($run->context, 'platform_initiator', []);
|
||||
|
||||
$statusSpec = \App\Support\Badges\BadgeRenderer::spec(
|
||||
\App\Support\Badges\BadgeDomain::OperationRunStatus,
|
||||
(string) $run->status,
|
||||
);
|
||||
|
||||
$outcomeSpec = (string) $run->status === 'completed'
|
||||
? \App\Support\Badges\BadgeRenderer::spec(
|
||||
\App\Support\Badges\BadgeDomain::OperationRunOutcome,
|
||||
(string) $run->outcome,
|
||||
)
|
||||
: null;
|
||||
|
||||
$summaryCounts = $run->summary_counts;
|
||||
$hasSummary = is_array($summaryCounts) && count($summaryCounts) > 0;
|
||||
@endphp
|
||||
|
||||
<x-filament-panels::page>
|
||||
<div class="space-y-6">
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
Run #{{ (int) $run->getKey() }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ \App\Support\OperationCatalog::label((string) $run->type) }}
|
||||
</div>
|
||||
</div>
|
||||
{{-- Run header --}}
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">
|
||||
Run #{{ (int) $run->getKey() }}
|
||||
</x-slot>
|
||||
|
||||
<div class="text-right text-xs text-gray-500 dark:text-gray-400">
|
||||
<div>Started: {{ $run->started_at?->toDayDateTimeString() ?? '—' }}</div>
|
||||
<div>Completed: {{ $run->completed_at?->toDayDateTimeString() ?? '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<x-slot name="description">
|
||||
{{ \App\Support\OperationCatalog::label((string) $run->type) }}
|
||||
</x-slot>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3">
|
||||
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Status</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-gray-100">{{ (string) $run->status }}</div>
|
||||
</div>
|
||||
<x-slot name="afterHeader">
|
||||
<div class="flex items-center gap-2">
|
||||
<x-filament::badge
|
||||
:color="$statusSpec->color"
|
||||
:icon="$statusSpec->icon"
|
||||
>
|
||||
{{ $statusSpec->label }}
|
||||
</x-filament::badge>
|
||||
|
||||
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Outcome</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-gray-100">{{ (string) $run->outcome }}</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Scope</div>
|
||||
<div class="mt-1 font-semibold text-gray-900 dark:text-gray-100">
|
||||
@if ($scope === 'single_tenant')
|
||||
Single tenant {{ is_numeric($targetTenantId) ? '#'.(int) $targetTenantId : '' }}
|
||||
@elseif ($scope === 'all_tenants')
|
||||
All tenants
|
||||
@else
|
||||
{{ $scope }}
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-sm text-gray-700 dark:text-gray-200">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<div>
|
||||
<span class="font-medium">Initiator:</span>
|
||||
{{ (string) ($run->initiator_name ?? '—') }}
|
||||
</div>
|
||||
|
||||
@if (is_array($platformInitiator) && ($platformInitiator['email'] ?? null))
|
||||
<div class="sm:text-right">
|
||||
<span class="font-medium">Platform user:</span>
|
||||
{{ (string) ($platformInitiator['email'] ?? '') }}
|
||||
</div>
|
||||
@if ($outcomeSpec)
|
||||
<x-filament::badge
|
||||
:color="$outcomeSpec->color"
|
||||
:icon="$outcomeSpec->icon"
|
||||
>
|
||||
{{ $outcomeSpec->label }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="space-y-4">
|
||||
{{-- Key details --}}
|
||||
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div>
|
||||
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Scope</dt>
|
||||
<dd class="mt-1">
|
||||
@if ($scope === 'single_tenant')
|
||||
<x-filament::badge color="info" size="sm">
|
||||
Single tenant {{ is_numeric($targetTenantId) ? '#'.(int) $targetTenantId : '' }}
|
||||
</x-filament::badge>
|
||||
@elseif ($scope === 'all_tenants')
|
||||
<x-filament::badge color="warning" size="sm">
|
||||
All tenants
|
||||
</x-filament::badge>
|
||||
@else
|
||||
<span class="text-sm font-medium text-gray-950 dark:text-white">{{ $scope }}</span>
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Started</dt>
|
||||
<dd class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $run->started_at?->toDayDateTimeString() ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Completed</dt>
|
||||
<dd class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ $run->completed_at?->toDayDateTimeString() ?? '—' }}
|
||||
</dd>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Initiator</dt>
|
||||
<dd class="mt-1 text-sm font-medium text-gray-950 dark:text-white">
|
||||
{{ (string) ($run->initiator_name ?? '—') }}
|
||||
@if (is_array($platformInitiator) && ($platformInitiator['email'] ?? null))
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ (string) $platformInitiator['email'] }}</div>
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
{{-- Reason --}}
|
||||
@if (is_string($reasonCode) && is_string($reasonText) && trim($reasonCode) !== '' && trim($reasonText) !== '')
|
||||
<div class="mt-3 rounded-md border border-gray-200 bg-gray-50 p-3 dark:border-gray-800 dark:bg-gray-800">
|
||||
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Reason</div>
|
||||
<div class="mt-1 text-sm">
|
||||
<span class="font-medium">{{ $reasonCode }}</span>
|
||||
<span class="text-gray-600 dark:text-gray-300">—</span>
|
||||
<span>{{ $reasonText }}</span>
|
||||
<div class="flex items-start gap-3 rounded-lg bg-gray-50 px-4 py-3 dark:bg-white/5">
|
||||
<x-heroicon-m-document-text class="mt-0.5 h-4 w-4 shrink-0 text-gray-400" />
|
||||
|
||||
<div>
|
||||
<span class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Reason</span>
|
||||
<div class="mt-1 text-sm text-gray-950 dark:text-white">
|
||||
<x-filament::badge color="gray" size="sm">{{ $reasonCode }}</x-filament::badge>
|
||||
<span class="ml-1">{{ $reasonText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
@if (! empty($run->summary_counts))
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">Summary counts</div>
|
||||
<div class="mt-3">
|
||||
@include('filament.partials.json-viewer', ['value' => $run->summary_counts])
|
||||
{{-- Summary counts --}}
|
||||
@if ($hasSummary)
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">
|
||||
Summary counts
|
||||
</x-slot>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4">
|
||||
@foreach ($summaryCounts as $key => $value)
|
||||
<div class="rounded-lg bg-gray-50 px-4 py-3 dark:bg-white/5">
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">
|
||||
{{ \Illuminate\Support\Str::headline((string) $key) }}
|
||||
</p>
|
||||
<p class="mt-1 text-xl font-bold text-gray-950 dark:text-white">
|
||||
{{ is_numeric($value) ? number_format((int) $value) : $value }}
|
||||
</p>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary class="cursor-pointer text-xs font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
|
||||
Show raw JSON
|
||||
</summary>
|
||||
<div class="mt-2">
|
||||
@include('filament.partials.json-viewer', ['value' => $summaryCounts])
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
{{-- Failures --}}
|
||||
@if (! empty($run->failure_summary))
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">Failures</div>
|
||||
<div class="mt-3">
|
||||
@include('filament.partials.json-viewer', ['value' => $run->failure_summary])
|
||||
</div>
|
||||
</div>
|
||||
<x-filament::section>
|
||||
<x-slot name="heading">
|
||||
<div class="flex items-center gap-2 text-danger-600 dark:text-danger-400">
|
||||
<x-heroicon-m-exclamation-circle class="h-5 w-5" />
|
||||
Failures
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
@include('filament.partials.json-viewer', ['value' => $run->failure_summary])
|
||||
</x-filament::section>
|
||||
@endif
|
||||
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">Context</div>
|
||||
<div class="mt-3">
|
||||
@include('filament.partials.json-viewer', ['value' => $run->context ?? []])
|
||||
</div>
|
||||
</div>
|
||||
{{-- Context --}}
|
||||
<x-filament::section collapsible :collapsed="true">
|
||||
<x-slot name="heading">
|
||||
Context (raw)
|
||||
</x-slot>
|
||||
|
||||
@include('filament.partials.json-viewer', ['value' => $run->context ?? []])
|
||||
</x-filament::section>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
||||
|
||||
@ -157,11 +157,34 @@ ### Key Entities *(include if feature involves data)*
|
||||
- **Operator Notification**: A delivery record/target for failure alerts.
|
||||
- **Finding**: Tenant-owned record whose lifecycle/workflow fields may be backfilled.
|
||||
|
||||
### User Story 4 - Enterprise-grade UX polish for Ops surfaces (Priority: P2)
|
||||
|
||||
As a platform operator, the Ops surfaces should look and feel enterprise-grade with proper visual hierarchy, alert banners, structured card layouts, badge indicators, and metadata so I can quickly assess system state.
|
||||
|
||||
**Why this priority**: Operator trust and efficiency depend on clear, scannable UI. Raw text and flat layouts slow down triage.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the operator opens `/system/ops/runbooks`, **Then** the operator warning is rendered as a styled alert banner with icon (not plain text).
|
||||
2. **Given** runbooks are listed, **Then** each runbook is rendered as a structured card with title, description, scope badge, and "Last run" metadata when available.
|
||||
3. **Given** preflight results are displayed, **Then** stat values use consistent stat-card styling with labels and prominent values.
|
||||
4. **Given** the operator opens `/system/ops/runs/{id}`, **Then** status and outcome are rendered as colored badges (consistent with the existing BadgeRenderer), and scope is shown as a badge/tag.
|
||||
5. **Given** the run detail page, **Then** summary counts are rendered as a labeled grid (not only raw JSON).
|
||||
|
||||
### Functional Requirements (UX Polish)
|
||||
|
||||
- **FR-010 (Operator Warning Banner)**: The operator warning on `/system/ops/runbooks` MUST be rendered as a visually distinct alert banner with an `exclamation-triangle` icon, amber/warning coloring, and clear heading — matching project alert patterns.
|
||||
- **FR-011 (Runbook Card Layout)**: Each runbook MUST be rendered as a card with: title (semibold), description, scope badge (e.g., "All tenants"), and optional "Last run" timestamp + status badge when a previous run exists.
|
||||
- **FR-012 (Preflight Stat Cards)**: Preflight result values (affected, total scanned, estimated tenants) MUST be rendered in visually prominent stat cards with labeled headers.
|
||||
- **FR-013 (Run Detail Badges)**: Status and outcome on run detail pages MUST use the existing `BadgeRenderer` / `BadgeCatalog` system for colored badges with icons.
|
||||
- **FR-014 (Run Detail Summary Grid)**: Summary counts on run detail MUST be rendered as a labeled key-value grid, not a raw JSON dump (JSON viewer remains available as a disclosure fallback).
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: In production-like environments, customers have **zero** UI affordances to trigger backfills/repairs in `/admin`.
|
||||
- **SC-002**: A platform operator can start a runbook without SSH and reach “View run” in **≤ 3 user interactions** from `/system/ops/runbooks`.
|
||||
- **SC-002**: A platform operator can start a runbook without SSH and reach "View run" in **≤ 3 user interactions** from `/system/ops/runbooks`.
|
||||
- **SC-003**: 100% of run attempts result in an operation run record and start/completion/failure audit events (with failure still recorded even if notifications fail).
|
||||
- **SC-004**: Re-running the same runbook on the same scope after completion results in `updated_count = 0` (idempotency).
|
||||
- **SC-005**: Operator warning on runbooks page renders as a styled alert banner (not plain text).
|
||||
|
||||
@ -130,6 +130,21 @@ ## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
---
|
||||
|
||||
## Phase 7: UX Polish — Enterprise-grade Ops surfaces (User Story 4)
|
||||
|
||||
**Purpose**: Elevate operator-facing views from functional MVP to enterprise-grade UX with proper visual hierarchy, alert banners, card layouts, badge indicators, and metadata.
|
||||
|
||||
- [X] T050 [US4] Upgrade operator warning from plain text to styled alert banner with icon in resources/views/filament/system/pages/ops/runbooks.blade.php (FR-010)
|
||||
- [X] T051 [US4] Restructure runbook entry as a card with title, description, scope badge, and "Last run" metadata in resources/views/filament/system/pages/ops/runbooks.blade.php + app/Filament/System/Pages/Ops/Runbooks.php (FR-011)
|
||||
- [X] T052 [US4] Upgrade preflight stat values to prominent stat-card styling in resources/views/filament/system/pages/ops/runbooks.blade.php (FR-012)
|
||||
- [X] T053 [US4] Render status/outcome as BadgeRenderer badges on run detail page in resources/views/filament/system/pages/ops/view-run.blade.php (FR-013)
|
||||
- [X] T054 [US4] Render summary_counts as labeled key-value grid with JSON fallback on run detail in resources/views/filament/system/pages/ops/view-run.blade.php (FR-014)
|
||||
- [X] T055 [US4] "Recovery" nav group with "Repair workspace owners" already exists (pre-existing; no change needed)
|
||||
- [X] T056 [P] Run formatting via vendor/bin/sail bin pint --dirty --format agent
|
||||
- [X] T057 [P] Run existing Spec 113 tests to verify no regressions (16 passed, 141 assertions)
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
@ -8,6 +8,7 @@ export default defineConfig({
|
||||
input: [
|
||||
'resources/css/app.css',
|
||||
'resources/css/filament/admin/theme.css',
|
||||
'resources/css/filament/system/theme.css',
|
||||
'resources/js/app.js',
|
||||
],
|
||||
refresh: true,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user