From add45df60938a7a13b8a8077acedd3f65c9b8ffa Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Fri, 27 Feb 2026 02:10:03 +0100 Subject: [PATCH] =?UTF-8?q?feat(113):=20UX=20polish=20=E2=80=94=20Filament?= =?UTF-8?q?-native=20section=20components,=20system=20panel=20theme,=20liv?= =?UTF-8?q?e=20scope=20selector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rewrote runbooks.blade.php and view-run.blade.php using 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) --- app/Filament/System/Pages/Ops/Runbooks.php | 17 ++ .../Filament/SystemPanelProvider.php | 3 +- resources/css/filament/system/theme.css | 4 + .../system/pages/ops/runbooks.blade.php | 142 ++++++++--- .../system/pages/ops/view-run.blade.php | 220 ++++++++++++------ specs/113-platform-ops-runbooks/spec.md | 25 +- specs/113-platform-ops-runbooks/tasks.md | 15 ++ vite.config.js | 1 + 8 files changed, 313 insertions(+), 114 deletions(-) create mode 100644 resources/css/filament/system/theme.css diff --git a/app/Filament/System/Pages/Ops/Runbooks.php b/app/Filament/System/Pages/Ops/Runbooks.php index e876caf..c4499ae 100644 --- a/app/Filament/System/Pages/Ops/Runbooks.php +++ b/app/Filament/System/Pages/Ops/Runbooks.php @@ -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') diff --git a/app/Providers/Filament/SystemPanelProvider.php b/app/Providers/Filament/SystemPanelProvider.php index c68c273..a858878 100644 --- a/app/Providers/Filament/SystemPanelProvider.php +++ b/app/Providers/Filament/SystemPanelProvider.php @@ -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'); } } diff --git a/resources/css/filament/system/theme.css b/resources/css/filament/system/theme.css new file mode 100644 index 0000000..35c6a74 --- /dev/null +++ b/resources/css/filament/system/theme.css @@ -0,0 +1,4 @@ +@import '../../../../vendor/filament/filament/resources/css/theme.css'; + +@source '../../../../app/Filament/System/**/*'; +@source '../../../../resources/views/filament/system/**/*.blade.php'; diff --git a/resources/views/filament/system/pages/ops/runbooks.blade.php b/resources/views/filament/system/pages/ops/runbooks.blade.php index 747738a..7efce46 100644 --- a/resources/views/filament/system/pages/ops/runbooks.blade.php +++ b/resources/views/filament/system/pages/ops/runbooks.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 +
-
-
Operator warning
-
- Runbooks can modify customer data across tenants. Always run preflight first, and ensure you have the correct scope selected. -
-
+ {{-- Operator warning banner --}} + +
+ -
-
-
- Rebuild Findings Lifecycle -
-
- Backfills legacy findings lifecycle fields, SLA due dates, and consolidates drift duplicate findings. -
-
- -
- Scope: {{ $this->scopeLabel() }} +

Operator warning

+

+ Runbooks can modify customer data across tenants. Always run preflight first, and ensure you have the correct scope selected. +

+ -
+ {{-- Runbook card: Rebuild Findings Lifecycle --}} + + + Rebuild Findings Lifecycle + + + + Backfills legacy findings lifecycle fields, SLA due dates, and consolidates drift duplicate findings. + + + + + {{ $this->scopeLabel() }} + + + +
+ {{-- Last run metadata --}} + @if ($lastRun) +
+ Last run + + + {{ $lastRun->created_at?->diffForHumans() ?? '—' }} + + + @if ($lastRunStatusSpec) + + {{ $lastRunStatusSpec->label }} + + @endif + + @if ($lastRunOutcomeSpec) + + {{ $lastRunOutcomeSpec->label }} + + @endif + + @if ($lastRun->initiator_name) + + by {{ $lastRun->initiator_name }} + + @endif +
+ @endif + + {{-- Preflight results --}} @if (is_array($this->preflight)) -
-
-
Affected
-
{{ (int) ($this->preflight['affected_count'] ?? 0) }}
-
+
+ +
+

Affected

+

+ {{ number_format((int) ($this->preflight['affected_count'] ?? 0)) }} +

+
+
-
-
Total scanned
-
{{ (int) ($this->preflight['total_count'] ?? 0) }}
-
+ +
+

Total scanned

+

+ {{ number_format((int) ($this->preflight['total_count'] ?? 0)) }} +

+
+
-
-
Estimated tenants
-
{{ is_numeric($this->preflight['estimated_tenants'] ?? null) ? (int) $this->preflight['estimated_tenants'] : '—' }}
-
+ +
+

Estimated tenants

+

+ {{ is_numeric($this->preflight['estimated_tenants'] ?? null) ? number_format((int) $this->preflight['estimated_tenants']) : '—' }} +

+
+
@if ((int) ($this->preflight['affected_count'] ?? 0) <= 0) -
+
+ Nothing to do for the current scope.
@endif @else -
- Run Preflight to see how many findings would change for the selected scope. + {{-- Preflight CTA --}} +
+ + Run Preflight to see how many findings would change for the selected scope.
@endif
-
+
diff --git a/resources/views/filament/system/pages/ops/view-run.blade.php b/resources/views/filament/system/pages/ops/view-run.blade.php index 0d13aa5..1b49721 100644 --- a/resources/views/filament/system/pages/ops/view-run.blade.php +++ b/resources/views/filament/system/pages/ops/view-run.blade.php @@ -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
-
-
-
-
- Run #{{ (int) $run->getKey() }} -
-
- {{ \App\Support\OperationCatalog::label((string) $run->type) }} -
-
+ {{-- Run header --}} + + + Run #{{ (int) $run->getKey() }} + -
-
Started: {{ $run->started_at?->toDayDateTimeString() ?? '—' }}
-
Completed: {{ $run->completed_at?->toDayDateTimeString() ?? '—' }}
-
-
+ + {{ \App\Support\OperationCatalog::label((string) $run->type) }} + -
-
-
Status
-
{{ (string) $run->status }}
-
+ +
+ + {{ $statusSpec->label }} + -
-
Outcome
-
{{ (string) $run->outcome }}
-
- -
-
Scope
-
- @if ($scope === 'single_tenant') - Single tenant {{ is_numeric($targetTenantId) ? '#'.(int) $targetTenantId : '' }} - @elseif ($scope === 'all_tenants') - All tenants - @else - {{ $scope }} - @endif -
-
-
- -
-
-
- Initiator: - {{ (string) ($run->initiator_name ?? '—') }} -
- - @if (is_array($platformInitiator) && ($platformInitiator['email'] ?? null)) -
- Platform user: - {{ (string) ($platformInitiator['email'] ?? '') }} -
+ @if ($outcomeSpec) + + {{ $outcomeSpec->label }} + @endif
+ +
+ {{-- Key details --}} +
+
+
Scope
+
+ @if ($scope === 'single_tenant') + + Single tenant {{ is_numeric($targetTenantId) ? '#'.(int) $targetTenantId : '' }} + + @elseif ($scope === 'all_tenants') + + All tenants + + @else + {{ $scope }} + @endif +
+
+ +
+
Started
+
+ {{ $run->started_at?->toDayDateTimeString() ?? '—' }} +
+
+ +
+
Completed
+
+ {{ $run->completed_at?->toDayDateTimeString() ?? '—' }} +
+
+ +
+
Initiator
+
+ {{ (string) ($run->initiator_name ?? '—') }} + @if (is_array($platformInitiator) && ($platformInitiator['email'] ?? null)) +
{{ (string) $platformInitiator['email'] }}
+ @endif +
+
+
+ + {{-- Reason --}} @if (is_string($reasonCode) && is_string($reasonText) && trim($reasonCode) !== '' && trim($reasonText) !== '') -
-
Reason
-
- {{ $reasonCode }} - - {{ $reasonText }} +
+ + +
+ Reason +
+ {{ $reasonCode }} + {{ $reasonText }} +
@endif
-
+ - @if (! empty($run->summary_counts)) -
-
Summary counts
-
- @include('filament.partials.json-viewer', ['value' => $run->summary_counts]) + {{-- Summary counts --}} + @if ($hasSummary) + + + Summary counts + + +
+
+ @foreach ($summaryCounts as $key => $value) +
+

+ {{ \Illuminate\Support\Str::headline((string) $key) }} +

+

+ {{ is_numeric($value) ? number_format((int) $value) : $value }} +

+
+ @endforeach +
+ +
+ + Show raw JSON + +
+ @include('filament.partials.json-viewer', ['value' => $summaryCounts]) +
+
-
+ @endif + {{-- Failures --}} @if (! empty($run->failure_summary)) -
-
Failures
-
- @include('filament.partials.json-viewer', ['value' => $run->failure_summary]) -
-
+ + +
+ + Failures +
+
+ + @include('filament.partials.json-viewer', ['value' => $run->failure_summary]) +
@endif -
-
Context
-
- @include('filament.partials.json-viewer', ['value' => $run->context ?? []]) -
-
+ {{-- Context --}} + + + Context (raw) + + + @include('filament.partials.json-viewer', ['value' => $run->context ?? []]) +
diff --git a/specs/113-platform-ops-runbooks/spec.md b/specs/113-platform-ops-runbooks/spec.md index 921888a..ec57978 100644 --- a/specs/113-platform-ops-runbooks/spec.md +++ b/specs/113-platform-ops-runbooks/spec.md @@ -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). diff --git a/specs/113-platform-ops-runbooks/tasks.md b/specs/113-platform-ops-runbooks/tasks.md index 1da16ab..7353b0d 100644 --- a/specs/113-platform-ops-runbooks/tasks.md +++ b/specs/113-platform-ops-runbooks/tasks.md @@ -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 diff --git a/vite.config.js b/vite.config.js index d9a581c..bd3d929 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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,