Compare commits

..

3 Commits

Author SHA1 Message Date
1b88d28739 feat: consolidate operation naming surfaces (#202)
## Summary
- align operator-visible OperationRun terminology to canonical `Operations` / `Operation` labels across shared links, notifications, verification/onboarding surfaces, summary widgets, and monitoring/detail pages
- add the Spec 171 planning artifacts under `specs/171-operations-naming-consolidation/`
- close the remaining tenant dashboard and admin copy drift found during browser smoke validation

## Validation
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && vendor/bin/sail artisan test --compact tests/Unit/Support/RelatedNavigationResolverTest.php tests/Unit/Support/References/RelatedContextReferenceAdapterTest.php tests/Feature/OpsUx/NotificationViewRunLinkTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/Filament/BackupSetResolvedReferencePresentationTest.php tests/Feature/Filament/TenantVerificationReportWidgetTest.php tests/Feature/Onboarding/OnboardingVerificationTest.php tests/Feature/Onboarding/OnboardingVerificationClustersTest.php tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php tests/Feature/Filament/WorkspaceOverviewContentTest.php tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php tests/Feature/Monitoring/OperationLifecycleAggregateVisibilityTest.php tests/Feature/System/Spec114/OpsTriageActionsTest.php tests/Feature/System/Spec114/OpsFailuresViewTest.php tests/Feature/System/Spec114/OpsStuckViewTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && vendor/bin/sail artisan test --compact tests/Browser/OnboardingDraftRefreshTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && vendor/bin/sail bin pint --dirty --format agent`

## Notes
- no schema or route renames
- Filament / Livewire surface behavior stays within the existing admin and tenant panels
- OperationRunResource remains excluded from global search

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #202
2026-03-30 22:51:06 +00:00
fdd3a85b64 feat: align system operations surfaces (#201)
## Summary
- align the system-panel Operations, Failed operations, and Stuck operations pages to the read-only registry contract by removing inline row triage and keeping row-click inspection
- keep retry, cancel, and mark-investigated behavior on the canonical system operation detail page while adding the explicit `Show all operations` return path and updated `Operations / Operation` copy
- add and update focused Pest and Livewire coverage for list CTA behavior, detail-owned triage, and view-only versus manage-capable platform access
- add Spec 170 implementation artifacts plus the follow-on Spec 171 and Spec 172 packages

## Testing
- `vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsTriageActionsTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsFailuresViewTest.php`
- `vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsStuckViewTest.php`
- integrated browser smoke on `/system/ops/runs`, `/system/ops/failures`, `/system/ops/stuck`, empty states via search filter, and detail-page retry confirmation visibility

## Notes
- branch pushed from `170-system-operations-surface-alignment`
- latest commit: `64b4d741 feat: align system operations surfaces`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #201
2026-03-30 19:08:56 +00:00
37c6d0622c feat: implement spec 169 action surface contract v1.1 (#200)
## Summary
- implement the Action Surface Contract v1.1 runtime changes for Spec 169
- add the new explicit ActionSurfaceType contract, validator/discovery updates, and enrolled surface declarations
- update Filament action-surface documentation, focused guard tests, and spec artifacts for the completed feature

## Included
- clickable-row vs explicit-inspect enforcement across monitoring, reporting, CRUD, and system reference surfaces
- helper-first, workflow-next, destructive-last overflow ordering checks
- system panel list discovery in the primary action-surface validator
- Spec 169 artifacts: spec, plan, tasks, research, data model, quickstart, and logical contract

## Verification
- focused Pest verification pack completed for:
  - tests/Feature/Guards/ActionSurfaceValidatorTest.php
  - tests/Feature/Guards/ActionSurfaceContractTest.php
  - tests/Feature/Rbac/TenantActionSurfaceConsistencyTest.php
- integrated browser smoke test completed for admin-side reference surfaces:
  - /admin/operations
  - /admin/audit-log
  - /admin/finding-exceptions/queue
  - /admin/reviews
  - /admin/tenants

## Notes
- system panel browser smoke coverage could not be exercised in the same session because /system routes require platform authentication in the integrated browser
- Livewire target remains v4-compliant and no provider registration or asset strategy changes are introduced by this PR

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #200
2026-03-30 09:21:39 +00:00
122 changed files with 2995 additions and 553 deletions

View File

@ -118,6 +118,10 @@ ## Active Technologies
- PostgreSQL unchanged; no new persistence, cache store, or durable summary artifac (168-tenant-governance-aggregate-contract) - PostgreSQL unchanged; no new persistence, cache store, or durable summary artifac (168-tenant-governance-aggregate-contract)
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `ActionSurfaceDeclaration`, `ActionSurfaceValidator`, `ActionSurfaceDiscovery`, `ActionSurfaceExemptions`, and Filament Tables / Actions APIs (169-action-surface-v11) - PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `ActionSurfaceDeclaration`, `ActionSurfaceValidator`, `ActionSurfaceDiscovery`, `ActionSurfaceExemptions`, and Filament Tables / Actions APIs (169-action-surface-v11)
- PostgreSQL unchanged; no new persistence, cache store, queue payload, or durable artifac (169-action-surface-v11) - PostgreSQL unchanged; no new persistence, cache store, queue payload, or durable artifac (169-action-surface-v11)
- PHP 8.4, Laravel 12, Livewire v4, Filament v5 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest` (170-system-operations-surface-alignment)
- PostgreSQL with existing `operation_runs` and `audit_logs` tables; no schema changes (170-system-operations-surface-alignment)
- PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest` (171-operations-naming-consolidation)
- PostgreSQL with existing `operation_runs`, notification payloads, workspace records, and tenant records; no schema changes (171-operations-naming-consolidation)
- PHP 8.4.15 (feat/005-bulk-operations) - PHP 8.4.15 (feat/005-bulk-operations)
@ -137,8 +141,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions PHP 8.4.15: Follow standard conventions
## Recent Changes ## Recent Changes
- 171-operations-naming-consolidation: Added PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest`
- 170-system-operations-surface-alignment: Added PHP 8.4, Laravel 12, Livewire v4, Filament v5 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest`
- 169-action-surface-v11: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `ActionSurfaceDeclaration`, `ActionSurfaceValidator`, `ActionSurfaceDiscovery`, `ActionSurfaceExemptions`, and Filament Tables / Actions APIs - 169-action-surface-v11: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `ActionSurfaceDeclaration`, `ActionSurfaceValidator`, `ActionSurfaceDiscovery`, `ActionSurfaceExemptions`, and Filament Tables / Actions APIs
- 168-tenant-governance-aggregate-contract: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareStats`, `BaselineCompareSummaryAssessor`, `BaselineCompareLanding`, `BaselineCompareNow`, `NeedsAttention`, `BaselineCompareCoverageBanner`, and `RequestScopedDerivedStateStore` from Spec 167
- 167-derived-state-memoization: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `ArtifactTruthPresenter`, `OperationUxPresenter`, `RelatedNavigationResolver`, `AppServiceProvider`, `BadgeCatalog`, `BadgeRenderer`, and current Filament resource/page seams
<!-- MANUAL ADDITIONS START --> <!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END --> <!-- MANUAL ADDITIONS END -->

View File

@ -388,7 +388,7 @@ private function compareNowAction(): Action
OperationUxPresenter::queuedToast($run instanceof OperationRun ? (string) $run->type : 'baseline_compare') OperationUxPresenter::queuedToast($run instanceof OperationRun ? (string) $run->type : 'baseline_compare')
->actions($run instanceof OperationRun ? [ ->actions($run instanceof OperationRun ? [
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($run, $tenant)), ->url(OperationRunLinks::view($run, $tenant)),
] : []) ] : [])
->send(); ->send();

View File

@ -36,6 +36,7 @@
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Schemas\Components\EmbeddedSchema; use Filament\Schemas\Components\EmbeddedSchema;
use Filament\Schemas\Schema; use Filament\Schemas\Schema;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -47,7 +48,7 @@ class TenantlessOperationRunViewer extends Page
protected static bool $isDiscovered = false; protected static bool $isDiscovered = false;
protected static ?string $title = 'Operation run'; protected static ?string $title = 'Operation';
protected string $view = 'filament.pages.operations.tenantless-operation-run-viewer'; protected string $view = 'filament.pages.operations.tenantless-operation-run-viewer';
@ -67,6 +68,11 @@ public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
public OperationRun $run; public OperationRun $run;
public function getTitle(): string|Htmlable
{
return OperationRunLinks::identifier($this->run);
}
/** /**
* @var array<string, mixed>|null * @var array<string, mixed>|null
*/ */
@ -196,7 +202,7 @@ public function blockedExecutionBanner(): ?array
$operatorExplanation->dominantCauseExplanation, $operatorExplanation->dominantCauseExplanation,
])) ]))
: ($reasonEnvelope?->toBodyLines(false) ?? [ : ($reasonEnvelope?->toBodyLines(false) ?? [
$this->surfaceFailureDetail() ?? 'The queued run was refused before side effects could begin.', $this->surfaceFailureDetail() ?? 'The queued operation was refused before side effects could begin.',
]); ]);
return [ return [
@ -226,7 +232,7 @@ public function lifecycleBanner(): ?array
return match ($this->run->freshnessState()->value) { return match ($this->run->freshnessState()->value) {
'likely_stale' => [ 'likely_stale' => [
'tone' => 'amber', 'tone' => 'amber',
'title' => 'Likely stale run', 'title' => 'Likely stale operation',
'body' => $detail, 'body' => $detail,
], ],
'reconciled_failed' => [ 'reconciled_failed' => [
@ -253,19 +259,19 @@ public function canonicalContextBanner(): ?array
if (! $runTenant instanceof Tenant) { if (! $runTenant instanceof Tenant) {
return [ return [
'tone' => 'slate', 'tone' => 'slate',
'title' => 'Workspace-level run', 'title' => 'Workspace-level operation',
'body' => $activeTenant instanceof Tenant 'body' => $activeTenant instanceof Tenant
? 'This canonical workspace view is not tied to the current tenant context ('.$activeTenant->name.').' ? 'This canonical workspace view is not tied to the current tenant context ('.$activeTenant->name.').'
: 'This canonical workspace view is not tied to any tenant.', : 'This canonical workspace view is not tied to any tenant.',
]; ];
} }
$messages = ['Run tenant: '.$runTenant->name.'.']; $messages = ['Operation tenant: '.$runTenant->name.'.'];
$tone = 'sky'; $tone = 'sky';
$title = null; $title = null;
if ($activeTenant instanceof Tenant && ! $activeTenant->is($runTenant)) { if ($activeTenant instanceof Tenant && ! $activeTenant->is($runTenant)) {
$title = 'Current tenant context differs from this run'; $title = 'Current tenant context differs from this operation';
array_unshift($messages, 'Current tenant context: '.$activeTenant->name.'.'); array_unshift($messages, 'Current tenant context: '.$activeTenant->name.'.');
$messages[] = 'This canonical workspace view remains valid without switching tenant context.'; $messages[] = 'This canonical workspace view remains valid without switching tenant context.';
} }
@ -273,7 +279,7 @@ public function canonicalContextBanner(): ?array
$referencedTenant = ReferencedTenantLifecyclePresentation::forOperationRun($runTenant); $referencedTenant = ReferencedTenantLifecyclePresentation::forOperationRun($runTenant);
if ($selectorAvailabilityMessage = $referencedTenant->selectorAvailabilityMessage()) { if ($selectorAvailabilityMessage = $referencedTenant->selectorAvailabilityMessage()) {
$title ??= 'Run tenant is not available in the current tenant selector'; $title ??= 'Operation tenant is not available in the current tenant selector';
$tone = 'amber'; $tone = 'amber';
$messages[] = $selectorAvailabilityMessage; $messages[] = $selectorAvailabilityMessage;
@ -334,7 +340,7 @@ private function resumeCaptureAction(): Action
if (! isset($this->run)) { if (! isset($this->run)) {
Notification::make() Notification::make()
->title('Run not loaded') ->title('Operation not loaded')
->danger() ->danger()
->send(); ->send();
@ -361,7 +367,7 @@ private function resumeCaptureAction(): Action
if (! $run instanceof OperationRun) { if (! $run instanceof OperationRun) {
Notification::make() Notification::make()
->title('Cannot resume capture') ->title('Cannot resume capture')
->body('Reason: missing operation run') ->body('Reason: missing operation')
->danger() ->danger()
->send(); ->send();
@ -369,7 +375,7 @@ private function resumeCaptureAction(): Action
} }
$viewAction = Action::make('view_run') $viewAction = Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::tenantlessView($run)); ->url(OperationRunLinks::tenantlessView($run));
if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) { if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) {

View File

@ -532,7 +532,7 @@ public function content(Schema $schema): Schema
}), }),
Step::make('Verify access') Step::make('Verify access')
->description('Run a queued verification check (Operation Run).') ->description('Run a queued verification check (Operation).')
->schema([ ->schema([
Section::make('Verification') Section::make('Verification')
->schema([ ->schema([
@ -541,7 +541,7 @@ public function content(Schema $schema): Schema
->color(fn (): string => $this->verificationStatusColor()), ->color(fn (): string => $this->verificationStatusColor()),
Text::make('Connection updated — re-run verification to refresh results.') Text::make('Connection updated — re-run verification to refresh results.')
->visible(fn (): bool => $this->connectionRecentlyUpdated()), ->visible(fn (): bool => $this->connectionRecentlyUpdated()),
Text::make('The selected provider connection has changed since this verification run. Start verification again to validate the current connection.') Text::make('The selected provider connection has changed since this verification operation. Start verification again to validate the current connection.')
->visible(fn (): bool => $this->verificationRunIsStaleForSelectedConnection()), ->visible(fn (): bool => $this->verificationRunIsStaleForSelectedConnection()),
Text::make('Verification is in progress. Status updates automatically about every 5 seconds. Use “Refresh” to re-check immediately.') Text::make('Verification is in progress. Status updates automatically about every 5 seconds. Use “Refresh” to re-check immediately.')
->visible(fn (): bool => $this->verificationStatus() === 'in_progress'), ->visible(fn (): bool => $this->verificationStatus() === 'in_progress'),
@ -2008,14 +2008,14 @@ private function bootstrapRunsLabel(): string
$failedRuns = array_filter($runs, static fn (array $run): bool => (bool) $run['is_failure'] || (bool) $run['is_partial_failure']); $failedRuns = array_filter($runs, static fn (array $run): bool => (bool) $run['is_failure'] || (bool) $run['is_partial_failure']);
if ($activeRuns !== []) { if ($activeRuns !== []) {
return sprintf('Bootstrap is running across %d operation run(s).', count($activeRuns)); return sprintf('Bootstrap is running across %d operation(s).', count($activeRuns));
} }
if ($failedRuns !== []) { if ($failedRuns !== []) {
return sprintf('Bootstrap needs attention for %d operation run(s).', count($failedRuns)); return sprintf('Bootstrap needs attention for %d operation(s).', count($failedRuns));
} }
return sprintf('Bootstrap completed across %d operation run(s).', count($runs)); return sprintf('Bootstrap completed across %d operation(s).', count($runs));
} }
private function touchOnboardingSessionStep(string $step): void private function touchOnboardingSessionStep(string $step): void
@ -2837,11 +2837,11 @@ public function startVerification(): void
Notification::make() Notification::make()
->title('Another operation is already running') ->title('Another operation is already running')
->body('Please wait for the active run to finish.') ->body('Please wait for the active operation to finish.')
->warning() ->warning()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())), ->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())),
]) ])
->send(); ->send();
@ -2856,7 +2856,7 @@ public function startVerification(): void
$actions = [ $actions = [
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())), ->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())),
]; ];
@ -2901,7 +2901,7 @@ public function startVerification(): void
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())), ->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())),
]) ])
->send(); ->send();
@ -2912,7 +2912,7 @@ public function startVerification(): void
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())), ->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())),
]) ])
->send(); ->send();
@ -3116,11 +3116,11 @@ public function startBootstrap(array $operationTypes): void
Notification::make() Notification::make()
->title('Another operation is already running') ->title('Another operation is already running')
->body('Please wait for the active run to finish.') ->body('Please wait for the active operation to finish.')
->warning() ->warning()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result['run']->getKey())), ->url($this->tenantlessOperationRunUrl((int) $result['run']->getKey())),
]) ])
->send(); ->send();
@ -3164,7 +3164,7 @@ public function startBootstrap(array $operationTypes): void
if ($runUrl !== null) { if ($runUrl !== null) {
$toast->actions([ $toast->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]); ]);
} }
@ -3404,7 +3404,7 @@ private function completionSummaryBootstrapDetail(): string
return 'No bootstrap actions selected'; return 'No bootstrap actions selected';
} }
return sprintf('%d operation run(s) started', count($runs)); return sprintf('%d operation(s) started', count($runs));
} }
private function completionSummaryBootstrapSummary(): string private function completionSummaryBootstrapSummary(): string
@ -3726,7 +3726,7 @@ private function verificationAssistStaleReason(): ?string
return null; return null;
} }
return 'The selected provider connection has changed since this verification run. Start verification again to validate the current connection.'; return 'The selected provider connection has changed since this verification operation. Start verification again to validate the current connection.';
} }
/** /**

View File

@ -491,7 +491,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $operationRun->type) OperationUxPresenter::queuedToast((string) $operationRun->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::view($operationRun, $tenant)), ->url(OperationRunLinks::view($operationRun, $tenant)),
]) ])
->send(); ->send();
@ -562,7 +562,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $operationRun->type) OperationUxPresenter::queuedToast((string) $operationRun->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::view($operationRun, $tenant)), ->url(OperationRunLinks::view($operationRun, $tenant)),
]) ])
->send(); ->send();

View File

@ -373,7 +373,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('backup_set.delete') OperationUxPresenter::queuedToast('backup_set.delete')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -443,7 +443,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('backup_set.restore') OperationUxPresenter::queuedToast('backup_set.restore')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -528,7 +528,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('backup_set.force_delete') OperationUxPresenter::queuedToast('backup_set.force_delete')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();

View File

@ -150,7 +150,7 @@ public function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -171,7 +171,7 @@ public function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -233,7 +233,7 @@ public function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -254,7 +254,7 @@ public function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();

View File

@ -141,7 +141,7 @@ private function captureAction(): Action
} }
$viewAction = Action::make('view_run') $viewAction = Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($run, $sourceTenant)); ->url(OperationRunLinks::view($run, $sourceTenant));
if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) { if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) {
@ -286,7 +286,7 @@ private function compareNowAction(): Action
} }
$viewAction = Action::make('view_run') $viewAction = Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($run, $targetTenant)); ->url(OperationRunLinks::view($run, $targetTenant));
if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) { if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) {

View File

@ -84,7 +84,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View Run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -105,7 +105,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View Run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();

View File

@ -164,7 +164,7 @@ public static function infolist(Schema $schema): Schema
TextEntry::make('generated_at')->dateTime()->placeholder('—'), TextEntry::make('generated_at')->dateTime()->placeholder('—'),
TextEntry::make('expires_at')->dateTime()->placeholder('—'), TextEntry::make('expires_at')->dateTime()->placeholder('—'),
TextEntry::make('operationRun.id') TextEntry::make('operationRun.id')
->label('Operation run') ->label('Operation')
->formatStateUsing(fn (?int $state): string => $state ? '#'.$state : '—') ->formatStateUsing(fn (?int $state): string => $state ? '#'.$state : '—')
->url(fn (EvidenceSnapshot $record): ?string => $record->operation_run_id ? OperationRunLinks::tenantlessView((int) $record->operation_run_id) : null) ->url(fn (EvidenceSnapshot $record): ?string => $record->operation_run_id ? OperationRunLinks::tenantlessView((int) $record->operation_run_id) : null)
->openUrlInNewTab(), ->openUrlInNewTab(),
@ -674,7 +674,7 @@ public static function executeGeneration(array $data): void
->body('The snapshot is being generated in the background.') ->body('The snapshot is being generated in the background.')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($snapshot->operation_run_id ? OperationRunLinks::tenantlessView((int) $snapshot->operation_run_id) : static::getUrl('view', ['record' => $snapshot], tenant: $tenant)), ->url($snapshot->operation_run_id ? OperationRunLinks::tenantlessView((int) $snapshot->operation_run_id) : static::getUrl('view', ['record' => $snapshot], tenant: $tenant)),
]) ])
->send(); ->send();

View File

@ -30,7 +30,7 @@ protected function getHeaderActions(): array
{ {
return [ return [
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->icon('heroicon-o-eye') ->icon('heroicon-o-eye')
->color('gray') ->color('gray')
->url(fn (): ?string => $this->record->operation_run_id ? OperationRunLinks::tenantlessView((int) $this->record->operation_run_id) : null) ->url(fn (): ?string => $this->record->operation_run_id ? OperationRunLinks::tenantlessView((int) $this->record->operation_run_id) : null)

View File

@ -855,13 +855,13 @@ public static function table(Table $table): Table
return $query->where('scope_key', $scopeKey); return $query->where('scope_key', $scopeKey);
}), }),
Tables\Filters\Filter::make('run_ids') Tables\Filters\Filter::make('run_ids')
->label('Run IDs') ->label('Operation IDs')
->form([ ->form([
TextInput::make('baseline_operation_run_id') TextInput::make('baseline_operation_run_id')
->label('Baseline run id') ->label('Baseline operation ID')
->numeric(), ->numeric(),
TextInput::make('current_operation_run_id') TextInput::make('current_operation_run_id')
->label('Current run id') ->label('Current operation ID')
->numeric(), ->numeric(),
]) ])
->query(function (Builder $query, array $data): Builder { ->query(function (Builder $query, array $data): Builder {

View File

@ -150,7 +150,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -172,7 +172,7 @@ protected function getHeaderActions(): array
->body('The backfill will run in the background. You can continue working while it completes.') ->body('The backfill will run in the background. You can continue working while it completes.')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();

View File

@ -187,7 +187,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View Run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -224,7 +224,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();

View File

@ -245,7 +245,7 @@ public static function table(Table $table): Table
]) ])
->actions([]) ->actions([])
->bulkActions([]) ->bulkActions([])
->emptyStateHeading('No operation runs found') ->emptyStateHeading('No operations found')
->emptyStateDescription('Queued, running, and completed operations will appear here when work is triggered in this scope.') ->emptyStateDescription('Queued, running, and completed operations will appear here when work is triggered in this scope.')
->emptyStateIcon('heroicon-o-queue-list'); ->emptyStateIcon('heroicon-o-queue-list');
} }
@ -256,7 +256,7 @@ private static function enterpriseDetailPage(OperationRun $record): \App\Support
$statusSpec = BadgeRenderer::spec(BadgeDomain::OperationRunStatus, static::statusBadgeState($record)); $statusSpec = BadgeRenderer::spec(BadgeDomain::OperationRunStatus, static::statusBadgeState($record));
$outcomeSpec = BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, static::outcomeBadgeState($record)); $outcomeSpec = BadgeRenderer::spec(BadgeDomain::OperationRunOutcome, static::outcomeBadgeState($record));
$targetScope = static::targetScopeDisplay($record) ?? 'No target scope details were recorded for this run.'; $targetScope = static::targetScopeDisplay($record) ?? 'No target scope details were recorded for this operation.';
$summaryLine = \App\Support\OpsUx\SummaryCountsNormalizer::renderSummaryLine(is_array($record->summary_counts) ? $record->summary_counts : []); $summaryLine = \App\Support\OpsUx\SummaryCountsNormalizer::renderSummaryLine(is_array($record->summary_counts) ? $record->summary_counts : []);
$referencedTenantLifecycle = $record->tenant instanceof Tenant $referencedTenantLifecycle = $record->tenant instanceof Tenant
? ReferencedTenantLifecyclePresentation::forOperationRun($record->tenant) ? ReferencedTenantLifecyclePresentation::forOperationRun($record->tenant)
@ -275,7 +275,7 @@ private static function enterpriseDetailPage(OperationRun $record): \App\Support
$builder = \App\Support\Ui\EnterpriseDetail\EnterpriseDetailBuilder::make('operation_run', 'workspace-context') $builder = \App\Support\Ui\EnterpriseDetail\EnterpriseDetailBuilder::make('operation_run', 'workspace-context')
->header(new \App\Support\Ui\EnterpriseDetail\SummaryHeaderData( ->header(new \App\Support\Ui\EnterpriseDetail\SummaryHeaderData(
title: OperationCatalog::label((string) $record->type), title: OperationCatalog::label((string) $record->type),
subtitle: 'Run #'.$record->getKey(), subtitle: OperationRunLinks::identifier($record),
statusBadges: [ statusBadges: [
$factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor), $factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor),
$factory->statusBadge($outcomeSpec->label, $outcomeSpec->color, $outcomeSpec->icon, $outcomeSpec->iconColor), $factory->statusBadge($outcomeSpec->label, $outcomeSpec->color, $outcomeSpec->icon, $outcomeSpec->iconColor),
@ -322,7 +322,7 @@ private static function enterpriseDetailPage(OperationRun $record): \App\Support
$primaryNextStep['source'], $primaryNextStep['source'],
$primaryNextStep['secondaryGuidance'], $primaryNextStep['secondaryGuidance'],
), ),
description: 'Start here to see how the run ended, whether the result is trustworthy enough to use, and the one primary next step.', description: 'Start here to see how the operation ended, whether the result is trustworthy enough to use, and the one primary next step.',
compactCounts: $summaryLine !== null compactCounts: $summaryLine !== null
? $factory->countPresentation(summaryLine: $summaryLine) ? $factory->countPresentation(summaryLine: $summaryLine)
: null, : null,

View File

@ -149,7 +149,7 @@ public static function makeSyncAction(string $name = 'sync'): Actions\Action
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -164,7 +164,7 @@ public static function makeSyncAction(string $name = 'sync'): Actions\Action
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -553,7 +553,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -599,7 +599,7 @@ public static function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -612,7 +612,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -741,7 +741,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -799,7 +799,7 @@ public static function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -812,7 +812,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -893,7 +893,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -978,7 +978,7 @@ public static function table(Table $table): Table
->body("Queued deletion for {$count} policies.") ->body("Queued deletion for {$count} policies.")
->actions([ ->actions([
\Filament\Actions\Action::make('view_run') \Filament\Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -990,7 +990,7 @@ public static function table(Table $table): Table
->body("Queued deletion for {$count} policies.") ->body("Queued deletion for {$count} policies.")
->actions([ ->actions([
\Filament\Actions\Action::make('view_run') \Filament\Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();

View File

@ -112,7 +112,7 @@ private function makeCaptureSnapshotAction(): Action
->body('An active run already exists for this policy. Opening run details.') ->body('An active run already exists for this policy. Opening run details.')
->actions([ ->actions([
\Filament\Actions\Action::make('view_run') \Filament\Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->info() ->info()
@ -145,7 +145,7 @@ private function makeCaptureSnapshotAction(): Action
OperationUxPresenter::queuedToast('policy.capture_snapshot') OperationUxPresenter::queuedToast('policy.capture_snapshot')
->actions([ ->actions([
\Filament\Actions\Action::make('view_run') \Filament\Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();

View File

@ -344,7 +344,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('policy_version.prune') OperationUxPresenter::queuedToast('policy_version.prune')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -419,7 +419,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('policy_version.restore') OperationUxPresenter::queuedToast('policy_version.restore')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -503,7 +503,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('policy_version.force_delete') OperationUxPresenter::queuedToast('policy_version.force_delete')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();

View File

@ -791,7 +791,7 @@ public static function table(Table $table): Table
->warning() ->warning()
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
Actions\Action::make('manage_connections') Actions\Action::make('manage_connections')
->label('Manage Provider Connections') ->label('Manage Provider Connections')
@ -808,7 +808,7 @@ public static function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
Actions\Action::make('manage_connections') Actions\Action::make('manage_connections')
->label('Manage Provider Connections') ->label('Manage Provider Connections')
@ -833,7 +833,7 @@ public static function table(Table $table): Table
->warning() ->warning()
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
Actions\Action::make('manage_connections') Actions\Action::make('manage_connections')
->label('Manage Provider Connections') ->label('Manage Provider Connections')
@ -849,7 +849,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -897,7 +897,7 @@ public static function table(Table $table): Table
->danger() ->danger()
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -911,7 +911,7 @@ public static function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -933,7 +933,7 @@ public static function table(Table $table): Table
->warning() ->warning()
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -946,7 +946,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -994,7 +994,7 @@ public static function table(Table $table): Table
->danger() ->danger()
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -1008,7 +1008,7 @@ public static function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -1030,7 +1030,7 @@ public static function table(Table $table): Table
->warning() ->warning()
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -1043,7 +1043,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();

View File

@ -245,7 +245,7 @@ protected function getHeaderActions(): array
->warning() ->warning()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
Action::make('manage_connections') Action::make('manage_connections')
->label('Manage Provider Connections') ->label('Manage Provider Connections')
@ -262,7 +262,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
Action::make('manage_connections') Action::make('manage_connections')
->label('Manage Provider Connections') ->label('Manage Provider Connections')
@ -287,7 +287,7 @@ protected function getHeaderActions(): array
->warning() ->warning()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
Action::make('manage_connections') Action::make('manage_connections')
->label('Manage Provider Connections') ->label('Manage Provider Connections')
@ -303,7 +303,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -623,7 +623,7 @@ protected function getHeaderActions(): array
->danger() ->danger()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -637,7 +637,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -659,7 +659,7 @@ protected function getHeaderActions(): array
->warning() ->warning()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -672,7 +672,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -737,7 +737,7 @@ protected function getHeaderActions(): array
->danger() ->danger()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -751,7 +751,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -773,7 +773,7 @@ protected function getHeaderActions(): array
->warning() ->warning()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();
@ -786,7 +786,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($result->run, $tenant)), ->url(OperationRunLinks::view($result->run, $tenant)),
]) ])
->send(); ->send();

View File

@ -1069,7 +1069,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('restore_run.delete') OperationUxPresenter::queuedToast('restore_run.delete')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -1150,7 +1150,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('restore_run.restore') OperationUxPresenter::queuedToast('restore_run.restore')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -1240,7 +1240,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('restore_run.force_delete') OperationUxPresenter::queuedToast('restore_run.force_delete')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -1666,7 +1666,7 @@ public static function createRestoreRun(array $data): RestoreRun
if ($existingOpRun) { if ($existingOpRun) {
$toast->actions([ $toast->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($existingOpRun, $tenant)), ->url(OperationRunLinks::view($existingOpRun, $tenant)),
]); ]);
} }
@ -1704,7 +1704,7 @@ public static function createRestoreRun(array $data): RestoreRun
if ($existingOpRun) { if ($existingOpRun) {
$toast->actions([ $toast->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($existingOpRun, $tenant)), ->url(OperationRunLinks::view($existingOpRun, $tenant)),
]); ]);
} }
@ -1760,7 +1760,7 @@ public static function createRestoreRun(array $data): RestoreRun
OperationUxPresenter::queuedToast('restore.execute') OperationUxPresenter::queuedToast('restore.execute')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -2064,7 +2064,7 @@ private static function rerunActionWithGate(): Actions\Action|BulkAction
if ($existingOpRun) { if ($existingOpRun) {
$toast->actions([ $toast->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($existingOpRun, $tenant)), ->url(OperationRunLinks::view($existingOpRun, $tenant)),
]); ]);
} }
@ -2104,7 +2104,7 @@ private static function rerunActionWithGate(): Actions\Action|BulkAction
if ($existingOpRun) { if ($existingOpRun) {
$toast->actions([ $toast->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($existingOpRun, $tenant)), ->url(OperationRunLinks::view($existingOpRun, $tenant)),
]); ]);
} }
@ -2178,7 +2178,7 @@ private static function rerunActionWithGate(): Actions\Action|BulkAction
OperationUxPresenter::queuedToast('restore.execute') OperationUxPresenter::queuedToast('restore.execute')
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();

View File

@ -196,7 +196,7 @@ public static function infolist(Schema $schema): Schema
->iconColor(BadgeRenderer::iconColor(BadgeDomain::TenantReviewStatus)) ->iconColor(BadgeRenderer::iconColor(BadgeDomain::TenantReviewStatus))
->placeholder('—'), ->placeholder('—'),
TextEntry::make('operationRun.id') TextEntry::make('operationRun.id')
->label('Operation run') ->label('Operation')
->url(fn (ReviewPack $record): ?string => $record->operation_run_id ->url(fn (ReviewPack $record): ?string => $record->operation_run_id
? route('admin.operations.view', ['run' => (int) $record->operation_run_id]) ? route('admin.operations.view', ['run' => (int) $record->operation_run_id])
: null) : null)

View File

@ -440,7 +440,7 @@ public static function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View Run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $record)), ->url(OperationRunLinks::view($opRun, $record)),
]) ])
->send(); ->send();
@ -465,7 +465,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View Run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $record)), ->url(OperationRunLinks::view($opRun, $record)),
]) ])
->send(); ->send();
@ -513,11 +513,11 @@ public static function table(Table $table): Table
Notification::make() Notification::make()
->title('Another operation is already running') ->title('Another operation is already running')
->body('Please wait for the active run to finish.') ->body('Please wait for the active operation to finish.')
->warning() ->warning()
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -531,7 +531,7 @@ public static function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -546,7 +546,7 @@ public static function table(Table $table): Table
$actions = [ $actions = [
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]; ];
@ -590,7 +590,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -808,7 +808,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('tenant.sync') OperationUxPresenter::queuedToast('tenant.sync')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::view($opRun, $tenantContext)), ->url(OperationRunLinks::view($opRun, $tenantContext)),
]) ])
->send(); ->send();
@ -1817,7 +1817,7 @@ public static function syncRoleDefinitionsAction(): Actions\Action
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -1828,7 +1828,7 @@ public static function syncRoleDefinitionsAction(): Actions\Action
OperationUxPresenter::queuedToast('directory_role_definitions.sync') OperationUxPresenter::queuedToast('directory_role_definitions.sync')
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();

View File

@ -119,11 +119,11 @@ protected function getHeaderActions(): array
Notification::make() Notification::make()
->title('Another operation is already running') ->title('Another operation is already running')
->body('Please wait for the active run to finish.') ->body('Please wait for the active operation to finish.')
->warning() ->warning()
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -137,7 +137,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -152,7 +152,7 @@ protected function getHeaderActions(): array
$actions = [ $actions = [
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]; ];
@ -196,7 +196,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -245,7 +245,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -264,7 +264,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();

View File

@ -440,7 +440,7 @@ public static function executeCreateReview(array $data): void
if ($review->operation_run_id) { if ($review->operation_run_id) {
$toast->actions([ $toast->actions([
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::tenantlessView((int) $review->operation_run_id)), ->url(OperationRunLinks::tenantlessView((int) $review->operation_run_id)),
]); ]);
} }

View File

@ -55,7 +55,7 @@ protected function getHeaderActions(): array
{ {
return [ return [
Actions\Action::make('view_run') Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->icon('heroicon-o-eye') ->icon('heroicon-o-eye')
->color('gray') ->color('gray')
->hidden(fn (): bool => ! is_numeric($this->record->operation_run_id)) ->hidden(fn (): bool => ! is_numeric($this->record->operation_run_id))

View File

@ -6,14 +6,12 @@
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\PlatformUser; use App\Models\PlatformUser;
use App\Services\SystemConsole\OperationRunTriageService;
use App\Support\Auth\PlatformCapabilities; use App\Support\Auth\PlatformCapabilities;
use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer; use App\Support\Badges\BadgeRenderer;
use App\Support\OperationCatalog; use App\Support\OperationCatalog;
use App\Support\OperationRunOutcome; use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus; use App\Support\OperationRunStatus;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\System\SystemOperationRunLinks; use App\Support\System\SystemOperationRunLinks;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
@ -21,8 +19,6 @@
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Concerns\InteractsWithTable;
@ -34,7 +30,9 @@ class Failures extends Page implements HasTable
{ {
use InteractsWithTable; use InteractsWithTable;
protected static ?string $navigationLabel = 'Failures'; protected static ?string $navigationLabel = 'Failed operations';
protected static ?string $title = 'Failed operations';
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-triangle'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-exclamation-triangle';
@ -47,10 +45,10 @@ class Failures extends Page implements HasTable
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{ {
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport) return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport)
->exempt(ActionSurfaceSlot::ListHeader, 'System failures stay scan-first and rely on row triage rather than page header actions.') ->satisfy(ActionSurfaceSlot::ListHeader, 'The page header exposes Show all operations while row clicks remain the only inspect model.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Failed-run triage stays per run and intentionally omits bulk actions.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Failed operations remain scan-first and intentionally omit bulk actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains when there are no failed runs to triage.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains when there are no failed operations and repeats the Show all operations CTA.')
->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the canonical system run detail page, which owns header actions.'); ->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the canonical system run detail page, which owns header actions.');
} }
@ -86,6 +84,18 @@ public function mount(): void
$this->mountInteractsWithTable(); $this->mountInteractsWithTable();
} }
/**
* @return array<Action>
*/
protected function getHeaderActions(): array
{
return [
Action::make('show_all_operations')
->label('Show all operations')
->url(SystemOperationRunLinks::index()),
];
}
public function table(Table $table): Table public function table(Table $table): Table
{ {
return $table return $table
@ -99,7 +109,7 @@ public function table(Table $table): Table
}) })
->columns([ ->columns([
TextColumn::make('id') TextColumn::make('id')
->label('Run') ->label('ID')
->state(fn (OperationRun $record): string => '#'.$record->getKey()), ->state(fn (OperationRun $record): string => '#'.$record->getKey()),
TextColumn::make('status') TextColumn::make('status')
->badge() ->badge()
@ -127,80 +137,15 @@ public function table(Table $table): Table
TextColumn::make('created_at')->label('Started')->since(), TextColumn::make('created_at')->label('Started')->since(),
]) ])
->recordUrl(fn (OperationRun $record): string => SystemOperationRunLinks::view($record)) ->recordUrl(fn (OperationRun $record): string => SystemOperationRunLinks::view($record))
->actions([ ->actions([])
Action::make('retry') ->emptyStateHeading('No failed operations found')
->label('Retry') ->emptyStateDescription('Failed operations will appear here when a run completes unsuccessfully.')
->requiresConfirmation() ->emptyStateActions([
->visible(fn (OperationRun $record): bool => $this->canManageOperations() && app(OperationRunTriageService::class)->canRetry($record)) Action::make('show_all_operations_empty')
->action(function (OperationRun $record, OperationRunTriageService $triageService): void { ->label('Show all operations')
$user = $this->requireManageUser(); ->url(SystemOperationRunLinks::index())
$retryRun = $triageService->retry($record, $user); ->button(),
OperationUxPresenter::queuedToast((string) $retryRun->type)
->actions([
\Filament\Actions\Action::make('view_run')
->label('View run')
->url(SystemOperationRunLinks::view($retryRun)),
]) ])
->send();
}),
Action::make('cancel')
->label('Cancel')
->color('danger')
->requiresConfirmation()
->visible(fn (OperationRun $record): bool => $this->canManageOperations() && app(OperationRunTriageService::class)->canCancel($record))
->action(function (OperationRun $record, OperationRunTriageService $triageService): void {
$user = $this->requireManageUser();
$triageService->cancel($record, $user);
Notification::make()
->title('Run cancelled')
->success()
->send();
}),
Action::make('mark_investigated')
->label('Mark investigated')
->requiresConfirmation()
->visible(fn (): bool => $this->canManageOperations())
->form([
Textarea::make('reason')
->label('Reason')
->required()
->minLength(5)
->maxLength(500)
->rows(4),
])
->action(function (OperationRun $record, array $data, OperationRunTriageService $triageService): void {
$user = $this->requireManageUser();
$triageService->markInvestigated($record, $user, (string) ($data['reason'] ?? ''));
Notification::make()
->title('Run marked as investigated')
->success()
->send();
}),
])
->emptyStateHeading('No failed runs found')
->emptyStateDescription('Failed operations will appear here for triage.')
->bulkActions([]); ->bulkActions([]);
} }
private function canManageOperations(): bool
{
$user = auth('platform')->user();
return $user instanceof PlatformUser
&& $user->hasCapability(PlatformCapabilities::OPERATIONS_MANAGE);
}
private function requireManageUser(): PlatformUser
{
$user = auth('platform')->user();
if (! $user instanceof PlatformUser || ! $user->hasCapability(PlatformCapabilities::OPERATIONS_MANAGE)) {
abort(403);
}
return $user;
}
} }

View File

@ -6,12 +6,10 @@
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\PlatformUser; use App\Models\PlatformUser;
use App\Services\SystemConsole\OperationRunTriageService;
use App\Support\Auth\PlatformCapabilities; use App\Support\Auth\PlatformCapabilities;
use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer; use App\Support\Badges\BadgeRenderer;
use App\Support\OperationCatalog; use App\Support\OperationCatalog;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\System\SystemOperationRunLinks; use App\Support\System\SystemOperationRunLinks;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
@ -19,8 +17,6 @@
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Concerns\InteractsWithTable;
@ -32,7 +28,9 @@ class Runs extends Page implements HasTable
{ {
use InteractsWithTable; use InteractsWithTable;
protected static ?string $navigationLabel = 'Runs'; protected static ?string $navigationLabel = 'Operations';
protected static ?string $title = 'Operations';
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-queue-list'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-queue-list';
@ -45,10 +43,10 @@ class Runs extends Page implements HasTable
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{ {
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport) return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport)
->exempt(ActionSurfaceSlot::ListHeader, 'System ops runs rely on inline row triage and do not expose page header actions.') ->satisfy(ActionSurfaceSlot::ListHeader, 'The page header exposes Go to runbooks while row clicks remain the only inspect model.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'System ops triage stays per run and intentionally omits bulk actions.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'System operations remain scan-first and intentionally omit bulk actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains when no system runs have been queued yet.') ->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.'); ->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the canonical system run detail page, which owns header actions.');
} }
@ -69,6 +67,18 @@ public function mount(): void
$this->mountInteractsWithTable(); $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 public function table(Table $table): Table
{ {
return $table return $table
@ -80,7 +90,7 @@ public function table(Table $table): Table
}) })
->columns([ ->columns([
TextColumn::make('id') TextColumn::make('id')
->label('Run') ->label('ID')
->state(fn (OperationRun $record): string => '#'.$record->getKey()), ->state(fn (OperationRun $record): string => '#'.$record->getKey()),
TextColumn::make('status') TextColumn::make('status')
->badge() ->badge()
@ -109,80 +119,15 @@ public function table(Table $table): Table
TextColumn::make('created_at')->label('Started')->since(), TextColumn::make('created_at')->label('Started')->since(),
]) ])
->recordUrl(fn (OperationRun $record): string => SystemOperationRunLinks::view($record)) ->recordUrl(fn (OperationRun $record): string => SystemOperationRunLinks::view($record))
->actions([ ->actions([])
Action::make('retry') ->emptyStateHeading('No operations yet')
->label('Retry') ->emptyStateDescription('Operations from all workspaces will appear here when they are queued.')
->requiresConfirmation() ->emptyStateActions([
->visible(fn (OperationRun $record): bool => $this->canManageOperations() && app(OperationRunTriageService::class)->canRetry($record)) Action::make('go_to_runbooks_empty')
->action(function (OperationRun $record, OperationRunTriageService $triageService): void { ->label('Go to runbooks')
$user = $this->requireManageUser(); ->url(Runbooks::getUrl(panel: 'system'))
$retryRun = $triageService->retry($record, $user); ->button(),
OperationUxPresenter::queuedToast((string) $retryRun->type)
->actions([
\Filament\Actions\Action::make('view_run')
->label('View run')
->url(SystemOperationRunLinks::view($retryRun)),
]) ])
->send();
}),
Action::make('cancel')
->label('Cancel')
->color('danger')
->requiresConfirmation()
->visible(fn (OperationRun $record): bool => $this->canManageOperations() && app(OperationRunTriageService::class)->canCancel($record))
->action(function (OperationRun $record, OperationRunTriageService $triageService): void {
$user = $this->requireManageUser();
$triageService->cancel($record, $user);
Notification::make()
->title('Run cancelled')
->success()
->send();
}),
Action::make('mark_investigated')
->label('Mark investigated')
->requiresConfirmation()
->visible(fn (): bool => $this->canManageOperations())
->form([
Textarea::make('reason')
->label('Reason')
->required()
->minLength(5)
->maxLength(500)
->rows(4),
])
->action(function (OperationRun $record, array $data, OperationRunTriageService $triageService): void {
$user = $this->requireManageUser();
$triageService->markInvestigated($record, $user, (string) ($data['reason'] ?? ''));
Notification::make()
->title('Run marked as investigated')
->success()
->send();
}),
])
->emptyStateHeading('No operation runs yet')
->emptyStateDescription('Runs from all workspaces will appear here when operations are queued.')
->bulkActions([]); ->bulkActions([]);
} }
private function canManageOperations(): bool
{
$user = auth('platform')->user();
return $user instanceof PlatformUser
&& $user->hasCapability(PlatformCapabilities::OPERATIONS_MANAGE);
}
private function requireManageUser(): PlatformUser
{
$user = auth('platform')->user();
if (! $user instanceof PlatformUser || ! $user->hasCapability(PlatformCapabilities::OPERATIONS_MANAGE)) {
abort(403);
}
return $user;
}
} }

View File

@ -6,13 +6,11 @@
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\PlatformUser; use App\Models\PlatformUser;
use App\Services\SystemConsole\OperationRunTriageService;
use App\Support\Auth\PlatformCapabilities; use App\Support\Auth\PlatformCapabilities;
use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer; use App\Support\Badges\BadgeRenderer;
use App\Support\OperationCatalog; use App\Support\OperationCatalog;
use App\Support\OperationRunStatus; use App\Support\OperationRunStatus;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\System\SystemOperationRunLinks; use App\Support\System\SystemOperationRunLinks;
use App\Support\SystemConsole\StuckRunClassifier; use App\Support\SystemConsole\StuckRunClassifier;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
@ -21,8 +19,6 @@
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use Filament\Actions\Action; use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn; use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Concerns\InteractsWithTable;
@ -34,7 +30,9 @@ class Stuck extends Page implements HasTable
{ {
use InteractsWithTable; use InteractsWithTable;
protected static ?string $navigationLabel = 'Stuck'; protected static ?string $navigationLabel = 'Stuck operations';
protected static ?string $title = 'Stuck operations';
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-clock'; protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-clock';
@ -47,10 +45,10 @@ class Stuck extends Page implements HasTable
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{ {
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport) return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport)
->exempt(ActionSurfaceSlot::ListHeader, 'System stuck-run triage relies on row actions and does not expose page header actions.') ->satisfy(ActionSurfaceSlot::ListHeader, 'The page header exposes Show all operations while row clicks remain the only inspect model.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value) ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Stuck-run triage stays per run and intentionally omits bulk actions.') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'Stuck operations remain scan-first and intentionally omit bulk actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains when no queued or running runs cross the stuck thresholds.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains when no operations cross the stuck thresholds and repeats the Show all operations CTA.')
->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the canonical system run detail page, which owns header actions.'); ->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the canonical system run detail page, which owns header actions.');
} }
@ -85,6 +83,18 @@ public function mount(): void
$this->mountInteractsWithTable(); $this->mountInteractsWithTable();
} }
/**
* @return array<Action>
*/
protected function getHeaderActions(): array
{
return [
Action::make('show_all_operations')
->label('Show all operations')
->url(SystemOperationRunLinks::index()),
];
}
public function table(Table $table): Table public function table(Table $table): Table
{ {
return $table return $table
@ -98,7 +108,7 @@ public function table(Table $table): Table
}) })
->columns([ ->columns([
TextColumn::make('id') TextColumn::make('id')
->label('Run') ->label('ID')
->state(fn (OperationRun $record): string => '#'.$record->getKey()), ->state(fn (OperationRun $record): string => '#'.$record->getKey()),
TextColumn::make('status') TextColumn::make('status')
->badge() ->badge()
@ -127,80 +137,15 @@ public function table(Table $table): Table
TextColumn::make('created_at')->label('Started')->since(), TextColumn::make('created_at')->label('Started')->since(),
]) ])
->recordUrl(fn (OperationRun $record): string => SystemOperationRunLinks::view($record)) ->recordUrl(fn (OperationRun $record): string => SystemOperationRunLinks::view($record))
->actions([ ->actions([])
Action::make('retry') ->emptyStateHeading('No stuck operations found')
->label('Retry') ->emptyStateDescription('Queued and running operations that exceed the thresholds will appear here.')
->requiresConfirmation() ->emptyStateActions([
->visible(fn (OperationRun $record): bool => $this->canManageOperations() && app(OperationRunTriageService::class)->canRetry($record)) Action::make('show_all_operations_empty')
->action(function (OperationRun $record, OperationRunTriageService $triageService): void { ->label('Show all operations')
$user = $this->requireManageUser(); ->url(SystemOperationRunLinks::index())
$retryRun = $triageService->retry($record, $user); ->button(),
OperationUxPresenter::queuedToast((string) $retryRun->type)
->actions([
\Filament\Actions\Action::make('view_run')
->label('View run')
->url(SystemOperationRunLinks::view($retryRun)),
]) ])
->send();
}),
Action::make('cancel')
->label('Cancel')
->color('danger')
->requiresConfirmation()
->visible(fn (OperationRun $record): bool => $this->canManageOperations() && app(OperationRunTriageService::class)->canCancel($record))
->action(function (OperationRun $record, OperationRunTriageService $triageService): void {
$user = $this->requireManageUser();
$triageService->cancel($record, $user);
Notification::make()
->title('Run cancelled')
->success()
->send();
}),
Action::make('mark_investigated')
->label('Mark investigated')
->requiresConfirmation()
->visible(fn (): bool => $this->canManageOperations())
->form([
Textarea::make('reason')
->label('Reason')
->required()
->minLength(5)
->maxLength(500)
->rows(4),
])
->action(function (OperationRun $record, array $data, OperationRunTriageService $triageService): void {
$user = $this->requireManageUser();
$triageService->markInvestigated($record, $user, (string) ($data['reason'] ?? ''));
Notification::make()
->title('Run marked as investigated')
->success()
->send();
}),
])
->emptyStateHeading('No stuck runs found')
->emptyStateDescription('Queued and running runs outside thresholds will appear here.')
->bulkActions([]); ->bulkActions([]);
} }
private function canManageOperations(): bool
{
$user = auth('platform')->user();
return $user instanceof PlatformUser
&& $user->hasCapability(PlatformCapabilities::OPERATIONS_MANAGE);
}
private function requireManageUser(): PlatformUser
{
$user = auth('platform')->user();
if (! $user instanceof PlatformUser || ! $user->hasCapability(PlatformCapabilities::OPERATIONS_MANAGE)) {
abort(403);
}
return $user;
}
} }

View File

@ -14,6 +14,7 @@
use Filament\Forms\Components\Textarea; use Filament\Forms\Components\Textarea;
use Filament\Notifications\Notification; use Filament\Notifications\Notification;
use Filament\Pages\Page; use Filament\Pages\Page;
use Illuminate\Contracts\Support\Htmlable;
class ViewRun extends Page class ViewRun extends Page
{ {
@ -44,12 +45,20 @@ public function mount(OperationRun $run): void
$this->run = $run; $this->run = $run;
} }
public function getTitle(): string|Htmlable
{
return 'Operation #'.(int) $this->run->getKey();
}
/** /**
* @return array<Action> * @return array<Action>
*/ */
protected function getHeaderActions(): array protected function getHeaderActions(): array
{ {
return [ return [
Action::make('show_all_operations')
->label('Show all operations')
->url(SystemOperationRunLinks::index()),
Action::make('go_to_runbooks') Action::make('go_to_runbooks')
->label('Go to runbooks') ->label('Go to runbooks')
->url(Runbooks::getUrl(panel: 'system')), ->url(Runbooks::getUrl(panel: 'system')),

View File

@ -79,7 +79,7 @@ protected function getStats(): array
->color($highSeverityDriftFindings > 0 ? 'danger' : 'gray') ->color($highSeverityDriftFindings > 0 ? 'danger' : 'gray')
->url(FindingResource::getUrl('index', tenant: $tenant)), ->url(FindingResource::getUrl('index', tenant: $tenant)),
Stat::make('Active operations', $activeRuns) Stat::make('Active operations', $activeRuns)
->description('backup, sync & compare runs') ->description('backup, sync & compare operations')
->color($activeRuns > 0 ? 'warning' : 'gray') ->color($activeRuns > 0 ? 'warning' : 'gray')
->url(route('admin.operations.index')), ->url(route('admin.operations.index')),
Stat::make('Inventory syncs running', $inventoryActiveRuns) Stat::make('Inventory syncs running', $inventoryActiveRuns)

View File

@ -34,8 +34,8 @@ public function table(Table $table): Table
->paginated(\App\Support\Filament\TablePaginationProfiles::widget()) ->paginated(\App\Support\Filament\TablePaginationProfiles::widget())
->columns([ ->columns([
TextColumn::make('short_id') TextColumn::make('short_id')
->label('Run') ->label(OperationRunLinks::identifierLabel())
->state(fn (OperationRun $record): string => '#'.$record->getKey()) ->state(fn (OperationRun $record): string => OperationRunLinks::identifier($record))
->copyable() ->copyable()
->copyableState(fn (OperationRun $record): string => (string) $record->getKey()), ->copyableState(fn (OperationRun $record): string => (string) $record->getKey()),
TextColumn::make('type') TextColumn::make('type')

View File

@ -124,7 +124,7 @@ protected function getStats(): array
@if ($viewUrl) @if ($viewUrl)
<x-filament::link :href="$viewUrl" size="sm"> <x-filament::link :href="$viewUrl" size="sm">
View run Open operation
</x-filament::link> </x-filament::link>
@endif @endif
</div> </div>

View File

@ -103,8 +103,8 @@ protected function getStats(): array
} }
return [ return [
Stat::make('Total Runs (30 days)', $totalRuns30Days), Stat::make('Total Operations (30 days)', $totalRuns30Days),
Stat::make('Active Runs', $activeRuns), Stat::make('Active Operations', $activeRuns),
Stat::make('Failed/Partial (7 days)', $failedOrPartial7Days), Stat::make('Failed/Partial (7 days)', $failedOrPartial7Days),
Stat::make('Avg Duration (7 days)', $avgDuration7Days), Stat::make('Avg Duration (7 days)', $avgDuration7Days),
]; ];

View File

@ -83,7 +83,7 @@ public function scanNow(): void
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -105,7 +105,7 @@ public function scanNow(): void
->body('The scan will run in the background. Results appear once complete.') ->body('The scan will run in the background. Results appear once complete.')
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();

View File

@ -81,7 +81,7 @@ public function generatePack(bool $includePii = true, bool $includeOperations =
->body('A review pack is already queued or running for this tenant.') ->body('A review pack is already queued or running for this tenant.')
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::tenantlessView($activeRun)), ->url(OperationRunLinks::tenantlessView($activeRun)),
]) ])
->send(); ->send();
@ -106,7 +106,7 @@ public function generatePack(bool $includePii = true, bool $includeOperations =
if ($runUrl !== null) { if ($runUrl !== null) {
$toast->actions([ $toast->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label('Open operation')
->url($runUrl), ->url($runUrl),
]); ]);
} }

View File

@ -75,11 +75,11 @@ public function startVerification(StartVerification $verification): void
Notification::make() Notification::make()
->title('Another operation is already running') ->title('Another operation is already running')
->body('Please wait for the active run to finish.') ->body('Please wait for the active operation to finish.')
->warning() ->warning()
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -93,7 +93,7 @@ public function startVerification(StartVerification $verification): void
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type) OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();
@ -108,7 +108,7 @@ public function startVerification(StartVerification $verification): void
$actions = [ $actions = [
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]; ];
@ -152,7 +152,7 @@ public function startVerification(StartVerification $verification): void
OperationUxPresenter::queuedToast((string) $result->run->type) OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([ ->actions([
Action::make('view_run') Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->send(); ->send();

View File

@ -326,7 +326,7 @@ public function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type) OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([ ->actions([
\Filament\Actions\Action::make('view_run') \Filament\Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();
@ -336,7 +336,7 @@ public function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type) OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([ ->actions([
\Filament\Actions\Action::make('view_run') \Filament\Actions\Action::make('view_run')
->label('View run') ->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)), ->url(OperationRunLinks::view($opRun, $tenant)),
]) ])
->send(); ->send();

View File

@ -41,7 +41,7 @@ public function toDatabase(object $notifiable): array
$notification->actions([ $notification->actions([
\Filament\Actions\Action::make('view_run') \Filament\Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]); ]);

View File

@ -49,11 +49,11 @@ public function toDatabase(object $notifiable): array
return FilamentNotification::make() return FilamentNotification::make()
->title("{$operationLabel} queued") ->title("{$operationLabel} queued")
->body('Queued for execution. Open the run for progress and next steps.') ->body('Queued for execution. Open the operation for progress and next steps.')
->info() ->info()
->actions([ ->actions([
\Filament\Actions\Action::make('view_run') \Filament\Actions\Action::make('view_run')
->label('View run') ->label(OperationRunLinks::openLabel())
->url($runUrl), ->url($runUrl),
]) ])
->getDatabaseMessage(); ->getDatabaseMessage();

View File

@ -8,6 +8,7 @@
use App\Models\Tenant; use App\Models\Tenant;
use App\Support\Filament\PanelThemeAsset; use App\Support\Filament\PanelThemeAsset;
use App\Support\Middleware\DenyNonMemberTenantAccess; use App\Support\Middleware\DenyNonMemberTenantAccess;
use App\Support\OperationRunLinks;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\AuthenticateSession; use Filament\Http\Middleware\AuthenticateSession;
@ -47,7 +48,7 @@ public function panel(Panel $panel): Panel
'primary' => Color::Indigo, 'primary' => Color::Indigo,
]) ])
->navigationItems([ ->navigationItems([
NavigationItem::make('Runs') NavigationItem::make(OperationRunLinks::collectionLabel())
->url(fn (): string => route('admin.operations.index')) ->url(fn (): string => route('admin.operations.index'))
->icon('heroicon-o-queue-list') ->icon('heroicon-o-queue-list')
->group('Monitoring') ->group('Monitoring')

View File

@ -373,7 +373,7 @@ final class OperatorOutcomeTaxonomy
], ],
'completed' => [ 'completed' => [
'axis' => 'execution_lifecycle', 'axis' => 'execution_lifecycle',
'label' => 'Run finished', 'label' => 'Operation finished',
'color' => 'gray', 'color' => 'gray',
'classification' => 'primary', 'classification' => 'primary',
'next_action_policy' => 'none', 'next_action_policy' => 'none',
@ -398,7 +398,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary', 'classification' => 'primary',
'next_action_policy' => 'none', 'next_action_policy' => 'none',
'legacy_aliases' => ['Succeeded'], 'legacy_aliases' => ['Succeeded'],
'notes' => 'The run finished without operator follow-up.', 'notes' => 'The operation finished without operator follow-up.',
], ],
'partially_succeeded' => [ 'partially_succeeded' => [
'axis' => 'execution_outcome', 'axis' => 'execution_outcome',
@ -407,7 +407,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary', 'classification' => 'primary',
'next_action_policy' => 'optional', 'next_action_policy' => 'optional',
'legacy_aliases' => ['Partially succeeded', 'Partial'], 'legacy_aliases' => ['Partially succeeded', 'Partial'],
'notes' => 'The run finished but needs operator review or cleanup.', 'notes' => 'The operation finished but needs operator review or cleanup.',
], ],
'blocked' => [ 'blocked' => [
'axis' => 'execution_outcome', 'axis' => 'execution_outcome',
@ -598,7 +598,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary', 'classification' => 'primary',
'next_action_policy' => 'none', 'next_action_policy' => 'none',
'legacy_aliases' => ['Checked'], 'legacy_aliases' => ['Checked'],
'notes' => 'Safety checks were completed for this run.', 'notes' => 'Safety checks were completed for this operation.',
], ],
'previewed' => [ 'previewed' => [
'axis' => 'execution_lifecycle', 'axis' => 'execution_lifecycle',
@ -643,7 +643,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary', 'classification' => 'primary',
'next_action_policy' => 'none', 'next_action_policy' => 'none',
'legacy_aliases' => ['Completed'], 'legacy_aliases' => ['Completed'],
'notes' => 'The restore run finished successfully.', 'notes' => 'The restore operation finished successfully.',
], ],
'partial' => [ 'partial' => [
'axis' => 'execution_outcome', 'axis' => 'execution_outcome',
@ -652,7 +652,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary', 'classification' => 'primary',
'next_action_policy' => 'optional', 'next_action_policy' => 'optional',
'legacy_aliases' => ['Partial'], 'legacy_aliases' => ['Partial'],
'notes' => 'The restore run finished but needs follow-up on a subset of items.', 'notes' => 'The restore operation finished but needs follow-up on a subset of items.',
], ],
'failed' => [ 'failed' => [
'axis' => 'execution_outcome', 'axis' => 'execution_outcome',

View File

@ -242,7 +242,7 @@ private function supportingMessage(
? 'Last compared '.$stats->lastComparedHuman.'.' ? 'Last compared '.$stats->lastComparedHuman.'.'
: 'The latest compare result is trustworthy enough to treat zero findings as current.', : 'The latest compare result is trustworthy enough to treat zero findings as current.',
BaselineCompareSummaryAssessment::STATE_CAUTION => match (true) { BaselineCompareSummaryAssessment::STATE_CAUTION => match (true) {
$evaluationResult === 'suppressed_result' => 'Review the run detail before treating zero visible findings as complete.', $evaluationResult === 'suppressed_result' => 'Review the operation detail before treating zero visible findings as complete.',
(int) ($stats->evidenceGapsCount ?? 0) > 0 => 'Review the compare detail to see which evidence gaps still limit trust.', (int) ($stats->evidenceGapsCount ?? 0) > 0 => 'Review the compare detail to see which evidence gaps still limit trust.',
in_array($stats->coverageStatus, ['warning', 'unproven'], true) => 'Coverage warnings mean zero visible findings are not an all-clear on their own.', in_array($stats->coverageStatus, ['warning', 'unproven'], true) => 'Coverage warnings mean zero visible findings are not an all-clear on their own.',
default => $stats->reasonMessage ?? $stats->message, default => $stats->reasonMessage ?? $stats->message,
@ -258,7 +258,7 @@ private function supportingMessage(
$findingsVisibleCount > 0 => 'Open findings remain on this tenant and need review.', $findingsVisibleCount > 0 => 'Open findings remain on this tenant and need review.',
default => $stats->message, default => $stats->message,
}, },
BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => 'Current counts are diagnostic only until the compare run finishes.', BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => 'Current counts are diagnostic only until the compare operation finishes.',
default => $stats->message, default => $stats->message,
}; };
} }
@ -295,7 +295,7 @@ private function nextAction(
return match ($stateFamily) { return match ($stateFamily) {
BaselineCompareSummaryAssessment::STATE_ACTION_REQUIRED => [ BaselineCompareSummaryAssessment::STATE_ACTION_REQUIRED => [
'label' => $evaluationResult === 'failed_result' ? 'Review the failed run' : 'Review compare detail', 'label' => $evaluationResult === 'failed_result' ? 'Review the failed operation' : 'Review compare detail',
'target' => $stats->operationRunId !== null 'target' => $stats->operationRunId !== null
? BaselineCompareSummaryAssessment::NEXT_TARGET_RUN ? BaselineCompareSummaryAssessment::NEXT_TARGET_RUN
: BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING, : BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
@ -311,7 +311,7 @@ private function nextAction(
'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING, 'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
], ],
BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => [ BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => [
'label' => $stats->operationRunId !== null ? 'View run' : 'Open Baseline Compare', 'label' => $stats->operationRunId !== null ? 'Open operation' : 'Open Baseline Compare',
'target' => $stats->operationRunId !== null 'target' => $stats->operationRunId !== null
? BaselineCompareSummaryAssessment::NEXT_TARGET_RUN ? BaselineCompareSummaryAssessment::NEXT_TARGET_RUN
: BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING, : BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,

View File

@ -4,6 +4,7 @@
namespace App\Support\Navigation; namespace App\Support\Navigation;
use App\Support\OperationRunLinks;
use Illuminate\Support\Str; use Illuminate\Support\Str;
final class RelatedActionLabelCatalog final class RelatedActionLabelCatalog
@ -20,7 +21,7 @@ final class RelatedActionLabelCatalog
'parent_policy' => 'Policy', 'parent_policy' => 'Policy',
'policy_version' => 'Policy version', 'policy_version' => 'Policy version',
'restore_run' => 'Restore run', 'restore_run' => 'Restore run',
'source_run' => 'Run', 'source_run' => 'Operation',
]; ];
/** /**
@ -35,16 +36,32 @@ final class RelatedActionLabelCatalog
'parent_policy' => 'View policy', 'parent_policy' => 'View policy',
'policy_version' => 'View policy version', 'policy_version' => 'View policy version',
'restore_run' => 'View restore run', 'restore_run' => 'View restore run',
'source_run' => 'View run', 'source_run' => 'Open operation',
]; ];
public function entryLabel(string $relationKey): string public function entryLabel(string $relationKey): string
{ {
if ($relationKey === 'operations') {
return OperationRunLinks::collectionLabel();
}
if ($relationKey === 'source_run') {
return OperationRunLinks::singularLabel();
}
return self::ENTRY_LABELS[$relationKey] ?? Str::headline(str_replace('_', ' ', $relationKey)); return self::ENTRY_LABELS[$relationKey] ?? Str::headline(str_replace('_', ' ', $relationKey));
} }
public function actionLabel(string $relationKey): string public function actionLabel(string $relationKey): string
{ {
if ($relationKey === 'operations') {
return OperationRunLinks::openCollectionLabel();
}
if ($relationKey === 'source_run') {
return OperationRunLinks::openLabel();
}
return self::ACTION_LABELS[$relationKey] ?? 'Open '.Str::headline(str_replace('_', ' ', $relationKey)); return self::ACTION_LABELS[$relationKey] ?? 'Open '.Str::headline(str_replace('_', ' ', $relationKey));
} }

View File

@ -127,9 +127,9 @@ public function operationLinks(OperationRun $run, ?Tenant $tenant): array
} }
if ($tenant instanceof Tenant) { if ($tenant instanceof Tenant) {
$links = ['Operations' => OperationRunLinks::index($tenant)] + $links; $links = [OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant)] + $links;
} else { } else {
$links = ['Operations' => OperationRunLinks::index()] + $links; $links = [OperationRunLinks::collectionLabel() => OperationRunLinks::index()] + $links;
} }
return $links; return $links;
@ -152,10 +152,10 @@ public function operationLinksFresh(OperationRun $run, ?Tenant $tenant): array
} }
if ($tenant instanceof Tenant) { if ($tenant instanceof Tenant) {
return ['Operations' => OperationRunLinks::index($tenant)] + $links; return [OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant)] + $links;
} }
return ['Operations' => OperationRunLinks::index()] + $links; return [OperationRunLinks::collectionLabel() => OperationRunLinks::index()] + $links;
} }
/** /**
@ -202,7 +202,7 @@ public function auditTargetLink(AuditLog $record): ?array
->whereKey($resourceId) ->whereKey($resourceId)
->where('workspace_id', (int) $workspace->getKey()) ->where('workspace_id', (int) $workspace->getKey())
->exists() ->exists()
? ['label' => 'Open operation run', 'url' => route('admin.operations.view', ['run' => $resourceId])] ? ['label' => OperationRunLinks::openLabel(), 'url' => route('admin.operations.view', ['run' => $resourceId])]
: null, : null,
'baseline_profile' => $workspace instanceof Workspace 'baseline_profile' => $workspace instanceof Workspace
&& $this->workspaceCapabilityResolver->isMember($user, $workspace) && $this->workspaceCapabilityResolver->isMember($user, $workspace)
@ -903,7 +903,7 @@ private function operationsEntry(
return RelatedContextEntry::available( return RelatedContextEntry::available(
key: $rule->relationKey, key: $rule->relationKey,
label: $this->labels->entryLabel($rule->relationKey), label: $this->labels->entryLabel($rule->relationKey),
value: 'Operations', value: OperationRunLinks::collectionLabel(),
secondaryValue: $tenant->name, secondaryValue: $tenant->name,
targetUrl: OperationRunLinks::index($tenant, $context), targetUrl: OperationRunLinks::index($tenant, $context),
targetKind: $rule->targetType, targetKind: $rule->targetType,

View File

@ -23,6 +23,43 @@
final class OperationRunLinks final class OperationRunLinks
{ {
public static function collectionLabel(): string
{
return 'Operations';
}
public static function openCollectionLabel(): string
{
return 'Open operations';
}
public static function viewInCollectionLabel(): string
{
return 'View in Operations';
}
public static function singularLabel(): string
{
return 'Operation';
}
public static function openLabel(): string
{
return 'Open operation';
}
public static function identifierLabel(): string
{
return 'Operation ID';
}
public static function identifier(OperationRun|int $run): string
{
$runId = $run instanceof OperationRun ? (int) $run->getKey() : (int) $run;
return 'Operation #'.$runId;
}
public static function index(?Tenant $tenant = null, ?CanonicalNavigationContext $context = null): string public static function index(?Tenant $tenant = null, ?CanonicalNavigationContext $context = null): string
{ {
return route('admin.operations.index', $context?->toQuery() ?? []); return route('admin.operations.index', $context?->toQuery() ?? []);
@ -52,7 +89,7 @@ public static function related(OperationRun $run, ?Tenant $tenant): array
$links = []; $links = [];
$links['Operations'] = self::index($tenant); $links[self::collectionLabel()] = self::index($tenant);
if (! $tenant instanceof Tenant) { if (! $tenant instanceof Tenant) {
return $links; return $links;

View File

@ -7,6 +7,7 @@
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\Tenant; use App\Models\Tenant;
use App\Support\OperationCatalog; use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
use App\Support\Operations\OperationRunFreshnessState; use App\Support\Operations\OperationRunFreshnessState;
use App\Support\ReasonTranslation\ReasonPresenter; use App\Support\ReasonTranslation\ReasonPresenter;
use App\Support\ReasonTranslation\ReasonResolutionEnvelope; use App\Support\ReasonTranslation\ReasonResolutionEnvelope;
@ -33,7 +34,7 @@ public static function queuedToast(string $operationType): FilamentNotification
return FilamentNotification::make() return FilamentNotification::make()
->title("{$operationLabel} queued") ->title("{$operationLabel} queued")
->body('Queued for execution. Open the run for progress and next steps.') ->body('Queued for execution. Open the operation for progress and next steps.')
->info() ->info()
->duration(self::QUEUED_TOAST_DURATION_MS); ->duration(self::QUEUED_TOAST_DURATION_MS);
} }
@ -47,7 +48,7 @@ public static function alreadyQueuedToast(string $operationType): FilamentNotifi
return FilamentNotification::make() return FilamentNotification::make()
->title("{$operationLabel} already queued") ->title("{$operationLabel} already queued")
->body('A matching run is already queued or running. No action needed unless it stays stuck.') ->body('A matching operation is already queued or running. No action needed unless it stays stuck.')
->info() ->info()
->duration(self::QUEUED_TOAST_DURATION_MS); ->duration(self::QUEUED_TOAST_DURATION_MS);
} }
@ -92,7 +93,7 @@ public static function terminalDatabaseNotification(OperationRun $run, ?Tenant $
if ($tenant instanceof Tenant) { if ($tenant instanceof Tenant) {
$notification->actions([ $notification->actions([
\Filament\Actions\Action::make('view') \Filament\Actions\Action::make('view')
->label('View run') ->label(OperationRunLinks::openLabel())
->url(OperationRunUrl::view($run, $tenant)), ->url(OperationRunUrl::view($run, $tenant)),
]); ]);
} }
@ -129,13 +130,13 @@ private static function buildSurfaceGuidance(OperationRun $run): ?string
$freshnessState = self::freshnessState($run); $freshnessState = self::freshnessState($run);
if ($freshnessState->isLikelyStale()) { if ($freshnessState->isLikelyStale()) {
return 'This run is past its lifecycle window. Review worker health and logs before retrying from the start surface.'; return 'This operation is past its lifecycle window. Review worker health and logs before retrying from the start surface.';
} }
if ($freshnessState->isReconciledFailed()) { if ($freshnessState->isReconciledFailed()) {
return $operatorExplanationGuidance return $operatorExplanationGuidance
?? $reasonGuidance ?? $reasonGuidance
?? 'TenantPilot reconciled this run after lifecycle truth was lost. Review the recorded evidence before retrying.'; ?? 'TenantPilot reconciled this operation after lifecycle truth was lost. Review the recorded evidence before retrying.';
} }
if (in_array($uxStatus, ['blocked', 'failed', 'partial'], true)) { if (in_array($uxStatus, ['blocked', 'failed', 'partial'], true)) {
@ -153,8 +154,8 @@ private static function buildSurfaceGuidance(OperationRun $run): ?string
} }
return match ($uxStatus) { return match ($uxStatus) {
'queued' => 'No action needed yet. The run is waiting for a worker.', 'queued' => 'No action needed yet. The operation is waiting for a worker.',
'running' => 'No action needed yet. The run is currently in progress.', 'running' => 'No action needed yet. The operation is currently in progress.',
'succeeded' => 'No action needed.', 'succeeded' => 'No action needed.',
'partial' => $nextStepLabel !== null 'partial' => $nextStepLabel !== null
? 'Next step: '.$nextStepLabel.'.' ? 'Next step: '.$nextStepLabel.'.'
@ -166,7 +167,7 @@ private static function buildSurfaceGuidance(OperationRun $run): ?string
: 'Review the blocked prerequisite before retrying.', : 'Review the blocked prerequisite before retrying.',
default => $nextStepLabel !== null default => $nextStepLabel !== null
? 'Next step: '.$nextStepLabel.'.' ? 'Next step: '.$nextStepLabel.'.'
: 'Review the run details before retrying.', : 'Review the operation details before retrying.',
}; };
} }
@ -211,7 +212,7 @@ private static function buildSurfaceFailureDetail(OperationRun $run): ?string
} }
if (self::freshnessState($run)->isLikelyStale()) { if (self::freshnessState($run)->isLikelyStale()) {
return 'This run is no longer within its normal lifecycle window and may no longer be progressing.'; return 'This operation is no longer within its normal lifecycle window and may no longer be progressing.';
} }
return null; return null;

View File

@ -16,7 +16,7 @@ final class ReferenceTypeLabelCatalog
ReferenceClass::PolicyVersion->value => 'Policy version', ReferenceClass::PolicyVersion->value => 'Policy version',
ReferenceClass::BaselineProfile->value => 'Baseline profile', ReferenceClass::BaselineProfile->value => 'Baseline profile',
ReferenceClass::BaselineSnapshot->value => 'Baseline snapshot', ReferenceClass::BaselineSnapshot->value => 'Baseline snapshot',
ReferenceClass::OperationRun->value => 'Operation run', ReferenceClass::OperationRun->value => 'Operation',
ReferenceClass::BackupSet->value => 'Backup set', ReferenceClass::BackupSet->value => 'Backup set',
ReferenceClass::RoleDefinition->value => 'Role definition', ReferenceClass::RoleDefinition->value => 'Role definition',
ReferenceClass::Principal->value => 'Principal', ReferenceClass::Principal->value => 'Principal',

View File

@ -55,11 +55,11 @@ public function resolve(ReferenceDescriptor $descriptor): ResolvedReference
return $this->resolved( return $this->resolved(
descriptor: $descriptor, descriptor: $descriptor,
primaryLabel: OperationCatalog::label((string) $run->type), primaryLabel: OperationCatalog::label((string) $run->type),
secondaryLabel: 'Run #'.$run->getKey(), secondaryLabel: OperationRunLinks::identifier($run),
linkTarget: new ReferenceLinkTarget( linkTarget: new ReferenceLinkTarget(
targetKind: ReferenceClass::OperationRun->value, targetKind: ReferenceClass::OperationRun->value,
url: OperationRunLinks::tenantlessView($run, $navigationContext), url: OperationRunLinks::tenantlessView($run, $navigationContext),
actionLabel: 'View run', actionLabel: OperationRunLinks::openLabel(),
contextBadge: $run->tenant_id ? 'Tenant context' : 'Workspace', contextBadge: $run->tenant_id ? 'Tenant context' : 'Workspace',
), ),
); );

View File

@ -753,7 +753,7 @@ private function buildOperationRunEnvelope(OperationRun $run): ArtifactTruthEnve
actionability: $artifactEnvelope->actionability, actionability: $artifactEnvelope->actionability,
primaryDomain: $artifactEnvelope->primaryDimension()?->badgeDomain ?? BadgeDomain::GovernanceArtifactExistence, primaryDomain: $artifactEnvelope->primaryDimension()?->badgeDomain ?? BadgeDomain::GovernanceArtifactExistence,
primaryState: $artifactEnvelope->primaryDimension()?->badgeState ?? $artifactEnvelope->artifactExistence, primaryState: $artifactEnvelope->primaryDimension()?->badgeState ?? $artifactEnvelope->artifactExistence,
primaryExplanation: $artifactEnvelope->primaryExplanation ?? $reason?->shortExplanation ?? 'The run finished, but the related artifact needs review.', primaryExplanation: $artifactEnvelope->primaryExplanation ?? $reason?->shortExplanation ?? 'The operation finished, but the related artifact needs review.',
diagnosticLabel: $diagnosticParts === [] ? null : implode(' · ', $diagnosticParts), diagnosticLabel: $diagnosticParts === [] ? null : implode(' · ', $diagnosticParts),
reason: $artifactEnvelope->reason, reason: $artifactEnvelope->reason,
nextActionLabel: $artifactEnvelope->nextActionLabel, nextActionLabel: $artifactEnvelope->nextActionLabel,
@ -801,15 +801,15 @@ private function buildOperationRunEnvelope(OperationRun $run): ArtifactTruthEnve
primaryDomain: BadgeDomain::GovernanceArtifactExistence, primaryDomain: BadgeDomain::GovernanceArtifactExistence,
primaryState: $primaryState, primaryState: $primaryState,
primaryExplanation: $reason?->shortExplanation ?? match ((string) $run->status) { primaryExplanation: $reason?->shortExplanation ?? match ((string) $run->status) {
OperationRunStatus::Queued->value, OperationRunStatus::Running->value => 'The artifact-producing run is still in progress, so no artifact is available yet.', OperationRunStatus::Queued->value, OperationRunStatus::Running->value => 'The artifact-producing operation is still in progress, so no artifact is available yet.',
default => 'The run finished without a usable artifact result.', default => 'The operation finished without a usable artifact result.',
}, },
diagnosticLabel: BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, $run->outcome)->label, diagnosticLabel: BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, $run->outcome)->label,
reason: ArtifactTruthCause::fromReasonResolutionEnvelope($reason, ReasonPresenter::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT), reason: ArtifactTruthCause::fromReasonResolutionEnvelope($reason, ReasonPresenter::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT),
nextActionLabel: $reason?->firstNextStep()?->label nextActionLabel: $reason?->firstNextStep()?->label
?? ($actionability === 'required' ?? ($actionability === 'required'
? 'Inspect the blocked run details before retrying' ? 'Inspect the blocked operation details before retrying'
: 'Wait for the artifact-producing run to finish'), : 'Wait for the artifact-producing operation to finish'),
nextActionUrl: null, nextActionUrl: null,
relatedRunId: (int) $run->getKey(), relatedRunId: (int) $run->getKey(),
relatedArtifactUrl: null, relatedArtifactUrl: null,

View File

@ -206,7 +206,7 @@ private function summaryMetrics(
'key' => 'active_operations', 'key' => 'active_operations',
'label' => 'Active operations', 'label' => 'Active operations',
'value' => $activeOperationsCount, 'value' => $activeOperationsCount,
'description' => 'Workspace-wide runs that are still queued or in progress.', 'description' => 'Workspace-wide operations that are still queued or in progress.',
'destination_url' => route('admin.operations.index'), 'destination_url' => route('admin.operations.index'),
'color' => $activeOperationsCount > 0 ? 'warning' : 'gray', 'color' => $activeOperationsCount > 0 ? 'warning' : 'gray',
], ],
@ -295,7 +295,7 @@ private function attentionItems(int $workspaceId, array $accessibleTenantIds, bo
if ($activeRunsCount > 0) { if ($activeRunsCount > 0) {
$items[] = [ $items[] = [
'title' => 'Operations are still running', 'title' => 'Operations are still running',
'body' => $activeRunsCount.' workspace run(s) are active right now.', 'body' => $activeRunsCount.' workspace operation(s) are active right now.',
'url' => route('admin.operations.index'), 'url' => route('admin.operations.index'),
'badge' => 'Operations', 'badge' => 'Operations',
'badge_color' => 'warning', 'badge_color' => 'warning',
@ -414,7 +414,7 @@ private function quickActions(Workspace $workspace, int $accessibleTenantCount,
[ [
'key' => 'operations', 'key' => 'operations',
'label' => 'Open operations', 'label' => 'Open operations',
'description' => 'Review current and recent workspace-wide runs.', 'description' => 'Review current and recent workspace-wide operations.',
'url' => route('admin.operations.index'), 'url' => route('admin.operations.index'),
'icon' => 'heroicon-o-queue-list', 'icon' => 'heroicon-o-queue-list',
'color' => 'gray', 'color' => 'gray',

View File

@ -102,7 +102,7 @@
Verification report unavailable Verification report unavailable
</div> </div>
<div class="mt-1"> <div class="mt-1">
This run doesnt have a report yet. If its still running, refresh in a moment. If it already completed, start verification again. This operation doesnt have a report yet. If its still running, refresh in a moment. If it already completed, start verification again.
</div> </div>
<div class="mt-2 text-xs text-gray-600 dark:text-gray-300"> <div class="mt-2 text-xs text-gray-600 dark:text-gray-300">
<span class="font-semibold">Read-only:</span> this view uses stored data and makes no external calls. <span class="font-semibold">Read-only:</span> this view uses stored data and makes no external calls.
@ -474,7 +474,7 @@ class="text-primary-600 hover:underline dark:text-primary-400"
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
@if ($run !== null) @if ($run !== null)
<div> <div>
<span class="text-gray-500 dark:text-gray-400">Run ID:</span> <span class="text-gray-500 dark:text-gray-400">Operation ID:</span>
<span class="font-mono">{{ (int) ($run['id'] ?? 0) }}</span> <span class="font-mono">{{ (int) ($run['id'] ?? 0) }}</span>
</div> </div>
<div> <div>
@ -497,7 +497,7 @@ class="text-primary-600 hover:underline dark:text-primary-400"
href="{{ $previousRunUrl }}" href="{{ $previousRunUrl }}"
class="font-medium text-primary-600 hover:underline dark:text-primary-400" class="font-medium text-primary-600 hover:underline dark:text-primary-400"
> >
Open previous verification Open previous operation
</a> </a>
</div> </div>
@endif @endif

View File

@ -155,15 +155,15 @@
<div class="space-y-4"> <div class="space-y-4">
<x-filament::section <x-filament::section
heading="Verification report" heading="Verification report"
:description="$completedAtLabel ? ('Completed: ' . $completedAtLabel) : 'Stored details for the latest verification run.'" :description="$completedAtLabel ? ('Completed: ' . $completedAtLabel) : 'Stored details for the latest verification operation.'"
> >
@if ($run === null) @if ($run === null)
<div class="text-sm text-gray-600 dark:text-gray-300"> <div class="text-sm text-gray-600 dark:text-gray-300">
No verification run has been started yet. No verification operation has been started yet.
</div> </div>
@elseif ($status !== 'completed') @elseif ($status !== 'completed')
<div class="text-sm text-gray-600 dark:text-gray-300"> <div class="text-sm text-gray-600 dark:text-gray-300">
Report unavailable while the run is in progress. Stored status updates automatically about every 5 seconds. Use “Refresh” to re-check immediately. Report unavailable while the operation is in progress. Stored status updates automatically about every 5 seconds. Use “Refresh” to re-check immediately.
</div> </div>
@else @else
<div class="space-y-4"> <div class="space-y-4">
@ -256,7 +256,7 @@
Verification report unavailable Verification report unavailable
</div> </div>
<div class="mt-1"> <div class="mt-1">
This run doesnt have a report yet. If it already completed, start verification again. This operation doesnt have a report yet. If it already completed, start verification again.
</div> </div>
</div> </div>
@else @else
@ -596,7 +596,7 @@ class="inline-flex items-center gap-2 text-primary-600 hover:underline dark:text
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<div> <div>
<span class="text-gray-500 dark:text-gray-400">Run ID:</span> <span class="text-gray-500 dark:text-gray-400">Operation ID:</span>
<span class="font-mono">{{ (int) ($run['id'] ?? 0) }}</span> <span class="font-mono">{{ (int) ($run['id'] ?? 0) }}</span>
</div> </div>
<div> <div>
@ -618,7 +618,7 @@ class="inline-flex items-center gap-2 text-primary-600 hover:underline dark:text
href="{{ $previousRunUrl }}" href="{{ $previousRunUrl }}"
class="font-medium text-primary-600 hover:underline dark:text-primary-400" class="font-medium text-primary-600 hover:underline dark:text-primary-400"
> >
Open previous verification Open previous operation
</a> </a>
</div> </div>
@endif @endif
@ -629,7 +629,7 @@ class="font-medium text-primary-600 hover:underline dark:text-primary-400"
href="{{ $runUrl }}" href="{{ $runUrl }}"
class="font-medium text-primary-600 hover:underline dark:text-primary-400" class="font-medium text-primary-600 hover:underline dark:text-primary-400"
> >
Open run details Open operation details
</a> </a>
</div> </div>
@endif @endif

View File

@ -75,12 +75,12 @@
<div class="space-y-4"> <div class="space-y-4">
@if ($run === null) @if ($run === null)
<div class="text-sm text-gray-600 dark:text-gray-300"> <div class="text-sm text-gray-600 dark:text-gray-300">
No verification run has been started yet. No verification operation has been started yet.
</div> </div>
@else @else
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
<div class="text-sm font-medium text-gray-900 dark:text-white"> <div class="text-sm font-medium text-gray-900 dark:text-white">
Run #{{ (int) ($run['id'] ?? 0) }} {{ \App\Support\OperationRunLinks::identifier((int) ($run['id'] ?? 0)) }}
</div> </div>
@if ($runStatusSpec) @if ($runStatusSpec)
@ -150,7 +150,7 @@ class="text-sm font-medium text-gray-600 hover:underline dark:text-gray-300"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
Open run in Monitoring (advanced) Open operation in Monitoring (advanced)
</a> </a>
</div> </div>
@endif @endif

View File

@ -42,8 +42,8 @@
@if (($lifecycleSummary['likely_stale'] ?? 0) > 0 || ($lifecycleSummary['reconciled'] ?? 0) > 0) @if (($lifecycleSummary['likely_stale'] ?? 0) > 0 || ($lifecycleSummary['reconciled'] ?? 0) > 0)
<div class="mb-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100"> <div class="mb-4 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100">
{{ ($lifecycleSummary['likely_stale'] ?? 0) }} active run(s) are beyond their lifecycle window. {{ ($lifecycleSummary['likely_stale'] ?? 0) }} active operation(s) are beyond their lifecycle window.
{{ ($lifecycleSummary['reconciled'] ?? 0) }} run(s) have already been automatically reconciled. {{ ($lifecycleSummary['reconciled'] ?? 0) }} operation(s) have already been automatically reconciled.
</div> </div>
@endif @endif

View File

@ -24,7 +24,7 @@
<div class="space-y-6"> <div class="space-y-6">
<x-filament::section> <x-filament::section>
<x-slot name="heading"> <x-slot name="heading">
Run #{{ (int) $run->getKey() }} Operation #{{ (int) $run->getKey() }}
</x-slot> </x-slot>
<x-slot name="description"> <x-slot name="description">
@ -88,11 +88,17 @@
</div> </div>
<div> <div>
<dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Runbooks</dt> <dt class="text-xs font-semibold uppercase tracking-wider text-gray-500 dark:text-gray-400">Navigation</dt>
<dd class="mt-1 text-sm"> <dd class="mt-1 text-sm">
<div class="flex flex-col gap-2">
<x-filament::link href="{{ \App\Support\System\SystemOperationRunLinks::index() }}">
Show all operations
</x-filament::link>
<x-filament::link href="{{ \App\Filament\System\Pages\Ops\Runbooks::getUrl(panel: 'system') }}"> <x-filament::link href="{{ \App\Filament\System\Pages\Ops\Runbooks::getUrl(panel: 'system') }}">
Go to runbooks Go to runbooks
</x-filament::link> </x-filament::link>
</div>
</dd> </dd>
</div> </div>
</dl> </dl>

View File

@ -63,7 +63,7 @@ class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-pri
href="{{ \App\Support\OperationRunLinks::tenantlessView($run) }}" href="{{ \App\Support\OperationRunLinks::tenantlessView($run) }}"
class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300" class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
> >
View Open operation
</a> </a>
</div> </div>
</li> </li>

View File

@ -29,7 +29,7 @@
<div class="space-y-4"> <div class="space-y-4">
@if ($run === null) @if ($run === null)
<div class="rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300"> <div class="rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
No verification run has been started yet. No verification operation has been started yet.
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -68,7 +68,7 @@
</div> </div>
@elseif ($isInProgress) @elseif ($isInProgress)
<div class="rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300"> <div class="rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
Verification is currently in progress. This section reads only stored run state and does not call external services. Verification is currently in progress. This section reads only stored operation state and does not call external services.
</div> </div>
<div class="flex flex-wrap items-center gap-2"> <div class="flex flex-wrap items-center gap-2">
@ -79,7 +79,7 @@
color="gray" color="gray"
size="sm" size="sm"
> >
View run Open operation
</x-filament::button> </x-filament::button>
@endif @endif
@ -131,7 +131,7 @@
color="gray" color="gray"
size="sm" size="sm"
> >
View run Open operation
</x-filament::button> </x-filament::button>
@endif @endif

View File

@ -34,7 +34,7 @@
href="{{ \App\Support\OpsUx\OperationRunUrl::view($run, $tenant) }}" href="{{ \App\Support\OpsUx\OperationRunUrl::view($run, $tenant) }}"
class="shrink-0 text-xs font-medium text-primary-700 hover:text-primary-800 dark:text-primary-300 dark:hover:text-primary-200" class="shrink-0 text-xs font-medium text-primary-700 hover:text-primary-800 dark:text-primary-300 dark:hover:text-primary-200"
> >
View run Open operation
</a> </a>
@endif @endif
</div> </div>

View File

@ -0,0 +1,35 @@
# Specification Quality Checklist: System Operations Surface Alignment
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-03-30
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validation pass 1: complete
- This spec intentionally excludes naming consolidation and deferred dashboard or onboarding retrofits, which are reserved for Specs 171 and 172.

View File

@ -0,0 +1,192 @@
openapi: 3.1.0
info:
title: System Operations Surface Alignment Contract
version: 1.0.0
summary: Route and UI contract for Spec 170.
paths:
/system/ops/runs:
get:
operationId: listSystemRuns
summary: Display the platform-wide operations registry.
responses:
'200':
description: System runs list rendered successfully.
'403':
description: Authenticated platform user lacks operations view capability.
'404':
description: Wrong plane or inaccessible system surface.
x-ui-surface:
surfaceType: read_only_registry_report
displayLabel: Operations
canonicalNoun:
collection: Operations
singular: Operation
inspectAffordance: clickable_row
canonicalDetailRoute: /system/ops/runs/{run}
headerActions:
- name: go_to_runbooks
label: Go to runbooks
type: navigation
rowActions: []
bulkActions: []
emptyStateCta:
name: go_to_runbooks
label: Go to runbooks
requiredCapabilities:
- platform.operations.view
defaultVisibleTruth:
- status
- outcome
- operation
- workspace
- tenant
- initiator
- activity_time
/system/ops/failures:
get:
operationId: listSystemFailures
summary: Display failed operations as a read-only registry.
responses:
'200':
description: Failed-runs list rendered successfully.
'403':
description: Authenticated platform user lacks operations view capability.
'404':
description: Wrong plane or inaccessible system surface.
x-ui-surface:
surfaceType: read_only_registry_report
displayLabel: Failed operations
canonicalNoun:
collection: Operations
singular: Operation
inspectAffordance: clickable_row
canonicalDetailRoute: /system/ops/runs/{run}
headerActions:
- name: show_all_operations
label: Show all operations
type: navigation
url: /system/ops/runs
rowActions: []
bulkActions: []
emptyStateCta:
name: show_all_operations
label: Show all operations
requiredCapabilities:
- platform.operations.view
filter:
status: completed
outcome: failed
defaultVisibleTruth:
- status
- outcome
- operation
- workspace
- tenant
- activity_time
/system/ops/stuck:
get:
operationId: listSystemStuckRuns
summary: Display queued or running operations that exceed the stuck threshold.
responses:
'200':
description: Stuck-runs list rendered successfully.
'403':
description: Authenticated platform user lacks operations view capability.
'404':
description: Wrong plane or inaccessible system surface.
x-ui-surface:
surfaceType: read_only_registry_report
displayLabel: Stuck operations
canonicalNoun:
collection: Operations
singular: Operation
inspectAffordance: clickable_row
canonicalDetailRoute: /system/ops/runs/{run}
headerActions:
- name: show_all_operations
label: Show all operations
type: navigation
url: /system/ops/runs
rowActions: []
bulkActions: []
emptyStateCta:
name: show_all_operations
label: Show all operations
requiredCapabilities:
- platform.operations.view
derivedFields:
- stuck_class
defaultVisibleTruth:
- status
- stuck_class
- operation
- workspace
- tenant
- activity_time
/system/ops/runs/{run}:
get:
operationId: viewSystemRun
summary: Display one system operation detail surface.
parameters:
- name: run
in: path
required: true
schema:
type: integer
responses:
'200':
description: System run detail rendered successfully.
'403':
description: Authenticated platform user lacks operations view capability.
'404':
description: Wrong plane or inaccessible system surface.
x-ui-surface:
surfaceType: detail_first_operational
displayLabel: Operation
canonicalNoun:
collection: Operations
singular: Operation
requiredCapabilities:
- platform.operations.view
headerActions:
- name: show_all_operations
label: Show all operations
type: navigation
url: /system/ops/runs
- name: go_to_runbooks
label: Go to runbooks
type: navigation
- name: retry
label: Retry
type: mutation
requiredCapabilities:
- platform.operations.manage
confirmationRequired: true
visibleWhen:
status: completed
outcome: failed
retryableType: true
- name: cancel
label: Cancel
type: destructive_mutation
requiredCapabilities:
- platform.operations.manage
confirmationRequired: true
visibleWhen:
statusIn:
- queued
- running
cancelableType: true
- name: mark_investigated
label: Mark investigated
type: mutation
requiredCapabilities:
- platform.operations.manage
confirmationRequired: true
form:
fields:
- name: reason
type: textarea
required: true
minLength: 5
maxLength: 500

View File

@ -0,0 +1,110 @@
# Data Model: System Operations Surface Alignment
## Overview
This feature introduces no new persisted entity, no new table, and no new status family. It reuses existing `OperationRun` truth and existing platform capability checks, while narrowing where triage actions are exposed in the UI.
## Entity: OperationRun
- **Type**: Existing persisted model
- **Purpose in this feature**: Canonical record shown on the three system list pages and on the system run detail page.
### Relevant Fields
| Field | Type | Notes |
|-------|------|-------|
| `id` | integer | Displayed as `Operation #<id>` and used in canonical detail routing. |
| `workspace_id` | integer | Required for platform-wide run context. |
| `tenant_id` | integer nullable | Nullable for tenantless/platform runs; still shown on system lists and detail. |
| `initiator_name` | string nullable | Default-visible operator truth on the all-runs list. |
| `type` | string | Rendered through `OperationCatalog::label(...)`. |
| `status` | string | Badge-rendered lifecycle dimension. |
| `outcome` | string | Badge-rendered execution-outcome dimension. |
| `context` | array/json | Stores triage metadata such as retry, cancel, and investigation context. |
| `created_at` | timestamp | Used for default-visible list activity time and recency on all three lists. |
| `started_at` | timestamp nullable | Supports stuck classification and detail timing. |
| `completed_at` | timestamp nullable | Preserved for completed/failed runs and detail history. |
### Relationships
| Relationship | Target | Purpose |
|--------------|--------|---------|
| `workspace` | `Workspace` | Default-visible context on all system lists and on the detail page. |
| `tenant` | `Tenant` | Default-visible context on all system lists and on the detail page. |
### Feature-Specific Invariants
- All three system lists eager-load `tenant` and `workspace`.
- `Runs` shows the full platform-wide run set.
- `Failures` filters to `status=completed` and `outcome=failed`.
- `Stuck` filters through `StuckRunClassifier` and exposes a derived `stuck_class` label.
- List inspection always resolves to `SystemOperationRunLinks::view($run)`.
### State Transitions Used By This Feature
| Transition | Preconditions | Result |
|------------|---------------|--------|
| Inspect list row | Operator has `platform.operations.view` | No state change; opens canonical detail page. |
| Retry run | Run is completed, failed, and retryable | Creates a new queued `OperationRun` with `context.triage.retry_of_run_id` and audit action `platform.system_console.retry`. |
| Cancel run | Run is queued or running and cancelable | Updates the current run to completed/failed through `OperationRunService` and logs `platform.system_console.cancel`. |
| Mark investigated | Operator has manage capability and supplies a valid reason | Updates `context.triage.investigated` on the current run and logs `platform.system_console.mark_investigated`. |
## Entity: PlatformUser Capability Gate
- **Type**: Existing persisted/authenticated actor model
- **Purpose in this feature**: Separates inspection access from triage access on system-panel surfaces.
### Relevant Capability Fields
| Capability | Purpose |
|------------|---------|
| `platform.access_system_panel` | Required to enter the system panel. |
| `platform.operations.view` | Required to access the system runs/failures/stuck/detail surfaces. |
| `platform.operations.manage` | Required to see and execute triage actions on the detail page. |
### Feature-Specific Invariants
- View-only users can load list and detail pages but cannot see triage actions.
- Manage-capable users retain retry/cancel/mark-investigated on the detail header.
- No new capability is introduced.
## Derived UI Contract: System Operations Lists
- **Type**: Derived UI surface, not persisted
- **Routes**:
- `/system/ops/runs`
- `/system/ops/failures`
- `/system/ops/stuck`
- **Surface Type**: `ReadOnlyRegistryReport`
- **Primary Question**: Which operation should I open next?
### Contract Rules
- Primary inspect affordance is full-row click only.
- Row actions are empty.
- Bulk actions are empty.
- Visible collection naming uses `Operations` and visible singular naming uses `Operation`.
- `/system/ops/runs` uses `Go to runbooks` as its single header and empty-state CTA.
- `/system/ops/failures` and `/system/ops/stuck` use `Show all operations` as their single header and empty-state CTA.
## Derived UI Contract: System Run Detail
- **Type**: Derived UI surface, not persisted
- **Route**: `/system/ops/runs/{run}`
- **Surface Type**: Detail-first operational surface
- **Primary Question**: What happened on this operation, and what follow-up is appropriate?
### Contract Rules
- Header actions remain the only triage location for retry, cancel, and mark investigated.
- The detail surface exposes `Show all operations` as the canonical return path and keeps `Go to runbooks` available as secondary navigation.
- `cancel` remains destructive and confirmation-gated.
- `mark investigated` retains the `reason` validation rule: required, minimum 5 characters, maximum 500 characters.
- No new page, modal surface, or alternate triage route is introduced.
## Persistence Impact
- **Schema changes**: None
- **Data migration**: None
- **New indexes**: None
- **Retention impact**: None

View File

@ -0,0 +1,127 @@
# Implementation Plan: System Operations Surface Alignment
**Branch**: `170-system-operations-surface-alignment` | **Date**: 2026-03-30 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/170-system-operations-surface-alignment/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/170-system-operations-surface-alignment/spec.md`
## Summary
Align the system-panel Operations, Failed operations, and Stuck operations pages to their declared `ReadOnlyRegistryReport` surface semantics by removing inline triage from the list rows, preserving full-row navigation to the canonical system operation detail page, standardizing visible naming to `Operations` / `Operation`, and adding the required list CTA and detail return-path behavior while keeping retry/cancel/mark-investigated ownership on the existing `ViewRun` page. The implementation stays narrow: no schema, new capability, or service-model changes; only Filament page behavior, visible operator copy, and the associated Pest/Livewire guard coverage are updated.
## Technical Context
**Language/Version**: PHP 8.4, Laravel 12, Livewire v4, Filament v5
**Primary Dependencies**: `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest`
**Storage**: PostgreSQL with existing `operation_runs` and `audit_logs` tables; no schema changes
**Testing**: Pest feature tests with Livewire component assertions, executed through Laravel Sail
**Target Platform**: Laravel web application running in Sail locally and containerized Linux environments in staging/production
**Project Type**: Laravel monolith with Filament panels
**Performance Goals**: Preserve current DB-only list rendering, eager loading of `tenant` and `workspace`, and existing pagination/sort behavior with no extra remote calls
**Constraints**: Platform plane only; no new pages/capabilities/persistence; visible naming must align to `Operations` / `Operation`; destructive triage stays confirmation-gated; existing queued-toast and audit behavior must remain intact
**Scale/Scope**: Four existing system-panel pages, one existing detail Blade view, one existing triage service, and the focused feature/guard tests that encode current row-action behavior
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- `PASS` Inventory-first / snapshots-second: the feature does not change inventory, backups, or snapshot truth.
- `PASS` Read/write separation: no new writes are introduced; existing retry/cancel/mark-investigated flows remain explicit operator actions with confirmation where required.
- `PASS` Graph contract path: no Microsoft Graph call path is touched.
- `PASS` Deterministic capabilities: existing `PlatformCapabilities::OPERATIONS_VIEW` and `PlatformCapabilities::OPERATIONS_MANAGE` remain the canonical gates.
- `PASS` RBAC-UX plane separation: the slice is limited to `/system` surfaces and existing platform guard semantics.
- `PASS` Destructive confirmation standard: `cancel` remains `->requiresConfirmation()` on the detail header.
- `PASS` Ops-UX 3-surface feedback: retry continues to use `OperationUxPresenter::queuedToast(...)`; cancel and mark-investigated remain local terminal confirmations without adding queued/running notifications.
- `PASS` Ops lifecycle ownership: status/outcome mutation continues to flow through `OperationRunService` inside `OperationRunTriageService`; no new lifecycle path is introduced.
- `PASS` Proportionality / bloat: the feature removes duplicated interaction semantics and adds no new persistence, abstraction, state family, or taxonomy.
- `PASS` Badge semantics: list/detail badge rendering stays on the shared badge catalog and renderer.
- `PASS` Filament-native UI: the change stays inside existing Filament pages, row navigation, header actions, and confirmations.
- `PASS` UI surface taxonomy: Runs, Failures, and Stuck remain `ReadOnlyRegistryReport`; `ViewRun` remains the detail-first operational surface.
- `PASS` UI inspect model: each list will expose exactly one primary inspect model, `recordUrl()` row click.
- `PASS` UI action hierarchy: registry lists will have no inline triage; detail header remains the single triage owner.
- `PASS` UI naming scope: the changed system surfaces standardize visible copy to `Operations` / `Operation` while keeping internal route stability explicit.
- `PASS` Empty-state CTA rule: each changed list surface will define exactly one page-appropriate CTA and matching header action.
- `PASS` Placeholder/empty-group ban: no empty groups are introduced; bulk actions remain absent because there is no real bulk use case.
- `PASS` OPSURF page contract: the governing spec already defines persona, operator question, default truth, mutation scope, and dangerous actions.
- `PASS` Testing truth: implementation will update behavior guards, not just declarations.
## Project Structure
### Documentation (this feature)
```text
specs/170-system-operations-surface-alignment/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── system-ops-surface-contract.yaml
└── tasks.md
```
### Source Code (repository root)
```text
app/
├── Filament/
│ └── System/
│ └── Pages/
│ └── Ops/
│ ├── Runs.php
│ ├── Failures.php
│ ├── Stuck.php
│ └── ViewRun.php
├── Services/
│ └── SystemConsole/
│ └── OperationRunTriageService.php
├── resources/
│ └── views/
│ └── filament/
│ └── system/
│ └── pages/
│ └── ops/
│ └── view-run.blade.php
└── Support/
└── System/
└── SystemOperationRunLinks.php
tests/
└── Feature/
├── Guards/
│ └── ActionSurfaceContractTest.php
└── System/
└── Spec114/
├── OpsTriageActionsTest.php
├── OpsFailuresViewTest.php
└── OpsStuckViewTest.php
```
**Structure Decision**: This is a single Laravel application. The implementation is confined to existing system-panel Filament pages and existing Pest feature/guard tests. No new application layer or directory is needed.
## Complexity Tracking
No constitution waiver is expected. This slice reduces surface complexity instead of introducing a new layer.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| None | Not applicable | Not applicable |
## Proportionality Review
- **Current operator problem**: the three system list pages are declared as read-only registry surfaces but still expose direct triage actions, leave empty-state navigation underdefined, and continue to present `Runs` as a competing visible noun beside the canonical `Operations` vocabulary.
- **Existing structure is insufficient because**: the current hybrid model forces operators to choose between acting in-row and opening the richer detail page, while the constitution requires one primary inspect model, stable nouns, and explicit CTA behavior for changed list surfaces.
- **Narrowest correct implementation**: remove list row triage actions, keep `recordUrl()` row-click navigation, standardize visible copy to `Operations` / `Operation`, add one clear list CTA per changed surface, add a `Show all operations` return path on the detail page, and update the existing tests to assert the aligned behavior.
- **Ownership cost created**: low. The change requires focused updates to Filament page classes and the behavior guards that currently encode list-level triage.
- **Alternative intentionally rejected**: reclassifying the list pages as queue/review surfaces or adding new exception types was rejected because these pages are scan-first registries, not context-preserving decision queues.
- **Release truth**: current-release truth. The canonical detail page and triage service already exist today; the feature removes duplication rather than preparing speculative future structure.
## Post-Design Constitution Re-check
- `PASS` `UI-CONST-001` / `UI-SURF-001`: the artifacts keep list pages in the registry class and the detail page as the triage owner.
- `PASS` `UI-HARD-001`: the target design leaves each system list with exactly one primary inspect model and zero inline destructive actions.
- `PASS` `UI-EX-001`: no new exception type or exemption is needed for this slice.
- `PASS` `OPSURF-001`: list pages stay scan-first, while the detail page remains the context-rich operational surface.
- `PASS` `RBAC-UX-001` to `RBAC-UX-005`: view/manage separation, server-side enforcement, and destructive confirmation rules remain unchanged.
- `PASS` `UI-NAMING-001`: visible system-surface nomenclature now aligns to `Operations` / `Operation`.
- `PASS` Empty-state CTA requirement: the design now defines a primary CTA for each changed list surface and a return path on the detail page.
- `PASS` `BLOAT-001`: no new persistence, abstraction, state family, or taxonomy was introduced during design.
- `PASS` Filament v5 / Livewire v4 guardrails: the plan keeps changes inside existing Filament pages and does not require provider or asset registration changes.

View File

@ -0,0 +1,73 @@
# Quickstart: System Operations Surface Alignment
## Goal
Bring the system-panel Operations surfaces into conformance with the declared read-only registry contract by removing inline triage from the list rows, standardizing visible naming to `Operations` / `Operation`, adding explicit list CTAs, and keeping triage on the canonical operation detail page.
## Prerequisites
1. Start the local stack:
```bash
vendor/bin/sail up -d
```
2. Work on branch `170-system-operations-surface-alignment`.
## Implementation Steps
1. Update the three list pages:
- `app/Filament/System/Pages/Ops/Runs.php`
- `app/Filament/System/Pages/Ops/Failures.php`
- `app/Filament/System/Pages/Ops/Stuck.php`
2. Remove the `retry`, `cancel`, and `mark_investigated` table actions from each list page.
3. Keep `recordUrl()` row-click navigation intact for each list page.
4. Update visible navigation labels, headings, empty-state copy, and page CTA behavior so the changed surfaces use `Operations` / `Operation` vocabulary.
5. Update each list page's `actionSurfaceDeclaration()` explanation text so it reflects detail-owned triage rather than row-level triage.
6. Update `app/Filament/System/Pages/Ops/ViewRun.php` and `resources/views/filament/system/pages/ops/view-run.blade.php` so the detail page keeps triage ownership, uses `Operation #...` copy, and exposes `Show all operations`.
7. Leave `app/Services/SystemConsole/OperationRunTriageService.php` unchanged unless a small refactor is needed to support the page move without changing behavior.
## Tests To Update
1. `tests/Feature/System/Spec114/OpsTriageActionsTest.php`
- Replace row-action execution assertions on `Runs` with detail-page action visibility/execution assertions on `ViewRun`.
- Keep view-only versus manage-capable separation explicit.
- Add assertions for visible `Operations` / `Operation` copy and the `Show all operations` return path.
2. `tests/Feature/Guards/ActionSurfaceContractTest.php`
- Replace the three expectations for direct row triage with expectations that the row action set is empty while `recordUrl()` still points to `SystemOperationRunLinks::view($run)`.
- Assert the list CTA behavior and canonical Operations naming on the changed surfaces.
3. Keep the existing page-access tests for failures/stuck passing.
## Focused Verification
Run the smallest relevant test set first:
```bash
vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsTriageActionsTest.php
vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php
```
If implementation touches other platform-ops behavior, add the page access tests:
```bash
vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsFailuresViewTest.php
vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsStuckViewTest.php
```
## Formatting
After code changes, run:
```bash
vendor/bin/sail bin pint --dirty --format agent
```
## Manual Review Checklist
1. The `Runs`, `Failures`, and `Stuck` tables still navigate by row click.
2. None of the three lists exposes inline triage actions.
3. The changed system surfaces use `Operations` / `Operation` as the visible noun.
4. The Operations list exposes `Go to runbooks`, while Failed operations and Stuck operations expose `Show all operations` as the single header and empty-state CTA.
5. The system operation detail page still shows `Retry`, `Cancel`, and `Mark investigated` only for manage-capable operators and exposes `Show all operations` as the return path.
6. `Cancel` still requires confirmation.
7. Retry still produces the existing queued toast with a `View run` link.

View File

@ -0,0 +1,43 @@
# Research: System Operations Surface Alignment
## Decision 1: Keep the three system list pages as read-only registry surfaces
- **Decision**: `Runs`, `Failures`, and `Stuck` stay classified as `ReadOnlyRegistryReport` surfaces with `recordUrl()` full-row navigation as the only primary inspect model.
- **Rationale**: The pages already declare `ActionSurfaceType::ReadOnlyRegistryReport`, already use clickable rows, and the constitution requires exactly one primary inspect/open model for registry surfaces. Inline triage on these lists is the specific behavior drift the spec is correcting.
- **Alternatives considered**: Reclassify the lists as queue/review surfaces. Rejected because the pages are scan-first status registers split by run state, not in-place decision queues.
## Decision 2: Keep triage ownership on the existing `ViewRun` page
- **Decision**: `Retry`, `Cancel`, and `Mark investigated` remain on `app/Filament/System/Pages/Ops/ViewRun.php` and are removed from the list rows.
- **Rationale**: The detail page already exposes all three actions, already loads `tenant` and `workspace`, and already provides the surrounding operational context the operator needs before acting. Repo analysis of the operation-run detail surface confirms that the page is already the rich context owner for run truth and next steps.
- **Alternatives considered**: Add a new dedicated triage modal or keep one “safe” triage action inline on the lists. Rejected because both options preserve split ownership and weaken the single-entry operational model.
## Decision 3: Reuse the existing triage service and links without abstraction changes
- **Decision**: `OperationRunTriageService` and `SystemOperationRunLinks` remain the canonical implementation path for retry/cancel/investigate and detail navigation.
- **Rationale**: The service already encapsulates retryability/cancelability rules, audit logging, `OperationRunService` lifecycle transitions, and triage context writes. This spec changes action placement, not domain behavior.
- **Alternatives considered**: Introduce a new presenter, coordinator, or list-specific triage wrapper. Rejected under `ABSTR-001` and `LAYER-001` because there is only one existing triage behavior and the current service already fits it.
## Decision 4: Move behavior guards from “direct row triage” to “detail-owned triage”
- **Decision**: Update `tests/Feature/Guards/ActionSurfaceContractTest.php` and `tests/Feature/System/Spec114/OpsTriageActionsTest.php` so they assert row-click-only lists and detail-header triage ownership.
- **Rationale**: The constitution explicitly prioritizes rendered behavior over declaration. The current guard suite still encodes the old hybrid interaction model by expecting `retry`, `cancel`, and `mark_investigated` on all three list pages.
- **Alternatives considered**: Leave the existing guards untouched and only rely on declarations, or delete the guards. Rejected because that would preserve declaration-only conformance and remove the regression protection this slice is supposed to strengthen.
## Decision 5: Absorb visible Operations naming into Spec 170 while keeping route stability explicit
- **Decision**: The changed system surfaces standardize their visible collection and singular nouns to `Operations` and `Operation` in navigation labels, headings, empty-state copy, and return links, while existing internal class names and `/system/ops/runs` route paths may remain stable for compatibility.
- **Rationale**: The constitution treats `Operations` as the canonical collection noun for run records. Leaving the changed system surfaces on `Runs` would keep the spec itself in conflict with the constitution.
- **Alternatives considered**: Defer naming to Spec 171. Rejected because it leaves a known constitution conflict inside the active slice.
## Decision 6: Give every changed list surface one explicit CTA and the detail page one explicit return path
- **Decision**: The Operations list uses `Go to runbooks` as its single header and empty-state CTA. Failed operations and Stuck operations use `Show all operations` as their single header and empty-state CTA. The detail page exposes `Show all operations` as the canonical return path and keeps `Go to runbooks` as secondary navigation.
- **Rationale**: The constitution requires changed list surfaces to define explicit empty-state CTA behavior and requires a clear canonical navigation model. These CTAs stay navigation-only and do not reintroduce inline row clutter.
- **Alternatives considered**: Keep explanation-only empty states or add multiple list CTAs. Rejected because the first violates the constitution and the second weakens scanability.
## Decision 7: No new API surface; document the route/UI contract explicitly
- **Decision**: Capture the expected route and interaction contract in a small OpenAPI-style YAML file under `contracts/` even though the feature does not add a public JSON API.
- **Rationale**: The implementation change is route-driven and operator-visible. A route contract that records surface type, inspect affordance, action placement, and capability boundaries gives later tasks and reviews a concrete artifact without inventing a new backend API.
- **Alternatives considered**: Skip `contracts/` entirely. Rejected because the planning workflow expects a concrete contract artifact, and this feature benefits from a machine-readable record of its operator-visible contract.

View File

@ -0,0 +1,206 @@
# Feature Specification: System Operations Surface Alignment
**Feature Branch**: `170-system-operations-surface-alignment`
**Created**: 2026-03-30
**Status**: Draft
**Input**: User description: "System Operations Surface Alignment"
## Spec Scope Fields *(mandatory)*
- **Scope**: platform
- **Primary Routes**:
- `/system/ops/runs`
- `/system/ops/failures`
- `/system/ops/stuck`
- `/system/ops/runs/{run}`
- **Data Ownership**:
- No new platform-owned, workspace-owned, or tenant-owned records are introduced
- Existing `OperationRun` records remain the only source of truth for system operations list and detail surfaces
- This feature changes only operator-facing interaction semantics for existing system operations pages
- **RBAC**:
- Platform plane only
- Existing `platform.operations.view` and `platform.operations.manage` capability boundaries remain authoritative
- Users without system access remain deny-as-not-found by existing platform routing and auth guards
- Users with view capability but without manage capability remain able to inspect runs but unable to execute triage actions
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
| Surface | Surface Type | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type |
|---|---|---|---|---|---|---|---|---|---|---|---|
| System operations list | Read-only Registry / Report | Full-row click to system operation detail | required | header CTA only | none on the list | `/system/ops/runs` | `/system/ops/runs/{run}` | Platform scope only | Operations / Operation | status, outcome, operation type, workspace, tenant, recency | none |
| System failed operations list | Read-only Registry / Report | Full-row click to system operation detail | required | header CTA only | none on the list | `/system/ops/failures` | `/system/ops/runs/{run}` | Platform scope only | Operations / Operation | failed outcome, operation type, workspace, tenant, recency | none |
| System stuck operations list | Read-only Registry / Report | Full-row click to system operation detail | required | header CTA only | none on the list | `/system/ops/stuck` | `/system/ops/runs/{run}` | Platform scope only | Operations / Operation | queued or running stale state, operation type, workspace, tenant, recency | none |
| System operation detail | Detail-first Operational Surface | Dedicated detail page | forbidden | detail header groups only | detail header only | `/system/ops/runs` | `/system/ops/runs/{run}` | Platform scope only | Operations / Operation | operation truth, failure cause, context, related navigation, next actions | none |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
| Surface | Primary Persona | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|
| System operations list | Platform operator | Read-only Registry / Report | Which operation should I open next? | status, outcome, operation label, workspace, tenant, initiator, activity time | raw payloads and deeper traces stay on detail | execution outcome, recency | Read-only list | Open operation, go to runbooks | none |
| System failed operations list | Platform operator | Read-only Registry / Report | Which failed operation needs investigation first? | failed outcome, operation label, workspace, tenant, activity time | raw payloads and deeper traces stay on detail | execution outcome, failure state, recency | Read-only list | Open operation, show all operations | none |
| System stuck operations list | Platform operator | Read-only Registry / Report | Which queued or running operation has crossed the stuck threshold? | stuck class, operation label, workspace, tenant, activity time | raw payloads and deeper traces stay on detail | lifecycle stall state, recency | Read-only list | Open operation, show all operations | none |
| System operation detail | Platform operator | Detail-first Operational Surface | What happened on this operation, and what follow-up is appropriate? | operation identity, status, outcome, related scope, dominant failure or stall context, related links | low-level payloads, internal traces, and extended diagnostics | execution outcome, lifecycle state, operability context | Existing platform triage only | Show all operations, go to runbooks, retry operation, mark investigated | Cancel operation when still cancellable |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: No
- **New persisted entity/table/artifact?**: No
- **New abstraction?**: No
- **New enum/state/reason family?**: No
- **New cross-domain UI framework/taxonomy?**: No
- **Current operator problem**: The three system operations list pages currently behave like scan-first registry surfaces but also expose direct triage actions, underdefined empty states, and competing Operations versus Runs naming that duplicate or dilute the canonical detail model.
- **Existing structure is insufficient because**: The current list surfaces split triage ownership between list and detail, which makes the lists behave like mini control centers instead of scan-first registries.
- **Narrowest correct implementation**: Keep the existing system lists and the existing system operation detail page, but move triage ownership fully onto the detail page, align visible naming to Operations / Operation, and give each list one clear navigation CTA without changing persistence or introducing new surfaces.
- **Ownership cost**: Existing list and guard tests need to be updated, and operators lose direct row-level triage from the lists in exchange for one consistent detail-first follow-up model.
- **Alternative intentionally rejected**: Reclassifying the system lists into a queue or review surface was rejected because the pages are scan-first registries split by status family, not context-preserving queue workflows.
- **Release truth**: Current-release truth. The repo already contains the canonical detail page and the duplicated list triage actions; this slice removes the duplication rather than adding new capability.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Scan Lists Without Competing Actions (Priority: P1)
As a platform operator, I want the Operations, Failed operations, and Stuck operations lists to behave as scan-first registries with one obvious open path and one clear navigation CTA, so that I can inspect the right operation without row-level action clutter.
**Why this priority**: This is the direct constitution violation on the current system operations surfaces.
**Independent Test**: Can be fully tested by loading each system operations list and asserting that rows remain clickable, row-level triage actions are absent, visible labels use Operations / Operation vocabulary, and each list exposes the expected primary CTA in header and empty state.
**Acceptance Scenarios**:
1. **Given** a platform operator opens the system Operations list, **When** the table renders, **Then** each row opens the system operation detail page through row click, exposes no row-level triage actions, and keeps `Go to runbooks` as the single header and empty-state CTA.
2. **Given** a platform operator opens the system Failed operations list, **When** the table renders, **Then** the row remains clickable, the list does not expose retry, cancel, or investigate actions inline, and `Show all operations` is the single header and empty-state CTA.
3. **Given** a platform operator opens the system Stuck operations list, **When** the table renders, **Then** the row remains clickable, the list does not expose retry, cancel, or investigate actions inline, and `Show all operations` is the single header and empty-state CTA.
---
### User Story 2 - Perform Triage From The Canonical Detail Page (Priority: P1)
As a platform operator with manage capability, I want system operation triage to live on the canonical operation detail page, so that every follow-up action happens in the surface that already owns full context and keeps a clear return path to all operations.
**Why this priority**: The lists can only become constitution-compliant if the detail page becomes the single triage destination.
**Independent Test**: Can be fully tested by opening a system operation detail page as a manage-capable operator and asserting that retry, cancel, and mark investigated remain available there with the same audit and queued-run behavior, while `Show all operations` remains available as the return path.
**Acceptance Scenarios**:
1. **Given** a failed operation and a manage-capable platform operator, **When** the operator opens the system operation detail page, **Then** retry remains available on the detail header and still queues a replacement operation.
2. **Given** a cancellable operation and a manage-capable platform operator, **When** the operator opens the system operation detail page, **Then** cancel remains available on the detail header and still requires confirmation.
3. **Given** an operation that needs documentation, **When** the operator opens the system operation detail page, **Then** mark investigated remains available there, still records the investigation action, and the page keeps `Show all operations` as the canonical return link.
---
### User Story 3 - Preserve View-Only Access Semantics (Priority: P2)
As a platform operator with view-only access, I want to inspect system operations without being offered triage controls, so that the platform plane stays capability-correct while the aligned surfaces remain usable.
**Why this priority**: Surface alignment must not weaken the existing view/manage separation.
**Independent Test**: Can be fully tested by rendering list and detail surfaces for a view-only system user and asserting that inspection and navigation remain available while triage actions remain hidden.
**Acceptance Scenarios**:
1. **Given** a platform user with operations view but not operations manage, **When** the user opens any system operations list, **Then** the user can inspect rows, use the page CTA, but sees no triage actions there.
2. **Given** a platform user with operations view but not operations manage, **When** the user opens a system operation detail page, **Then** retry, cancel, and mark investigated remain hidden while `Show all operations` and `Go to runbooks` remain available.
### Edge Cases
- A failed operation appears on both the all-operations list and the failed-operations list; both surfaces must expose the same single open model and must not diverge in row actions or naming.
- A queued or running operation later becomes cancellable or non-cancellable; the list remains read-only while the detail page resolves whether cancel is available.
- The system operation detail page must keep a clear return path to the canonical Operations list even when opened from Failed operations or Stuck operations.
- Empty Operations, Failed operations, and Stuck operations states must remain explanation-first while still exposing exactly one primary CTA that matches the page contract.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature introduces no Microsoft Graph calls, no new write workflow, and no new long-running work. Existing `OperationRun` and triage services remain the underlying execution model. The feature only realigns where platform operators inspect and triage existing system runs.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature adds no new structure, persistence, abstraction, or state family. It reduces surface complexity by removing duplicated triage ownership from the lists.
**Constitution alignment (OPS-UX):** Existing retry and cancel flows continue to reuse the current queued-run UX, terminal notification rules, and service-owned `OperationRun` lifecycle. This feature does not introduce a new run type or change lifecycle ownership.
**Constitution alignment (RBAC-UX):** This feature stays in the platform plane only. Existing `OPERATIONS_VIEW` and `OPERATIONS_MANAGE` capability checks remain the server-side source of truth. List alignment must not weaken the current distinction between view-only inspection and manage-capable triage.
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable.
**Constitution alignment (BADGE-001):** Existing status and outcome badge semantics remain unchanged and centralized.
**Constitution alignment (UI-FIL-001):** The feature continues to use Filament-native tables, row navigation, header actions, confirmation modals, and notifications. No custom local action framework or styling language is introduced.
**Constitution alignment (UI-NAMING-001):** This slice standardizes the changed system-plane surfaces to the canonical visible nouns `Operations` and `Operation`. Existing internal PHP class names and route paths may remain stable, but operator-facing labels, headings, and return links MUST stop presenting `Runs` as the primary noun.
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001):** The system Runs, Failures, and Stuck pages MUST align to the Read-only Registry / Report surface rules: one-click row open, no competing inline triage controls, and no destructive actions on the list rows. The system run detail page MUST remain the sole triage surface for retry, cancel, and mark investigated.
**Constitution alignment (OPSURF-001):** The lists remain operator-first scan surfaces that show only the truth needed to choose the next run to inspect. Full follow-up context and triage remain on the detail page, where diagnostic depth already exists.
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** This feature does not add a new semantic layer. It removes duplicated action ownership and keeps tests focused on operator-visible behavior: list inspect model, detail triage ownership, and manage-vs-view capability behavior.
**Constitution alignment (Filament Action Surfaces):** The Action Surface Contract is satisfied when Operations, Failed operations, and Stuck operations expose row click only, keep bulk actions absent by explicit no-bulk need, provide one header and empty-state CTA each, and leave retry, cancel, and mark investigated to the detail header. No new exemption is introduced.
**Constitution alignment (UX-001 — Layout & Information Architecture):** List layouts remain scanable registry tables. The system run detail page remains the operational detail surface and continues to own richer follow-up actions. No create or edit layout changes are introduced.
### Functional Requirements
- **FR-170-001**: The system Operations list MUST behave as a read-only registry surface with one primary inspect model: full-row click to the canonical system operation detail page.
- **FR-170-002**: The system Failed operations list MUST behave as a read-only registry surface with one primary inspect model: full-row click to the canonical system operation detail page.
- **FR-170-003**: The system Stuck operations list MUST behave as a read-only registry surface with one primary inspect model: full-row click to the canonical system operation detail page.
- **FR-170-004**: the Operations, Failed operations, and Stuck operations lists MUST NOT render retry, cancel, or mark investigated as row actions.
- **FR-170-005**: The canonical system operation detail page MUST remain the only system-plane surface that exposes retry, cancel, and mark investigated actions.
- **FR-170-006**: Retry on the system operation detail page MUST preserve the current queued-run feedback behavior and the current link back to the newly queued operation.
- **FR-170-007**: Cancel on the system operation detail page MUST remain confirmation-gated and MUST stay available only when the current operation is still cancellable.
- **FR-170-008**: Mark investigated on the system operation detail page MUST remain confirmation-gated and MUST continue to require an operator-supplied reason.
- **FR-170-009**: View-only platform operators MUST remain able to open list rows and detail pages while triage actions remain hidden.
- **FR-170-010**: Manage-capable platform operators MUST retain triage capability on the system operation detail page after list row actions are removed.
- **FR-170-011**: Existing audit behavior for retry and mark investigated MUST remain unchanged.
- **FR-170-012**: The system operation detail page MUST provide a clear `Show all operations` return path to the canonical collection route while preserving `Go to runbooks` navigation.
- **FR-170-013**: This feature MUST NOT introduce a new page, a new system operations capability, or a new persisted artifact.
- **FR-170-014**: Repository guard tests for the system operations surfaces MUST be updated so they assert row-click-only lists, list CTAs, canonical Operations / Operation naming, and detail-owned triage instead of direct row triage.
- **FR-170-015**: The changed system surfaces MUST use `Operations` as the canonical visible collection noun and `Operation` as the canonical visible singular noun.
- **FR-170-016**: Each changed system list surface MUST expose exactly one primary empty-state CTA and the corresponding header action when records exist.
## UI Action Matrix *(mandatory when Filament is changed)*
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|---|---|---|---|---|---|---|---|---|---|---|
| System operations list | `app/Filament/System/Pages/Ops/Runs.php` | `Go to runbooks` | `recordUrl()` full-row click | none | none | `Go to runbooks` | n/a | n/a | no new audit behavior | Read-only Registry / Report; visible label becomes `Operations` |
| System failed operations list | `app/Filament/System/Pages/Ops/Failures.php` | `Show all operations` | `recordUrl()` full-row click | none | none | `Show all operations` | n/a | n/a | no new audit behavior | Read-only Registry / Report; visible label becomes `Failed operations` |
| System stuck operations list | `app/Filament/System/Pages/Ops/Stuck.php` | `Show all operations` | `recordUrl()` full-row click | none | none | `Show all operations` | n/a | n/a | no new audit behavior | Read-only Registry / Report; visible label becomes `Stuck operations` |
| System operation detail | `app/Filament/System/Pages/Ops/ViewRun.php`, `resources/views/filament/system/pages/ops/view-run.blade.php` | `Show all operations`, `Go to runbooks` | n/a | n/a | n/a | n/a | `Retry`, `Cancel`, `Mark investigated` | n/a | existing retry and mark-investigated audit behavior remains | Detail-first operational owner of triage; heading and return link use `Operation` |
### Key Entities *(include if feature involves data)*
- **System operations list surface**: The system-panel Operations, Failed operations, and Stuck operations pages that scan existing `OperationRun` records.
- **System operation detail surface**: The canonical system detail page for one selected `OperationRun`.
- **System operation triage action**: Existing follow-up actions for retry, cancel, and mark investigated.
## Success Criteria *(mandatory)*
- **SC-170-001**: Operations, Failed operations, and Stuck operations each expose exactly one primary inspect model in automated coverage: row click to the canonical system operation detail page.
- **SC-170-002**: Automated coverage verifies that Operations, Failed operations, and Stuck operations expose zero row-level triage actions.
- **SC-170-003**: Automated coverage verifies that the system operation detail page still exposes retry, cancel, and mark investigated for manage-capable operators when each action is legitimately available.
- **SC-170-004**: Automated coverage verifies that view-only platform operators can inspect system operations but cannot see manage-only triage actions on the detail page.
- **SC-170-005**: Automated coverage verifies that the changed system surfaces use the canonical visible nouns `Operations` and `Operation` and expose the expected header or empty-state CTA on each list surface.
- **SC-170-006**: The feature ships without adding any new capability, persistence, or UI exemption.
## Assumptions
- The current system run detail page remains the correct canonical place for triage ownership.
- Existing audit behavior for retry and mark investigated is correct and does not need redesign in this slice.
- Existing internal route paths under `/system/ops/runs` may remain stable while visible system-surface naming is standardized in this slice.
## Non-Goals
- Renaming internal PHP class names or changing existing `/system/ops/runs` route paths
- Retrofitting deferred dashboard, onboarding, or landing surfaces
- Changing `OperationRun` lifecycle semantics, run creation behavior, or notification taxonomy
- Introducing a queue/review model for the system Operations, Failed operations, or Stuck operations pages
## Dependencies
- Existing system operations list pages: Runs, Failures, and Stuck
- Existing system run detail page
- Existing `OperationRunTriageService`
- Existing platform capability and audit behavior
- Existing action-surface and system operations guard coverage
## Definition of Done
Spec 170 is complete when the three changed system list pages are scan-first row-click-only registry surfaces, visible system naming uses Operations / Operation, each list has one matching header and empty-state CTA, the system operation detail page is the sole owner of retry/cancel/investigate triage and exposes `Show all operations`, existing view/manage capability semantics remain intact, and guard tests reflect the aligned interaction model.

View File

@ -0,0 +1,199 @@
# Tasks: System Operations Surface Alignment
**Input**: Design documents from `/specs/170-system-operations-surface-alignment/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md, contracts/system-ops-surface-contract.yaml
**Tests**: Runtime behavior changes in this repo require Pest coverage. Each story below includes test work and focused verification.
**Organization**: Tasks are grouped by user story so each story can be implemented and validated independently where the current surface boundaries allow it.
## Phase 1: Setup
**Purpose**: Lock the implementation targets to the generated plan artifacts before runtime edits begin.
- [X] T001 Reconfirm the implementation and verification targets in `specs/170-system-operations-surface-alignment/contracts/system-ops-surface-contract.yaml` and `specs/170-system-operations-surface-alignment/quickstart.md` before editing runtime files.
- [X] T002 Inspect the current system ops touchpoints in `app/Filament/System/Pages/Ops/Runs.php`, `app/Filament/System/Pages/Ops/Failures.php`, `app/Filament/System/Pages/Ops/Stuck.php`, `app/Filament/System/Pages/Ops/ViewRun.php`, `tests/Feature/System/Spec114/OpsTriageActionsTest.php`, and `tests/Feature/Guards/ActionSurfaceContractTest.php` so every direct row-triage assertion is mapped to the aligned target behavior.
---
## Phase 2: Foundational
**Purpose**: Establish the shared regression baseline that all story work must satisfy.
**⚠️ CRITICAL**: No user story work should be considered complete until these shared assertions are updated.
- [X] T003 Update the shared system surface regression contract in `tests/Feature/Guards/ActionSurfaceContractTest.php` so the baseline expects clickable rows, canonical `Operations` naming, explicit list CTA behavior, and no row triage on the changed system pages.
- [X] T004 Update the shared system triage and navigation scenario coverage in `tests/Feature/System/Spec114/OpsTriageActionsTest.php` so list-surface, detail-surface, view-only, and `Show all operations` assertions can be expressed separately.
**Checkpoint**: Shared contract coverage is ready; story implementation can now proceed.
---
## Phase 3: User Story 1 - Scan Lists Without Competing Actions (Priority: P1) 🎯 MVP
**Goal**: Make the Operations, Failed operations, and Stuck operations pages behave as scan-first registry surfaces with row-click-only inspection, stable naming, and one clear CTA each.
**Independent Test**: Load each list page and verify `recordUrl()` row navigation remains intact, all row-level triage actions are absent, visible copy uses `Operations` / `Operation`, and the correct single CTA appears in both header and empty state.
### Tests for User Story 1
- [X] T005 [US1] Add failing list-behavior and CTA assertions in `tests/Feature/System/Spec114/OpsTriageActionsTest.php` that require row-click-only behavior, canonical `Operations` naming, and the correct single CTA on the changed system lists.
### Implementation for User Story 1
- [X] T006 [P] [US1] Remove `retry`, `cancel`, and `mark_investigated` row actions, rename the visible label to `Operations`, add the `Go to runbooks` header and empty-state CTA, and update the action-surface declaration wording in `app/Filament/System/Pages/Ops/Runs.php`.
- [X] T007 [P] [US1] Remove `retry`, `cancel`, and `mark_investigated` row actions, rename the visible label to `Failed operations`, add the `Show all operations` header and empty-state CTA, and update the action-surface declaration wording in `app/Filament/System/Pages/Ops/Failures.php`.
- [X] T008 [P] [US1] Remove `retry`, `cancel`, and `mark_investigated` row actions, rename the visible label to `Stuck operations`, add the `Show all operations` header and empty-state CTA, and update the action-surface declaration wording in `app/Filament/System/Pages/Ops/Stuck.php`.
- [X] T009 [US1] Verify the three list pages still use `recordUrl()` and expose the correct CTA behavior in `app/Filament/System/Pages/Ops/Runs.php`, `app/Filament/System/Pages/Ops/Failures.php`, and `app/Filament/System/Pages/Ops/Stuck.php`.
- [X] T010 [US1] Run the focused list-surface regression checks in `tests/Feature/Guards/ActionSurfaceContractTest.php` and `tests/Feature/System/Spec114/OpsTriageActionsTest.php`.
**Checkpoint**: The three system list pages are row-click-only registry surfaces and can be validated independently.
---
## Phase 4: User Story 2 - Perform Triage From The Canonical Detail Page (Priority: P1)
**Goal**: Keep the existing `ViewRun` page as the sole triage surface for retry, cancel, and mark investigated while exposing `Show all operations` as the canonical return path.
**Independent Test**: Open a system operation detail page as a manage-capable platform user and verify header-based retry, cancel, and mark-investigated behavior still works with the same queued-toast, confirmation, and audit expectations, while `Show all operations` and `Go to runbooks` remain available.
### Tests for User Story 2
- [X] T011 [US2] Add failing detail-page triage and return-path assertions in `tests/Feature/System/Spec114/OpsTriageActionsTest.php` for `Retry`, `Cancel`, `Mark investigated`, `Show all operations`, and visible `Operation` copy on `app/Filament/System/Pages/Ops/ViewRun.php`.
### Implementation for User Story 2
- [X] T012 [US2] Keep `app/Filament/System/Pages/Ops/ViewRun.php` as the sole triage owner and ensure header actions expose `Show all operations`, `Go to runbooks`, and the existing manage-only triage behavior through `app/Support/System/SystemOperationRunLinks.php`.
- [X] T013 [US2] Update the visible detail copy and return-link presentation in `resources/views/filament/system/pages/ops/view-run.blade.php` so the page uses `Operation #...` and shows `Show all operations` alongside `Go to runbooks`.
- [X] T014 [US2] Preserve existing retry, cancel, and investigation behavior without new lifecycle or audit changes in `app/Services/SystemConsole/OperationRunTriageService.php`.
- [X] T015 [US2] Run the focused detail-triage and return-path checks in `tests/Feature/System/Spec114/OpsTriageActionsTest.php`.
**Checkpoint**: Detail-page triage ownership is intact and independently verified.
---
## Phase 5: User Story 3 - Preserve View-Only Access Semantics (Priority: P2)
**Goal**: Preserve the existing view/manage capability split so view-only operators can inspect and navigate but not triage.
**Independent Test**: Render the aligned list and detail surfaces for a view-only platform user and verify inspection and navigation remain available while triage actions stay hidden.
### Tests for User Story 3
- [X] T016 [US3] Add failing view-only authorization assertions in `tests/Feature/System/Spec114/OpsTriageActionsTest.php` covering list inspection, visible CTA navigation, and hidden detail-header triage.
### Implementation for User Story 3
- [X] T017 [US3] Keep manage-only detail action visibility intact in `app/Filament/System/Pages/Ops/ViewRun.php` while preserving view access and list CTA visibility in `app/Filament/System/Pages/Ops/Runs.php`, `app/Filament/System/Pages/Ops/Failures.php`, and `app/Filament/System/Pages/Ops/Stuck.php`.
- [X] T018 [US3] Reconfirm platform operations access expectations in `tests/Feature/System/Spec114/OpsFailuresViewTest.php` and `tests/Feature/System/Spec114/OpsStuckViewTest.php` after the surface alignment changes.
- [X] T019 [US3] Run the focused authorization checks in `tests/Feature/System/Spec114/OpsTriageActionsTest.php`, `tests/Feature/System/Spec114/OpsFailuresViewTest.php`, and `tests/Feature/System/Spec114/OpsStuckViewTest.php`.
**Checkpoint**: View-only operators can inspect aligned system surfaces without gaining triage controls.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Final cleanup and verification across all stories.
- [X] T020 Run formatting with `vendor/bin/sail bin pint --dirty --format agent` for `app/Filament/System/Pages/Ops/Runs.php`, `app/Filament/System/Pages/Ops/Failures.php`, `app/Filament/System/Pages/Ops/Stuck.php`, `app/Filament/System/Pages/Ops/ViewRun.php`, `resources/views/filament/system/pages/ops/view-run.blade.php`, `tests/Feature/System/Spec114/OpsTriageActionsTest.php`, and `tests/Feature/Guards/ActionSurfaceContractTest.php`.
- [X] T021 Run the full focused regression pack from `specs/170-system-operations-surface-alignment/quickstart.md` against `tests/Feature/Guards/ActionSurfaceContractTest.php`, `tests/Feature/System/Spec114/OpsTriageActionsTest.php`, `tests/Feature/System/Spec114/OpsFailuresViewTest.php`, and `tests/Feature/System/Spec114/OpsStuckViewTest.php`.
- [X] T022 Validate the final behavior against `specs/170-system-operations-surface-alignment/contracts/system-ops-surface-contract.yaml` and `specs/170-system-operations-surface-alignment/quickstart.md` before handoff.
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies.
- **Foundational (Phase 2)**: Depends on Setup and establishes the shared regression baseline.
- **User Story 1 (Phase 3)**: Depends on Foundational.
- **User Story 2 (Phase 4)**: Depends on Foundational.
- **User Story 3 (Phase 5)**: Depends on User Story 1 and User Story 2 because it validates the final aligned list-plus-detail authorization and navigation behavior.
- **Polish (Phase 6)**: Depends on the desired user stories being complete.
### User Story Dependencies
- **US1**: Independent after Phase 2; it only changes list-surface semantics.
- **US2**: Independent after Phase 2; it preserves existing detail-surface triage ownership.
- **US3**: Validates the final permission split across the aligned list and detail surfaces, so it should run after US1 and US2 settle the interaction model.
### Within Each User Story
- Update story-specific tests first and make them fail for the intended new behavior.
- Apply source changes next.
- Run the smallest focused verification pack before moving on.
---
## Parallel Opportunities
- T006, T007, and T008 can run in parallel because they modify different list-page files.
- US1 and US2 can proceed in parallel after Phase 2 if test-file coordination is handled deliberately.
- Polish verification can start as soon as the last required story is merged locally.
---
## Parallel Example: User Story 1
```bash
Task: "T006 Remove row triage actions in app/Filament/System/Pages/Ops/Runs.php"
Task: "T007 Remove row triage actions in app/Filament/System/Pages/Ops/Failures.php"
Task: "T008 Remove row triage actions in app/Filament/System/Pages/Ops/Stuck.php"
```
---
## Parallel Example: User Story 2
```bash
Task: "T011 Add detail-page triage and return-path assertions in tests/Feature/System/Spec114/OpsTriageActionsTest.php"
Task: "T013 Update visible detail copy in resources/views/filament/system/pages/ops/view-run.blade.php"
```
---
## Parallel Example: User Story 3
```bash
Task: "T016 Add view-only authorization assertions in tests/Feature/System/Spec114/OpsTriageActionsTest.php"
Task: "T018 Reconfirm access expectations in tests/Feature/System/Spec114/OpsFailuresViewTest.php and tests/Feature/System/Spec114/OpsStuckViewTest.php"
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup.
2. Complete Phase 2: Foundational.
3. Complete Phase 3: User Story 1.
4. Validate the row-click-only list behavior with the focused regression pack.
5. Demo the aligned registry surfaces before touching any further hardening.
### Incremental Delivery
1. Finish Setup + Foundational to lock the shared regression baseline.
2. Deliver US1 to normalize the list surfaces.
3. Deliver US2 to prove detail-page triage remains the sole operational owner and preserves the canonical return path.
4. Deliver US3 to confirm the final view/manage capability split.
5. Run Phase 6 polish checks and hand off.
### Parallel Team Strategy
1. One engineer updates the shared regression baseline in Phase 2.
2. After Phase 2:
- Engineer A can take US1 list-page updates.
- Engineer B can take US2 detail-page preservation work.
3. Once US1 and US2 land locally, a final pass can execute US3 authorization hardening and the polish phase.
---
## Notes
- `[P]` tasks touch different files and have no unfinished dependencies.
- Story labels map directly to the three user stories in `spec.md`.
- This feature intentionally avoids route renames, capability changes, schema changes, and new UI exemptions.
- Keep `OperationRunTriageService` narrow unless a behavior-preserving tweak is genuinely required.

View File

@ -0,0 +1,35 @@
# Specification Quality Checklist: Operations Naming Consolidation
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-03-30
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validation pass 1: complete
- This spec intentionally starts after Spec 170 and is limited to residual operator-visible naming drift outside the already-aligned system operations surfaces.

View File

@ -0,0 +1,153 @@
openapi: 3.1.0
info:
title: Operations Naming Consolidation Surface Contract
version: 1.0.0
summary: Visible naming contract for existing OperationRun references outside Spec 170.
paths:
/admin/operations:
get:
operationId: listAdminOperations
summary: Display the canonical admin-plane operations collection.
responses:
'200':
description: Admin operations surface rendered successfully.
'403':
description: Authenticated member lacks the required capability within an established scope.
'404':
description: Wrong plane, missing scope membership, or inaccessible workspace/tenant context.
x-visible-naming:
collectionLabel: Operations
collectionActionLabels:
- Open operations
- View in Operations
forbiddenCollectionLabels:
- Runs
- Open all runs
summaryPluralNoun: operations
unchangedBehavior:
- Existing route helper remains admin.operations.index.
- Existing workspace and tenant entitlement rules remain authoritative.
/admin/operations/{run}:
get:
operationId: viewAdminOperation
summary: Display one existing operation record in the admin plane.
parameters:
- name: run
in: path
required: true
schema:
type: integer
responses:
'200':
description: Admin operation detail rendered successfully.
'403':
description: Authenticated member lacks the required capability within an established scope.
'404':
description: Wrong plane, missing scope membership, or inaccessible operation record.
x-visible-naming:
singularLabel: Operation
actionLabel: Open operation
identifierPattern: Operation #{id}
identifierFieldLabel: Operation ID
forbiddenVisibleLabels:
- View run
- Run #{id}
- Run ID
unchangedBehavior:
- Existing route helper remains admin.operations.view.
- Existing authorization and scope checks remain unchanged.
/system/ops/runs/{run}:
get:
operationId: viewSystemOperation
summary: Display one existing operation record in the system plane when a shared surface targets platform detail.
parameters:
- name: run
in: path
required: true
schema:
type: integer
responses:
'200':
description: System operation detail rendered successfully.
'403':
description: Authenticated platform user lacks the required capability.
'404':
description: Wrong plane or inaccessible operation record.
x-visible-naming:
singularLabel: Operation
actionLabel: Open operation
identifierPattern: Operation #{id}
identifierFieldLabel: Operation ID
forbiddenVisibleLabels:
- View run
- Run #{id}
- Run ID
unchangedBehavior:
- System-plane interaction semantics remain owned by Spec 170.
- Existing route helper remains the system run-detail helper.
/admin/onboarding:
get:
operationId: renderWorkspaceOnboardingOperationReports
summary: Render workspace-context onboarding and verification surfaces that may link to an existing operation.
responses:
'200':
description: Workspace onboarding surface rendered successfully.
'403':
description: Authenticated workspace member lacks the required capability within an established scope.
'404':
description: Wrong plane, missing workspace membership, or inaccessible onboarding context.
x-embedded-surface-rules:
workflowActionsMayRemain:
- Start verification
requiredRecordActionLabel: Open operation
requiredIdentifierLabels:
- Operation ID
- Operation #{id}
forbiddenRecordLabels:
- View run
- Run ID
- Run #{id}
notes:
- Task verbs may remain task-oriented when the UI starts new work rather than naming an existing record.
- Existing onboarding routing and workspace entitlement semantics remain unchanged.
/admin/t/{tenant}:
get:
operationId: renderTenantEmbeddedOperationReports
summary: Render tenant-context embedded verification surfaces that may link to an existing operation.
parameters:
- name: tenant
in: path
required: true
schema:
type: string
responses:
'200':
description: Tenant-context surface rendered successfully.
'403':
description: Authenticated tenant member lacks the required capability.
'404':
description: Wrong plane, missing scope membership, or inaccessible tenant context.
x-embedded-surface-rules:
workflowActionsMayRemain:
- Start verification
requiredRecordActionLabel: Open operation
requiredIdentifierLabels:
- Operation ID
- Operation #{id}
forbiddenRecordLabels:
- View run
- Run ID
- Run #{id}
notes:
- Task verbs may remain task-oriented when the UI starts new work rather than naming an existing record.
x-summary-copy-rules:
appliesTo:
- workspace overviews
- admin monitoring lifecycle helper banners
- tenant recent-operations widgets
- onboarding bootstrap summaries
requiredPluralNoun: operations
forbiddenPluralNoun: runs
notes:
- Existing counts, route targets, and status semantics remain unchanged.
- Collection drill-ins may use panel-appropriate collection wording such as View in Operations.

View File

@ -0,0 +1,131 @@
# Data Model: Operations Naming Consolidation
## Overview
This feature introduces no new persisted entity, no new table, and no new state family. It reuses existing `OperationRun` truth and aligns the derived labels emitted by existing helpers, notifications, Filament surfaces, and Blade partials.
## Entity: OperationRun
- **Type**: Existing persisted model
- **Purpose in this feature**: Canonical record whose visible names, identifiers, and related links must read as `Operation` / `Operations` on covered non-system surfaces.
### Relevant Fields
| Field | Type | Notes |
|-------|------|-------|
| `id` | integer | Rendered as `Operation #<id>` or referenced by `Operation ID` on covered surfaces. |
| `type` | string | Human label continues to come from `OperationCatalog::label(...)`. |
| `workspace_id` | integer nullable | Preserves workspace-context routing and summary copy scope. |
| `tenant_id` | integer nullable | Preserves tenant-context and tenantless routing choices. |
| `status` | string | Used by summary/helper copy but not renamed semantically by this feature. |
| `outcome` | string | Used by summary/helper copy but not renamed semantically by this feature. |
| `summary_counts` | array/json | Existing count payload used by summary banners and helper text; count semantics stay unchanged. |
| `context` | array/json | Holds wizard, verification, and route-selection context already used by embedded surfaces. |
| `created_at` | timestamp | Used for recency copy on widgets and viewers. |
### Relationships
| Relationship | Target | Purpose |
|--------------|--------|---------|
| `workspace` | `Workspace` | Keeps workspace context explicit in related links and summaries. |
| `tenant` | `Tenant` | Keeps tenant context explicit in related links and summaries. |
| `user` | `User` or `PlatformUser` via notification context | Preserves initiator-specific notification routing; unchanged by this feature. |
### Feature-Specific Invariants
- Existing `OperationRun` routes remain the only canonical destinations for operation history.
- Visible operator nouns for covered existing records use `Operation` / `Operations`, while internal implementation names may remain `run`.
- System-plane interaction semantics remain owned by Spec 170 even if a shared label source is reused.
- No new `OperationRun` state transition, audit behavior, or notification timing is introduced.
### State Transitions Used By This Feature
| Transition | Preconditions | Result |
|------------|---------------|--------|
| Render existing record link | Existing `OperationRun` is already available on a covered surface | No state change; visible link/identifier copy changes to `Operation` terminology. |
| Render summary/helper copy | Existing `OperationRun` collection or counts are already available | No state change; plural nouns become `operations` where the subject is existing operation history. |
## Derived Label Source: Shared Operation Link And Reference Emitters
- **Type**: Existing derived helper layer, not persisted
- **Sources**:
- `app/Support/OperationRunLinks.php`
- `app/Support/OpsUx/OperationUxPresenter.php`
- `app/Notifications/OperationRunCompleted.php`
- `app/Notifications/OperationRunQueued.php`
- `app/Support/Navigation/RelatedActionLabelCatalog.php`
- `app/Support/Navigation/RelatedNavigationResolver.php`
- `app/Support/References/ReferenceTypeLabelCatalog.php`
- `app/Support/References/Resolvers/OperationRunReferenceResolver.php`
### Relevant Outputs
| Output | Current Role | Target Rule |
|--------|--------------|-------------|
| Detail action label | Opens a specific existing record | Use `Open operation` or a panel-appropriate equivalent, not `View run`. |
| Collection action label | Opens the operations collection | Use `Operations` wording, such as `Open operations` or `View in Operations`. |
| Identifier label | Displays one record identifier | Use `Operation #<id>` or `Operation ID`, not `Run #<id>` or `Run ID`. |
| Reference class label | Names an `OperationRun` in reference summaries | Use `Operation`, not `Operation run`. |
### Feature-Specific Invariants
- Existing URL generation helpers remain canonical.
- Context badges such as `Tenant context` and `Workspace` stay unchanged.
- Shared emitters must not regress Spec 170 system-surface naming while fixing non-system surfaces.
## Derived Surface: Embedded Operation Report Surface
- **Type**: Existing derived UI surface, not persisted
- **Sources**:
- `app/Filament/Widgets/Tenant/TenantVerificationReport.php`
- `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php`
- `resources/views/filament/components/verification-report-viewer.blade.php`
- `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php`
- `resources/views/filament/modals/onboarding-verification-technical-details.blade.php`
- `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`
### Relevant Fields
| Field | Type | Purpose |
|-------|------|---------|
| `runUrl` | string nullable | Existing operation-detail destination for the rendered record. |
| `run.id` | integer | Visible identifier that becomes `Operation ID` or `Operation #<id>`. |
| `lifecycleNotice` | string nullable | Helper copy that may reference the record collection in plural form. |
| `showStartAction` / workflow CTA | boolean/string | Keeps task verbs such as `Start verification` intact when the UI is about starting new work. |
### Feature-Specific Invariants
- Workflow verbs may remain task-oriented when they describe starting work.
- Any link or identifier for an already-created record uses `Operation` terminology.
- Empty-state copy must not fall back to `verification run` or `Run #...` for the future record on covered surfaces.
## Derived Surface: Operation Summary Copy
- **Type**: Existing derived UI text, not persisted
- **Sources**:
- `app/Support/Workspaces/WorkspaceOverviewBuilder.php`
- `app/Support/Baselines/BaselineCompareSummaryAssessor.php`
- `resources/views/filament/pages/monitoring/operations.blade.php`
- `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php`
- `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`
### Relevant Fields
| Field | Type | Purpose |
|-------|------|---------|
| Count values | integer | Existing counts stay numerically identical while the visible plural noun changes to `operations`. |
| Status helper strings | string | Existing status or lifecycle summaries retain meaning but stop describing historical records as `run(s)`. |
| Drill-in URL | string nullable | Existing navigation to operation collections or detail remains unchanged. |
### Feature-Specific Invariants
- Plural references to historical, failed, active, or stuck `OperationRun` records use `operations` on covered surfaces.
- No new status dimension, badge taxonomy, or presenter layer is introduced.
- Collection drill-ins may still use panel-appropriate wording such as `View in Operations` where they open the operations collection.
## Persistence Impact
- **Schema changes**: None
- **Data migration**: None
- **New indexes**: None
- **Retention impact**: None

View File

@ -0,0 +1,164 @@
# Implementation Plan: Operations Naming Consolidation
**Branch**: `171-operations-naming-consolidation` | **Date**: 2026-03-30 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/171-operations-naming-consolidation/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/171-operations-naming-consolidation/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
Consolidate non-system operator-visible references to existing `OperationRun` records so shared links, notifications, embedded verification/onboarding reports, tenantless admin viewers, and summary/helper copy consistently use `Operations` / `Operation` while preserving existing route targets, capability checks, and operation lifecycle behavior. The implementation stays narrow: no schema, route, capability, or internal class renames; only shared label emitters, representative Filament/Blade surfaces, and focused Pest/Browser coverage are updated.
## Technical Context
**Language/Version**: PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4
**Primary Dependencies**: `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest`
**Storage**: PostgreSQL with existing `operation_runs`, notification payloads, workspace records, and tenant records; no schema changes
**Testing**: Pest unit, feature, Livewire, and browser tests executed through Laravel Sail
**Target Platform**: Laravel web application running in Sail locally and containerized Linux environments for staging/production
**Project Type**: Laravel monolith with admin and system Filament panels plus shared Blade partials
**Performance Goals**: Preserve existing DB-only render paths and current route/navigation behavior; add no remote calls, no new queued work, and no broader query fan-out than the current eager-loaded surfaces
**Constraints**: Spec 170 remains authoritative for `/system/ops/*` interaction semantics; no internal class, route, enum, or persistence renames; workflow verbs such as `Start verification` remain where they describe new work; authorization, notification timing, and `OperationRun` lifecycle behavior stay unchanged
**Scale/Scope**: One naming-only slice spanning shared navigation/presentation helpers, representative admin/tenant/workspace/platform-embedded surfaces, and focused unit/feature/browser regression coverage
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- `PASS` Inventory-first / snapshots-second: the slice does not change inventory, snapshot, backup, or artifact truth.
- `PASS` Read/write separation: no new write path is introduced; existing queued/terminal notifications and viewer actions keep their current behavior and only change visible naming.
- `PASS` Graph contract path: no Microsoft Graph integration path is touched.
- `PASS` Deterministic capabilities: no capability registry or role mapping changes are introduced.
- `PASS` RBAC-UX plane separation: the feature reuses existing admin, tenant, workspace-context, and platform routes without widening access or changing 404/403 semantics.
- `PASS` Workspace and tenant isolation: shared links continue to resolve through the current scoped routes and existing membership/capability checks remain authoritative.
- `PASS` Destructive confirmation standard: no destructive action behavior changes; existing confirmation requirements remain unchanged.
- `PASS` Ops-UX 3-surface feedback: queued and terminal notifications remain the same surfaces; only visible action labels and helper copy change.
- `PASS` Ops lifecycle ownership and summary-count rules: no `OperationRun.status`, `OperationRun.outcome`, or `summary_counts` behavior changes are introduced.
- `PASS` Proportionality / bloat: the feature adds no persistence, abstraction, state family, or cross-domain taxonomy and instead reduces visible drift through existing helpers.
- `PASS` UI naming (`UI-NAMING-001`): this feature directly standardizes operator-facing `OperationRun` nomenclature outside Spec 170.
- `PASS` UI surface taxonomy and inspect model: no new surface type or competing inspect model is introduced; embedded reports and viewers retain their current interaction model.
- `PASS` Filament-native UI (`UI-FIL-001`): the slice stays inside existing Filament actions, widgets, pages, and shared Blade views with no new local UI framework.
- `PASS` Testing truth (`TEST-TRUTH-001`): the plan updates representative rendered-surface tests instead of introducing a blanket string-ban guard that would catch valid uses of `run`.
## Project Structure
### Documentation (this feature)
```text
specs/171-operations-naming-consolidation/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── operation-naming-surface-contract.yaml
└── tasks.md
```
### Source Code (repository root)
```text
app/
├── Filament/
│ ├── Pages/
│ │ ├── Operations/
│ │ │ └── TenantlessOperationRunViewer.php
│ │ └── Workspaces/
│ │ └── ManagedTenantOnboardingWizard.php
│ ├── Resources/
│ │ ├── BackupScheduleResource.php
│ │ ├── BackupSetResource.php
│ │ ├── EvidenceSnapshotResource.php
│ │ └── EvidenceSnapshotResource/
│ │ └── Pages/
│ │ └── ViewEvidenceSnapshot.php
│ └── Widgets/
│ └── Tenant/
│ └── TenantVerificationReport.php
├── Notifications/
│ ├── OperationRunCompleted.php
│ └── OperationRunQueued.php
└── Support/
├── Baselines/
│ └── BaselineCompareSummaryAssessor.php
├── Navigation/
│ ├── RelatedActionLabelCatalog.php
│ └── RelatedNavigationResolver.php
├── OpsUx/
│ └── OperationUxPresenter.php
├── References/
│ ├── ReferenceTypeLabelCatalog.php
│ └── Resolvers/
│ └── OperationRunReferenceResolver.php
├── Workspaces/
│ └── WorkspaceOverviewBuilder.php
└── OperationRunLinks.php
resources/
└── views/
└── filament/
├── components/
│ └── verification-report-viewer.blade.php
├── forms/
│ └── components/
│ └── managed-tenant-onboarding-verification-report.blade.php
├── modals/
│ └── onboarding-verification-technical-details.blade.php
├── pages/
│ └── monitoring/
│ └── operations.blade.php
└── widgets/
└── tenant/
├── recent-operations-summary.blade.php
└── tenant-verification-report.blade.php
tests/
├── Browser/
│ └── OnboardingDraftRefreshTest.php
├── Feature/
│ ├── Filament/
│ │ ├── BackupSetResolvedReferencePresentationTest.php
│ │ └── BaselineCompareSummaryConsistencyTest.php
│ ├── Guards/
│ │ └── ActionSurfaceContractTest.php
│ ├── Monitoring/
│ │ └── OperationLifecycleAggregateVisibilityTest.php
│ ├── Operations/
│ │ └── TenantlessOperationRunViewerTest.php
│ └── OpsUx/
│ └── NotificationViewRunLinkTest.php
└── Unit/
└── Support/
├── References/
│ └── RelatedContextReferenceAdapterTest.php
└── RelatedNavigationResolverTest.php
```
**Structure Decision**: This is a single Laravel application. The implementation stays inside existing shared support classes, existing Filament/Blade surfaces, and the existing Pest/Browser test suite. No new application layer or directory is needed.
**Focused test inventory (authoritative)**: The tree above is representative, not exhaustive. The additional focused coverage for this feature is `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`, `tests/Feature/Onboarding/OnboardingVerificationTest.php`, `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`, `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`, `tests/Feature/Filament/WorkspaceOverviewContentTest.php`, `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, and the Spec 170/shared-helper regression guard `tests/Feature/System/Spec114/OpsTriageActionsTest.php`, `tests/Feature/System/Spec114/OpsFailuresViewTest.php`, and `tests/Feature/System/Spec114/OpsStuckViewTest.php`.
## Complexity Tracking
No constitution waiver is expected. This slice narrows existing visible language drift instead of adding structure.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| None | Not applicable | Not applicable |
## Proportionality Review
- **Current operator problem**: outside the already-aligned system-panel surfaces from Spec 170, the same `OperationRun` record is still presented as `run`, `operation run`, and `operation` depending on the page, widget, notification, or embedded report, which makes historical records feel like different domain objects.
- **Existing structure is insufficient because**: shared label catalogs, navigation resolvers, reference presenters, notifications, and embedded report partials each emit their own visible copy, so one-off local fixes would leave cross-surface drift in place and invite regression.
- **Narrowest correct implementation**: update the existing shared label emitters first, then patch only the representative non-system surfaces that still render local `run` terminology, while leaving routes, classes, enums, and lifecycle behavior intact.
- **Ownership cost created**: low. The feature requires targeted wording updates and representative unit/feature/browser assertions for rendered copy, but adds no new runtime abstraction or persistence burden.
- **Alternative intentionally rejected**: renaming internal PHP classes, route slugs, or operation-type identifiers was rejected because the feature scope is operator-visible naming consistency, not internal refactoring.
- **Release truth**: current-release truth. The naming drift already exists across the shipped workspace, tenant, and embedded operation-related surfaces.
## Post-Design Constitution Re-check
- `PASS` `UI-NAMING-001`: the design centralizes canonical `Operations` / `Operation` nouns in shared emitters and applies them consistently to links, identifiers, and helper copy.
- `PASS` `UI-CONST-001` / `UI-SURF-001` / `UI-HARD-001`: no surface classification, inspect model, or action-hierarchy change is introduced; only operator-visible naming changes.
- `PASS` `OPSURF-001`: verification and onboarding surfaces keep workflow verbs for starting work while naming the resulting historical record as an operation.
- `PASS` `RBAC-UX-001` through `RBAC-UX-005`: existing route, visibility, and confirmation semantics remain unchanged because only labels and helper copy move.
- `PASS` `TEST-TRUTH-001`: design coverage focuses on rendered links, identifiers, and plural summary copy across unit, feature, and browser tests.
- `PASS` `BLOAT-001`: no new persistence, abstraction, state, or taxonomy was introduced during design.
- `PASS` Filament v5 / Livewire v4 guardrails: the plan touches existing Filament and Livewire-compatible components only; no provider registration or asset strategy changes are required.

View File

@ -0,0 +1,119 @@
# Quickstart: Operations Naming Consolidation
## Goal
Align non-system operator-visible references to existing `OperationRun` records so shared links, identifiers, embedded verification/onboarding surfaces, and summary/helper copy use `Operations` / `Operation` consistently without changing routes, capabilities, or operation behavior.
## Prerequisites
1. Start the local stack:
```bash
vendor/bin/sail up -d
```
2. Work on branch `171-operations-naming-consolidation`.
## Implementation Steps
1. Update the shared label emitters first:
- `app/Support/OperationRunLinks.php`
- `app/Support/OpsUx/OperationUxPresenter.php`
- `app/Notifications/OperationRunCompleted.php`
- `app/Notifications/OperationRunQueued.php`
- `app/Support/Navigation/RelatedActionLabelCatalog.php`
- `app/Support/Navigation/RelatedNavigationResolver.php`
- `app/Support/References/ReferenceTypeLabelCatalog.php`
- `app/Support/References/Resolvers/OperationRunReferenceResolver.php`
2. Update representative non-system Filament pages, resources, and widgets that still render local `run` terminology:
- `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
- `app/Filament/Widgets/Tenant/TenantVerificationReport.php`
- `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`
- `app/Filament/Resources/BackupScheduleResource.php`
- `app/Filament/Resources/BackupSetResource.php`
- `app/Filament/Resources/EvidenceSnapshotResource.php`
- `app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php`
- `app/Support/Baselines/BaselineCompareSummaryAssessor.php`
- `app/Support/Workspaces/WorkspaceOverviewBuilder.php`
3. Update the covered Blade views and embedded report partials:
- `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php`
- `resources/views/filament/components/verification-report-viewer.blade.php`
- `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php`
- `resources/views/filament/modals/onboarding-verification-technical-details.blade.php`
- `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php`
- `resources/views/filament/pages/monitoring/operations.blade.php`
4. Keep Spec 170 system pages unchanged unless a shared-helper copy update requires a small regression assertion adjustment; do not re-open their interaction model in this slice.
5. Run a final search for residual `View run`, `Run ID`, `Run #`, and plural `runs` collection wording such as `Open all runs`, and leave only valid internal or explicitly out-of-scope matches.
## Tests To Update
1. Unit coverage for shared navigation and reference emitters:
- `tests/Unit/Support/RelatedNavigationResolverTest.php`
- `tests/Unit/Support/References/RelatedContextReferenceAdapterTest.php`
2. Feature coverage for shared notifications, representative viewers, verification surfaces, and summary widgets:
- `tests/Feature/OpsUx/NotificationViewRunLinkTest.php`
- `tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
- `tests/Feature/Filament/BackupSetResolvedReferencePresentationTest.php`
- `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`
- `tests/Feature/Onboarding/OnboardingVerificationTest.php`
- `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`
- `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`
- `tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php`
- `tests/Feature/Filament/WorkspaceOverviewContentTest.php`
- `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`
- `tests/Feature/Monitoring/OperationLifecycleAggregateVisibilityTest.php`
- `tests/Feature/Guards/ActionSurfaceContractTest.php`
3. Browser coverage for onboarding/bootstrap summary wording when the embedded report changes:
- `tests/Browser/OnboardingDraftRefreshTest.php`
4. System-surface regression coverage whenever shared emitters or cross-panel operation labels change:
- `tests/Feature/System/Spec114/OpsTriageActionsTest.php`
- `tests/Feature/System/Spec114/OpsFailuresViewTest.php`
- `tests/Feature/System/Spec114/OpsStuckViewTest.php`
## Focused Verification
Run the smallest relevant regression set first:
```bash
vendor/bin/sail artisan test --compact tests/Unit/Support/RelatedNavigationResolverTest.php
vendor/bin/sail artisan test --compact tests/Unit/Support/References/RelatedContextReferenceAdapterTest.php
vendor/bin/sail artisan test --compact tests/Feature/OpsUx/NotificationViewRunLinkTest.php
vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php
vendor/bin/sail artisan test --compact tests/Feature/Operations/TenantlessOperationRunViewerTest.php
vendor/bin/sail artisan test --compact tests/Feature/Filament/BackupSetResolvedReferencePresentationTest.php
vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantVerificationReportWidgetTest.php
vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingVerificationTest.php
vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingVerificationClustersTest.php
vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php
vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php
vendor/bin/sail artisan test --compact tests/Feature/Filament/WorkspaceOverviewContentTest.php
vendor/bin/sail artisan test --compact tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php
vendor/bin/sail artisan test --compact tests/Feature/Monitoring/OperationLifecycleAggregateVisibilityTest.php
vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsTriageActionsTest.php
vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsFailuresViewTest.php
vendor/bin/sail artisan test --compact tests/Feature/System/Spec114/OpsStuckViewTest.php
```
If onboarding/bootstrap copy changes are touched, add:
```bash
vendor/bin/sail artisan test --compact tests/Browser/OnboardingDraftRefreshTest.php
```
## Formatting
After code changes, run:
```bash
vendor/bin/sail bin pint --dirty --format agent
```
## Manual Review Checklist
1. Covered links to existing records use the canonical operation action label for that surface, including `Open operation` where the contract fixes it, instead of `View run`.
2. Covered identifiers say `Operation #...` or `Operation ID` instead of `Run #...` or `Run ID`.
3. Verification and onboarding surfaces still use task verbs such as `Start verification` when starting new work.
4. Covered summary/helper copy uses plural `operations` when referring to existing historical records.
5. The tenantless admin operation viewer uses `Operation` terminology without changing its route target.
6. Spec 170 system pages still keep their existing aligned `Operations` / `Operation` semantics and interaction model.
7. No route slug, class name, enum value, capability, or persistence artifact was renamed.

View File

@ -0,0 +1,43 @@
# Research: Operations Naming Consolidation
## Decision 1: Update shared label emitters first, then patch remaining local copy
- **Decision**: Treat `OperationRunLinks`, `OperationUxPresenter`, `OperationRunCompleted`, `OperationRunQueued`, `RelatedActionLabelCatalog`, `RelatedNavigationResolver`, `ReferenceTypeLabelCatalog`, and `OperationRunReferenceResolver` as the primary change points for deep-link labels and identifiers.
- **Rationale**: Repo analysis shows that these helpers feed multiple notifications, related-navigation affordances, reference summaries, and embedded viewers. Updating them first removes the largest source of naming drift without inventing a new naming layer.
- **Alternatives considered**: Patch every visible `View run` and `Run #...` string independently. Rejected because shared helpers would continue emitting drift into newly touched surfaces.
## Decision 2: Preserve internal `run` implementation names and route helpers
- **Decision**: Keep existing class names, route names, action names such as `view_run`, route slugs, and enum values intact, while changing only operator-visible labels, titles, and helper copy.
- **Rationale**: The spec explicitly forbids internal renames, and a naming-only slice should not widen into compatibility or refactor work.
- **Alternatives considered**: Rename internal helpers and route slugs to `operation`. Rejected because it would create unnecessary migration and regression risk outside the current release truth.
## Decision 3: Keep Spec 170 system-plane behavior out of scope while protecting against shared-helper regressions
- **Decision**: Leave `/system/ops/*` interaction semantics under Spec 170 ownership and only accept indirect system-plane copy changes if a shared helper already used by both slices requires it.
- **Rationale**: Spec 170 already aligned visible `Operations` / `Operation` naming and interaction semantics on the dedicated system pages. Spec 171 is about residual drift elsewhere.
- **Alternatives considered**: Re-open the system pages inside Spec 171. Rejected because it would overlap active scope and blur ownership between the two specs.
## Decision 4: Preserve workflow verbs where they describe starting work, not an existing record
- **Decision**: Keep verbs such as `Start verification` and bootstrap-progress wording when they describe initiating work, but require `Open operation`, `Operation ID`, and `Operation #...` when the UI references an already-created `OperationRun` record.
- **Rationale**: Verification and onboarding surfaces currently mix task language with record language. The operator needs both concepts, but they must not collapse into the same ambiguous `run` noun.
- **Alternatives considered**: Replace every visible use of `run` with `operation`, including workflow verbs. Rejected because the spec explicitly allows task-oriented verbs when they describe initiating new work.
## Decision 5: Update plural summary copy in place instead of introducing a presenter or taxonomy
- **Decision**: Adjust existing builders and Blade views such as `WorkspaceOverviewBuilder`, `BaselineCompareSummaryAssessor`, `ManagedTenantOnboardingWizard`, `resources/views/filament/pages/monitoring/operations.blade.php`, and tenant summary widgets directly.
- **Rationale**: The slice is about copy consistency, not about inventing a new cross-domain explanation layer. Direct updates satisfy `ABSTR-001` and `LAYER-001`.
- **Alternatives considered**: Introduce a reusable naming presenter or a pluralization catalog just for operation copy. Rejected because one concrete noun family does not justify a new abstraction.
## Decision 6: Use representative rendered-surface tests instead of a global string-ban guard
- **Decision**: Update focused unit, feature, Livewire, and browser tests that already render the affected links, summaries, and embedded reports.
- **Rationale**: Valid uses of `run` still exist internally and in workflow-verb contexts. The constitution prefers business-truth assertions over thin indirection or over-broad grep-style guards.
- **Alternatives considered**: Add an architecture test that forbids `View run`, `Run ID`, or `Run #` repo-wide. Rejected because it would either fail on valid contexts or require an exception list that encodes the same drift the slice is meant to simplify.
## Decision 7: Record the route and embedded-surface naming expectations in an OpenAPI-style contract
- **Decision**: Add `contracts/operation-naming-surface-contract.yaml` to capture existing route targets, identifier-label rules, embedded verification expectations, and plural-summary guidance.
- **Rationale**: The planning workflow expects a concrete contract artifact, and this feature benefits from a machine-readable record of which route targets and visible labels are considered canonical without inventing a new JSON API.
- **Alternatives considered**: Skip `contracts/` because no API route changes are introduced. Rejected because the route/UI contract is still central to the implementation and review workflow.

View File

@ -0,0 +1,182 @@
# Feature Specification: Operations Naming Consolidation
**Feature Branch**: `171-operations-naming-consolidation`
**Created**: 2026-03-30
**Status**: Draft
**Input**: User description: "Operations Naming Consolidation"
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace + tenant + canonical-view + platform
- **Primary Routes**:
- `/admin/operations`
- `/admin/operations/{run}`
- `/admin/onboarding`
- `/admin/t/{tenant}`
- `/system/ops/runs/{run}`
- **Data Ownership**:
- No new platform-owned, workspace-owned, or tenant-owned records are introduced
- Existing `OperationRun` records remain the only source of truth for operation history, status, and deep-link destinations
- Existing notifications, related-navigation payloads, reference summaries, and embedded report components remain derived presentation layers only
- **RBAC**:
- No new capability, role, or authorization plane is introduced
- Existing admin, tenant, and platform route guards remain authoritative for whether an operator may open a given operation destination
- This feature changes visible operator vocabulary only; it does not widen access to any operation route or action
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
| Surface | Surface Type | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Shared operation links and related navigation | Embedded related-navigation affordance | Explicit safe link to one existing operation | n/a | inline or footer placement only | none | panel-appropriate operations list | panel-appropriate operation detail | current panel and current scope remain legible | Operations / Operation | destination label, operation identity, scope cue | none |
| Tenantless/admin operation detail viewers | Detail-first Operational Surface | Dedicated detail page | forbidden | detail header or related-links group only | detail header only | `/admin/operations` or `/system/ops/runs` | panel-appropriate operation detail route | workspace or platform context explicit | Operations / Operation | operation identity, outcome, context, next navigation | none |
| Verification and onboarding operation report surfaces | Embedded operator detail panel | One primary CTA to the existing operation when present | forbidden | low-emphasis advanced links only when justified | none | panel-appropriate operations list when needed | panel-appropriate operation detail route | tenant/workspace context and verification context visible | Operations / Operation | verification status, operation identity, recency | none |
| Summary and health widgets that reference operations | Embedded status summary | Explicit single CTA or passive summary with no competing link | forbidden | card footer or summary action only | none | panel-appropriate operations list | panel-appropriate operation detail route when singular | current scope stays explicit | Operations / Operation | counts, status family, scope | none |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
| Surface | Primary Persona | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|
| Shared operation links and related navigation | Workspace, tenant, or platform operator | Embedded related-navigation affordance | Which operation does this link open? | explicit `Operation` noun, stable verb, scope cue when needed | raw route names or internal type strings stay hidden | navigation context only | none | `Open operation` or route-equivalent collection navigation | none |
| Tenantless/admin operation detail viewers | Workspace or platform operator | Detail-first Operational Surface | What happened on this operation? | operation title, `Operation #<id>`, outcome, scope, next steps | extended payloads and traces stay in diagnostics sections | execution outcome, lifecycle, operability | existing detail-owned actions only | existing detail navigation and triage actions | existing detail-owned destructive actions only |
| Verification and onboarding operation report surfaces | Workspace or tenant operator | Embedded operator detail panel | What verification operation ran, and how do I inspect it? | verification state, operation identity, timestamp, one primary link | low-level JSON, hashes, and raw provider details stay behind explicit reveal | verification status, operation recency | none | `Open operation`, workflow-native next-step CTA such as `Start verification` | none |
| Summary and health widgets that reference operations | Workspace, tenant, or platform operator | Embedded status summary | Are there failed, stuck, or recent operations I should inspect? | pluralized operation labels, counts, scope, one clear drill-in | raw IDs and detailed traces stay on destination surfaces | count state, health family, recency | none | collection drill-in or none | none |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: No
- **New persisted entity/table/artifact?**: No
- **New abstraction?**: No
- **New enum/state/reason family?**: No
- **New cross-domain UI framework/taxonomy?**: No
- **Current operator problem**: Outside the system-panel surfaces already aligned by Spec 170, the repo still exposes `View run`, `Run ID`, `Run #...`, and plural `runs` wording across notifications, related links, verification reports, admin viewers, and health widgets, which makes the same `OperationRun` record look like different domain objects depending on where the operator encounters it.
- **Existing structure is insufficient because**: Shared label catalogs, reference resolvers, and embedded components still permit local wording drift, so isolated copy fixes would keep reintroducing inconsistent nouns.
- **Narrowest correct implementation**: Standardize only operator-visible naming for existing `OperationRun` record references outside Spec 170, reuse the existing routes and destinations, and update the shared label-producing layers that feed multiple surfaces.
- **Ownership cost**: Existing feature and guard tests will need wording updates, and a small number of embedded report components will need representative coverage so naming drift does not return.
- **Alternative intentionally rejected**: Renaming PHP classes, route slugs, enum values, or persistence structures was rejected because this slice is about operator-visible language, not internal refactoring.
- **Release truth**: Current-release truth. The inconsistent labels already exist in the repo today across tenant, workspace, canonical-view, and platform surfaces.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Shared Operation Links Use One Vocabulary (Priority: P1)
As an operator, I want every link to an existing operation record to use the same noun and verb pattern, so that I immediately recognize it as the same destination regardless of which page, widget, or notification I came from.
**Why this priority**: Shared deep-link labels are the highest-leverage root cause because they feed many surfaces at once.
**Independent Test**: Can be fully tested by rendering representative resources, widgets, shared related-navigation payloads, and notifications that link to existing operation records, then asserting that visible labels use `Open operation` and `Operation #<id>` or `Operation ID` rather than `View run`, `Run #`, or `Run ID`.
**Acceptance Scenarios**:
1. **Given** a resource, widget, or notification exposes a link to an existing operation, **When** the operator sees that affordance, **Then** the visible action uses `Operation` terminology rather than `run` terminology.
2. **Given** a shared reference or related-navigation summary exposes an operation identifier, **When** it renders, **Then** the visible identifier uses `Operation #<id>` or `Operation ID` rather than `Run #<id>` or `Run ID`.
---
### User Story 2 - Verification Surfaces Distinguish Workflow Verbs From Operation Records (Priority: P1)
As a workspace or tenant operator, I want verification and onboarding surfaces to keep workflow verbs like `Start verification` while naming the resulting historical record as an operation, so that the current task and the inspectable record are not collapsed into one ambiguous `run` concept.
**Why this priority**: Verification widgets, modals, and onboarding reports currently mix workflow language and operation-record language most visibly.
**Independent Test**: Can be fully tested by rendering representative verification and onboarding report surfaces and asserting that workflow actions keep their intended verbs while record links and identifiers use `Operation` terminology.
**Acceptance Scenarios**:
1. **Given** a verification surface offers the operator a next step, **When** the action starts new work, **Then** it may continue to use a task verb such as `Start verification`.
2. **Given** a verification surface shows a historical or in-progress operation record, **When** the operator inspects the metadata or opens the destination, **Then** the visible link and identifier use `Operation` terminology rather than `run` terminology.
3. **Given** no verification-backed operation exists yet, **When** the empty state renders, **Then** the explanation avoids using `run` as a fallback noun for the future record.
---
### User Story 3 - Summary Surfaces Use Consistent Operation Plurals (Priority: P2)
As an operator scanning health and summary surfaces, I want counts and helper text to refer to failed, stuck, or recent operations consistently, so that summary cards and widgets reinforce the same vocabulary as detailed destinations.
**Why this priority**: Summary copy is lower-risk than deep-link labels, but it is high-frequency and shapes daily operator language.
**Independent Test**: Can be fully tested by rendering representative summary widgets or health indicators and asserting that plural copy uses `operations` instead of `runs` when referring to existing `OperationRun` records.
**Acceptance Scenarios**:
1. **Given** a summary widget describes failed or stuck execution records, **When** it renders, **Then** it uses `operations` rather than `runs` as the visible plural noun.
2. **Given** a platform or tenant health indicator refers to recent operation history, **When** it renders, **Then** the summary reinforces the same `Operations / Operation` vocabulary used by links and details.
### Edge Cases
- Workflow verbs that describe starting new work, such as `Start verification`, remain valid when they describe the task being initiated rather than an already-created `OperationRun` record.
- Existing internal route slugs, PHP class names, enum values, and table names may continue to use `run` terminology; this slice only governs operator-visible language.
- Shared helper layers must not regress Spec 170 system-surface naming while this slice updates non-system surfaces.
- A surface may link to either workspace/admin or platform detail, but the visible noun must still describe the destination record as an `Operation`.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature introduces no new execution path, no new outbound work, and no new long-running process. It only standardizes visible operator-facing language for existing `OperationRun` references.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature adds no new structure, persistence, abstraction, or state family. It reduces naming drift in the existing presentation layer.
**Constitution alignment (UI-NAMING-001):** Operator-visible references to `OperationRun` records outside Spec 170 MUST resolve to the canonical visible nouns `Operations` and `Operation`, while internal implementation names may remain stable.
**Constitution alignment (OPSURF-001):** Shared links, embedded reports, and summary widgets must describe existing operation records with the same vocabulary as the canonical destinations they open.
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / UI-HARD-001 / UI-REVIEW-001):** This slice does not introduce new operator surfaces, but it must keep primary navigation labels, identifiers, and summary nouns consistent across existing surfaces so operators do not learn multiple names for the same domain object.
**Constitution alignment (UI-FIL-001):** Existing Filament actions, widgets, notifications, and embedded components remain the delivery mechanism. No new local UI framework or styling system is introduced.
**Constitution alignment (TEST-TRUTH-001):** Representative coverage must assert visible operator language on shared links and representative components so naming regressions fail in CI instead of relying on manual review.
### Functional Requirements
- **FR-171-001**: Operator-visible references to existing `OperationRun` records outside Spec 170 MUST use `Operations` as the canonical visible collection noun and `Operation` as the canonical visible singular noun.
- **FR-171-002**: Operator-visible navigation actions that open a specific existing operation record MUST use `Open operation` or a panel-appropriate equivalent rather than `View run`.
- **FR-171-003**: Operator-visible identifier labels for existing operation records MUST use `Operation #<id>` and `Operation ID` rather than `Run #<id>` and `Run ID`.
- **FR-171-004**: Verification and onboarding surfaces MAY keep workflow verbs such as `Start verification`, but any visible link, identifier, or helper copy that refers to the resulting historical record MUST use `operation` terminology rather than `run` terminology.
- **FR-171-005**: Summary or helper copy that refers to multiple existing `OperationRun` records MUST use `operations` rather than `runs` when the subject is operation history, failures, or stuck work.
- **FR-171-006**: The implementation MUST update shared label-producing layers, including catalogs, resolvers, reference presenters, or notification builders, wherever those layers are the source of visible naming drift across multiple surfaces.
- **FR-171-007**: This feature MUST NOT rename internal PHP class names, route slugs, enum values, persistence artifacts, or operation type identifiers.
- **FR-171-008**: Existing route destinations, authorization checks, and operation lifecycle semantics MUST remain unchanged.
- **FR-171-009**: Representative automated coverage MUST verify that covered non-system operator surfaces no longer render `View run`, `Run ID`, or `Run #` when referring to an existing `OperationRun` record.
## UI Action Matrix *(mandatory when Filament is changed)*
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|---|---|---|---|---|---|---|---|---|---|---|
| Shared related links and references | shared presenters, catalogs, and resolvers used across resources and widgets | n/a | explicit `Open operation` link when present | existing safe link only | n/a | n/a | n/a | n/a | no new audit behavior | Naming-only slice; no new action family |
| Tenantless/admin operation viewers | `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` and shared reference summaries | existing viewer actions remain | n/a | existing related links remain | n/a | n/a | existing header actions remain | n/a | no new audit behavior | Title, secondary labels, and related links use `Operation` |
| Verification and onboarding report surfaces | tenant widgets and onboarding report/modals under `resources/views/filament/**` and their backing widgets/pages | existing workflow actions remain | n/a | one existing safe operation link when present | n/a | existing workflow CTA remains on owning surface | n/a | n/a | no new audit behavior | Workflow verbs stay task-oriented; record links and IDs use `Operation` |
| Summary and health widgets | representative tenant/platform widgets that describe operation history | existing page/widget actions remain | n/a | single existing drill-in or passive summary | n/a | existing surface-specific CTA behavior remains | n/a | n/a | no new audit behavior | Summary nouns use `operations` rather than `runs` |
### Key Entities *(include if feature involves data)*
- **Shared operation label source**: Existing catalogs, resolvers, presenters, or notifications that emit operator-facing operation labels.
- **Embedded operation report surface**: Existing widget, modal, or Blade component that exposes one operation record inside a broader workflow.
- **Operation summary copy**: Existing pluralized helper or status text that describes multiple `OperationRun` records.
## Success Criteria *(mandatory)*
- **SC-171-001**: Representative automated coverage verifies that shared operation links on covered non-system surfaces use `Operation` terminology rather than `run` terminology.
- **SC-171-002**: Representative automated coverage verifies that covered verification/onboarding surfaces use `Operation ID` or `Operation #<id>` rather than `Run ID` or `Run #<id>` for existing operation records.
- **SC-171-003**: Representative automated coverage verifies that covered summary widgets use plural `operations` wording rather than plural `runs` wording when referring to existing `OperationRun` records.
- **SC-171-004**: The feature ships without adding any new route, capability, persistence artifact, or internal rename requirement.
## Assumptions
- Spec 170 already owns visible Operations / Operation naming for the system-panel operations surfaces.
- Panel-appropriate detail routes remain the correct canonical destinations for existing operation records.
- Not every use of the English verb `run` is wrong; this slice only governs operator-visible references to `OperationRun` records.
## Non-Goals
- Renaming PHP classes, routes, tables, or enum values that contain `run`
- Reworking the action-surface behavior of deferred dashboard or onboarding surfaces beyond visible naming alignment
- Changing operation lifecycle semantics, queued-run feedback, or notifications beyond their visible labels
- Revisiting operation type taxonomy, `OperationCatalog` strategy, or provider-domain naming beyond the visible operator copy needed for this slice
## Dependencies
- Spec 170 system-surface naming alignment
- Existing operation detail routes in admin and system panels
- Existing shared label catalogs, related-navigation resolvers, reference presenters, notifications, widgets, and verification report components
## Definition of Done
Spec 171 is complete when representative non-system operator surfaces that refer to existing `OperationRun` records use `Operations / Operation` as the canonical visible noun family, shared deep-link labels and identifiers no longer say `View run`, `Run ID`, or `Run #`, verification surfaces keep task verbs distinct from record nouns, and automated coverage protects the updated naming without requiring route or persistence refactors.

View File

@ -0,0 +1,203 @@
# Tasks: Operations Naming Consolidation
**Input**: Design documents from `/specs/171-operations-naming-consolidation/`
**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md, contracts/operation-naming-surface-contract.yaml
**Tests**: Runtime behavior changes in this repo require Pest coverage. Each story below includes focused regression work before implementation is considered complete.
**Organization**: Tasks are grouped by user story so each story can be implemented and validated independently after the shared naming emitters are stabilized.
## Phase 1: Setup
**Purpose**: Lock the implementation targets to the generated plan artifacts before runtime edits begin.
- [X] T001 Reconfirm the implementation and verification targets in `specs/171-operations-naming-consolidation/contracts/operation-naming-surface-contract.yaml` and `specs/171-operations-naming-consolidation/quickstart.md` before editing runtime files.
- [X] T002 Inspect the current naming-drift touchpoints in `app/Support/OperationRunLinks.php`, `app/Support/OpsUx/OperationUxPresenter.php`, `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`, and `resources/views/filament/pages/monitoring/operations.blade.php` so every shared/root copy source is mapped before editing.
---
## Phase 2: Foundational
**Purpose**: Establish the shared regression baseline and canonical label emitters that all story work depends on.
**⚠️ CRITICAL**: No user story work should be considered complete until these shared expectations and emitters are updated.
- [X] T003 Update the shared terminology baseline in `tests/Unit/Support/RelatedNavigationResolverTest.php`, `tests/Unit/Support/References/RelatedContextReferenceAdapterTest.php`, `tests/Feature/OpsUx/NotificationViewRunLinkTest.php`, and `tests/Feature/Guards/ActionSurfaceContractTest.php` so canonical operation action labels and identifiers are enforced according to `specs/171-operations-naming-consolidation/contracts/operation-naming-surface-contract.yaml` before surface-specific edits begin.
- [X] T004 [P] Implement canonical link and action labels in `app/Support/OperationRunLinks.php`, `app/Support/Navigation/RelatedActionLabelCatalog.php`, and `app/Support/Navigation/RelatedNavigationResolver.php` so shared deep-link affordances stop emitting `View run` wording.
- [X] T005 [P] Implement canonical identifier, reference, and notification copy in `app/Support/References/ReferenceTypeLabelCatalog.php`, `app/Support/References/Resolvers/OperationRunReferenceResolver.php`, `app/Support/OpsUx/OperationUxPresenter.php`, `app/Notifications/OperationRunCompleted.php`, and `app/Notifications/OperationRunQueued.php` so shared emitters consistently describe existing records as operations.
- [X] T006 Run the shared-helper regression pack for `tests/Unit/Support/RelatedNavigationResolverTest.php`, `tests/Unit/Support/References/RelatedContextReferenceAdapterTest.php`, `tests/Feature/OpsUx/NotificationViewRunLinkTest.php`, and `tests/Feature/Guards/ActionSurfaceContractTest.php` before moving on to story-specific surfaces.
**Checkpoint**: Shared naming emitters and regression guards are stable; user story surfaces can now be updated independently.
---
## Phase 3: User Story 1 - Shared Operation Links Use One Vocabulary (Priority: P1) 🎯 MVP
**Goal**: Make representative non-system viewers, resources, and related record affordances present existing `OperationRun` records with one canonical `Operation` vocabulary.
**Independent Test**: Render the tenantless viewer and representative backup/evidence reference surfaces and verify visible labels use canonical operation action labels and identifiers, including `Open operation` where the contract fixes it, rather than `View run`, `Run #`, or `Run ID`.
### Tests for User Story 1
- [X] T007 [US1] Add failing viewer and related-record assertions in `tests/Feature/Operations/TenantlessOperationRunViewerTest.php` and `tests/Feature/Filament/BackupSetResolvedReferencePresentationTest.php` for canonical operation action labels and `Operation #...` or `Operation ID` copy.
### Implementation for User Story 1
- [X] T008 [P] [US1] Update canonical viewer wording in `app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` and `app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php` so existing record titles and helper text use `Operation` terminology.
- [X] T009 [P] [US1] Update related operation reference copy in `app/Filament/Resources/BackupScheduleResource.php`, `app/Filament/Resources/BackupSetResource.php`, and `app/Filament/Resources/EvidenceSnapshotResource.php` so embedded resource affordances use canonical operation labels and identifiers.
- [X] T010 [US1] Run the focused US1 regression pack for `tests/Feature/Operations/TenantlessOperationRunViewerTest.php` and `tests/Feature/Filament/BackupSetResolvedReferencePresentationTest.php`.
**Checkpoint**: Shared links and representative non-system record viewers use one canonical operation vocabulary.
---
## Phase 4: User Story 2 - Verification Surfaces Distinguish Workflow Verbs From Operation Records (Priority: P1)
**Goal**: Keep workflow verbs such as `Start verification` for new work while making verification and onboarding report surfaces name the resulting persisted record as an operation.
**Independent Test**: Render the tenant verification widget and representative onboarding verification surfaces, then verify workflow CTAs keep task verbs while persisted record links, identifiers, and empty states use `Operation` terminology.
### Tests for User Story 2
- [X] T011 [US2] Add failing verification and onboarding wording assertions in `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`, `tests/Feature/Onboarding/OnboardingVerificationTest.php`, `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`, `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`, and `tests/Browser/OnboardingDraftRefreshTest.php` so task verbs remain intact while historical records use `Operation` wording.
### Implementation for User Story 2
- [X] T012 [P] [US2] Update verification widget wording in `app/Filament/Widgets/Tenant/TenantVerificationReport.php` and `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php` so `Start verification` stays task-oriented while record links and empty-state copy use `Operation` terminology.
- [X] T013 [P] [US2] Update embedded verification report copy in `resources/views/filament/components/verification-report-viewer.blade.php`, `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php`, and `resources/views/filament/modals/onboarding-verification-technical-details.blade.php` so identifiers and drill-ins use `Operation` wording.
- [X] T014 [US2] Update onboarding flow wording in `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php` so workflow verbs remain task-based while persisted record references and helper copy use `Operation` or `Operations` consistently.
- [X] T015 [US2] Run the focused US2 regression pack for `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`, `tests/Feature/Onboarding/OnboardingVerificationTest.php`, `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`, `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`, and `tests/Browser/OnboardingDraftRefreshTest.php`.
**Checkpoint**: Verification and onboarding surfaces distinguish new work from existing operation records without ambiguous `run` terminology.
---
## Phase 5: User Story 3 - Summary Surfaces Use Consistent Operation Plurals (Priority: P2)
**Goal**: Make summary and health surfaces describe failed, stuck, active, or recent historical records as operations so high-frequency helper copy matches the canonical destinations.
**Independent Test**: Render representative baseline, monitoring, workspace, and tenant summary surfaces and verify plural helper text uses `operations` rather than `runs` when describing existing `OperationRun` history.
### Tests for User Story 3
- [X] T016 [US3] Add failing plural-summary assertions in `tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php`, `tests/Feature/Monitoring/OperationLifecycleAggregateVisibilityTest.php`, `tests/Feature/Filament/WorkspaceOverviewContentTest.php`, and `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php` so summary nouns must use `operations` for historical records.
### Implementation for User Story 3
- [X] T017 [P] [US3] Update summary helper copy in `app/Support/Baselines/BaselineCompareSummaryAssessor.php` and `app/Support/Workspaces/WorkspaceOverviewBuilder.php` so historical record counts and guidance are described as operations.
- [X] T018 [P] [US3] Update rendered summary copy in `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php` and `resources/views/filament/pages/monitoring/operations.blade.php` so plural nouns and drill-in labels align with `Operations` terminology.
- [X] T019 [US3] Run the focused US3 regression pack for `tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php`, `tests/Feature/Monitoring/OperationLifecycleAggregateVisibilityTest.php`, `tests/Feature/Filament/WorkspaceOverviewContentTest.php`, and `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`.
**Checkpoint**: Summary and health copy reinforces the same `Operations / Operation` vocabulary as the linked detail surfaces.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Final cleanup and verification across all stories.
- [X] T020 Run a residual copy sweep in `app/`, `resources/views/`, and `tests/` for `View run`, `Run ID`, `Run #`, and plural `runs` collection wording such as `Open all runs`, then confirm any remaining matches are internal, workflow-verb, or explicitly out of scope under `specs/171-operations-naming-consolidation/spec.md`.
- [X] T021 Run formatting with `vendor/bin/sail bin pint --dirty --format agent` for the `app/`, `resources/views/`, and `tests/` files touched by `specs/171-operations-naming-consolidation/quickstart.md`.
- [X] T022 Run the consolidated focused verification pack from `specs/171-operations-naming-consolidation/quickstart.md`, including `tests/Unit/Support/RelatedNavigationResolverTest.php`, `tests/Unit/Support/References/RelatedContextReferenceAdapterTest.php`, `tests/Feature/OpsUx/NotificationViewRunLinkTest.php`, `tests/Feature/Guards/ActionSurfaceContractTest.php`, `tests/Feature/Operations/TenantlessOperationRunViewerTest.php`, `tests/Feature/Filament/BackupSetResolvedReferencePresentationTest.php`, `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`, `tests/Feature/Onboarding/OnboardingVerificationTest.php`, `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`, `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`, `tests/Feature/Filament/BaselineCompareSummaryConsistencyTest.php`, `tests/Feature/Filament/WorkspaceOverviewContentTest.php`, `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `tests/Feature/Monitoring/OperationLifecycleAggregateVisibilityTest.php`, and `tests/Browser/OnboardingDraftRefreshTest.php` when onboarding copy changed.
- [X] T023 Run the explicit Spec 170/shared-helper regression guard for `tests/Feature/System/Spec114/OpsTriageActionsTest.php`, `tests/Feature/System/Spec114/OpsFailuresViewTest.php`, and `tests/Feature/System/Spec114/OpsStuckViewTest.php` so shared naming changes do not regress aligned `/system/ops/*` surfaces.
- [X] T024 Validate the final behavior against `specs/171-operations-naming-consolidation/contracts/operation-naming-surface-contract.yaml` and `specs/171-operations-naming-consolidation/quickstart.md` before handoff.
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies.
- **Foundational (Phase 2)**: Depends on Setup and establishes the shared regression baseline plus shared naming emitters.
- **User Story 1 (Phase 3)**: Depends on Foundational.
- **User Story 2 (Phase 4)**: Depends on Foundational.
- **User Story 3 (Phase 5)**: Depends on Foundational and should follow the P1 stories in delivery order, but it does not require new behavior from US1 or US2 beyond the shared emitters completed in Phase 2.
- **Polish (Phase 6)**: Depends on the desired user stories being complete.
### User Story Dependencies
- **US1**: Independent after Phase 2; it only changes representative viewer and related-record surfaces.
- **US2**: Independent after Phase 2; it only changes verification and onboarding surfaces while reusing the shared emitters.
- **US3**: Independent after Phase 2; it only changes summary/helper copy and related coverage.
### Within Each User Story
- Update story-specific tests first and make them fail for the intended behavior.
- Apply the runtime changes next.
- Run the smallest focused verification pack before moving on.
---
## Parallel Opportunities
- T004 and T005 can run in parallel because they modify different shared file groups after T003 lands.
- T008 and T009 can run in parallel because they touch different representative viewer/resource files inside US1.
- T012 and T013 can run in parallel because they split verification-widget work from embedded-report Blade partials inside US2.
- T017 and T018 can run in parallel because they split summary builders from rendered summary views inside US3.
---
## Parallel Example: Foundational
```bash
Task: "T004 Implement canonical link and action labels in app/Support/OperationRunLinks.php, app/Support/Navigation/RelatedActionLabelCatalog.php, and app/Support/Navigation/RelatedNavigationResolver.php"
Task: "T005 Implement canonical identifier, reference, and notification copy in app/Support/References/ReferenceTypeLabelCatalog.php, app/Support/References/Resolvers/OperationRunReferenceResolver.php, app/Support/OpsUx/OperationUxPresenter.php, app/Notifications/OperationRunCompleted.php, and app/Notifications/OperationRunQueued.php"
```
---
## Parallel Example: User Story 2
```bash
Task: "T012 Update verification widget wording in app/Filament/Widgets/Tenant/TenantVerificationReport.php and resources/views/filament/widgets/tenant/tenant-verification-report.blade.php"
Task: "T013 Update embedded verification report copy in resources/views/filament/components/verification-report-viewer.blade.php, resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php, and resources/views/filament/modals/onboarding-verification-technical-details.blade.php"
```
---
## Parallel Example: User Story 3
```bash
Task: "T017 Update summary helper copy in app/Support/Baselines/BaselineCompareSummaryAssessor.php and app/Support/Workspaces/WorkspaceOverviewBuilder.php"
Task: "T018 Update rendered summary copy in resources/views/filament/widgets/tenant/recent-operations-summary.blade.php and resources/views/filament/pages/monitoring/operations.blade.php"
```
---
## Implementation Strategy
### MVP First (Foundational + User Story 1)
1. Complete Phase 1: Setup.
2. Complete Phase 2: Foundational.
3. Complete Phase 3: User Story 1.
4. Validate the shared emitters plus representative viewer/resource surfaces before expanding into verification and summary copy.
### Incremental Delivery
1. Finish Setup + Foundational to lock the shared naming baseline.
2. Deliver US1 to normalize representative viewer and related-record language.
3. Deliver US2 to separate workflow verbs from persisted operation-record nouns on verification/onboarding surfaces.
4. Deliver US3 to align high-frequency summary and helper copy with the same vocabulary.
5. Run Phase 6 polish checks and hand off.
### Parallel Team Strategy
1. One engineer updates the shared test baseline in T003.
2. After T003:
- Engineer A can take T004.
- Engineer B can take T005.
3. After Phase 2:
- Engineer A can take US1.
- Engineer B can take US2.
- Engineer C can prepare US3 if the team wants to batch the P2 copy follow-up.
---
## Notes
- `[P]` tasks touch different files and have no unfinished dependencies.
- Story labels map directly to the three user stories in `spec.md`.
- This feature intentionally avoids route renames, capability changes, provider registration changes, global-search changes, and persistence changes.
- Spec 170 remains authoritative for aligned `/system/ops/*` surfaces; only shared-helper regressions may require incidental attention during implementation.

View File

@ -0,0 +1,35 @@
# Specification Quality Checklist: Deferred Operator Surfaces Retrofit
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-03-30
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validation pass 1: complete
- This spec intentionally targets only deferred non-table surfaces that already expose operation affordances; unrelated deferred pages remain explicit non-goals unless a later spec enrolls them.

View File

@ -0,0 +1,180 @@
# Feature Specification: Deferred Operator Surfaces Retrofit
**Feature Branch**: `172-deferred-operator-surfaces-retrofit`
**Created**: 2026-03-30
**Status**: Draft
**Input**: User description: "Deferred Operator Surfaces Retrofit"
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace + tenant + canonical-view
- **Primary Routes**:
- `/admin/t/{tenant}`
- `/admin/operations`
- `/admin/operations/{run}`
- managed-tenant onboarding flow routes that expose verification operation reports
- **Data Ownership**:
- No new platform-owned, workspace-owned, or tenant-owned records are introduced
- Existing `OperationRun` records remain the only source of truth for operation status, deep-link destinations, and verification history
- Tenant dashboard widgets, onboarding report components, and related embedded operation affordances remain derived presentation layers only
- **RBAC**:
- No new capability family is introduced
- Existing tenant, workspace, and admin access rules remain authoritative for every destination that a retrofitted surface may open
- Retrofitted surfaces must not imply broader scope than the operator can actually inspect
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
| Surface | Surface Type | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Tenant dashboard operation cards and widgets | Embedded status summary / drill-in surface | Explicit CTA to a tenant-scoped operations destination | forbidden | card footer or secondary widget action only | none | tenant-scoped operations destination in admin panel | panel-appropriate operation detail when singular | current tenant remains explicit before navigation | Operations / Operation | active or recent operation truth, tenant context, next destination | retrofit existing deferred surface |
| Tenant verification report widget | Embedded operator detail panel | One primary inspect CTA to the existing operation when present | forbidden | advanced admin/monitoring link only if justified and clearly secondary | none | panel-appropriate operations collection when needed | panel-appropriate operation detail route | tenant context and current verification state explicit | Operations / Operation | verification state, operation identity, recency | retrofit existing deferred surface |
| Managed-tenant onboarding verification report and technical-details surfaces | Guided workflow sub-surface | One primary inspect CTA to the existing operation when present, otherwise one workflow-next-step CTA | forbidden | low-emphasis advanced links only when justified | none | panel-appropriate operations collection when needed | panel-appropriate operation detail route | workspace, tenant, and verification context explicit | Operations / Operation | verification status, operation identity, stale-state explanation | retrofit existing deferred surface |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
| Surface | Primary Persona | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|
| Tenant dashboard operation cards and widgets | Tenant operator | Embedded status summary / drill-in surface | What is happening in this tenant, and where do I inspect it? | tenant-scoped count or recent activity, explicit destination scope, one clear CTA | raw operation payloads and extended traces stay on destination surfaces | recency, active-state, failure/stuck summary | none | tenant-scoped operations drill-in | none |
| Tenant verification report widget | Tenant operator | Embedded operator detail panel | What verification operation ran for this tenant, and how do I inspect it? | verification result, one primary operation link, operation identity, timestamp | raw provider diagnostics stay behind explicit reveal or destination detail | verification outcome, recency, stale-state | none | `Open operation` or current-step CTA | none |
| Managed-tenant onboarding verification report and technical details | Workspace operator running onboarding | Guided workflow sub-surface | What verification operation supports this onboarding step, and what should I do next? | workflow state, operation identity when present, one primary CTA, scope cue | low-level payloads, hashes, and verbose traces stay in diagnostics sections or canonical detail | workflow status, verification outcome, stale-state | none | `Open operation` or `Start verification` | none |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: No
- **New persisted entity/table/artifact?**: No
- **New abstraction?**: No
- **New enum/state/reason family?**: No
- **New cross-domain UI framework/taxonomy?**: No
- **Current operator problem**: Dashboard widgets, tenant verification widgets, and onboarding verification components still behave like deferred or exempt surfaces even though they expose meaningful operation drill-ins, which leaves CTA count, scope cues, and deep-link behavior underdefined compared with the now-aligned table and detail surfaces.
- **Existing structure is insufficient because**: Spec 169 intentionally left these embedded surfaces out of the table-centric action-surface enforcement path, so the repo still permits tenant-context leaks, competing links, and scope-ambiguous operation affordances on high-traffic summary surfaces.
- **Narrowest correct implementation**: Retrofit only the deferred non-table surfaces that already expose operation affordances, give them explicit operator contracts and representative coverage, and keep unrelated deferred pages out of scope.
- **Ownership cost**: Existing dashboard and onboarding tests will need to assert CTA count, destination scope, and advanced-link visibility, and the exemption baseline or equivalent governance notes must be narrowed for the retrofitted surfaces.
- **Alternative intentionally rejected**: Broadly enrolling every deferred dashboard, chooser, landing page, or page-class route into the main action-surface validator was rejected because this slice only needs to retrofit operation-bearing embedded surfaces, not redesign every deferred surface family.
- **Release truth**: Current-release truth. The tenant dashboard and onboarding verification flows already expose operation links today, and current audits show scope and affordance drift on those surfaces.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Tenant Dashboard Drill-Ins Preserve Tenant Context (Priority: P1)
As a tenant operator, I want dashboard operation cards and widgets to send me to a destination that clearly preserves my current tenant context, so that I am not silently dropped into a broader workspace-wide operations surface.
**Why this priority**: Tenant dashboard drill-ins are a frequent entry point and currently carry the clearest cross-scope surprise risk.
**Independent Test**: Can be fully tested by rendering the relevant tenant dashboard operation affordances and asserting that their visible destination semantics remain tenant-scoped and do not silently link to an unfiltered workspace-wide operations surface.
**Acceptance Scenarios**:
1. **Given** a tenant operator on the tenant dashboard, **When** the operator opens a dashboard operation drill-in, **Then** the destination preserves tenant context or makes the broader scope explicit before navigation.
2. **Given** a tenant dashboard widget summarizes recent or active operations, **When** it renders, **Then** it exposes at most one primary operations drill-in rather than multiple competing operation links.
---
### User Story 2 - Onboarding And Verification Surfaces Expose One Clear Operation Path (Priority: P1)
As a workspace or tenant operator, I want verification widgets, onboarding report blocks, and technical-detail overlays to show one clear primary action for the relevant operation record, so that I know exactly where to inspect execution truth without sorting through competing links.
**Why this priority**: These surfaces are currently exempt yet already act like operator-facing execution summaries.
**Independent Test**: Can be fully tested by rendering representative tenant verification and onboarding verification surfaces in empty, in-progress, and completed states and asserting that each state has one primary CTA aligned to the operator's next step.
**Acceptance Scenarios**:
1. **Given** a verification-backed operation exists, **When** the report surface renders, **Then** exactly one primary `Open operation` affordance is visible for that record.
2. **Given** no verification-backed operation exists yet, **When** the owning verification or onboarding surface renders, **Then** the operator sees one clear next-step CTA such as `Start verification` and no competing inline operation links.
3. **Given** an advanced monitoring destination is still useful for some operators, **When** it is shown, **Then** it is explicitly secondary and does not compete with the primary inspect path.
---
### User Story 3 - Retrofitted Deferred Surfaces Gain Explicit Governance (Priority: P2)
As a reviewer, I want the deferred surfaces that already expose operations to stop relying on blanket exemptions, so that future regressions in CTA count, scope signals, or deep-link behavior are caught automatically.
**Why this priority**: Without explicit governance, the retrofitted surfaces will remain drift-prone even after a one-time UX fix.
**Independent Test**: Can be fully tested by proving that representative tenant dashboard and onboarding verification surfaces are covered by dedicated tests or governance checks, while unrelated deferred surfaces remain explicit non-goals.
**Acceptance Scenarios**:
1. **Given** the retrofit is complete, **When** representative tests or governance checks run, **Then** tenant dashboard and onboarding verification operation affordances are covered explicitly rather than relying on a blanket deferred exemption.
2. **Given** unrelated deferred surfaces such as chooser pages or landing pages remain untouched, **When** the retrofit ships, **Then** they remain explicit non-goals rather than being swept in accidentally.
### Edge Cases
- A tenant verification surface may need to show no current operation, an in-progress operation, or a stale completed operation; each state still needs one primary next-step affordance.
- Some operators may have access to the tenant-local surface but not to an advanced admin monitoring destination; advanced links must respect destination access.
- A retrofitted surface may expose a collection drill-in or a single-operation drill-in depending on context, but the scope must remain explicit either way.
- Unrelated deferred surfaces such as `ChooseTenant`, `ChooseWorkspace`, `ManagedTenantsLanding`, and the Monitoring Alerts page-class route remain out of scope unless they later gain operation affordances that justify a dedicated spec.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature introduces no new execution path, no new long-running work, and no new persistence. It only retrofits existing embedded operator surfaces that already summarize or link to `OperationRun` records.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature adds no new structure, persistence, abstraction, or state family. It narrows an existing deferred UX gap on current-release surfaces.
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / UI-HARD-001 / UI-REVIEW-001):** Even though these are embedded or guided-flow surfaces rather than table pages, they must still expose one clear primary inspect or next-step model, keep scope truthful, and avoid competing actions.
**Constitution alignment (OPSURF-001):** Dashboard and onboarding verification surfaces must be operator-first summary surfaces: the default-visible content should answer what happened, what scope it affected, and where the operator should go next without exposing low-level diagnostics by default.
**Constitution alignment (UI-FIL-001):** Existing Filament pages, widgets, and embedded components remain the implementation shape. No local mini-framework for embedded operation actions is introduced.
**Constitution alignment (UI-NAMING-001):** The retrofitted surfaces use the canonical visible nouns `Operations` and `Operation`, consistent with Specs 170 and 171.
**Constitution alignment (TEST-TRUTH-001):** Representative automated coverage must assert CTA count, scope cues, and visibility of advanced links on the retrofitted surfaces.
### Functional Requirements
- **FR-172-001**: Tenant dashboard operation-bearing widgets or cards MUST expose navigation that preserves tenant context or makes any broader scope explicit before the operator leaves the tenant dashboard.
- **FR-172-002**: Retrofitted embedded surfaces that reference one existing operation record MUST expose exactly one primary inspect affordance for that record.
- **FR-172-003**: Retrofitted embedded surfaces in a no-history or no-operation state MUST expose exactly one primary next-step CTA on the owning surface and MUST NOT render competing inline operation links.
- **FR-172-004**: Any retained advanced admin or monitoring destination link MUST be clearly secondary, explicitly labeled for its scope or audience, and visible only when the operator can access that destination.
- **FR-172-005**: Retrofitted tenant or workspace surfaces MUST keep tenant, workspace, or admin scope explicit in their visible copy or destination semantics before navigation occurs.
- **FR-172-006**: Governance artifacts, exemption handling, or dedicated tests MUST stop treating the retrofitted operation-bearing parts of `TenantDashboard` and `ManagedTenantOnboardingWizard` as fully out of scope.
- **FR-172-007**: Unrelated deferred surfaces that do not expose operation affordances today, including `ChooseTenant`, `ChooseWorkspace`, `ManagedTenantsLanding`, and the Monitoring Alerts page-class route, MUST remain explicit non-goals for this slice.
- **FR-172-008**: Existing operation destinations, authorization rules, and lifecycle semantics MUST remain unchanged.
- **FR-172-009**: Representative automated coverage MUST verify CTA count, scope-truthful navigation, and advanced-link visibility on the tenant dashboard and onboarding verification surfaces affected by this slice.
- **FR-172-010**: This feature MUST NOT introduce a new dashboard page, a new onboarding flow, or a new operations capability.
## UI Action Matrix *(mandatory when Filament is changed)*
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|---|---|---|---|---|---|---|---|---|---|---|
| Tenant dashboard operation cards/widgets | `app/Filament/Pages/TenantDashboard.php` and its operation-bearing widgets | existing dashboard/header actions remain | n/a | one explicit operations drill-in per card/widget maximum | n/a | existing surface-specific next-step CTA remains singular | n/a | n/a | no new audit behavior | Retrofit current deferred widget/card surfaces with scope-truthful operations drill-ins |
| Tenant verification report widget | tenant verification widget and embedded report view | existing widget actions remain | n/a | one primary `Open operation` link when a record exists | n/a | owning surface keeps one workflow CTA when no operation exists | n/a | n/a | no new audit behavior | Advanced admin/monitoring link may remain only as secondary |
| Managed-tenant onboarding verification report and technical details | `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php` and related onboarding report/modal views | existing workflow actions remain | n/a | one primary `Open operation` link when a record exists | n/a | `Start verification` or equivalent next-step CTA remains singular when no operation exists | n/a | n/a | no new audit behavior | Guided-flow retrofit; no new page or route family |
### Key Entities *(include if feature involves data)*
- **Deferred operation-bearing embedded surface**: Existing dashboard card, widget, report block, or modal that is not a table page but still exposes operation truth or navigation.
- **Primary inspect affordance**: The one visible link or CTA that opens the canonical operation destination for the current embedded context.
- **Advanced destination link**: A clearly secondary operation-related link that exposes a broader monitoring destination only for operators who can access it.
## Success Criteria *(mandatory)*
- **SC-172-001**: Representative automated coverage verifies that tenant dashboard operation drill-ins preserve tenant context or make any broader scope explicit before navigation.
- **SC-172-002**: Representative automated coverage verifies that covered verification and onboarding surfaces expose exactly one primary CTA per state.
- **SC-172-003**: Representative automated coverage verifies that any retained advanced monitoring/admin link is secondary and access-aware.
- **SC-172-004**: The feature ships without adding a new dashboard page, onboarding flow, operations capability, or persistence artifact.
## Assumptions
- Specs 170 and 171 establish the canonical visible noun family `Operations / Operation` for aligned and residual naming surfaces.
- Existing admin operation list and detail destinations remain the correct canonical inspect targets for embedded tenant/workspace surfaces.
- Retrofitting embedded operation-bearing surfaces is sufficient for this slice; not every currently exempt page needs to be enrolled.
## Non-Goals
- Building a new workspace home or redesigning the tenant dashboard as a whole
- Reworking onboarding flow mechanics or verification execution semantics
- Enrolling chooser pages, `ManagedTenantsLanding`, or the Monitoring Alerts page-class route into this retrofit when they do not currently expose operation affordances that need the same treatment
- Introducing a broad action-surface framework for all widgets and embedded components beyond the explicit retrofits in this slice
## Dependencies
- Spec 169 deferred-surface exemption baseline
- Spec 170 system operations surface alignment
- Spec 171 operations naming consolidation
- Existing tenant dashboard widgets, verification report widgets, onboarding verification report components, and canonical admin operation destinations
## Definition of Done
Spec 172 is complete when the deferred non-table surfaces that already expose operations, especially tenant dashboard operation drill-ins and onboarding/verification report surfaces, provide one clear primary CTA per state, preserve or explicitly announce scope before navigation, keep any advanced monitoring/admin links secondary and access-aware, and are protected by explicit governance or representative tests instead of relying on a blanket deferred exemption.

View File

@ -286,7 +286,7 @@
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]) ->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Bootstrap (optional)') ->assertSee('Bootstrap (optional)')
->assertSee('Bootstrap is running across 1 operation run(s).'); ->assertSee('Bootstrap is running across 1 operation(s).');
$bootstrapRun->forceFill([ $bootstrapRun->forceFill([
'status' => OperationRunStatus::Completed->value, 'status' => OperationRunStatus::Completed->value,
@ -297,5 +297,5 @@
$page $page
->wait(7) ->wait(7)
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertSee('Bootstrap completed across 1 operation run(s).'); ->assertSee('Bootstrap completed across 1 operation(s).');
}); });

View File

@ -46,7 +46,7 @@ public function test_renders_canonical_detail_for_a_workspace_member_when_tenant
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk() ->assertOk()
->assertSee('Operation run') ->assertSee(\App\Support\OperationRunLinks::identifier($run))
->assertSee('Policy sync') ->assertSee('Policy sync')
->assertSee('Counts') ->assertSee('Counts')
->assertSee('Context') ->assertSee('Context')
@ -72,7 +72,7 @@ public function test_renders_canonical_detail_gracefully_when_tenant_id_is_null(
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk() ->assertOk()
->assertSee('No target scope details were recorded for this run.'); ->assertSee('No target scope details were recorded for this operation.');
} }
public function test_returns_404_on_canonical_detail_for_non_members(): void public function test_returns_404_on_canonical_detail_for_non_members(): void

View File

@ -27,7 +27,8 @@ public function test_hides_operations_kpi_stats_when_tenant_context_is_absent():
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index')) ->get(route('admin.operations.index'))
->assertOk() ->assertOk()
->assertDontSee('Total Runs (30 days)') ->assertDontSee('Total Operations (30 days)')
->assertDontSee('Active Operations')
->assertDontSee('Failed/Partial (7 days)') ->assertDontSee('Failed/Partial (7 days)')
->assertDontSee('Avg Duration (7 days)'); ->assertDontSee('Avg Duration (7 days)');
} }

View File

@ -90,7 +90,7 @@ public function test_does_not_show_legacy_admin_details_cta_and_keeps_canonical_
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index')) ->get(route('admin.operations.index'))
->assertOk() ->assertOk()
->assertSee('View run'); ->assertSee('Open operation');
} }
public function test_hides_tenant_scoped_follow_up_links_when_the_run_tenant_is_not_selector_eligible(): void public function test_hides_tenant_scoped_follow_up_links_when_the_run_tenant_is_not_selector_eligible(): void

View File

@ -51,7 +51,7 @@ public function test_trusts_canonical_run_links_opened_from_a_tenant_surface_aft
->assertOk() ->assertOk()
->assertSee('Back to tenant') ->assertSee('Back to tenant')
->assertSee(route('filament.admin.resources.tenants.view', ['record' => $runTenant]), false) ->assertSee(route('filament.admin.resources.tenants.view', ['record' => $runTenant]), false)
->assertSee('Current tenant context differs from this run'); ->assertSee('Current tenant context differs from this operation');
} }
public function test_trusts_notification_style_run_links_with_no_selected_tenant_context(): void public function test_trusts_notification_style_run_links_with_no_selected_tenant_context(): void
@ -71,7 +71,7 @@ public function test_trusts_notification_style_run_links_with_no_selected_tenant
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(OperationRunLinks::tenantlessView($run)) ->get(OperationRunLinks::tenantlessView($run))
->assertOk() ->assertOk()
->assertSee('Operation run') ->assertSee(OperationRunLinks::identifier($run))
->assertSee('Canonical workspace view'); ->assertSee('Canonical workspace view');
} }

View File

@ -61,6 +61,5 @@
->assertSee('Operations') ->assertSee('Operations')
->assertSee('No backup metadata was recorded for this backup set.') ->assertSee('No backup metadata was recorded for this backup set.')
->assertSee('Metadata keys') ->assertSee('Metadata keys')
->assertDontSee('Related record') ->assertDontSee('Related record');
->assertDontSee('View run');
}); });

View File

@ -33,6 +33,6 @@
$this->get(BackupSetResource::getUrl('index', tenant: $tenant)) $this->get(BackupSetResource::getUrl('index', tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('View run') ->assertSee('Open operation')
->assertSee('/admin/operations/'.$run->getKey(), false); ->assertSee('/admin/operations/'.$run->getKey(), false);
}); });

View File

@ -6,6 +6,7 @@
use App\Models\BackupSet; use App\Models\BackupSet;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Support\OperationCatalog; use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
it('renders backup set related context with the source operation label and id', function (): void { it('renders backup set related context with the source operation label and id', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
@ -26,5 +27,7 @@
$this->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant)) $this->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant))
->assertOk() ->assertOk()
->assertSee(OperationCatalog::label((string) $run->type)) ->assertSee(OperationCatalog::label((string) $run->type))
->assertSee('Run #'.$run->getKey()); ->assertSee(OperationRunLinks::identifier($run))
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Run #'.$run->getKey());
}); });

View File

@ -147,7 +147,7 @@ function createBaselineCompareWidgetTenant(): array
Livewire::test(BaselineCompareNow::class) Livewire::test(BaselineCompareNow::class)
->assertSee('Action required') ->assertSee('Action required')
->assertSee('The latest baseline compare failed before it produced a usable result.') ->assertSee('The latest baseline compare failed before it produced a usable result.')
->assertSee('Review the failed run') ->assertSee('Review the failed operation')
->assertDontSee('Aligned'); ->assertDontSee('Aligned');
}); });
@ -174,7 +174,7 @@ function createBaselineCompareWidgetTenant(): array
Livewire::test(BaselineCompareNow::class) Livewire::test(BaselineCompareNow::class)
->assertSee('In progress') ->assertSee('In progress')
->assertSee('Baseline compare is in progress.') ->assertSee('Baseline compare is in progress.')
->assertSee('View run') ->assertSee('Open operation')
->assertDontSee('Aligned'); ->assertDontSee('Aligned');
}); });

View File

@ -175,14 +175,14 @@ function createBaselineCompareSummaryConsistencyTenant(): array
Livewire::test(BaselineCompareNow::class) Livewire::test(BaselineCompareNow::class)
->assertSee('In progress') ->assertSee('In progress')
->assertSee('Baseline compare is in progress.') ->assertSee('Baseline compare is in progress.')
->assertSee('View run'); ->assertSee('Open operation');
Livewire::test(BaselineCompareLanding::class) Livewire::test(BaselineCompareLanding::class)
->assertSee('In progress') ->assertSee('In progress')
->assertSee('Baseline compare is in progress.') ->assertSee('Baseline compare is in progress.')
->assertSee('View run'); ->assertSee('Open operation');
Livewire::test(BaselineCompareCoverageBanner::class) Livewire::test(BaselineCompareCoverageBanner::class)
->assertSee('Baseline compare is in progress.') ->assertSee('Baseline compare is in progress.')
->assertSee('View run'); ->assertSee('Open operation');
}); });

View File

@ -61,5 +61,5 @@
->assertSee('Windows Lockdown') ->assertSee('Windows Lockdown')
->assertSee('Version 3') ->assertSee('Version 3')
->assertSee('Baseline compare') ->assertSee('Baseline compare')
->assertSee('Run #'.$run->getKey()); ->assertSee('Operation #'.$run->getKey());
}); });

View File

@ -173,11 +173,11 @@ function visibleLivewireText(Testable $component): string
->test(TenantlessOperationRunViewer::class, ['run' => $run]) ->test(TenantlessOperationRunViewer::class, ['run' => $run])
->assertSee('Decision') ->assertSee('Decision')
->assertSee('Artifact truth details') ->assertSee('Artifact truth details')
->assertSee('The run finished without a usable artifact result.'); ->assertSee('The operation finished without a usable artifact result.');
$pageText = visibleLivewireText($component); $pageText = visibleLivewireText($component);
expect(mb_substr_count($pageText, 'The run finished without a usable artifact result.'))->toBe(1) expect(mb_substr_count($pageText, 'The operation finished without a usable artifact result.'))->toBe(1)
->and(mb_strpos($pageText, 'Decision'))->toBeLessThan(mb_strpos($pageText, 'Artifact truth details')); ->and(mb_strpos($pageText, 'Decision'))->toBeLessThan(mb_strpos($pageText, 'Artifact truth details'));
}); });

View File

@ -218,13 +218,13 @@ function baselineCompareGapContext(array $overrides = []): array
->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk() ->assertOk()
->assertSee('Current tenant context differs from this run') ->assertSee('Current tenant context differs from this operation')
->assertSee('Decision') ->assertSee('Decision')
->assertSee('Related context'); ->assertSee('Related context');
$pageText = visiblePageText($response); $pageText = visiblePageText($response);
$bannerPosition = mb_strpos($pageText, 'Current tenant context differs from this run'); $bannerPosition = mb_strpos($pageText, 'Current tenant context differs from this operation');
$decisionPosition = mb_strpos($pageText, 'Decision'); $decisionPosition = mb_strpos($pageText, 'Decision');
expect($bannerPosition)->not->toBeFalse() expect($bannerPosition)->not->toBeFalse()
@ -259,7 +259,7 @@ function baselineCompareGapContext(array $overrides = []): array
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk() ->assertOk()
->assertSee('No target scope details were recorded for this run.') ->assertSee('No target scope details were recorded for this operation.')
->assertSee('Verification report') ->assertSee('Verification report')
->assertSee('Verification report unavailable') ->assertSee('Verification report unavailable')
->assertDontSee('Counts'); ->assertDontSee('Counts');

View File

@ -23,7 +23,8 @@
->test(RecentOperationsSummary::class, ['record' => $tenant]) ->test(RecentOperationsSummary::class, ['record' => $tenant])
->assertSee('Recent operations') ->assertSee('Recent operations')
->assertSee('Provider connection check') ->assertSee('Provider connection check')
->assertSee('Run finished') ->assertSee('Operation finished')
->assertSee('Open operation')
->assertSee('No action needed.') ->assertSee('No action needed.')
->assertDontSee('No operations yet.'); ->assertDontSee('No operations yet.');
}); });

View File

@ -43,10 +43,10 @@
[ [
'entries' => [ 'entries' => [
[ [
'label' => 'Run', 'label' => 'Operation',
'reference' => [ 'reference' => [
'primaryLabel' => 'Backup set update', 'primaryLabel' => 'Backup set update',
'secondaryLabel' => 'Run #189', 'secondaryLabel' => 'Operation #189',
'stateLabel' => 'Resolved', 'stateLabel' => 'Resolved',
'stateColor' => 'success', 'stateColor' => 'success',
'stateIcon' => 'heroicon-m-check-circle', 'stateIcon' => 'heroicon-m-check-circle',
@ -55,7 +55,7 @@
'isLinkable' => true, 'isLinkable' => true,
'linkTarget' => [ 'linkTarget' => [
'url' => '/admin/operations/189', 'url' => '/admin/operations/189',
'actionLabel' => 'Inspect run', 'actionLabel' => 'Open operation',
'contextBadge' => 'Tenant context', 'contextBadge' => 'Tenant context',
], ],
'technicalDetail' => [ 'technicalDetail' => [
@ -82,7 +82,7 @@
); );
$referenceBadgePosition = strpos($html, 'Tenant context'); $referenceBadgePosition = strpos($html, 'Tenant context');
$referenceActionPosition = strpos($html, 'Inspect run'); $referenceActionPosition = strpos($html, 'Open operation');
$fallbackBadgePosition = strpos($html, 'Workspace context'); $fallbackBadgePosition = strpos($html, 'Workspace context');
$fallbackActionPosition = strpos($html, 'Inspect operations'); $fallbackActionPosition = strpos($html, 'Inspect operations');

View File

@ -226,7 +226,7 @@ function spec125CriticalTenantContext(bool $ensureDefaultMicrosoftProviderConnec
expect($table->persistsSearchInSession())->toBeTrue(); expect($table->persistsSearchInSession())->toBeTrue();
expect($table->persistsSortInSession())->toBeTrue(); expect($table->persistsSortInSession())->toBeTrue();
expect($table->persistsFiltersInSession())->toBeTrue(); expect($table->persistsFiltersInSession())->toBeTrue();
expect($table->getEmptyStateHeading())->toBe('No operation runs found'); expect($table->getEmptyStateHeading())->toBe('No operations found');
expect($table->getEmptyStateDescription())->toBe('Queued, running, and completed operations will appear here when work is triggered in this scope.'); expect($table->getEmptyStateDescription())->toBe('Queued, running, and completed operations will appear here when work is triggered in this scope.');
expect($table->getEmptyStateActions())->toBeEmpty(); expect($table->getEmptyStateActions())->toBeEmpty();
expect($table->getColumn('type')?->isSearchable())->toBeTrue(); expect($table->getColumn('type')?->isSearchable())->toBeTrue();

View File

@ -3,9 +3,13 @@
declare(strict_types=1); declare(strict_types=1);
use App\Filament\Pages\TenantDashboard; use App\Filament\Pages\TenantDashboard;
use App\Filament\Widgets\Dashboard\DashboardKpis;
use App\Filament\Widgets\Dashboard\RecentOperations as DashboardRecentOperations;
use App\Models\Finding; use App\Models\Finding;
use App\Models\OperationRun; use App\Models\OperationRun;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Bus;
use Livewire\Livewire;
it('renders the tenant dashboard DB-only (no outbound HTTP, no background work)', function (): void { it('renders the tenant dashboard DB-only (no outbound HTTP, no background work)', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
@ -17,7 +21,7 @@
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
]); ]);
OperationRun::factory()->create([ $operation = OperationRun::factory()->create([
'tenant_id' => $tenant->getKey(), 'tenant_id' => $tenant->getKey(),
'type' => 'inventory_sync', 'type' => 'inventory_sync',
'status' => 'queued', 'status' => 'queued',
@ -28,14 +32,24 @@
$this->actingAs($user); $this->actingAs($user);
Bus::fake(); Bus::fake();
Filament::setTenant($tenant, true);
assertNoOutboundHttp(function () use ($tenant): void { assertNoOutboundHttp(function () use ($operation, $tenant): void {
$this->get(TenantDashboard::getUrl(tenant: $tenant)) $this->get(TenantDashboard::getUrl(tenant: $tenant))
->assertOk() ->assertOk()
->assertSee('/admin/choose-workspace', false); ->assertSee('/admin/choose-workspace', false);
// NeedsAttention, RecentOperations and RecentDriftFindings are // NeedsAttention, RecentOperations and RecentDriftFindings are
// lazy-loaded widgets and will not appear in the initial // lazy-loaded widgets and will not appear in the initial
// server-rendered HTML. // server-rendered HTML.
Livewire::test(DashboardKpis::class)
->assertSee('Active operations')
->assertSee('backup, sync & compare operations');
Livewire::test(DashboardRecentOperations::class)
->assertSee('Operation ID')
->assertSee('Operation #'.$operation->getKey())
->assertSee('Inventory sync');
}); });
Bus::assertNothingDispatched(); Bus::assertNothingDispatched();

View File

@ -59,7 +59,7 @@
->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk() ->assertOk()
->assertSee('Execution state') ->assertSee('Execution state')
->assertSee('Run finished') ->assertSee('Operation finished')
->assertSee('Outcome') ->assertSee('Outcome')
->assertSee('Tenant lifecycle') ->assertSee('Tenant lifecycle')
->assertSee('Onboarding') ->assertSee('Onboarding')

View File

@ -60,8 +60,10 @@
expect($notifications)->not->toBeEmpty(); expect($notifications)->not->toBeEmpty();
$last = $notifications->last(); $last = $notifications->last();
$actionLabels = collect($last['actions'] ?? [])->pluck('label')->filter()->values()->all();
$actionUrls = collect($last['actions'] ?? [])->pluck('url')->filter()->values()->all(); $actionUrls = collect($last['actions'] ?? [])->pluck('url')->filter()->values()->all();
expect($actionLabels)->toContain(OperationRunLinks::openLabel());
expect($actionUrls)->toContain(OperationRunLinks::tenantlessView($run)); expect($actionUrls)->toContain(OperationRunLinks::tenantlessView($run));
Queue::assertPushed(ProviderConnectionHealthCheckJob::class, function (ProviderConnectionHealthCheckJob $job) use ($run, $connection): bool { Queue::assertPushed(ProviderConnectionHealthCheckJob::class, function (ProviderConnectionHealthCheckJob $job) use ($run, $connection): bool {
@ -109,6 +111,7 @@
Livewire::actingAs($user) Livewire::actingAs($user)
->test(TenantVerificationReport::class) ->test(TenantVerificationReport::class)
->assertSee('Provider connection preflight') ->assertSee('Provider connection preflight')
->assertSee(OperationRunLinks::identifierLabel().':')
->assertSee('Read-only:') ->assertSee('Read-only:')
->assertSee('Insufficient permission — ask a tenant Owner.'); ->assertSee('Insufficient permission — ask a tenant Owner.');
}); });
@ -169,7 +172,7 @@
]); ]);
Livewire::test(TenantVerificationReport::class, ['record' => $tenant]) Livewire::test(TenantVerificationReport::class, ['record' => $tenant])
->assertSee('No verification run has been started yet.') ->assertSee('No verification operation has been started yet.')
->call('startVerification'); ->call('startVerification');
$run = OperationRun::query() $run = OperationRun::query()
@ -184,8 +187,10 @@
expect($notifications)->not->toBeEmpty(); expect($notifications)->not->toBeEmpty();
$last = $notifications->last(); $last = $notifications->last();
$actionLabels = collect($last['actions'] ?? [])->pluck('label')->filter()->values()->all();
$actionUrls = collect($last['actions'] ?? [])->pluck('url')->filter()->values()->all(); $actionUrls = collect($last['actions'] ?? [])->pluck('url')->filter()->values()->all();
expect($actionLabels)->toContain(OperationRunLinks::openLabel());
expect($actionUrls)->toContain(OperationRunLinks::tenantlessView($run)); expect($actionUrls)->toContain(OperationRunLinks::tenantlessView($run));
Queue::assertPushed(ProviderConnectionHealthCheckJob::class, 1); Queue::assertPushed(ProviderConnectionHealthCheckJob::class, 1);
@ -200,7 +205,7 @@
Livewire::actingAs($user) Livewire::actingAs($user)
->test(TenantVerificationReport::class, ['record' => $tenant]) ->test(TenantVerificationReport::class, ['record' => $tenant])
->assertSee('No verification run has been started yet.') ->assertSee('No verification operation has been started yet.')
->assertSee('Verification can be started from tenant management only while the tenant is active.') ->assertSee('Verification can be started from tenant management only while the tenant is active.')
->assertDontSee('Start verification'); ->assertDontSee('Start verification');
}); });
@ -247,6 +252,9 @@
->flatMap(static fn (array $notification): array => is_array($notification['actions'] ?? null) ->flatMap(static fn (array $notification): array => is_array($notification['actions'] ?? null)
? $notification['actions'] ? $notification['actions']
: []) : [])
->tap(function (\Illuminate\Support\Collection $actions): void {
expect($actions->pluck('label')->filter()->values()->all())->toContain(OperationRunLinks::openLabel());
})
->pluck('url') ->pluck('url')
->filter(static fn (mixed $url): bool => is_string($url) && trim($url) !== '') ->filter(static fn (mixed $url): bool => is_string($url) && trim($url) !== '')
->values() ->values()

View File

@ -29,5 +29,7 @@
->assertSee('Choose tenant') ->assertSee('Choose tenant')
->assertSee('Open operations') ->assertSee('Open operations')
->assertSee('Open alerts') ->assertSee('Open alerts')
->assertSee('Review current and recent workspace-wide operations.')
->assertSee('Workspace-wide operations that are still queued or in progress.')
->assertSee('Inventory sync'); ->assertSee('Inventory sync');
}); });

View File

@ -94,9 +94,12 @@
use App\Support\Auth\PlatformCapabilities; use App\Support\Auth\PlatformCapabilities;
use App\Support\Evidence\EvidenceCompletenessState; use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus; use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Navigation\RelatedActionLabelCatalog;
use App\Support\OperationRunLinks; use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome; use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus; use App\Support\OperationRunStatus;
use App\Support\References\ReferenceClass;
use App\Support\References\ReferenceTypeLabelCatalog;
use App\Support\System\SystemDirectoryLinks; use App\Support\System\SystemDirectoryLinks;
use App\Support\System\SystemOperationRunLinks; use App\Support\System\SystemOperationRunLinks;
use App\Support\Ui\ActionSurface\ActionSurfaceExemptions; use App\Support\Ui\ActionSurface\ActionSurfaceExemptions;
@ -108,6 +111,7 @@
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use App\Support\WorkspaceIsolation\TenantOwnedModelFamilies; use App\Support\WorkspaceIsolation\TenantOwnedModelFamilies;
use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceContext;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup; use Filament\Actions\ActionGroup;
use Filament\Actions\BulkActionGroup; use Filament\Actions\BulkActionGroup;
use Filament\Facades\Filament; use Filament\Facades\Filament;
@ -1211,6 +1215,15 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
->toBe(route('admin.operations.view', ['run' => (int) $run->getKey()])); ->toBe(route('admin.operations.view', ['run' => (int) $run->getKey()]));
}); });
it('keeps canonical operation labels on shared representative links', function (): void {
$labels = app(RelatedActionLabelCatalog::class);
$typeLabels = new ReferenceTypeLabelCatalog;
expect($labels->entryLabel('source_run'))->toBe('Operation')
->and($labels->actionLabel('source_run'))->toBe('Open operation')
->and($typeLabels->label(ReferenceClass::OperationRun))->toBe('Operation');
});
it('uses clickable rows without a lone View action on the monitoring operations list', function (): void { it('uses clickable rows without a lone View action on the monitoring operations list', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
@ -1436,7 +1449,7 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
->toContain('no-data'); ->toContain('no-data');
}); });
it('uses clickable rows with direct triage actions on the system runs list', function (): void { it('uses clickable rows without row triage on the system runs list', function (): void {
$run = OperationRun::factory()->create([ $run = OperationRun::factory()->create([
'status' => OperationRunStatus::Completed->value, 'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value, 'outcome' => OperationRunOutcome::Failed->value,
@ -1449,21 +1462,25 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
]); ]);
$livewire = Livewire::test(SystemRunsPage::class) $livewire = Livewire::test(SystemRunsPage::class)
->assertCanSeeTableRecords([$run]); ->assertCanSeeTableRecords([$run])
->assertActionVisible('go_to_runbooks')
->assertActionExists('go_to_runbooks', fn (Action $action): bool => $action->getLabel() === 'Go to runbooks' && $action->getUrl() === \App\Filament\System\Pages\Ops\Runbooks::getUrl(panel: 'system'));
$table = $livewire->instance()->getTable(); $table = $livewire->instance()->getTable();
$rowActionNames = collect($table->getActions()) $emptyStateActions = collect($table->getEmptyStateActions())
->map(static fn ($action): ?string => $action->getName()) ->map(static fn ($action): ?string => $action->getName())
->filter() ->filter()
->values() ->values()
->all(); ->all();
expect($rowActionNames)->toEqualCanonicalizing(['retry', 'cancel', 'mark_investigated']) expect($livewire->instance()->getTitle())->toBe('Operations')
->and($table->getActions())->toBeEmpty()
->and($table->getBulkActions())->toBeEmpty() ->and($table->getBulkActions())->toBeEmpty()
->and($emptyStateActions)->toBe(['go_to_runbooks_empty'])
->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run)); ->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run));
}); });
it('uses clickable rows with direct triage actions on the system failures list', function (): void { it('uses clickable rows without row triage on the system failures list', function (): void {
$run = OperationRun::factory()->create([ $run = OperationRun::factory()->create([
'status' => OperationRunStatus::Completed->value, 'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value, 'outcome' => OperationRunOutcome::Failed->value,
@ -1476,21 +1493,25 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
]); ]);
$livewire = Livewire::test(SystemFailuresPage::class) $livewire = Livewire::test(SystemFailuresPage::class)
->assertCanSeeTableRecords([$run]); ->assertCanSeeTableRecords([$run])
->assertActionVisible('show_all_operations')
->assertActionExists('show_all_operations', fn (Action $action): bool => $action->getLabel() === 'Show all operations' && $action->getUrl() === SystemOperationRunLinks::index());
$table = $livewire->instance()->getTable(); $table = $livewire->instance()->getTable();
$rowActionNames = collect($table->getActions()) $emptyStateActions = collect($table->getEmptyStateActions())
->map(static fn ($action): ?string => $action->getName()) ->map(static fn ($action): ?string => $action->getName())
->filter() ->filter()
->values() ->values()
->all(); ->all();
expect($rowActionNames)->toEqualCanonicalizing(['retry', 'cancel', 'mark_investigated']) expect($livewire->instance()->getTitle())->toBe('Failed operations')
->and($table->getActions())->toBeEmpty()
->and($table->getBulkActions())->toBeEmpty() ->and($table->getBulkActions())->toBeEmpty()
->and($emptyStateActions)->toBe(['show_all_operations_empty'])
->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run)); ->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run));
}); });
it('uses clickable rows with direct triage actions on the system stuck list', function (): void { it('uses clickable rows without row triage on the system stuck list', function (): void {
$run = OperationRun::factory()->create([ $run = OperationRun::factory()->create([
'status' => OperationRunStatus::Queued->value, 'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value, 'outcome' => OperationRunOutcome::Pending->value,
@ -1505,17 +1526,21 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
]); ]);
$livewire = Livewire::test(SystemStuckPage::class) $livewire = Livewire::test(SystemStuckPage::class)
->assertCanSeeTableRecords([$run]); ->assertCanSeeTableRecords([$run])
->assertActionVisible('show_all_operations')
->assertActionExists('show_all_operations', fn (Action $action): bool => $action->getLabel() === 'Show all operations' && $action->getUrl() === SystemOperationRunLinks::index());
$table = $livewire->instance()->getTable(); $table = $livewire->instance()->getTable();
$rowActionNames = collect($table->getActions()) $emptyStateActions = collect($table->getEmptyStateActions())
->map(static fn ($action): ?string => $action->getName()) ->map(static fn ($action): ?string => $action->getName())
->filter() ->filter()
->values() ->values()
->all(); ->all();
expect($rowActionNames)->toEqualCanonicalizing(['retry', 'cancel', 'mark_investigated']) expect($livewire->instance()->getTitle())->toBe('Stuck operations')
->and($table->getActions())->toBeEmpty()
->and($table->getBulkActions())->toBeEmpty() ->and($table->getBulkActions())->toBeEmpty()
->and($emptyStateActions)->toBe(['show_all_operations_empty'])
->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run)); ->and($table->getRecordUrl($run))->toBe(SystemOperationRunLinks::view($run));
}); });

View File

@ -87,8 +87,8 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get('/admin/operations') ->get('/admin/operations')
->assertOk() ->assertOk()
->assertSee('active run(s) are beyond their lifecycle window') ->assertSee('active operation(s) are beyond their lifecycle window')
->assertSee('run(s) have already been automatically reconciled'); ->assertSee('operation(s) have already been automatically reconciled');
}); });
it('renders completed operation rows without leaking array-state unknown badges', function (): void { it('renders completed operation rows without leaking array-state unknown badges', function (): void {
@ -110,8 +110,8 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get('/admin/operations') ->get('/admin/operations')
->assertOk() ->assertOk()
->assertSee('Run finished') ->assertSee('Operation finished')
->assertSee('Completed with follow-up') ->assertSee('Completed with follow-up')
->assertDontSee('Run finished Unknown') ->assertDontSee('Operation finished Unknown')
->assertDontSee('Completed with follow-up Unknown'); ->assertDontSee('Completed with follow-up Unknown');
}); });

View File

@ -39,6 +39,6 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get('/admin/operations') ->get('/admin/operations')
->assertOk() ->assertOk()
->assertSee('1 active run(s) are beyond their lifecycle window.') ->assertSee('1 active operation(s) are beyond their lifecycle window.')
->assertSee('1 run(s) have already been automatically reconciled.'); ->assertSee('1 operation(s) have already been automatically reconciled.');
}); });

Some files were not shown because too many files have changed in this diff Show More