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:
Ahmed Darrazi 2026-02-27 02:10:03 +01:00
parent cf6e2b1f6a
commit add45df609
8 changed files with 313 additions and 114 deletions

View File

@ -4,6 +4,7 @@
namespace App\Filament\System\Pages\Ops; namespace App\Filament\System\Pages\Ops;
use App\Models\OperationRun;
use App\Models\PlatformUser; use App\Models\PlatformUser;
use App\Models\Tenant; use App\Models\Tenant;
use App\Services\Auth\BreakGlassSession; use App\Services\Auth\BreakGlassSession;
@ -59,6 +60,21 @@ public function scopeLabel(): string
return $this->tenantId !== null ? "Single tenant (#{$this->tenantId})" : 'Single tenant'; 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 public function selectedTenantName(): ?string
{ {
if ($this->tenantId === null) { if ($this->tenantId === null) {
@ -192,6 +208,7 @@ private function scopeForm(): array
FindingsLifecycleBackfillScope::MODE_SINGLE_TENANT => 'Single tenant', FindingsLifecycleBackfillScope::MODE_SINGLE_TENANT => 'Single tenant',
]) ])
->default($this->scopeMode) ->default($this->scopeMode)
->live()
->required(), ->required(),
Select::make('tenant_id') Select::make('tenant_id')

View File

@ -58,6 +58,7 @@ public function panel(Panel $panel): Panel
->authMiddleware([ ->authMiddleware([
Authenticate::class, Authenticate::class,
'ensure-platform-capability:'.PlatformCapabilities::ACCESS_SYSTEM_PANEL, 'ensure-platform-capability:'.PlatformCapabilities::ACCESS_SYSTEM_PANEL,
]); ])
->viteTheme('resources/css/filament/system/theme.css');
} }
} }

View File

@ -0,0 +1,4 @@
@import '../../../../vendor/filament/filament/resources/css/theme.css';
@source '../../../../app/Filament/System/**/*';
@source '../../../../resources/views/filament/system/**/*.blade.php';

View File

@ -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> <x-filament-panels::page>
<div class="space-y-6"> <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"> {{-- Operator warning banner --}}
<div class="text-sm font-semibold">Operator warning</div> <x-filament::section>
<div class="mt-1 text-sm"> <div class="flex items-start gap-3">
Runbooks can modify customer data across tenants. Always run preflight first, and ensure you have the correct scope selected. <x-heroicon-o-exclamation-triangle class="h-6 w-6 shrink-0 text-amber-500 dark:text-amber-400" />
</div>
</div>
<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>
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100"> <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>
{{-- Runbook card: Rebuild Findings Lifecycle --}}
<x-filament::section>
<x-slot name="heading">
Rebuild Findings Lifecycle Rebuild Findings Lifecycle
</div> </x-slot>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
<x-slot name="description">
Backfills legacy findings lifecycle fields, SLA due dates, and consolidates drift duplicate findings. Backfills legacy findings lifecycle fields, SLA due dates, and consolidates drift duplicate findings.
</div> </x-slot>
</div>
<div class="text-xs text-gray-500 dark:text-gray-400"> <x-slot name="afterHeader">
Scope: {{ $this->scopeLabel() }} <x-filament::badge color="info" size="sm">
</div> {{ $this->scopeLabel() }}
</div> </x-filament::badge>
</x-slot>
<div class="mt-4 border-t border-gray-100 pt-4 text-sm text-gray-700 dark:border-gray-800 dark:text-gray-200"> <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)) @if (is_array($this->preflight))
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800"> <x-filament::section>
<div class="text-xs text-gray-500 dark:text-gray-400">Affected</div> <div class="text-center">
<div class="mt-1 font-semibold">{{ (int) ($this->preflight['affected_count'] ?? 0) }}</div> <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> </div>
</x-filament::section>
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800"> <x-filament::section>
<div class="text-xs text-gray-500 dark:text-gray-400">Total scanned</div> <div class="text-center">
<div class="mt-1 font-semibold">{{ (int) ($this->preflight['total_count'] ?? 0) }}</div> <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> </div>
</x-filament::section>
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800"> <x-filament::section>
<div class="text-xs text-gray-500 dark:text-gray-400">Estimated tenants</div> <div class="text-center">
<div class="mt-1 font-semibold">{{ is_numeric($this->preflight['estimated_tenants'] ?? null) ? (int) $this->preflight['estimated_tenants'] : '—' }}</div> <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> </div>
</x-filament::section>
</div> </div>
@if ((int) ($this->preflight['affected_count'] ?? 0) <= 0) @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. Nothing to do for the current scope.
</div> </div>
@endif @endif
@else @else
<div> {{-- Preflight CTA --}}
Run <span class="font-medium">Preflight</span> to see how many findings would change for the selected scope. <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> </div>
@endif @endif
</div> </div>
</div> </x-filament::section>
</div> </div>
</x-filament-panels::page> </x-filament-panels::page>

View File

@ -8,104 +8,172 @@
$reasonText = data_get($run->context, 'reason.reason_text'); $reasonText = data_get($run->context, 'reason.reason_text');
$platformInitiator = data_get($run->context, 'platform_initiator', []); $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 @endphp
<x-filament-panels::page> <x-filament-panels::page>
<div class="space-y-6"> <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"> {{-- Run header --}}
<div class="flex items-start justify-between gap-4"> <x-filament::section>
<div> <x-slot name="heading">
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
Run #{{ (int) $run->getKey() }} Run #{{ (int) $run->getKey() }}
</div> </x-slot>
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
<x-slot name="description">
{{ \App\Support\OperationCatalog::label((string) $run->type) }} {{ \App\Support\OperationCatalog::label((string) $run->type) }}
</div> </x-slot>
</div>
<div class="text-right text-xs text-gray-500 dark:text-gray-400"> <x-slot name="afterHeader">
<div>Started: {{ $run->started_at?->toDayDateTimeString() ?? '—' }}</div> <div class="flex items-center gap-2">
<div>Completed: {{ $run->completed_at?->toDayDateTimeString() ?? '—' }}</div> <x-filament::badge
</div> :color="$statusSpec->color"
</div> :icon="$statusSpec->icon"
>
{{ $statusSpec->label }}
</x-filament::badge>
<div class="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3"> @if ($outcomeSpec)
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800"> <x-filament::badge
<div class="text-xs text-gray-500 dark:text-gray-400">Status</div> :color="$outcomeSpec->color"
<div class="mt-1 font-semibold text-gray-900 dark:text-gray-100">{{ (string) $run->status }}</div> :icon="$outcomeSpec->icon"
</div> >
{{ $outcomeSpec->label }}
<div class="rounded-md bg-gray-50 p-3 dark:bg-gray-800"> </x-filament::badge>
<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 @endif
</div> </div>
</div> </x-slot>
</div>
<div class="mt-4 text-sm text-gray-700 dark:text-gray-200"> <div class="space-y-4">
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2"> {{-- Key details --}}
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div> <div>
<span class="font-medium">Initiator:</span> <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 ?? '—') }} {{ (string) ($run->initiator_name ?? '—') }}
</div>
@if (is_array($platformInitiator) && ($platformInitiator['email'] ?? null)) @if (is_array($platformInitiator) && ($platformInitiator['email'] ?? null))
<div class="sm:text-right"> <div class="text-xs text-gray-500 dark:text-gray-400">{{ (string) $platformInitiator['email'] }}</div>
<span class="font-medium">Platform user:</span>
{{ (string) ($platformInitiator['email'] ?? '') }}
</div>
@endif @endif
</dd>
</div> </div>
</dl>
{{-- Reason --}}
@if (is_string($reasonCode) && is_string($reasonText) && trim($reasonCode) !== '' && trim($reasonText) !== '') @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="flex items-start gap-3 rounded-lg bg-gray-50 px-4 py-3 dark:bg-white/5">
<div class="text-xs font-semibold text-gray-500 dark:text-gray-400">Reason</div> <x-heroicon-m-document-text class="mt-0.5 h-4 w-4 shrink-0 text-gray-400" />
<div class="mt-1 text-sm">
<span class="font-medium">{{ $reasonCode }}</span> <div>
<span class="text-gray-600 dark:text-gray-300"></span> <span class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Reason</span>
<span>{{ $reasonText }}</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>
</div> </div>
@endif @endif
</div> </div>
</x-filament::section>
{{-- 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> </div>
@if (! empty($run->summary_counts)) <details>
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900"> <summary class="cursor-pointer text-xs font-medium text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">Summary counts</div> Show raw JSON
<div class="mt-3"> </summary>
@include('filament.partials.json-viewer', ['value' => $run->summary_counts]) <div class="mt-2">
@include('filament.partials.json-viewer', ['value' => $summaryCounts])
</div> </div>
</details>
</div> </div>
</x-filament::section>
@endif @endif
{{-- Failures --}}
@if (! empty($run->failure_summary)) @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"> <x-filament::section>
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">Failures</div> <x-slot name="heading">
<div class="mt-3"> <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]) @include('filament.partials.json-viewer', ['value' => $run->failure_summary])
</div> </x-filament::section>
</div>
@endif @endif
<div class="rounded-lg border border-gray-200 bg-white p-4 dark:border-gray-800 dark:bg-gray-900"> {{-- Context --}}
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">Context</div> <x-filament::section collapsible :collapsed="true">
<div class="mt-3"> <x-slot name="heading">
Context (raw)
</x-slot>
@include('filament.partials.json-viewer', ['value' => $run->context ?? []]) @include('filament.partials.json-viewer', ['value' => $run->context ?? []])
</div> </x-filament::section>
</div>
</div> </div>
</x-filament-panels::page> </x-filament-panels::page>

View File

@ -157,11 +157,34 @@ ### Key Entities *(include if feature involves data)*
- **Operator Notification**: A delivery record/target for failure alerts. - **Operator Notification**: A delivery record/target for failure alerts.
- **Finding**: Tenant-owned record whose lifecycle/workflow fields may be backfilled. - **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)* ## Success Criteria *(mandatory)*
### Measurable Outcomes ### Measurable Outcomes
- **SC-001**: In production-like environments, customers have **zero** UI affordances to trigger backfills/repairs in `/admin`. - **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-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-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).

View File

@ -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 ## Dependencies & Execution Order
### Phase Dependencies ### Phase Dependencies

View File

@ -8,6 +8,7 @@ export default defineConfig({
input: [ input: [
'resources/css/app.css', 'resources/css/app.css',
'resources/css/filament/admin/theme.css', 'resources/css/filament/admin/theme.css',
'resources/css/filament/system/theme.css',
'resources/js/app.js', 'resources/js/app.js',
], ],
refresh: true, refresh: true,