feat: consolidate operation naming surfaces

This commit is contained in:
Ahmed Darrazi 2026-03-31 00:44:48 +02:00
parent fdd3a85b64
commit 4eee3cf60c
102 changed files with 1217 additions and 275 deletions

View File

@ -120,6 +120,8 @@ ## Active Technologies
- 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)
@ -139,8 +141,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions
## 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
- 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
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

View File

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

View File

@ -36,6 +36,7 @@
use Filament\Pages\Page;
use Filament\Schemas\Components\EmbeddedSchema;
use Filament\Schemas\Schema;
use Illuminate\Contracts\Support\Htmlable;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Str;
@ -47,7 +48,7 @@ class TenantlessOperationRunViewer extends Page
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';
@ -67,6 +68,11 @@ public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
public OperationRun $run;
public function getTitle(): string|Htmlable
{
return OperationRunLinks::identifier($this->run);
}
/**
* @var array<string, mixed>|null
*/
@ -196,7 +202,7 @@ public function blockedExecutionBanner(): ?array
$operatorExplanation->dominantCauseExplanation,
]))
: ($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 [
@ -226,7 +232,7 @@ public function lifecycleBanner(): ?array
return match ($this->run->freshnessState()->value) {
'likely_stale' => [
'tone' => 'amber',
'title' => 'Likely stale run',
'title' => 'Likely stale operation',
'body' => $detail,
],
'reconciled_failed' => [
@ -253,19 +259,19 @@ public function canonicalContextBanner(): ?array
if (! $runTenant instanceof Tenant) {
return [
'tone' => 'slate',
'title' => 'Workspace-level run',
'title' => 'Workspace-level operation',
'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 any tenant.',
];
}
$messages = ['Run tenant: '.$runTenant->name.'.'];
$messages = ['Operation tenant: '.$runTenant->name.'.'];
$tone = 'sky';
$title = null;
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.'.');
$messages[] = 'This canonical workspace view remains valid without switching tenant context.';
}
@ -273,7 +279,7 @@ public function canonicalContextBanner(): ?array
$referencedTenant = ReferencedTenantLifecyclePresentation::forOperationRun($runTenant);
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';
$messages[] = $selectorAvailabilityMessage;
@ -334,7 +340,7 @@ private function resumeCaptureAction(): Action
if (! isset($this->run)) {
Notification::make()
->title('Run not loaded')
->title('Operation not loaded')
->danger()
->send();
@ -361,7 +367,7 @@ private function resumeCaptureAction(): Action
if (! $run instanceof OperationRun) {
Notification::make()
->title('Cannot resume capture')
->body('Reason: missing operation run')
->body('Reason: missing operation')
->danger()
->send();
@ -369,7 +375,7 @@ private function resumeCaptureAction(): Action
}
$viewAction = Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::tenantlessView($run));
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')
->description('Run a queued verification check (Operation Run).')
->description('Run a queued verification check (Operation).')
->schema([
Section::make('Verification')
->schema([
@ -541,7 +541,7 @@ public function content(Schema $schema): Schema
->color(fn (): string => $this->verificationStatusColor()),
Text::make('Connection updated — re-run verification to refresh results.')
->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()),
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'),
@ -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']);
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 !== []) {
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
@ -2837,11 +2837,11 @@ public function startVerification(): void
Notification::make()
->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()
->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())),
])
->send();
@ -2856,7 +2856,7 @@ public function startVerification(): void
$actions = [
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())),
];
@ -2901,7 +2901,7 @@ public function startVerification(): void
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())),
])
->send();
@ -2912,7 +2912,7 @@ public function startVerification(): void
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result->run->getKey())),
])
->send();
@ -3116,11 +3116,11 @@ public function startBootstrap(array $operationTypes): void
Notification::make()
->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()
->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($this->tenantlessOperationRunUrl((int) $result['run']->getKey())),
])
->send();
@ -3164,7 +3164,7 @@ public function startBootstrap(array $operationTypes): void
if ($runUrl !== null) {
$toast->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
]);
}
@ -3404,7 +3404,7 @@ private function completionSummaryBootstrapDetail(): string
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
@ -3726,7 +3726,7 @@ private function verificationAssistStaleReason(): ?string
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)
->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::view($operationRun, $tenant)),
])
->send();
@ -562,7 +562,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $operationRun->type)
->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::view($operationRun, $tenant)),
])
->send();

View File

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

View File

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

View File

@ -141,7 +141,7 @@ private function captureAction(): Action
}
$viewAction = Action::make('view_run')
->label('View run')
->label('Open operation')
->url(OperationRunLinks::view($run, $sourceTenant));
if (! $run->wasRecentlyCreated && in_array((string) $run->status, ['queued', 'running'], true)) {
@ -286,7 +286,7 @@ private function compareNowAction(): Action
}
$viewAction = Action::make('view_run')
->label('View run')
->label('Open operation')
->url(OperationRunLinks::view($run, $targetTenant));
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)
->actions([
Action::make('view_run')
->label('View Run')
->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)),
])
->send();
@ -105,7 +105,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([
Action::make('view_run')
->label('View Run')
->label('Open operation')
->url(OperationRunLinks::view($opRun, $tenant)),
])
->send();

View File

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

View File

@ -30,7 +30,7 @@ protected function getHeaderActions(): array
{
return [
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->icon('heroicon-o-eye')
->color('gray')
->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);
}),
Tables\Filters\Filter::make('run_ids')
->label('Run IDs')
->label('Operation IDs')
->form([
TextInput::make('baseline_operation_run_id')
->label('Baseline run id')
->label('Baseline operation ID')
->numeric(),
TextInput::make('current_operation_run_id')
->label('Current run id')
->label('Current operation ID')
->numeric(),
])
->query(function (Builder $query, array $data): Builder {

View File

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

View File

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

View File

@ -245,7 +245,7 @@ public static function table(Table $table): Table
])
->actions([])
->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.')
->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));
$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 : []);
$referencedTenantLifecycle = $record->tenant instanceof 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')
->header(new \App\Support\Ui\EnterpriseDetail\SummaryHeaderData(
title: OperationCatalog::label((string) $record->type),
subtitle: 'Run #'.$record->getKey(),
subtitle: OperationRunLinks::identifier($record),
statusBadges: [
$factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->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['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
? $factory->countPresentation(summaryLine: $summaryLine)
: null,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -440,7 +440,7 @@ public static function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([
Actions\Action::make('view_run')
->label('View Run')
->label('Open operation')
->url(OperationRunLinks::view($opRun, $record)),
])
->send();
@ -465,7 +465,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([
Actions\Action::make('view_run')
->label('View Run')
->label('Open operation')
->url(OperationRunLinks::view($opRun, $record)),
])
->send();
@ -513,11 +513,11 @@ public static function table(Table $table): Table
Notification::make()
->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()
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -531,7 +531,7 @@ public static function table(Table $table): Table
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -546,7 +546,7 @@ public static function table(Table $table): Table
$actions = [
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
];
@ -590,7 +590,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -808,7 +808,7 @@ public static function table(Table $table): Table
OperationUxPresenter::queuedToast('tenant.sync')
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url(OperationRunLinks::view($opRun, $tenantContext)),
])
->send();
@ -1817,7 +1817,7 @@ public static function syncRoleDefinitionsAction(): Actions\Action
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -1828,7 +1828,7 @@ public static function syncRoleDefinitionsAction(): Actions\Action
OperationUxPresenter::queuedToast('directory_role_definitions.sync')
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();

View File

@ -119,11 +119,11 @@ protected function getHeaderActions(): array
Notification::make()
->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()
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -137,7 +137,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -152,7 +152,7 @@ protected function getHeaderActions(): array
$actions = [
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
];
@ -196,7 +196,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -245,7 +245,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -264,7 +264,7 @@ protected function getHeaderActions(): array
OperationUxPresenter::queuedToast((string) $opRun->type)
->actions([
Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -83,7 +83,7 @@ public function scanNow(): void
OperationUxPresenter::alreadyQueuedToast((string) $opRun->type)
->actions([
Action::make('view_run')
->label('View run')
->label('Open operation')
->url($runUrl),
])
->send();
@ -105,7 +105,7 @@ public function scanNow(): void
->body('The scan will run in the background. Results appear once complete.')
->actions([
Action::make('view_run')
->label('View run')
->label('Open operation')
->url($runUrl),
])
->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.')
->actions([
Action::make('view_run')
->label('View run')
->label('Open operation')
->url(OperationRunLinks::tenantlessView($activeRun)),
])
->send();
@ -106,7 +106,7 @@ public function generatePack(bool $includePii = true, bool $includeOperations =
if ($runUrl !== null) {
$toast->actions([
Action::make('view_run')
->label('View run')
->label('Open operation')
->url($runUrl),
]);
}

View File

@ -75,11 +75,11 @@ public function startVerification(StartVerification $verification): void
Notification::make()
->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()
->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -93,7 +93,7 @@ public function startVerification(StartVerification $verification): void
OperationUxPresenter::alreadyQueuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();
@ -108,7 +108,7 @@ public function startVerification(StartVerification $verification): void
$actions = [
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
];
@ -152,7 +152,7 @@ public function startVerification(StartVerification $verification): void
OperationUxPresenter::queuedToast((string) $result->run->type)
->actions([
Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->send();

View File

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

View File

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

View File

@ -49,11 +49,11 @@ public function toDatabase(object $notifiable): array
return FilamentNotification::make()
->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()
->actions([
\Filament\Actions\Action::make('view_run')
->label('View run')
->label(OperationRunLinks::openLabel())
->url($runUrl),
])
->getDatabaseMessage();

View File

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

View File

@ -373,7 +373,7 @@ final class OperatorOutcomeTaxonomy
],
'completed' => [
'axis' => 'execution_lifecycle',
'label' => 'Run finished',
'label' => 'Operation finished',
'color' => 'gray',
'classification' => 'primary',
'next_action_policy' => 'none',
@ -398,7 +398,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary',
'next_action_policy' => 'none',
'legacy_aliases' => ['Succeeded'],
'notes' => 'The run finished without operator follow-up.',
'notes' => 'The operation finished without operator follow-up.',
],
'partially_succeeded' => [
'axis' => 'execution_outcome',
@ -407,7 +407,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary',
'next_action_policy' => 'optional',
'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' => [
'axis' => 'execution_outcome',
@ -598,7 +598,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary',
'next_action_policy' => 'none',
'legacy_aliases' => ['Checked'],
'notes' => 'Safety checks were completed for this run.',
'notes' => 'Safety checks were completed for this operation.',
],
'previewed' => [
'axis' => 'execution_lifecycle',
@ -643,7 +643,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary',
'next_action_policy' => 'none',
'legacy_aliases' => ['Completed'],
'notes' => 'The restore run finished successfully.',
'notes' => 'The restore operation finished successfully.',
],
'partial' => [
'axis' => 'execution_outcome',
@ -652,7 +652,7 @@ final class OperatorOutcomeTaxonomy
'classification' => 'primary',
'next_action_policy' => 'optional',
'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' => [
'axis' => 'execution_outcome',

View File

@ -242,7 +242,7 @@ private function supportingMessage(
? 'Last compared '.$stats->lastComparedHuman.'.'
: 'The latest compare result is trustworthy enough to treat zero findings as current.',
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.',
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,
@ -258,7 +258,7 @@ private function supportingMessage(
$findingsVisibleCount > 0 => 'Open findings remain on this tenant and need review.',
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,
};
}
@ -295,7 +295,7 @@ private function nextAction(
return match ($stateFamily) {
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
? BaselineCompareSummaryAssessment::NEXT_TARGET_RUN
: BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
@ -311,7 +311,7 @@ private function nextAction(
'target' => BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,
],
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
? BaselineCompareSummaryAssessment::NEXT_TARGET_RUN
: BaselineCompareSummaryAssessment::NEXT_TARGET_LANDING,

View File

@ -4,6 +4,7 @@
namespace App\Support\Navigation;
use App\Support\OperationRunLinks;
use Illuminate\Support\Str;
final class RelatedActionLabelCatalog
@ -20,7 +21,7 @@ final class RelatedActionLabelCatalog
'parent_policy' => 'Policy',
'policy_version' => 'Policy version',
'restore_run' => 'Restore run',
'source_run' => 'Run',
'source_run' => 'Operation',
];
/**
@ -35,16 +36,32 @@ final class RelatedActionLabelCatalog
'parent_policy' => 'View policy',
'policy_version' => 'View policy version',
'restore_run' => 'View restore run',
'source_run' => 'View run',
'source_run' => 'Open operation',
];
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));
}
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));
}

View File

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

View File

@ -23,6 +23,43 @@
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
{
return route('admin.operations.index', $context?->toQuery() ?? []);
@ -52,7 +89,7 @@ public static function related(OperationRun $run, ?Tenant $tenant): array
$links = [];
$links['Operations'] = self::index($tenant);
$links[self::collectionLabel()] = self::index($tenant);
if (! $tenant instanceof Tenant) {
return $links;

View File

@ -7,6 +7,7 @@
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
use App\Support\Operations\OperationRunFreshnessState;
use App\Support\ReasonTranslation\ReasonPresenter;
use App\Support\ReasonTranslation\ReasonResolutionEnvelope;
@ -33,7 +34,7 @@ public static function queuedToast(string $operationType): FilamentNotification
return FilamentNotification::make()
->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()
->duration(self::QUEUED_TOAST_DURATION_MS);
}
@ -47,7 +48,7 @@ public static function alreadyQueuedToast(string $operationType): FilamentNotifi
return FilamentNotification::make()
->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()
->duration(self::QUEUED_TOAST_DURATION_MS);
}
@ -92,7 +93,7 @@ public static function terminalDatabaseNotification(OperationRun $run, ?Tenant $
if ($tenant instanceof Tenant) {
$notification->actions([
\Filament\Actions\Action::make('view')
->label('View run')
->label(OperationRunLinks::openLabel())
->url(OperationRunUrl::view($run, $tenant)),
]);
}
@ -129,13 +130,13 @@ private static function buildSurfaceGuidance(OperationRun $run): ?string
$freshnessState = self::freshnessState($run);
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()) {
return $operatorExplanationGuidance
?? $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)) {
@ -153,8 +154,8 @@ private static function buildSurfaceGuidance(OperationRun $run): ?string
}
return match ($uxStatus) {
'queued' => 'No action needed yet. The run is waiting for a worker.',
'running' => 'No action needed yet. The run is currently in progress.',
'queued' => 'No action needed yet. The operation is waiting for a worker.',
'running' => 'No action needed yet. The operation is currently in progress.',
'succeeded' => 'No action needed.',
'partial' => $nextStepLabel !== null
? 'Next step: '.$nextStepLabel.'.'
@ -166,7 +167,7 @@ private static function buildSurfaceGuidance(OperationRun $run): ?string
: 'Review the blocked prerequisite before retrying.',
default => $nextStepLabel !== null
? '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()) {
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;

View File

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

View File

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

View File

@ -753,7 +753,7 @@ private function buildOperationRunEnvelope(OperationRun $run): ArtifactTruthEnve
actionability: $artifactEnvelope->actionability,
primaryDomain: $artifactEnvelope->primaryDimension()?->badgeDomain ?? BadgeDomain::GovernanceArtifactExistence,
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),
reason: $artifactEnvelope->reason,
nextActionLabel: $artifactEnvelope->nextActionLabel,
@ -801,15 +801,15 @@ private function buildOperationRunEnvelope(OperationRun $run): ArtifactTruthEnve
primaryDomain: BadgeDomain::GovernanceArtifactExistence,
primaryState: $primaryState,
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.',
default => 'The run finished without a usable artifact result.',
OperationRunStatus::Queued->value, OperationRunStatus::Running->value => 'The artifact-producing operation is still in progress, so no artifact is available yet.',
default => 'The operation finished without a usable artifact result.',
},
diagnosticLabel: BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, $run->outcome)->label,
reason: ArtifactTruthCause::fromReasonResolutionEnvelope($reason, ReasonPresenter::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT),
nextActionLabel: $reason?->firstNextStep()?->label
?? ($actionability === 'required'
? 'Inspect the blocked run details before retrying'
: 'Wait for the artifact-producing run to finish'),
? 'Inspect the blocked operation details before retrying'
: 'Wait for the artifact-producing operation to finish'),
nextActionUrl: null,
relatedRunId: (int) $run->getKey(),
relatedArtifactUrl: null,

View File

@ -206,7 +206,7 @@ private function summaryMetrics(
'key' => 'active_operations',
'label' => 'Active operations',
'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'),
'color' => $activeOperationsCount > 0 ? 'warning' : 'gray',
],
@ -295,7 +295,7 @@ private function attentionItems(int $workspaceId, array $accessibleTenantIds, bo
if ($activeRunsCount > 0) {
$items[] = [
'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'),
'badge' => 'Operations',
'badge_color' => 'warning',
@ -414,7 +414,7 @@ private function quickActions(Workspace $workspace, int $accessibleTenantCount,
[
'key' => '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'),
'icon' => 'heroicon-o-queue-list',
'color' => 'gray',

View File

@ -102,7 +102,7 @@
Verification report unavailable
</div>
<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 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.
@ -474,7 +474,7 @@ class="text-primary-600 hover:underline dark:text-primary-400"
<div class="flex flex-col gap-1">
@if ($run !== null)
<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>
</div>
<div>
@ -497,7 +497,7 @@ class="text-primary-600 hover:underline dark:text-primary-400"
href="{{ $previousRunUrl }}"
class="font-medium text-primary-600 hover:underline dark:text-primary-400"
>
Open previous verification
Open previous operation
</a>
</div>
@endif

View File

@ -155,15 +155,15 @@
<div class="space-y-4">
<x-filament::section
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)
<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>
@elseif ($status !== 'completed')
<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>
@else
<div class="space-y-4">
@ -256,7 +256,7 @@
Verification report unavailable
</div>
<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>
@else
@ -596,7 +596,7 @@ class="inline-flex items-center gap-2 text-primary-600 hover:underline dark:text
</div>
<div class="flex flex-col gap-1">
<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>
</div>
<div>
@ -618,7 +618,7 @@ class="inline-flex items-center gap-2 text-primary-600 hover:underline dark:text
href="{{ $previousRunUrl }}"
class="font-medium text-primary-600 hover:underline dark:text-primary-400"
>
Open previous verification
Open previous operation
</a>
</div>
@endif
@ -629,7 +629,7 @@ class="font-medium text-primary-600 hover:underline dark:text-primary-400"
href="{{ $runUrl }}"
class="font-medium text-primary-600 hover:underline dark:text-primary-400"
>
Open run details
Open operation details
</a>
</div>
@endif

View File

@ -75,12 +75,12 @@
<div class="space-y-4">
@if ($run === null)
<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>
@else
<div class="flex flex-wrap items-center gap-2">
<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>
@if ($runStatusSpec)
@ -150,7 +150,7 @@ class="text-sm font-medium text-gray-600 hover:underline dark:text-gray-300"
target="_blank"
rel="noreferrer"
>
Open run in Monitoring (advanced)
Open operation in Monitoring (advanced)
</a>
</div>
@endif

View File

@ -42,8 +42,8 @@
@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">
{{ ($lifecycleSummary['likely_stale'] ?? 0) }} active run(s) are beyond their lifecycle window.
{{ ($lifecycleSummary['reconciled'] ?? 0) }} run(s) have already been automatically reconciled.
{{ ($lifecycleSummary['likely_stale'] ?? 0) }} active operation(s) are beyond their lifecycle window.
{{ ($lifecycleSummary['reconciled'] ?? 0) }} operation(s) have already been automatically reconciled.
</div>
@endif

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) }}"
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>
</div>
</li>

View File

@ -29,7 +29,7 @@
<div class="space-y-4">
@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">
No verification run has been started yet.
No verification operation has been started yet.
</div>
<div class="flex items-center gap-2">
@ -68,7 +68,7 @@
</div>
@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">
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 class="flex flex-wrap items-center gap-2">
@ -79,7 +79,7 @@
color="gray"
size="sm"
>
View run
Open operation
</x-filament::button>
@endif
@ -131,7 +131,7 @@
color="gray"
size="sm"
>
View run
Open operation
</x-filament::button>
@endif

View File

@ -34,7 +34,7 @@
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"
>
View run
Open operation
</a>
@endif
</div>

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

@ -11,6 +11,7 @@ ## Spec Scope Fields *(mandatory)*
- **Primary Routes**:
- `/admin/operations`
- `/admin/operations/{run}`
- `/admin/onboarding`
- `/admin/t/{tenant}`
- `/system/ops/runs/{run}`
- **Data Ownership**:

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

@ -286,7 +286,7 @@
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Bootstrap (optional)')
->assertSee('Bootstrap is running across 1 operation run(s).');
->assertSee('Bootstrap is running across 1 operation(s).');
$bootstrapRun->forceFill([
'status' => OperationRunStatus::Completed->value,
@ -297,5 +297,5 @@
$page
->wait(7)
->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])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operation run')
->assertSee(\App\Support\OperationRunLinks::identifier($run))
->assertSee('Policy sync')
->assertSee('Counts')
->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])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->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

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])
->get(route('admin.operations.index'))
->assertOk()
->assertDontSee('Total Runs (30 days)')
->assertDontSee('Total Operations (30 days)')
->assertDontSee('Active Operations')
->assertDontSee('Failed/Partial (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])
->get(route('admin.operations.index'))
->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

View File

@ -51,7 +51,7 @@ public function test_trusts_canonical_run_links_opened_from_a_tenant_surface_aft
->assertOk()
->assertSee('Back to tenant')
->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
@ -71,7 +71,7 @@ public function test_trusts_notification_style_run_links_with_no_selected_tenant
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(OperationRunLinks::tenantlessView($run))
->assertOk()
->assertSee('Operation run')
->assertSee(OperationRunLinks::identifier($run))
->assertSee('Canonical workspace view');
}

View File

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

View File

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

View File

@ -6,6 +6,7 @@
use App\Models\BackupSet;
use App\Models\OperationRun;
use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
it('renders backup set related context with the source operation label and id', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
@ -26,5 +27,7 @@
$this->get(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant))
->assertOk()
->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)
->assertSee('Action required')
->assertSee('The latest baseline compare failed before it produced a usable result.')
->assertSee('Review the failed run')
->assertSee('Review the failed operation')
->assertDontSee('Aligned');
});
@ -174,7 +174,7 @@ function createBaselineCompareWidgetTenant(): array
Livewire::test(BaselineCompareNow::class)
->assertSee('In progress')
->assertSee('Baseline compare is in progress.')
->assertSee('View run')
->assertSee('Open operation')
->assertDontSee('Aligned');
});

View File

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

View File

@ -61,5 +61,5 @@
->assertSee('Windows Lockdown')
->assertSee('Version 3')
->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])
->assertSee('Decision')
->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);
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'));
});

View File

@ -218,13 +218,13 @@ function baselineCompareGapContext(array $overrides = []): array
->withSession([WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Current tenant context differs from this run')
->assertSee('Current tenant context differs from this operation')
->assertSee('Decision')
->assertSee('Related context');
$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');
expect($bannerPosition)->not->toBeFalse()
@ -259,7 +259,7 @@ function baselineCompareGapContext(array $overrides = []): array
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->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 unavailable')
->assertDontSee('Counts');

View File

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

View File

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

View File

@ -226,7 +226,7 @@ function spec125CriticalTenantContext(bool $ensureDefaultMicrosoftProviderConnec
expect($table->persistsSearchInSession())->toBeTrue();
expect($table->persistsSortInSession())->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->getEmptyStateActions())->toBeEmpty();
expect($table->getColumn('type')?->isSearchable())->toBeTrue();

View File

@ -3,9 +3,13 @@
declare(strict_types=1);
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\OperationRun;
use Filament\Facades\Filament;
use Illuminate\Support\Facades\Bus;
use Livewire\Livewire;
it('renders the tenant dashboard DB-only (no outbound HTTP, no background work)', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
@ -17,7 +21,7 @@
'status' => Finding::STATUS_NEW,
]);
OperationRun::factory()->create([
$operation = OperationRun::factory()->create([
'tenant_id' => $tenant->getKey(),
'type' => 'inventory_sync',
'status' => 'queued',
@ -28,14 +32,24 @@
$this->actingAs($user);
Bus::fake();
Filament::setTenant($tenant, true);
assertNoOutboundHttp(function () use ($tenant): void {
assertNoOutboundHttp(function () use ($operation, $tenant): void {
$this->get(TenantDashboard::getUrl(tenant: $tenant))
->assertOk()
->assertSee('/admin/choose-workspace', false);
// NeedsAttention, RecentOperations and RecentDriftFindings are
// lazy-loaded widgets and will not appear in the initial
// 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();

View File

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

View File

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

View File

@ -29,5 +29,7 @@
->assertSee('Choose tenant')
->assertSee('Open operations')
->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');
});

View File

@ -94,9 +94,12 @@
use App\Support\Auth\PlatformCapabilities;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Navigation\RelatedActionLabelCatalog;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\References\ReferenceClass;
use App\Support\References\ReferenceTypeLabelCatalog;
use App\Support\System\SystemDirectoryLinks;
use App\Support\System\SystemOperationRunLinks;
use App\Support\Ui\ActionSurface\ActionSurfaceExemptions;
@ -1212,6 +1215,15 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
->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 {
[$user, $tenant] = createUserWithTenant(role: 'owner');

View File

@ -87,8 +87,8 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get('/admin/operations')
->assertOk()
->assertSee('active run(s) are beyond their lifecycle window')
->assertSee('run(s) have already been automatically reconciled');
->assertSee('active operation(s) are beyond their lifecycle window')
->assertSee('operation(s) have already been automatically reconciled');
});
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])
->get('/admin/operations')
->assertOk()
->assertSee('Run finished')
->assertSee('Operation finished')
->assertSee('Completed with follow-up')
->assertDontSee('Run finished Unknown')
->assertDontSee('Operation finished Unknown')
->assertDontSee('Completed with follow-up Unknown');
});

View File

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

View File

@ -70,7 +70,7 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operation run')
->assertSee(OperationRunLinks::identifier($run))
->assertDontSee('/admin/t/'.((int) $tenant->getKey()).'/operations/r/'.((int) $run->getKey()));
Filament::setTenant($tenant, true);
@ -79,7 +79,7 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operation run');
->assertSee(OperationRunLinks::identifier($run));
});
it('keeps operation detail accessible when the active tenant context does not match the run tenant', function (): void {
@ -104,7 +104,7 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $runB->getKey()]))
->assertOk()
->assertSee('Operation run');
->assertSee(OperationRunLinks::identifier($runB));
});
it('keeps canonical operation detail accessible for selector-excluded tenant runs', function (): void {
@ -125,7 +125,7 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Run tenant is not available in the current tenant selector')
->assertSee('Operation tenant is not available in the current tenant selector')
->assertSee('This tenant is currently onboarding');
});

View File

@ -32,7 +32,7 @@
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operation run');
->assertSee(\App\Support\OperationRunLinks::identifier($run));
});
Bus::assertNothingDispatched();

View File

@ -34,7 +34,7 @@
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operation run');
->assertSee(\App\Support\OperationRunLinks::identifier($run));
});
Bus::assertNothingDispatched();

View File

@ -26,8 +26,8 @@
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get('/admin/operations')
->assertOk()
->assertDontSee('Total Runs (30 days)')
->assertDontSee('Active Runs')
->assertDontSee('Total Operations (30 days)')
->assertDontSee('Active Operations')
->assertDontSee('Failed/Partial (7 days)')
->assertDontSee('Avg Duration (7 days)')
->assertSee('All')
@ -61,7 +61,7 @@
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operation run');
->assertSee(\App\Support\OperationRunLinks::identifier($run));
});
Bus::assertNothingDispatched();

View File

@ -71,8 +71,8 @@ function operationsKpiValues($component): array
$values = operationsKpiValues(Livewire::test(OperationsKpiHeader::class));
expect($values)->toMatchArray([
'Total Runs (30 days)' => '2',
'Active Runs' => '1',
'Total Operations (30 days)' => '2',
'Active Operations' => '1',
'Failed/Partial (7 days)' => '1',
]);
});
@ -114,8 +114,8 @@ function operationsKpiValues($component): array
$values = operationsKpiValues(Livewire::test(OperationsKpiHeader::class));
expect($values)->toMatchArray([
'Total Runs (30 days)' => '1',
'Active Runs' => '0',
'Total Operations (30 days)' => '1',
'Active Operations' => '0',
'Failed/Partial (7 days)' => '0',
]);
});

View File

@ -130,7 +130,7 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertSuccessful()
->assertSee('Operation run');
->assertSee(\App\Support\OperationRunLinks::identifier($run));
});
it('returns 404 when viewing an operation run outside workspace membership', function (): void {

View File

@ -12,6 +12,7 @@
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Support\Links\RequiredPermissionsLinks;
use App\Support\OperationRunLinks;
use App\Support\Verification\VerificationReportWriter;
use App\Support\Workspaces\WorkspaceContext;
use Livewire\Livewire;
@ -118,6 +119,8 @@
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Technical details')
->assertSee(OperationRunLinks::identifierLabel())
->assertSee('Open operation details')
->assertSee('Required application permissions')
->assertSee('Open required permissions')
->assertSee('Issues')

View File

@ -77,6 +77,9 @@
->flatMap(static fn (array $notification): array => is_array($notification['actions'] ?? null)
? $notification['actions']
: [])
->tap(function (\Illuminate\Support\Collection $actions): void {
expect($actions->pluck('label')->filter()->values()->all())->toContain(OperationRunLinks::openLabel());
})
->pluck('url')
->filter(static fn (mixed $url): bool => is_string($url) && trim($url) !== '')
->values()
@ -159,6 +162,9 @@
->flatMap(static fn (array $notification): array => is_array($notification['actions'] ?? null)
? $notification['actions']
: [])
->tap(function (\Illuminate\Support\Collection $actions): void {
expect($actions->pluck('label')->filter()->values()->all())->toContain(OperationRunLinks::openLabel());
})
->pluck('url')
->filter(static fn (mixed $url): bool => is_string($url) && trim($url) !== '')
->values()
@ -321,6 +327,8 @@
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Status: Blocked')
->assertSee(OperationRunLinks::identifierLabel())
->assertSee('Open operation details')
->assertSee('Missing required Graph permissions.')
->assertSee('Graph permissions')
->assertSee($entraTenantId);

View File

@ -56,6 +56,7 @@
->followingRedirects()
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Run a queued verification check (Operation).')
->assertSee('Start verification')
->assertDontSee('Refresh');

View File

@ -308,7 +308,7 @@
])
->get("/admin/operations/{$run->getKey()}")
->assertSuccessful()
->assertSee('Operation run')
->assertSee(OperationRunLinks::identifier($run))
->assertSee('Back to Operations');
});
@ -335,7 +335,7 @@
])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertSuccessful()
->assertSee('Workspace-level run')
->assertSee('Workspace-level operation')
->assertSee('This canonical workspace view is not tied to the current tenant context');
});
@ -420,8 +420,8 @@
])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertSuccessful()
->assertSee('Current tenant context differs from this run')
->assertSee('Run tenant: '.$runTenant->name.'.')
->assertSee('Current tenant context differs from this operation')
->assertSee('Operation tenant: '.$runTenant->name.'.')
->assertSee('Back to Operations');
});

View File

@ -7,7 +7,7 @@
use App\Support\OperationRunLinks;
use Filament\Facades\Filament;
it('uses canonical “View run” URL in terminal notifications', function (): void {
it('uses canonical “Open operation” links in terminal notifications', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user);
@ -37,7 +37,9 @@
$notification = $user->notifications()->latest('id')->first();
expect($notification)->not->toBeNull();
expect($notification->data['actions'][0]['url'] ?? null)
expect($notification->data['actions'][0]['label'] ?? null)
->toBe('Open operation')
->and($notification->data['actions'][0]['url'] ?? null)
->toBe(OperationRunLinks::view($run, $tenant));
})->group('ops-ux');
@ -72,6 +74,7 @@
expect($notification)->not->toBeNull();
$url = $notification->data['actions'][0]['url'] ?? null;
expect($notification->data['actions'][0]['label'] ?? null)->toBe('Open operation');
expect($url)->toBe(OperationRunLinks::view($run, $tenant));
expect((string) $url)->not->toContain('bulk-operation-runs');
})->group('ops-ux');

View File

@ -116,7 +116,7 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
->assertOk()
->assertSee('Operation run');
->assertSee(\App\Support\OperationRunLinks::identifier($run));
});
test('tenant-associated run viewer requires tenant entitlement even for workspace members', function (): void {

View File

@ -32,9 +32,9 @@
->values()
->all();
expect($monitoringLabels)->toContain('Runs');
expect($monitoringLabels)->toContain('Operations');
expect($monitoringLabels)->toContain('Alerts');
expect($monitoringLabels)->toContain('Audit Log');
expect($monitoringLabels)->not->toContain('Operations');
expect($monitoringLabels)->not->toContain('Runs');
});

View File

@ -15,7 +15,7 @@
expect($running->color)->toBe('info');
$completed = BadgeCatalog::spec(BadgeDomain::OperationRunStatus, 'completed');
expect($completed->label)->toBe('Run finished');
expect($completed->label)->toBe('Operation finished');
expect($completed->color)->toBe('gray');
$stale = BadgeCatalog::spec(BadgeDomain::OperationRunStatus, [
@ -29,7 +29,7 @@
'status' => 'completed',
'freshness_state' => 'terminal_normal',
]);
expect($completedArray->label)->toBe('Run finished');
expect($completedArray->label)->toBe('Operation finished');
expect($completedArray->color)->toBe('gray');
});

View File

@ -15,7 +15,7 @@
expect($running->color)->toBe('info');
$completed = BadgeCatalog::spec(BadgeDomain::OperationRunStatus, 'completed');
expect($completed->label)->toBe('Run finished');
expect($completed->label)->toBe('Operation finished');
expect($completed->color)->toBe('gray');
});

View File

@ -26,13 +26,13 @@
referenceClass: ReferenceClass::OperationRun,
rawIdentifier: '44',
primaryLabel: 'Baseline compare',
secondaryLabel: 'Run #44',
secondaryLabel: 'Operation #44',
state: ReferenceResolutionState::Resolved,
stateLabel: null,
linkTarget: new ReferenceLinkTarget(
targetKind: ReferenceClass::OperationRun->value,
url: '/admin/operations/44',
actionLabel: 'View run',
actionLabel: 'Open operation',
contextBadge: 'Tenant context',
),
technicalDetail: ReferenceTechnicalDetail::forIdentifier('44'),
@ -41,7 +41,11 @@
expect($entry)->not->toBeNull()
->and($entry?->targetUrl)->toBe('/admin/operations/44')
->and($entry?->secondaryValue)->toBe('Operation #44 · ID 44')
->and($entry?->actionLabel)->toBe('Open operation')
->and($entry?->reference)->not->toBeNull()
->and($entry?->reference['typeLabel'])->toBe('Operation')
->and($entry?->reference['secondaryLabel'])->toBe('Operation #44')
->and($entry?->reference['stateLabel'])->toBe('Resolved')
->and($entry?->reference['showStateBadge'])->toBeFalse();
});

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