## Spec 178 — Operations Lifecycle Alignment & Cross-Surface Truth Consistency Härtet die Run-Lifecycle-Wahrheit und Cross-Surface-Konsistenz über alle zentralen Operator-Flächen hinweg. ### Kern-Änderungen **Lifecycle Truth Alignment** - Einheitliche stale/stuck-Semantik zwischen Tenant-, Workspace-, Admin- und System-Surfaces - `OperationRunFreshnessState` wird konsistent über alle Widgets und Seiten propagiert - Gemeinsame Problem-Klassen-Trennung: `terminal_follow_up` vs. `active_stale_attention` **BulkOperationProgress Freshness** - Overlay zeigt nur noch `healthyActive()` Runs statt alle aktiven Runs - Likely-stale Runs halten das Polling nicht mehr künstlich aktiv - Terminal Runs verschwinden zeitnah aus dem Progress-Overlay **Decision Zone im Run Detail** - Stale/reconciled Attention in der primären Decision-Hierarchie - Klare Antworten: aktiv? stale? reconciled? nächster Schritt? - Artifact-reiche Runs behalten Lifecycle-Truth vor Deep-Diagnostics **Cross-Surface Link-Continuity** - Dashboard → Operations Hub → Run Detail erzählen dieselbe Geschichte - Notifications referenzieren korrekte Problem-Klasse - Workspace/Tenant-Attention verlinken problemklassengerecht **System-Plane Fixes** - `/system/ops/failures` 500-Error behoben (panel-sichere Artifact-URLs) - System-Stuck/Failures zeigen reconciled stale lineage ### Weitere Fixes - Inventory auth guard bereinigt (Gate statt ad-hoc Facades) - Browser-Smoke-Tests stabilisiert (DOM-Assertions statt fragile Klicks) - Test-Assertion-Drift für Verification/Lifecycle-Texte korrigiert ### Test-Ergebnis Full Suite: **3269 passed**, 8 skipped, 0 failed ### Spec-Artefakte - `specs/178-ops-truth-alignment/spec.md` - `specs/178-ops-truth-alignment/plan.md` - `specs/178-ops-truth-alignment/tasks.md` - `specs/178-ops-truth-alignment/research.md` - `specs/178-ops-truth-alignment/data-model.md` - `specs/178-ops-truth-alignment/quickstart.md` - `specs/178-ops-truth-alignment/contracts/operations-truth-alignment.openapi.yaml` Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #209
165 lines
7.9 KiB
PHP
165 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\System\Pages\Ops;
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\PlatformUser;
|
|
use App\Support\Auth\PlatformCapabilities;
|
|
use App\Support\Badges\BadgeDomain;
|
|
use App\Support\Badges\BadgeRenderer;
|
|
use App\Support\OperationCatalog;
|
|
use App\Support\OpsUx\OperationUxPresenter;
|
|
use App\Support\System\SystemOperationRunLinks;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
|
|
use Filament\Actions\Action;
|
|
use Filament\Pages\Page;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
|
|
class Runs extends Page implements HasTable
|
|
{
|
|
use InteractsWithTable;
|
|
|
|
protected static ?string $navigationLabel = 'Operations';
|
|
|
|
protected static ?string $title = 'Operations';
|
|
|
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-queue-list';
|
|
|
|
protected static string|\UnitEnum|null $navigationGroup = 'Ops';
|
|
|
|
protected static ?string $slug = 'ops/runs';
|
|
|
|
protected string $view = 'filament.system.pages.ops.runs';
|
|
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport)
|
|
->satisfy(ActionSurfaceSlot::ListHeader, 'The page header exposes Go to runbooks while row clicks remain the only inspect model.')
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
|
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'System operations remain scan-first and intentionally omit bulk actions.')
|
|
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains when no operations have been queued yet and repeats the Go to runbooks CTA.')
|
|
->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the canonical system run detail page, which owns header actions.');
|
|
}
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = auth('platform')->user();
|
|
|
|
if (! $user instanceof PlatformUser) {
|
|
return false;
|
|
}
|
|
|
|
return $user->hasCapability(PlatformCapabilities::OPERATIONS_VIEW)
|
|
|| ($user->hasCapability(PlatformCapabilities::OPS_VIEW) && $user->hasCapability(PlatformCapabilities::RUNBOOKS_VIEW));
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->mountInteractsWithTable();
|
|
}
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
Action::make('go_to_runbooks')
|
|
->label('Go to runbooks')
|
|
->url(Runbooks::getUrl(panel: 'system')),
|
|
];
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->defaultSort('id', 'desc')
|
|
->paginated(\App\Support\Filament\TablePaginationProfiles::customPage())
|
|
->query(function (): Builder {
|
|
return OperationRun::query()
|
|
->with(['tenant', 'workspace']);
|
|
})
|
|
->columns([
|
|
TextColumn::make('id')
|
|
->label('ID')
|
|
->state(fn (OperationRun $record): string => '#'.$record->getKey()),
|
|
TextColumn::make('status')
|
|
->badge()
|
|
->formatStateUsing(fn (mixed $state, OperationRun $record): string => BadgeRenderer::spec(BadgeDomain::OperationRunStatus, [
|
|
'status' => (string) $record->status,
|
|
'freshness_state' => $record->freshnessState()->value,
|
|
])->label)
|
|
->color(fn (mixed $state, OperationRun $record): string => BadgeRenderer::spec(BadgeDomain::OperationRunStatus, [
|
|
'status' => (string) $record->status,
|
|
'freshness_state' => $record->freshnessState()->value,
|
|
])->color)
|
|
->icon(fn (mixed $state, OperationRun $record): ?string => BadgeRenderer::spec(BadgeDomain::OperationRunStatus, [
|
|
'status' => (string) $record->status,
|
|
'freshness_state' => $record->freshnessState()->value,
|
|
])->icon)
|
|
->iconColor(fn (mixed $state, OperationRun $record): ?string => BadgeRenderer::spec(BadgeDomain::OperationRunStatus, [
|
|
'status' => (string) $record->status,
|
|
'freshness_state' => $record->freshnessState()->value,
|
|
])->iconColor)
|
|
->description(fn (OperationRun $record): ?string => OperationUxPresenter::lifecycleAttentionSummary($record)),
|
|
TextColumn::make('outcome')
|
|
->badge()
|
|
->formatStateUsing(fn (mixed $state, OperationRun $record): string => BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, [
|
|
'outcome' => (string) $record->outcome,
|
|
'status' => (string) $record->status,
|
|
'freshness_state' => $record->freshnessState()->value,
|
|
])->label)
|
|
->color(fn (mixed $state, OperationRun $record): string => BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, [
|
|
'outcome' => (string) $record->outcome,
|
|
'status' => (string) $record->status,
|
|
'freshness_state' => $record->freshnessState()->value,
|
|
])->color)
|
|
->icon(fn (mixed $state, OperationRun $record): ?string => BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, [
|
|
'outcome' => (string) $record->outcome,
|
|
'status' => (string) $record->status,
|
|
'freshness_state' => $record->freshnessState()->value,
|
|
])->icon)
|
|
->iconColor(fn (mixed $state, OperationRun $record): ?string => BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, [
|
|
'outcome' => (string) $record->outcome,
|
|
'status' => (string) $record->status,
|
|
'freshness_state' => $record->freshnessState()->value,
|
|
])->iconColor)
|
|
->description(fn (OperationRun $record): ?string => OperationUxPresenter::surfaceGuidance($record)),
|
|
TextColumn::make('type')
|
|
->label('Operation')
|
|
->formatStateUsing(fn (?string $state): string => OperationCatalog::label((string) $state))
|
|
->searchable(),
|
|
TextColumn::make('workspace.name')
|
|
->label('Workspace')
|
|
->toggleable(),
|
|
TextColumn::make('tenant.name')
|
|
->label('Tenant')
|
|
->formatStateUsing(fn (?string $state): string => $state ?: 'Tenantless')
|
|
->toggleable(),
|
|
TextColumn::make('initiator_name')->label('Initiator'),
|
|
TextColumn::make('created_at')->label('Started')->since(),
|
|
])
|
|
->recordUrl(fn (OperationRun $record): string => SystemOperationRunLinks::view($record))
|
|
->actions([])
|
|
->emptyStateHeading('No operations yet')
|
|
->emptyStateDescription('Operations from all workspaces will appear here when they are queued.')
|
|
->emptyStateActions([
|
|
Action::make('go_to_runbooks_empty')
|
|
->label('Go to runbooks')
|
|
->url(Runbooks::getUrl(panel: 'system'))
|
|
->button(),
|
|
])
|
|
->bulkActions([]);
|
|
}
|
|
}
|