feat: productize operations hub decision-first workbench
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 6m23s

This commit is contained in:
Ahmed Darrazi 2026-05-19 02:48:39 +02:00
parent 1c38a08919
commit 9873095f31
34 changed files with 2713 additions and 174 deletions

1
.gitignore vendored
View File

@ -55,6 +55,7 @@ Homestead.yaml
Thumbs.db
/references
/tests/Browser/Screenshots
/apps/platform/tests/Browser/Screenshots
*.tmp
*.swp
/apps/platform/.env

View File

@ -6,7 +6,6 @@
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\OperationRunResource;
use App\Filament\Widgets\Operations\OperationsKpiHeader;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\User;
@ -18,9 +17,12 @@
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\OperateHub\OperateHubShell;
use App\Support\OperationCatalog;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OpsUx\OperationRunProgressContract;
use App\Support\OpsUx\OperationUxPresenter;
use App\Support\Operations\OperationLifecyclePolicy;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\ActionSurfaceDefaults;
@ -39,6 +41,8 @@
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use UnitEnum;
class Operations extends Page implements HasForms, HasTable
@ -211,9 +215,7 @@ public function mount(): void
protected function getHeaderWidgets(): array
{
return [
OperationsKpiHeader::class,
];
return [];
}
/**
@ -295,26 +297,264 @@ public function landingHierarchySummary(): array
$returnBody = 'Return to the originating monitoring surface without competing with the current tab, filters, or row inspection flow.';
} elseif ($activeEnvironment instanceof ManagedEnvironment) {
$returnLabel = 'Back to '.$activeEnvironment->name;
$returnBody = 'Return to the tenant dashboard when you need tenant-specific context outside this workspace monitoring landing.';
$returnBody = 'Return to the environment dashboard when you need environment-specific context outside this workspace operations view.';
}
return [
'scope_label' => $operateHubShell->scopeLabel(request()),
'scope_body' => $filteredTenant instanceof ManagedEnvironment
? 'The landing is workspace-scoped and filtered by an explicit environment filter.'
? 'Operations Hub is workspace-scoped and filtered by an explicit environment filter.'
: ($activeEnvironment instanceof ManagedEnvironment
? 'The landing is currently narrowed to one environment inside the active workspace.'
: 'The landing is currently showing workspace-wide monitoring across all entitled environments.'),
? 'Operations Hub is currently narrowed to one environment inside the active workspace.'
: 'Operations Hub is showing workspace-wide execution records across all entitled environments.'),
'return_label' => $returnLabel,
'return_body' => $returnBody,
'scope_reset_label' => $activeEnvironment instanceof ManagedEnvironment ? __('localization.shell.show_all_environments') : null,
'scope_reset_body' => $activeEnvironment instanceof ManagedEnvironment
? 'Reset the landing back to workspace-wide monitoring when environment-specific context is no longer needed.'
? 'Reset Operations Hub back to workspace-wide execution records when environment-specific context is no longer needed.'
: null,
'inspect_body' => 'Open a run from the table to enter the canonical monitoring detail viewer.',
];
}
/**
* @return array{
* question:string,
* has_attention:bool,
* selected_operation:array<string,mixed>|null,
* diagnostics:array{label:string,state:string,body:string}
* }
*/
public function decisionWorkbench(): array
{
$selectedOperation = $this->selectedWorkbenchOperation();
$needsAttention = $this->summaryCount(
fn (Builder $query): Builder => $query->dashboardNeedsFollowUp(),
);
return [
'question' => 'Which operation needs attention now?',
'has_attention' => $needsAttention > 0,
'selected_operation' => $selectedOperation instanceof OperationRun
? $this->workbenchOperationPayload($selectedOperation, $needsAttention > 0)
: null,
'diagnostics' => [
'label' => 'Diagnostics',
'state' => 'Collapsed',
'body' => 'Raw context, provider payloads, stack traces, debug metadata, and support diagnostics stay on authorized operation detail surfaces.',
],
];
}
private function selectedWorkbenchOperation(): ?OperationRun
{
$attentionRun = $this->topOperationFromQuery(
fn (Builder $query): Builder => $query->dashboardNeedsFollowUp(),
sortByAttention: true,
);
if ($attentionRun instanceof OperationRun) {
return $attentionRun;
}
$activeRun = $this->topOperationFromQuery(
fn (Builder $query): Builder => $query->active(),
);
if ($activeRun instanceof OperationRun) {
return $activeRun;
}
return $this->topOperationFromQuery();
}
private function topOperationFromQuery(?callable $scope = null, bool $sortByAttention = false): ?OperationRun
{
$query = $this->scopedSummaryQuery();
if (! $query instanceof Builder) {
return null;
}
$query
->with('tenant')
->latest('id')
->limit(50);
if ($scope !== null) {
$query = $scope($query);
}
/** @var Collection<int, OperationRun> $runs */
$runs = $query->get();
if ($runs->isEmpty()) {
return null;
}
if (! $sortByAttention) {
return $runs->first();
}
return $runs
->sort(function (OperationRun $left, OperationRun $right): int {
return [
$this->attentionPriority($right),
$right->created_at?->getTimestamp() ?? 0,
(int) $right->getKey(),
] <=> [
$this->attentionPriority($left),
$left->created_at?->getTimestamp() ?? 0,
(int) $left->getKey(),
];
})
->first();
}
private function attentionPriority(OperationRun $run): int
{
if ((string) $run->outcome === OperationRunOutcome::Blocked->value) {
return 50;
}
if ((string) $run->outcome === OperationRunOutcome::Failed->value) {
return 40;
}
if ((string) $run->outcome === OperationRunOutcome::PartiallySucceeded->value) {
return 30;
}
if ($run->problemClass() === OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION) {
return 20;
}
if ($run->problemClass() === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP) {
return 10;
}
return 0;
}
/**
* @return array<string, mixed>
*/
private function workbenchOperationPayload(OperationRun $run, bool $hasAttention): array
{
$progress = OperationRunProgressContract::forRun($run);
$decisionTruth = OperationUxPresenter::decisionZoneTruth($run);
$tenant = $run->tenant;
return [
'id' => (int) $run->getKey(),
'title' => OperationCatalog::label((string) $run->type),
'identifier' => OperationRunLinks::identifier($run),
'status_label' => $this->humanizeState((string) $run->status),
'outcome_label' => $this->humanizeState((string) $run->outcome),
'attention_label' => $hasAttention && $run->requiresOperatorReview()
? 'Needs attention'
: ($run->isCurrentlyActive() ? 'Active operation' : 'No attention needed'),
'reason' => $this->operationReason($run, $decisionTruth),
'impact' => $this->operationImpact($run),
'environment' => $tenant instanceof ManagedEnvironment ? (string) $tenant->name : 'Workspace-level operation',
'timing' => $this->operationTiming($run),
'proof_label' => 'Operation detail available',
'proof_body' => 'Open operation for stored proof, related links, and authorized diagnostics. Artifact or evidence links are unavailable here unless the detail surface proves them.',
'primary_action_label' => OperationRunLinks::openLabel(),
'primary_action_url' => OperationRunLinks::tenantlessView($run),
'progress' => $progress,
'progress_label' => is_string($progress['label'] ?? null) ? $progress['label'] : null,
'show_progress_bar' => ($progress['display'] ?? null) === OperationRunProgressContract::COUNTED,
'progress_percent' => is_int($progress['percent'] ?? null) ? $progress['percent'] : null,
'outcome_guidance' => OperationUxPresenter::surfaceGuidance($run) ?? 'Review the operation detail for the next safe step.',
'diagnostics_available' => ! empty($run->failure_summary) || ! empty($run->context),
];
}
/**
* @param array<string, mixed> $decisionTruth
*/
private function operationReason(OperationRun $run, array $decisionTruth): string
{
$attentionNote = $decisionTruth['attentionNote'] ?? null;
if (is_string($attentionNote) && trim($attentionNote) !== '') {
return $attentionNote;
}
$lifecycleLabel = $decisionTruth['freshnessLabel'] ?? null;
if (is_string($lifecycleLabel) && trim($lifecycleLabel) !== '') {
return $lifecycleLabel;
}
return match ((string) $run->outcome) {
OperationRunOutcome::Blocked->value => 'The operation is blocked by an execution prerequisite recorded on the run.',
OperationRunOutcome::Failed->value => 'The operation finished with a failed outcome and needs review before retrying or relying on the result.',
OperationRunOutcome::PartiallySucceeded->value => 'The operation finished partially and needs follow-up on affected items.',
OperationRunOutcome::Succeeded->value => 'The operation completed successfully. This is execution truth only, not an environment health claim.',
default => $run->isCurrentlyActive()
? 'The operation is still active. Progress is shown only from trusted run counts.'
: 'Reason unavailable from stored operation truth.',
};
}
private function operationImpact(OperationRun $run): string
{
if ($run->problemClass() === OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION) {
return 'Follow-up is needed before starting overlapping work for this operation scope.';
}
return match ((string) $run->outcome) {
OperationRunOutcome::Blocked->value => 'Execution did not proceed; inspect the blocked prerequisite before retrying from the source surface.',
OperationRunOutcome::Failed->value => 'The expected result may be incomplete or unavailable until the failure is reviewed.',
OperationRunOutcome::PartiallySucceeded->value => 'Some work completed, but affected items may still require review or a targeted rerun.',
OperationRunOutcome::Succeeded->value => 'Execution completed; use operation detail for proof rather than treating this as governance health.',
default => $run->isCurrentlyActive()
? 'Work is in progress; avoid duplicate starts until this run settles or becomes stale.'
: 'Impact unavailable from stored operation truth.',
};
}
private function operationTiming(OperationRun $run): string
{
if ($run->completed_at !== null) {
return 'Completed '.$run->completed_at->diffForHumans();
}
if ($run->started_at !== null) {
return 'Started '.$run->started_at->diffForHumans();
}
if ($run->created_at !== null) {
return 'Created '.$run->created_at->diffForHumans();
}
return 'Timing unavailable';
}
private function summaryCount(callable $scope): int
{
$query = $this->scopedSummaryQuery();
if (! $query instanceof Builder) {
return 0;
}
return (int) $scope($query)->count();
}
private function humanizeState(string $state): string
{
$label = OperationRunOutcome::uiLabels(includeReserved: true)[$state] ?? null;
if (is_string($label)) {
return $label;
}
return Str::of($state)->replace('_', ' ')->headline()->toString();
}
public function tabUrl(string $tab): string
{
$normalizedTab = in_array($tab, self::supportedTabs(), true) ? $tab : 'all';

View File

@ -15,7 +15,6 @@
use App\Services\Baselines\BaselineEvidenceCaptureResumeService;
use App\Services\Tenants\TenantOperabilityService;
use App\Support\Auth\Capabilities;
use App\Support\ManagedEnvironmentLinks;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\Navigation\RelatedNavigationResolver;
use App\Support\OperateHub\OperateHubShell;
@ -123,11 +122,6 @@ protected function getHeaderActions(): array
->label($navigationContext->backLinkLabel)
->color('gray')
->url($navigationContext->backLinkUrl);
} elseif ($activeEnvironment instanceof ManagedEnvironment && (int) $activeEnvironment->getKey() === $runTenantId) {
$actions[] = Action::make('operate_hub_back_to_tenant_run_detail')
->label('← Back to '.$activeEnvironment->name)
->color('gray')
->url(ManagedEnvironmentLinks::viewUrl($activeEnvironment));
} else {
$actions[] = Action::make('operate_hub_back_to_operations')
->label('Back to Operations')
@ -210,9 +204,6 @@ public function monitoringDetailSummary(): array
if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) {
$navigationLabel = $navigationContext->backLinkLabel;
$navigationBody = 'Return to the originating surface while keeping refresh and follow-up work separate from navigation.';
} elseif ($activeEnvironment instanceof ManagedEnvironment && (int) $activeEnvironment->getKey() === $runTenantId) {
$navigationLabel = 'Back to '.$activeEnvironment->name;
$navigationBody = 'Return to the active tenant dashboard, then widen back to the workspace view only when you need broader monitoring context.';
}
$relatedLabels = array_values(array_keys($this->relatedLinks()));

View File

@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace App\Filament\Widgets\Operations;
use App\Models\OperationRun;
use App\Models\User;
use App\Models\Workspace;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Support\Enums\IconPosition;
use Filament\Widgets\StatsOverviewWidget;
use Filament\Widgets\StatsOverviewWidget\Stat;
use Illuminate\Database\Eloquent\Builder;
class OperationsWorkbenchStats extends StatsOverviewWidget
{
protected static bool $isLazy = false;
protected int|string|array $columnSpan = 'full';
protected int|array|null $columns = [
'@xl' => 4,
'!@lg' => 4,
];
protected ?string $pollingInterval = null;
/**
* @return array<Stat>
*/
protected function getStats(): array
{
$needsAttention = $this->summaryCount(
fn (Builder $query): Builder => $query->dashboardNeedsFollowUp(),
);
$activeOperations = $this->summaryCount(
fn (Builder $query): Builder => $query->active(),
);
$failedOrBlocked = $this->summaryCount(fn (Builder $query): Builder => $query
->where('status', OperationRunStatus::Completed->value)
->whereIn('outcome', [
OperationRunOutcome::Failed->value,
OperationRunOutcome::Blocked->value,
]));
$completedRecently = $this->summaryCount(fn (Builder $query): Builder => $query
->where('status', OperationRunStatus::Completed->value)
->where('completed_at', '>=', now()->subDay()));
return [
$this->workbenchStat(
key: 'needs-attention',
label: 'Needs attention',
value: $needsAttention,
description: 'Failed, blocked, partial, or stale OperationRuns in scope.',
color: $needsAttention > 0 ? 'warning' : 'gray',
descriptionIcon: 'heroicon-m-exclamation-triangle',
),
$this->workbenchStat(
key: 'active-operations',
label: 'Active operations',
value: $activeOperations,
description: 'Queued or running records with trusted progress only.',
color: 'info',
descriptionIcon: 'heroicon-m-bolt',
),
$this->workbenchStat(
key: 'failed-or-blocked',
label: 'Failed or blocked',
value: $failedOrBlocked,
description: 'Terminal execution records that need review before retrying.',
color: 'danger',
descriptionIcon: 'heroicon-m-no-symbol',
),
$this->workbenchStat(
key: 'completed-recently',
label: 'Completed recently',
value: $completedRecently,
description: 'Recent execution results, not environment or governance health.',
color: 'success',
descriptionIcon: 'heroicon-m-check-circle',
),
];
}
private function workbenchStat(
string $key,
string $label,
int $value,
string $description,
string $color,
string $descriptionIcon,
): Stat {
return Stat::make($label, (string) $value)
->description($description)
->descriptionIcon($descriptionIcon, IconPosition::Before)
->color($color)
->extraAttributes([
'data-testid' => 'operations-workbench-stat-'.$key,
'data-stat-key' => $key,
'data-stat-value' => (string) $value,
'data-stat-color' => $color,
'data-stat-label' => $label,
]);
}
private function summaryCount(callable $scope): int
{
$query = $this->scopedOperationRunQuery();
if (! $query instanceof Builder) {
return 0;
}
return (int) $scope($query)->count();
}
private function scopedOperationRunQuery(): ?Builder
{
$workspace = app(WorkspaceContext::class)->currentWorkspace(request());
$user = auth()->user();
if (! $workspace instanceof Workspace || ! $user instanceof User) {
return null;
}
$workspaceId = (int) $workspace->getKey();
$environmentFilter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
$allowedEnvironmentIds = app(ManagedEnvironmentAccessScopeResolver::class)
->allowedManagedEnvironmentIdsForWorkspace($user, $workspaceId);
return OperationRun::query()
->where('workspace_id', $workspaceId)
->when(
$allowedEnvironmentIds !== null,
function (Builder $query) use ($allowedEnvironmentIds): Builder {
return $query->where(function (Builder $query) use ($allowedEnvironmentIds): void {
$query->whereNull('managed_environment_id');
if ($allowedEnvironmentIds !== []) {
$query->orWhereIn(
'managed_environment_id',
array_values(array_unique(array_map('intval', $allowedEnvironmentIds))),
);
}
});
},
)
->when(
$environmentFilter instanceof WorkspaceHubEnvironmentFilter,
fn (Builder $query): Builder => $environmentFilter->applyToQuery($query),
);
}
}

View File

@ -99,8 +99,8 @@ public function forcesEnvironmentlessShellContext(): bool
return match ($this) {
self::WorkspaceWideSurface,
self::WorkspaceOwnedAnalysisSurface,
self::WorkspaceChooserException,
self::CanonicalWorkspaceRecordViewer => true,
self::WorkspaceChooserException => true,
self::CanonicalWorkspaceRecordViewer => false,
default => false,
};
}

View File

@ -1,124 +1,310 @@
<x-filament-panels::page>
@php($landingHierarchy = $this->landingHierarchySummary())
@php($environmentFilterChip = $this->environmentFilterChip())
@php($lifecycleSummary = $this->lifecycleVisibilitySummary())
@php($staleAttentionTab = \App\Models\OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION)
@php($terminalFollowUpTab = \App\Models\OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP)
@php
$landingHierarchy = $this->landingHierarchySummary();
$environmentFilterChip = $this->environmentFilterChip();
$lifecycleSummary = $this->lifecycleVisibilitySummary();
$workbench = $this->decisionWorkbench();
$selectedOperation = $workbench['selected_operation'] ?? null;
$diagnostics = $workbench['diagnostics'] ?? [];
$staleAttentionTab = \App\Models\OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION;
$terminalFollowUpTab = \App\Models\OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP;
@endphp
<x-filament::section heading="Monitoring landing" class="mb-6">
<p class="text-sm text-gray-600 dark:text-gray-400">
Tabs, filters, and row inspection define the active work lane. Scope context and return navigation stay secondary.
</p>
<div class="space-y-6">
<section class="space-y-4" data-testid="operations-hub-scope">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="max-w-3xl space-y-2">
<p class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Operations Hub</p>
<h2 class="text-2xl font-semibold text-gray-950 dark:text-white">Execution follow-up workbench</h2>
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
OperationRuns are execution truth. This page prioritizes stored operation outcomes, proof paths, and follow-up without claiming environment or governance health.
</p>
</div>
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Scope context</p>
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $landingHierarchy['scope_label'] }}</p>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $landingHierarchy['scope_body'] }}</p>
<div class="flex flex-wrap gap-2 text-sm">
<span class="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ $landingHierarchy['scope_label'] }}
</span>
<span class="inline-flex items-center rounded-md bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ $landingHierarchy['scope_body'] }}
</span>
</div>
</div>
@if ($landingHierarchy['return_label'] !== null && $landingHierarchy['return_body'] !== null)
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Return path</p>
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $landingHierarchy['return_label'] }}</p>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $landingHierarchy['return_body'] }}</p>
</div>
@endif
@if ($landingHierarchy['scope_reset_label'] !== null && $landingHierarchy['scope_reset_body'] !== null)
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Scope reset</p>
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $landingHierarchy['scope_reset_label'] }}</p>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $landingHierarchy['scope_reset_body'] }}</p>
</div>
@endif
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Inspect flow</p>
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">Open run detail</p>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $landingHierarchy['inspect_body'] }}</p>
</div>
</div>
@if ($environmentFilterChip !== null)
<div class="mt-4">
@if ($environmentFilterChip !== null)
@include('filament.partials.workspace-hub-environment-filter-chip', [
'label' => $environmentFilterChip['label'],
'clearUrl' => $environmentFilterChip['clear_url'],
])
@endif
<div data-testid="operations-hub-summary-cards">
@livewire(\App\Filament\Widgets\Operations\OperationsWorkbenchStats::class, [], key('operations-workbench-stats'))
</div>
@endif
</x-filament::section>
</section>
<x-filament::tabs label="Operations tabs">
<x-filament::tabs.item
:active="$this->activeTab === 'all'"
:href="$this->tabUrl('all')"
tag="a"
:spa-mode="true"
>
All
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === 'active'"
:href="$this->tabUrl('active')"
tag="a"
:spa-mode="true"
>
Active
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === $staleAttentionTab"
:href="$this->tabUrl($staleAttentionTab)"
tag="a"
:spa-mode="true"
>
Likely stale
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === $terminalFollowUpTab"
:href="$this->tabUrl($terminalFollowUpTab)"
tag="a"
:spa-mode="true"
>
Terminal follow-up
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === 'succeeded'"
:href="$this->tabUrl('succeeded')"
tag="a"
:spa-mode="true"
>
Succeeded
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === 'partial'"
:href="$this->tabUrl('partial')"
tag="a"
:spa-mode="true"
>
Partial
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === 'failed'"
:href="$this->tabUrl('failed')"
tag="a"
:spa-mode="true"
>
Failed
</x-filament::tabs.item>
</x-filament::tabs>
<div class="grid grid-cols-1 gap-6 lg:grid-cols-[minmax(0,1fr)_22rem]" data-testid="operations-hub-decision-workbench">
<section class="rounded-lg border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900" data-testid="operations-hub-priority-card">
<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
<div class="space-y-2">
<p class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Decision workbench</p>
<h2 class="text-xl font-semibold text-gray-950 dark:text-white">
{{ $workbench['question'] }}
</h2>
</div>
<p class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Environment filters and the selected operations tab remain shareable through the URL. Additional table filters still restore from the last compatible session state.
</p>
@if ($selectedOperation !== null)
<span @class([
'inline-flex w-fit items-center rounded-lg px-2.5 py-1 text-xs font-medium',
'bg-warning-50 text-warning-700 dark:bg-warning-500/10 dark:text-warning-300' => ($workbench['has_attention'] ?? false) === true,
'bg-info-50 text-info-700 dark:bg-info-950/40 dark:text-info-300' => ($workbench['has_attention'] ?? false) === false && ($selectedOperation['progress']['display'] ?? null) !== \App\Support\OpsUx\OperationRunProgressContract::NONE,
'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-200' => ($workbench['has_attention'] ?? false) === false && ($selectedOperation['progress']['display'] ?? null) === \App\Support\OpsUx\OperationRunProgressContract::NONE,
])>
{{ $selectedOperation['attention_label'] }}
</span>
@endif
</div>
@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 operation(s) are beyond their lifecycle window and belong in the stale-attention view.
{{ ($lifecycleSummary['reconciled'] ?? 0) }} operation(s) already carry reconciled stale lineage and belong in terminal follow-up.
@if (! ($workbench['has_attention'] ?? false))
<div class="mt-5 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-800 dark:bg-gray-950/40" data-testid="operations-hub-no-attention-state">
<h3 class="text-base font-semibold text-gray-950 dark:text-white">No operations need attention</h3>
<p class="mt-1 text-sm leading-6 text-gray-600 dark:text-gray-300">
No failed, blocked, partial, or stale OperationRuns are visible in this scope. This is execution follow-up only, not an environment health claim.
</p>
</div>
@endif
@if ($selectedOperation === null)
<div class="mt-5 rounded-lg border border-dashed border-gray-300 bg-gray-50 p-5 dark:border-gray-700 dark:bg-gray-950/40" data-testid="operations-hub-empty-record-state">
<h3 class="text-base font-semibold text-gray-950 dark:text-white">No operation records are visible</h3>
<p class="mt-1 text-sm leading-6 text-gray-600 dark:text-gray-300">
Queued, running, and completed OperationRuns will appear here when work is triggered in this workspace scope.
</p>
</div>
@else
<div class="mt-5 space-y-5">
<div class="space-y-2">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex items-center rounded-lg bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ $selectedOperation['identifier'] }}
</span>
<span class="inline-flex items-center rounded-lg bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
Environment: {{ $selectedOperation['environment'] }}
</span>
</div>
<h3 class="text-lg font-semibold text-gray-950 dark:text-white">
{{ $selectedOperation['title'] }}
</h3>
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
{{ $selectedOperation['outcome_guidance'] }}
</p>
</div>
<dl class="grid grid-cols-1 gap-3 md:grid-cols-2">
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Outcome</dt>
<dd class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedOperation['outcome_label'] }} · {{ $selectedOperation['status_label'] }}</dd>
</div>
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Timing</dt>
<dd class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedOperation['timing'] }}</dd>
</div>
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Reason</dt>
<dd class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedOperation['reason'] }}</dd>
</div>
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800">
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Impact</dt>
<dd class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedOperation['impact'] }}</dd>
</div>
</dl>
<div class="rounded-lg border border-gray-200 p-3 dark:border-gray-800" data-testid="operations-hub-progress-state">
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">
{{ $selectedOperation['show_progress_bar'] ? 'Progress' : 'Outcome guidance' }}
</p>
@if ($selectedOperation['show_progress_bar'])
<div class="mt-3 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-gray-800" role="progressbar" aria-valuenow="{{ $selectedOperation['progress_percent'] }}" aria-valuemin="0" aria-valuemax="100">
<div class="h-full rounded-full bg-primary-500" style="width: {{ $selectedOperation['progress_percent'] }}%"></div>
</div>
<p class="mt-2 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedOperation['progress_label'] }}</p>
@elseif (filled($selectedOperation['progress_label']))
<p class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedOperation['progress_label'] }}</p>
@else
<p class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedOperation['outcome_guidance'] }}</p>
@endif
</div>
<div class="flex flex-col gap-2 rounded-lg border border-gray-200 p-3 dark:border-gray-800 sm:flex-row sm:items-center sm:justify-between">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Next action</p>
<p class="mt-1 text-sm leading-6 text-gray-800 dark:text-gray-100">{{ $selectedOperation['primary_action_label'] }}</p>
</div>
<x-filament::button
tag="a"
href="{{ $selectedOperation['primary_action_url'] }}"
icon="heroicon-o-arrow-top-right-on-square"
>
{{ $selectedOperation['primary_action_label'] }}
</x-filament::button>
</div>
</div>
@endif
</section>
<aside class="rounded-lg border border-gray-200 bg-white p-5 shadow-sm dark:border-gray-800 dark:bg-gray-900" data-testid="operations-hub-operation-proof-panel">
<div class="space-y-4">
<div class="space-y-1">
<p class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400">Operation summary</p>
<h2 class="text-base font-semibold text-gray-950 dark:text-white">
{{ $selectedOperation['title'] ?? 'No selected operation' }}
</h2>
</div>
@if ($selectedOperation !== null)
<div class="space-y-3 text-sm">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Outcome</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">{{ $selectedOperation['outcome_label'] }} · {{ $selectedOperation['status_label'] }}</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Environment</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">{{ $selectedOperation['environment'] }}</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Proof</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">{{ $selectedOperation['proof_label'] }}</p>
<p class="mt-1 text-gray-600 dark:text-gray-300">{{ $selectedOperation['proof_body'] }}</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Primary next action</p>
<x-filament::button
tag="a"
color="gray"
href="{{ $selectedOperation['primary_action_url'] }}"
icon="heroicon-o-arrow-top-right-on-square"
class="mt-2 w-full"
>
{{ $selectedOperation['primary_action_label'] }}
</x-filament::button>
</div>
</div>
@else
<div class="space-y-3 text-sm">
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Outcome</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">Unavailable</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Environment</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">Unavailable</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Proof</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">Proof unavailable</p>
</div>
<div>
<p class="text-xs font-medium text-gray-500 dark:text-gray-400">Primary next action</p>
<p class="mt-1 text-gray-800 dark:text-gray-100">No action available</p>
</div>
</div>
@endif
<details class="rounded-lg border border-gray-200 p-3 dark:border-gray-800" data-testid="operations-hub-diagnostics">
<summary class="cursor-pointer text-sm font-medium text-gray-700 dark:text-gray-200">
{{ $diagnostics['label'] ?? 'Diagnostics' }} · {{ $diagnostics['state'] ?? 'Collapsed' }}
</summary>
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
{{ $diagnostics['body'] ?? 'Diagnostics are not default-visible.' }}
</p>
</details>
</div>
</aside>
</div>
@endif
{{ $this->table }}
<section class="space-y-4" data-testid="operations-hub-secondary-history">
<div class="space-y-1">
<h2 class="text-lg font-semibold text-gray-950 dark:text-white">Operations history</h2>
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
Secondary context for scanning OperationRun history after the top decision path is clear.
</p>
</div>
<x-filament::tabs label="Operations tabs">
<x-filament::tabs.item
:active="$this->activeTab === 'all'"
:href="$this->tabUrl('all')"
tag="a"
:spa-mode="true"
>
All
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === 'active'"
:href="$this->tabUrl('active')"
tag="a"
:spa-mode="true"
>
Active
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === $staleAttentionTab"
:href="$this->tabUrl($staleAttentionTab)"
tag="a"
:spa-mode="true"
>
Likely stale
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === $terminalFollowUpTab"
:href="$this->tabUrl($terminalFollowUpTab)"
tag="a"
:spa-mode="true"
>
Terminal follow-up
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === 'succeeded'"
:href="$this->tabUrl('succeeded')"
tag="a"
:spa-mode="true"
>
Succeeded
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === 'partial'"
:href="$this->tabUrl('partial')"
tag="a"
:spa-mode="true"
>
Partial
</x-filament::tabs.item>
<x-filament::tabs.item
:active="$this->activeTab === 'failed'"
:href="$this->tabUrl('failed')"
tag="a"
:spa-mode="true"
>
Failed
</x-filament::tabs.item>
</x-filament::tabs>
<p class="text-sm text-gray-600 dark:text-gray-400">
Environment filters and the selected operations tab remain shareable through the URL. Additional table filters still restore from the last compatible session state.
</p>
@if (($lifecycleSummary['likely_stale'] ?? 0) > 0 || ($lifecycleSummary['reconciled'] ?? 0) > 0)
<div class="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 operation(s) are beyond their lifecycle window and belong in the stale-attention view.
{{ ($lifecycleSummary['reconciled'] ?? 0) }} operation(s) already carry reconciled stale lineage and belong in terminal follow-up.
</div>
@endif
{{ $this->table }}
</section>
</div>
</x-filament-panels::page>

View File

@ -99,9 +99,9 @@
'environment_id' => (int) $tenant->getKey(),
'activeTab' => 'active',
]))
->waitForText('Monitoring landing')
->waitForText('Operations Hub')
->assertSee('Environment filters and the selected operations tab remain shareable through the URL.')
->assertSee('Open run detail')
->assertSee('Open operation')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();

View File

@ -95,8 +95,8 @@
$dashboard
->click('Show all operations')
->waitForText('Monitoring landing')
->assertSee('Open run detail')
->waitForText('Operations Hub')
->assertSee('Open operation')
->assertSee('Spec 280 Production')
->assertScript("window.location.pathname.includes('/admin/workspaces/{$workspace->getRouteKey()}/operations')", true)
->assertScript("window.location.search.includes('environment_id={$tenant->getKey()}')", true)

View File

@ -101,7 +101,7 @@
->assertNoConsoleLogs();
visit(ManagedEnvironmentLinks::operationsUrl($environment))
->waitForText('Monitoring landing')
->waitForText('Operations Hub')
->assertSee('Spec 300 Production')
->assertScript("window.location.pathname === '{$operationsPath}'", true)
->assertScript("window.location.search.includes('environment_id={$environment->getKey()}')", true)

View File

@ -106,8 +106,7 @@
->assertDontSee($hub['wide_text'])
->assertNoJavaScriptErrors();
$page
->click('[data-testid="workspace-hub-environment-filter-clear"]')
spec316BrowserClearEnvironmentFilter($page)
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($hub['wide_text'])
@ -160,8 +159,7 @@
->assertSee($environmentA->name)
->assertDontSee($environmentB->name);
$page
->click('[data-testid="workspace-hub-environment-filter-clear"]')
spec316BrowserClearEnvironmentFilter($page)
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($environmentB->name);
@ -286,6 +284,14 @@ function spec316BrowserClearFilterWorkspace(): array
return [$user, $environmentA, $environmentB];
}
function spec316BrowserClearEnvironmentFilter(mixed $page): mixed
{
$page->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true);
$page->script('window.location.assign(document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\').href);');
return $page;
}
function spec316BrowserFindingException(
ManagedEnvironment $environment,
User $actor,

View File

@ -0,0 +1,343 @@
<?php
declare(strict_types=1);
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\User;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Workspaces\WorkspaceContext;
pest()->browser()->timeout(60_000);
it('Spec328 smokes non-empty operations hub decision workbench entry', function (): void {
[$user, $environmentA, $environmentB] = spec328OperationsHubFixture();
spec328AuthenticateOperationsHubBrowser($this, $user, $environmentA);
$page = visit(OperationRunLinks::index(workspace: $environmentA->workspace))
->resize(1440, 1100)
->waitForText('Operations Hub')
->assertSee(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee('Execution follow-up workbench')
->assertSee('Which operation needs attention now?')
->assertSee('Decision workbench')
->assertSee('Needs attention')
->assertSee('Active operations')
->assertSee('Failed or blocked')
->assertSee('Completed recently')
->assertDontSee('Total Operations')
->assertDontSee('Avg Duration')
->assertDontSee('sparkline')
->assertDontSee('trend')
->assertSee('Inventory sync')
->assertSee('Outcome')
->assertSee('Blocked')
->assertSee('Reason')
->assertSee('Impact')
->assertSee($environmentA->name)
->assertSee('Proof')
->assertSee('Operation detail available')
->assertSee('Primary next action')
->assertSee('Open operation')
->assertSee('Operations history')
->assertSee('Policy sync')
->assertDontSee('tenant filter')
->assertDontSee('current tenant')
->assertDontSee('entitled tenant')
->assertDontSee('all tenants')
->assertDontSee('raw payload should stay hidden')
->assertDontSee('stack trace should stay hidden')
->assertDontSee('provider secret should stay hidden')
->assertDontSee('debug metadata should stay hidden')
->assertDontSee('internal exception should stay hidden')
->assertScript('document.querySelector("[data-testid=\"operations-hub-diagnostics\"]")?.open === false', true)
->assertScript('(() => {
const summaryCards = document.querySelector("[data-testid=\"operations-hub-summary-cards\"]");
const nativeStats = summaryCards?.querySelector(".fi-wi-stats-overview");
const needsAttention = document.querySelector("[data-testid=\"operations-workbench-stat-needs-attention\"]");
const activeOperations = document.querySelector("[data-testid=\"operations-workbench-stat-active-operations\"]");
const failedOrBlocked = document.querySelector("[data-testid=\"operations-workbench-stat-failed-or-blocked\"]");
const completedRecently = document.querySelector("[data-testid=\"operations-workbench-stat-completed-recently\"]");
if (! summaryCards || ! nativeStats || ! needsAttention || ! activeOperations || ! failedOrBlocked || ! completedRecently) {
return false;
}
if (
needsAttention.dataset.statColor !== "warning"
|| needsAttention.dataset.statValue !== "5"
|| activeOperations.dataset.statColor !== "info"
|| activeOperations.dataset.statValue !== "0"
|| failedOrBlocked.dataset.statColor !== "danger"
|| failedOrBlocked.dataset.statValue !== "5"
|| completedRecently.dataset.statColor !== "success"
|| completedRecently.dataset.statValue !== "0"
) {
return false;
}
if (summaryCards.textContent.includes("Total Operations") || summaryCards.textContent.includes("Avg Duration")) {
return false;
}
if (
summaryCards.querySelector("canvas")
|| summaryCards.querySelector("[data-card-style]")
|| summaryCards.querySelector("[data-accent-placement]")
|| summaryCards.outerHTML.includes("wire:poll")
) {
return false;
}
const decisionGrid = document.querySelector("[data-testid=\"operations-hub-decision-workbench\"]");
const workbench = document.querySelector("[data-testid=\"operations-hub-priority-card\"]");
const detail = document.querySelector("[data-testid=\"operations-hub-operation-proof-panel\"]");
if (! decisionGrid || ! workbench || ! detail) {
return false;
}
const children = Array.from(decisionGrid.children);
const summaryBox = summaryCards.getBoundingClientRect();
const decisionBox = decisionGrid.getBoundingClientRect();
const workbenchBox = workbench.getBoundingClientRect();
const detailBox = detail.getBoundingClientRect();
return window.innerWidth >= 1024
&& summaryBox.bottom <= decisionBox.top
&& decisionGrid.classList.contains("lg:grid-cols-[minmax(0,1fr)_22rem]")
&& detail.tagName === "ASIDE"
&& children.indexOf(workbench) !== -1
&& children.indexOf(detail) > children.indexOf(workbench)
&& detailBox.left > workbenchBox.right
&& Math.abs(detailBox.top - workbenchBox.top) <= 8;
})()', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$page
->click('[data-testid="operations-hub-diagnostics"] summary')
->assertScript('document.querySelector("[data-testid=\"operations-hub-diagnostics\"]")?.open === true', true)
->assertSee('Raw context, provider payloads, stack traces, debug metadata, and support diagnostics stay on authorized operation detail surfaces.')
->click('[data-testid="operations-hub-diagnostics"] summary')
->assertScript('document.querySelector("[data-testid=\"operations-hub-diagnostics\"]")?.open === false', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs();
$page->script('window.scrollTo(0, 0);');
$page
->screenshot(false, spec328OperationsHubScreenshot('operations-hub-premium-summary-cards'))
->screenshot(true, spec328OperationsHubScreenshot('operations-hub--clean'));
spec328CopyBrowserScreenshot('operations-hub--clean');
spec328CopyBrowserScreenshot('operations-hub--clean', 'operations-hub-decision-workbench.png');
spec328CopyBrowserScreenshot('operations-hub-premium-summary-cards');
});
it('Spec328 smokes filtered operations hub clear and reload behavior', function (): void {
[$user, $environmentA, $environmentB] = spec328OperationsHubFixture();
$cleanPath = json_encode((string) parse_url(OperationRunLinks::index(workspace: $environmentA->workspace), PHP_URL_PATH), JSON_THROW_ON_ERROR);
spec328AuthenticateOperationsHubBrowser($this, $user, $environmentA);
$page = visit(OperationRunLinks::index($environmentA))
->waitForText('Environment filter:')
->assertSee('Environment filter: '.$environmentA->name)
->assertSee('Which operation needs attention now?')
->assertSee($environmentA->name)
->assertDontSee('Policy sync')
->assertScript('document.querySelector("[data-testid=\"operations-hub-diagnostics\"]")?.open === false', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec328OperationsHubScreenshot('operations-hub--filtered'));
spec328CopyBrowserScreenshot('operations-hub--filtered');
spec328ClearEnvironmentFilter($page)
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee('Policy sync')
->assertScript("window.location.pathname === {$cleanPath}", true)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec328OperationsHubScreenshot('operations-hub--after-clear'));
spec328CopyBrowserScreenshot('operations-hub--after-clear');
$page->script('window.location.reload();');
$page
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee('Policy sync')
->assertScript("window.location.pathname === {$cleanPath}", true)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec328OperationsHubScreenshot('operations-hub--after-reload'));
spec328CopyBrowserScreenshot('operations-hub--after-reload');
});
it('Spec328 smokes no-attention operations hub state', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec328 Browser Empty Environment',
'external_id' => 'spec328-browser-empty-environment',
]);
[$user, $environment] = createUserWithTenant(
tenant: $environment,
role: 'owner',
workspaceRole: 'owner',
);
OperationRun::factory()->forTenant($environment)->create([
'type' => 'policy.sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'completed_at' => now()->subMinutes(5),
]);
spec328AuthenticateOperationsHubBrowser($this, $user, $environment);
visit(OperationRunLinks::index(workspace: $environment->workspace))
->waitForText('Operations Hub')
->assertSee('No operations need attention')
->assertSee('This is execution follow-up only, not an environment health claim.')
->assertSee('Operation detail available')
->assertDontSee('environment is healthy')
->assertDontSee('governance health is complete')
->assertNoJavaScriptErrors()
->assertNoConsoleLogs()
->screenshot(true, spec328OperationsHubScreenshot('operations-hub--empty'));
spec328CopyBrowserScreenshot('operations-hub--empty');
});
/**
* @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment}
*/
function spec328OperationsHubFixture(): array
{
$environmentA = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec328 Browser Environment A',
'external_id' => 'spec328-browser-environment-a',
]);
[$user, $environmentA] = createUserWithTenant(
tenant: $environmentA,
role: 'owner',
workspaceRole: 'owner',
);
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'name' => 'Spec328 Browser Environment B',
'external_id' => 'spec328-browser-environment-b',
]);
createUserWithTenant(
tenant: $environmentB,
user: $user,
role: 'owner',
workspaceRole: 'owner',
);
OperationRun::factory()->forTenant($environmentA)->create([
'type' => 'inventory_sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Blocked->value,
'context' => [
'reason_code' => 'write_gate_blocked',
'raw_payload' => 'raw payload should stay hidden',
'stack_trace' => 'stack trace should stay hidden',
'provider_secret' => 'provider secret should stay hidden',
'debug_metadata' => 'debug metadata should stay hidden',
'internal_exception' => 'internal exception should stay hidden',
],
'completed_at' => null,
]);
foreach (range(1, 4) as $index) {
OperationRun::factory()->forTenant($environmentA)->create([
'type' => 'backup.schedule.execute',
'status' => OperationRunStatus::Completed->value,
'outcome' => $index % 2 === 0
? OperationRunOutcome::Blocked->value
: OperationRunOutcome::Failed->value,
'completed_at' => null,
]);
}
OperationRun::factory()->forTenant($environmentB)->create([
'type' => 'policy.sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'completed_at' => now()->subDays(2),
]);
return [$user, $environmentA, $environmentB];
}
function spec328AuthenticateOperationsHubBrowser(
mixed $test,
User $user,
ManagedEnvironment $rememberedEnvironment,
): void {
$workspaceId = (int) $rememberedEnvironment->workspace_id;
$session = [
WorkspaceContext::SESSION_KEY => $workspaceId,
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
(string) $workspaceId => (int) $rememberedEnvironment->getKey(),
],
];
$test->actingAs($user)->withSession($session);
foreach ($session as $key => $value) {
session()->put($key, $value);
}
setAdminPanelContext($rememberedEnvironment);
}
function spec328ClearEnvironmentFilter(mixed $page): mixed
{
$page->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true);
$page->script('window.location.assign(document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\').href);');
return $page;
}
function spec328OperationsHubScreenshot(string $name): string
{
return 'spec328-'.$name;
}
function spec328CopyBrowserScreenshot(string $name, ?string $targetFilename = null): void
{
$filename = spec328OperationsHubScreenshot($name).'.png';
$source = base_path('tests/Browser/Screenshots/'.$filename);
$targetDirectory = repo_path('specs/328-operations-hub-decision-first-workbench-productization/artifacts/screenshots');
$targetFilename ??= $filename;
if (! is_dir($targetDirectory)) {
@mkdir($targetDirectory, 0755, true);
}
if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
return;
}
if (! is_file($source)) {
$source = \Pest\Browser\Support\Screenshot::path($filename);
}
if (is_file($source)) {
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$targetFilename);
}
}

View File

@ -27,9 +27,12 @@ public function test_shows_workspace_operations_kpi_stats_when_environment_shell
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(\App\Support\OperationRunLinks::index())
->assertOk()
->assertSee('Total Operations (30 days)')
->assertSee('Active Operations')
->assertSee('Failed/Partial (7 days)')
->assertSee('Avg Duration (7 days)');
->assertSee('Needs attention')
->assertSee('Active operations')
->assertSee('Failed or blocked')
->assertSee('Completed recently')
->assertDontSee('Total Operations (30 days)')
->assertDontSee('Failed/Partial (7 days)')
->assertDontSee('Avg Duration (7 days)');
}
}

View File

@ -29,6 +29,15 @@ function visiblePageText(TestResponse $response): string
return trim((string) preg_replace('/\s+/', ' ', strip_tags($html)));
}
function firstSectionHeadingPosition(TestResponse $response, string $heading): int|false
{
$html = (string) $response->getContent();
return preg_match('/fi-section-header-heading[^>]*>\s*'.preg_quote($heading, '/').'\s*</', $html, $matches, PREG_OFFSET_CAPTURE) === 1
? $matches[0][1]
: false;
}
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
@ -145,7 +154,6 @@ function baselineCompareGapContext(array $overrides = []): array
->and($relatedContextPosition)->not->toBeFalse()
->and($countDiagnosticsPosition)->not->toBeFalse()
->and($identityHashPosition)->not->toBeFalse()
->and($policySyncPosition)->toBeLessThan($decisionPosition)
->and($decisionPosition)->toBeLessThan($timingPosition)
->and($timingPosition)->toBeLessThan($metadataPosition)
->and($metadataPosition)->toBeLessThan($relatedContextPosition)
@ -225,10 +233,10 @@ function baselineCompareGapContext(array $overrides = []): array
->assertSee('Decision')
->assertSee('Related context');
$pageText = visiblePageText($response);
$html = (string) $response->getContent();
$bannerPosition = mb_strpos($pageText, 'Current environment context differs from this operation');
$decisionPosition = mb_strpos($pageText, 'Decision');
$bannerPosition = mb_strpos($html, 'Current environment context differs from this operation');
$decisionPosition = firstSectionHeadingPosition($response, 'Decision');
expect($bannerPosition)->not->toBeFalse()
->and($decisionPosition)->not->toBeFalse()

View File

@ -229,7 +229,7 @@ function operationRunFilterIndicatorLabels($component): array
]);
});
it('clears tenant-sensitive persisted filters when the canonical tenant context changes', function (): void {
it('keeps operations filters workspace-scoped when remembered environment context changes', function (): void {
$tenantA = ManagedEnvironment::factory()->create();
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner');
@ -275,7 +275,7 @@ function operationRunFilterIndicatorLabels($component): array
]);
Livewire::test(Operations::class)
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
->assertSet('tableFilters.type.value', null)
->assertSet('tableFilters.initiator_name.value', null);
->assertSet('tableFilters.environment_id.value', null)
->assertSet('tableFilters.type.value', 'policy.sync')
->assertSet('tableFilters.initiator_name.value', 'Alpha');
});

View File

@ -72,7 +72,7 @@ function monitoringPageStateFieldSummary(array $contract): array
'stateFields' => [
'event' => ['stateClass' => 'inspect', 'queryRole' => 'durable_restorable', 'shareable' => true, 'restorableOnRefresh' => true],
'supportAccess' => ['stateClass' => 'contextual_prefilter', 'queryRole' => 'durable_restorable', 'shareable' => true, 'restorableOnRefresh' => true],
'managed_environment_id' => ['stateClass' => 'contextual_prefilter', 'queryRole' => 'durable_restorable', 'shareable' => false, 'restorableOnRefresh' => true],
'environment_id' => ['stateClass' => 'contextual_prefilter', 'queryRole' => 'durable_restorable', 'shareable' => true, 'restorableOnRefresh' => true],
'tableSearch' => ['stateClass' => 'shareable_restorable', 'queryRole' => 'unsupported', 'shareable' => false, 'restorableOnRefresh' => true],
],
],

View File

@ -197,8 +197,8 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(OperationRunLinks::index($tenant, $context))
->assertOk()
->assertSee('Monitoring landing')
->assertSee('Return path')
->assertSee('Operations Hub')
->assertSee('Which operation needs attention now?')
->assertSee('Back to backup set')
->assertSee('/admin/tenant/backup-sets/1', false);
});

View File

@ -26,10 +26,9 @@
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(\App\Support\OperationRunLinks::index())
->assertOk()
->assertDontSee('Total Operations (30 days)')
->assertDontSee('Active Operations')
->assertDontSee('Failed/Partial (7 days)')
->assertDontSee('Avg Duration (7 days)')
->assertSee('Operations Hub')
->assertSee('Which operation needs attention now?')
->assertSee('Operations history')
->assertSee('All')
->assertSee('Active')
->assertSee('Likely stale')

View File

@ -12,7 +12,7 @@
uses(RefreshDatabase::class);
it('renders the operations landing as a quiet monitoring surface', function (): void {
it('renders the operations hub as a decision-first monitoring surface', function (): void {
$tenant = ManagedEnvironment::factory()->create();
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
@ -28,12 +28,11 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(route('admin.operations.index', ['workspace' => $tenant->workspace]))
->assertOk()
->assertSee('Monitoring landing')
->assertSee('Tabs, filters, and row inspection define the active work lane.')
->assertSee('Scope context')
->assertSee('Scope reset')
->assertSee('Inspect flow')
->assertSee(__('localization.shell.show_all_environments'));
->assertSee('Operations Hub')
->assertSee('Execution follow-up workbench')
->assertSee('Which operation needs attention now?')
->assertSee('Operations history')
->assertSee('Open operation');
});
it('surfaces canonical return context separately from the operations work lane', function (): void {
@ -54,9 +53,9 @@
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
->get(OperationRunLinks::index($tenant, $context))
->assertOk()
->assertSee('Monitoring landing')
->assertSee('Return path')
->assertSee('Operations Hub')
->assertSee('Which operation needs attention now?')
->assertSee('Back to backup set')
->assertSee('/admin/tenant/backup-sets/1', false)
->assertSee('Inspect flow');
});
->assertSee('Operations history');
});

View File

@ -0,0 +1,329 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Monitoring\Operations;
use App\Filament\Widgets\Operations\OperationsWorkbenchStats;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Workspaces\WorkspaceContext;
use Livewire\Livewire;
it('documents the Spec 328 operations hub repo truth map', function (): void {
$path = repo_path('specs/328-operations-hub-decision-first-workbench-productization/repo-truth-map.md');
expect($path)->toBeFile();
$contents = (string) file_get_contents($path);
expect($contents)
->toContain('Required Data Areas')
->toContain('OperationRun table')
->toContain('OperationRun problem classes')
->toContain('Workspace / Environment filter state')
->toContain('Diagnostics');
});
it('renders the Spec 328 decision-first workbench for the highest-priority operation', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec328 Environment Alpha',
]);
[$user, $environment] = createUserWithTenant($environment, role: 'owner', workspaceRole: 'owner');
OperationRun::factory()->forTenant($environment)->create([
'type' => 'policy.sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'completed_at' => now()->subDays(2),
]);
OperationRun::factory()->forTenant($environment)->create([
'type' => 'inventory_sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Blocked->value,
'context' => [
'reason_code' => 'write_gate_blocked',
'raw_payload' => 'raw payload should stay hidden',
'stack_trace' => 'stack trace should stay hidden',
'provider_secret' => 'provider secret should stay hidden',
'debug_metadata' => 'debug metadata should stay hidden',
'internal_exception' => 'internal exception should stay hidden',
],
'completed_at' => null,
]);
foreach (range(1, 4) as $index) {
OperationRun::factory()->forTenant($environment)->create([
'type' => 'backup.schedule.execute',
'status' => OperationRunStatus::Completed->value,
'outcome' => $index % 2 === 0
? OperationRunOutcome::Blocked->value
: OperationRunOutcome::Failed->value,
'completed_at' => null,
]);
}
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
->get(OperationRunLinks::index(workspace: $environment->workspace))
->assertOk()
->assertSee('Operations Hub')
->assertSee('Execution follow-up workbench')
->assertSee('Which operation needs attention now?')
->assertSee('Decision workbench')
->assertSee('Needs attention')
->assertSee('data-testid="operations-workbench-stat-needs-attention"', false)
->assertSee('data-stat-key="needs-attention"', false)
->assertSee('data-stat-value="5"', false)
->assertSee('data-stat-color="warning"', false)
->assertSee('Failed, blocked, partial, or stale OperationRuns in scope.')
->assertSee('data-testid="operations-workbench-stat-active-operations"', false)
->assertSee('data-stat-key="active-operations"', false)
->assertSee('data-stat-color="info"', false)
->assertSee('data-testid="operations-workbench-stat-failed-or-blocked"', false)
->assertSee('data-stat-key="failed-or-blocked"', false)
->assertSee('data-stat-color="danger"', false)
->assertSee('data-testid="operations-workbench-stat-completed-recently"', false)
->assertSee('data-stat-key="completed-recently"', false)
->assertSee('data-stat-color="success"', false)
->assertSee('data-stat-value="0"', false)
->assertSee('Recent execution results, not environment or governance health.')
->assertSee('Inventory sync')
->assertSee('Outcome')
->assertSee('Blocked')
->assertSee('Reason')
->assertSee('Impact')
->assertSee('Environment: Spec328 Environment Alpha')
->assertSee('Proof')
->assertSee('Operation detail available')
->assertSee('Next action')
->assertSee('Open operation')
->assertSee('Operation summary')
->assertSee('Operations history')
->assertSee('Secondary context for scanning OperationRun history')
->assertSee('Diagnostics')
->assertSee('Collapsed')
->assertDontSee('raw payload should stay hidden')
->assertDontSee('stack trace should stay hidden')
->assertDontSee('provider secret should stay hidden')
->assertDontSee('debug metadata should stay hidden')
->assertDontSee('internal exception should stay hidden')
->assertDontSee('Total Operations')
->assertDontSee('Avg Duration')
->assertDontSee('sparkline')
->assertDontSee('trend')
->assertDontSee('wire:poll', false)
->assertDontSee('tenant filter')
->assertDontSee('current tenant')
->assertDontSee('entitled tenant')
->assertDontSee('all tenants');
});
it('renders native Operations Workbench Stat labels and scoped values', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec328 Native Stats Environment',
]);
[$user, $environment] = createUserWithTenant($environment, role: 'owner', workspaceRole: 'owner');
foreach (range(1, 5) as $index) {
OperationRun::factory()->forTenant($environment)->create([
'type' => $index === 1 ? 'inventory_sync' : 'backup.schedule.execute',
'status' => OperationRunStatus::Completed->value,
'outcome' => $index % 2 === 0
? OperationRunOutcome::Blocked->value
: OperationRunOutcome::Failed->value,
'completed_at' => null,
]);
}
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id);
Livewire::test(OperationsWorkbenchStats::class)
->assertSee('Needs attention')
->assertSee('Failed, blocked, partial, or stale OperationRuns in scope.')
->assertSee('data-testid="operations-workbench-stat-needs-attention"', false)
->assertSee('data-stat-value="5"', false)
->assertSee('data-stat-color="warning"', false)
->assertSee('Active operations')
->assertSee('Queued or running records with trusted progress only.')
->assertSee('data-testid="operations-workbench-stat-active-operations"', false)
->assertSee('data-stat-value="0"', false)
->assertSee('data-stat-color="info"', false)
->assertSee('Failed or blocked')
->assertSee('Terminal execution records that need review before retrying.')
->assertSee('data-testid="operations-workbench-stat-failed-or-blocked"', false)
->assertSee('data-stat-color="danger"', false)
->assertSee('Completed recently')
->assertSee('Recent execution results, not environment or governance health.')
->assertSee('data-testid="operations-workbench-stat-completed-recently"', false)
->assertSee('data-stat-color="success"', false)
->assertDontSee('Total Operations')
->assertDontSee('Avg Duration')
->assertDontSee('sparkline')
->assertDontSee('trend')
->assertDontSee('wire:poll', false);
});
it('renders an honest no-attention state without false health claims', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec328 Empty Environment',
]);
[$user, $environment] = createUserWithTenant($environment, role: 'owner', workspaceRole: 'owner');
OperationRun::factory()->forTenant($environment)->create([
'type' => 'policy.sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'completed_at' => now()->subMinutes(10),
]);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
->get(OperationRunLinks::index(workspace: $environment->workspace))
->assertOk()
->assertSee('Which operation needs attention now?')
->assertSee('No operations need attention')
->assertSee('This is execution follow-up only, not an environment health claim.')
->assertSee('Operation detail available')
->assertDontSee('environment is healthy')
->assertDontSee('governance health is complete')
->assertDontSee('customer-safe evidence readiness');
});
it('renders trusted active progress only for active counted runs', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec328 Progress Environment',
]);
[$user, $environment] = createUserWithTenant($environment, role: 'owner', workspaceRole: 'owner');
OperationRun::factory()->forTenant($environment)->create([
'type' => 'inventory_sync',
'status' => OperationRunStatus::Running->value,
'outcome' => OperationRunOutcome::Pending->value,
'summary_counts' => [
'processed' => 3,
'total' => 4,
],
'started_at' => now()->subMinute(),
]);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
->get(OperationRunLinks::index(workspace: $environment->workspace))
->assertOk()
->assertSee('Active operation')
->assertSee('Progress')
->assertSee('3 / 4 processed (75%)');
});
it('does not show progress bars for terminal outcomes', function (): void {
$environment = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec328 Terminal Environment',
]);
[$user, $environment] = createUserWithTenant($environment, role: 'owner', workspaceRole: 'owner');
OperationRun::factory()->forTenant($environment)->create([
'type' => 'inventory_sync',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value,
'summary_counts' => [
'processed' => 3,
'total' => 4,
],
'completed_at' => now()->subMinute(),
]);
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
->get(OperationRunLinks::index(workspace: $environment->workspace))
->assertOk()
->assertSee('Outcome guidance')
->assertSee('Failed')
->assertDontSee('3 / 4 processed (75%)');
});
it('supports canonical environment filtering and keeps history scoped', function (): void {
$environmentA = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec328 Environment A',
]);
[$user, $environmentA] = createUserWithTenant($environmentA, role: 'owner', workspaceRole: 'owner');
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'name' => 'Spec328 Environment B',
]);
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner');
$runA = OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
$runB = OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
Livewire::withQueryParams(['environment_id' => (int) $environmentA->getKey()])
->actingAs($user)
->test(Operations::class)
->assertSee('Environment filter:')
->assertSee('Spec328 Environment A')
->assertCanSeeTableRecords([$runA])
->assertCanNotSeeTableRecords([$runB]);
});
it('rejects legacy environment aliases on Operations Hub', function (array $query): void {
$environmentA = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec328 Alias Environment A',
]);
[$user, $environmentA] = createUserWithTenant($environmentA, role: 'owner', workspaceRole: 'owner');
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'name' => 'Spec328 Alias Environment B',
]);
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner');
$runA = OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
$runB = OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
Livewire::withQueryParams($query)
->actingAs($user)
->test(Operations::class)
->assertDontSee('Environment filter:')
->assertCanSeeTableRecords([$runA, $runB]);
})->with([
'tenant' => fn (): array => ['tenant' => '1'],
'tenant_id' => fn (): array => ['tenant_id' => 1],
'managed_environment_id' => fn (): array => ['managed_environment_id' => 1],
'environment' => fn (): array => ['environment' => '1'],
'tenant_scope' => fn (): array => ['tenant_scope' => 'environment'],
'tableFilters' => fn (): array => [
'tableFilters' => [
'managed_environment_id' => ['value' => '1'],
],
],
]);
it('rejects cross-workspace environment filters as not found', function (): void {
$environment = ManagedEnvironment::factory()->active()->create();
[$user, $environment] = createUserWithTenant($environment, role: 'owner', workspaceRole: 'owner');
$foreignEnvironment = ManagedEnvironment::factory()->active()->create();
createUserWithTenant(tenant: $foreignEnvironment, user: $user, role: 'owner', workspaceRole: 'owner');
$this->actingAs($user)
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
->get(route('admin.operations.index', [
'workspace' => $environment->workspace,
'environment_id' => (int) $foreignEnvironment->getKey(),
]))
->assertNotFound();
});

View File

@ -68,7 +68,7 @@
expect(OperationRunLinks::index($tenant, activeTab: 'active'))
->toBe(route('admin.operations.index', [
'workspace' => $workspaceId,
'managed_environment_id' => (int) $tenant->getKey(),
'environment_id' => (int) $tenant->getKey(),
'activeTab' => 'active',
]))
->and(OperationRunLinks::index(
@ -78,7 +78,7 @@
))
->toBe(route('admin.operations.index', [
'workspace' => $workspaceId,
'managed_environment_id' => (int) $tenant->getKey(),
'environment_id' => (int) $tenant->getKey(),
'activeTab' => OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION,
'problemClass' => OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION,
]))
@ -89,7 +89,7 @@
))
->toBe(route('admin.operations.index', [
'workspace' => $workspaceId,
'managed_environment_id' => (int) $tenant->getKey(),
'environment_id' => (int) $tenant->getKey(),
'activeTab' => OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
'problemClass' => OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP,
]));
@ -101,7 +101,7 @@
expect(OperationRunLinks::index($tenant, operationType: 'inventory_sync'))
->toBe(route('admin.operations.index', [
'workspace' => (int) $tenant->workspace_id,
'managed_environment_id' => (int) $tenant->getKey(),
'environment_id' => (int) $tenant->getKey(),
'tableFilters' => [
'type' => [
'value' => 'inventory.sync',

View File

@ -200,8 +200,8 @@
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
])->get(route('admin.operations.index', ['workspace' => (int) $tenant->workspace_id]))
->assertOk()
->assertSee('Monitoring landing')
->assertSee('Scope context')
->assertSee('Operations Hub')
->assertSee('Which operation needs attention now?')
->assertDontSee('Scope reset');
$this->withSession([

View File

@ -159,6 +159,7 @@
it('classifies aggregate multi-run work as composite fallback', function (): void {
$run = OperationRun::factory()->create([
'type' => 'policy.sync',
'status' => 'running',
'outcome' => 'pending',
'summary_counts' => [
@ -218,4 +219,4 @@
->and($progress['display'])->toBe('indeterminate')
->and($progress['label'])->toBe('Review composition is aggregating 4 operations. 1 failed operation currently needs review.')
->and($progress['percent'])->toBeNull();
});
});

View File

@ -0,0 +1,59 @@
# Requirements Checklist: Spec 328 - Operations Hub Decision-First Workbench Productization
**Purpose**: Validate preparation artifact quality before implementation.
**Created**: 2026-05-18
**Feature**: `specs/328-operations-hub-decision-first-workbench-productization/spec.md`
## Content Quality
- [x] No implementation details leak into product requirements beyond required repo constraints.
- [x] User value and operator workflow are clear.
- [x] Scope is bounded to one existing runtime surface.
- [x] Non-goals explicitly prevent backend/workflow/observability overbuild.
- [x] Dependencies and historical specs are listed.
## Repo Truth And Safety
- [x] Existing route/class/view/resource/detail surfaces are named.
- [x] Repo truth map exists and uses required classifications.
- [x] No new persisted truth is proposed.
- [x] No migrations/packages/env/queues/scheduler/storage changes are expected.
- [x] No legacy tenant query alias support is allowed.
## Workspace / Environment Contract
- [x] Clean workspace-wide entry is specified.
- [x] Canonical `environment_id` filter is specified.
- [x] Visible chip and clear filter are specified.
- [x] Legacy aliases are rejected.
- [x] Cross-workspace environment guard is specified.
## OperationRun Semantics
- [x] OperationRun remains execution truth, not governance/environment health.
- [x] Terminal outcomes do not show progress.
- [x] Determinate progress is limited to trustworthy active counts.
- [x] Proof/artifact/evidence states must be repo-backed or unavailable.
- [x] Diagnostics are collapsed/hidden by default.
## RBAC / Audit / Diagnostics
- [x] Existing capabilities and policies remain authoritative.
- [x] Unauthorized action behavior is specified.
- [x] Dangerous/high-impact actions are out of scope unless spec/plan are updated.
- [x] No raw payloads/provider secrets/debug traces are default-visible.
- [x] No new audit event is required unless implementation adds mutation.
## Testability
- [x] Feature tests are listed.
- [x] Browser smoke flows are listed.
- [x] Navigation/scope guard tests are listed.
- [x] `pint --dirty` and `git diff --check` are listed.
- [x] Full-suite status must be reported honestly.
## Readiness Decision
- [x] Spec is ready for implementation planning.
- [x] No open question blocks a bounded implementation loop.
- [x] No application implementation was performed during this preparation step.

View File

@ -0,0 +1,343 @@
# Implementation Plan: Spec 328 - Operations Hub Decision-First Workbench Productization
**Branch**: `328-operations-hub-decision-first-workbench-productization` | **Date**: 2026-05-18 | **Spec**: `specs/328-operations-hub-decision-first-workbench-productization/spec.md`
**Input**: User-provided Spec 328 and repo inspection.
## Summary
Productize the existing workspace-scoped Operations Hub into a decision-first OperationRun workbench. The implementation must keep the current route and OperationRun truth, introduce no backend foundation, and make the first viewport answer:
```text
Which operation needs attention now?
```
The workbench will elevate the highest-priority operation, outcome, reason, impact, affected environment, trustworthy progress/proof state, and a single next action, while keeping the existing OperationRun table as secondary context and diagnostics collapsed.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52.0.
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Tailwind CSS 4.2.2.
**Storage**: PostgreSQL; no schema change expected.
**Testing**: Pest 4 Feature/Livewire/Browser tests.
**Validation Lanes**: confidence and browser; targeted navigation guard tests.
**Target Platform**: Laravel Sail locally; Dokploy/container deployment posture unchanged.
**Project Type**: Laravel monolith under `apps/platform`.
**Performance Goals**: DB-only page render; no Graph calls during render; no extra heavy query family beyond existing operations table/detail queries unless bounded and eager-loaded.
**Constraints**: No new persisted truth, migration, package, queue/scheduler/storage/env change, deployment asset, compatibility route, or legacy alias support.
**Scale/Scope**: One existing Filament page, its Blade view, existing OperationRun table/resource helpers, feature tests, browser smoke, and spec artifacts.
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed existing operator-facing strategic surface.
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
- `/admin/workspaces/{workspace}/operations`
- `/admin/workspaces/{workspace}/operations/{run}` only as linked canonical detail context
- `apps/platform/app/Filament/Pages/Monitoring/Operations.php`
- `apps/platform/resources/views/filament/pages/monitoring/operations.blade.php`
- `apps/platform/app/Filament/Resources/OperationRunResource.php`
- `apps/platform/app/Filament/Widgets/Operations/OperationsKpiHeader.php` only if summary card/KPI wording must be adjusted
- **No-impact class, if applicable**: N/A.
- **Native vs custom classification summary**: Native Filament page/table plus existing Blade composition; no new UI framework.
- **Shared-family relevance**: status messaging, OperationRun links, progress contract, proof/artifact links, workspace/environment filter chip, diagnostics disclosure.
- **State layers in scope**: page payload, URL query (`environment_id`, `activeTab`, `problemClass`), table filters/session state, highest-priority operation selection, diagnostics disclosure.
- **Audience modes in scope**: operator-MSP, manager, support reviewer, auditor.
- **Decision/diagnostic/raw hierarchy plan**: decision-first, proof/artifact second, diagnostics/support raw third.
- **Raw/support gating plan**: collapsed by default and capability-gated where existing capabilities support access.
- **One-primary-action / duplicate-truth control**: selected/highest-priority operation owns one primary next action; table rows and related links remain secondary.
- **Handling modes by drift class or surface**: review-mandatory for UI-003 strategic surface; document-in-feature for any UI coverage registry no-change decision.
- **Repository-signal treatment**: Spec 325 target image is visual direction only; runtime claims must be repo-verified or unavailable.
## Constitution Check
- **Inventory-first, snapshots-second**: N/A, no Graph, inventory, snapshot, backup, or restore data model change.
- **Read/write separation by default**: Page remains read-first. Any unexpected mutation requires spec/plan update, confirmation, authorization, audit, notification, and tests.
- **Single Contract Path to Graph**: No Graph calls may be added.
- **Deterministic Capabilities**: Reuse existing `OperationRunPolicy`, `OperationRunCapabilityResolver`, source policies, and capability resolvers.
- **Proportionality / anti-bloat**: No new source of truth, persisted entity, enum/status family, public abstraction, priority engine, or cross-domain UI framework.
- **Workspace isolation**: Clean workspace URL stays workspace-wide; `environment_id` is resolved through current workspace and actor entitlement.
- **Tenant/environment language**: Runtime copy must avoid tenant as platform context; provider-specific tenant wording only where explicitly provider-bound.
- **OperationRun UX**: Presentation/link semantics only. No operation start, queueing, lifecycle transition, notification, or summary-count writer changes.
- **Progress contract**: `OperationRunProgressContract` remains authoritative. Terminal runs show outcome guidance, not progress.
- **UI-COV-001**: Existing strategic surface UI-003 changes; active spec package carries repo-truth map, tests, and browser screenshots. Implementation close-out decides whether route inventory/coverage matrix updates are needed.
- **TEST-GOV-001**: Targeted Feature and Browser tests are explicit; no broad heavy-governance lane unless implementation reveals structural risk.
## Current Repo Truth Summary
Existing verified surfaces:
- `Operations` is a Filament `Page` rendered by `admin.operations.index`.
- `admin.operations.index` is `/admin/workspaces/{workspace}/operations`.
- The view currently renders `Monitoring landing`, scope/return/reset cards, tabs, lifecycle warning, and the OperationRun table.
- `OperationRunResource::table()` owns columns, row detail URL, filters, empty state, status/outcome badges, and no row/bulk actions.
- `OperationRun` owns `status`, `outcome`, problem classes, freshness/lifecycle helpers, workspace and managed-environment scope.
- `OperationRunProgressContract` already separates active counted/activity/phased/composite/none progress and returns `none` for terminal runs.
- `TenantlessOperationRunViewer` and `OperationRunResource::infolist()` already provide richer run detail, related links, proof/artifact sections, support diagnostics, and technical sections.
- `OperationRunLinks` provides canonical index/detail URLs and related resource links.
- `WorkspaceHubEnvironmentFilter::fromRequest()` accepts canonical `environment_id`, scopes to current workspace, and rejects inaccessible/cross-workspace IDs.
- Existing tests cover canonical operations URLs, workspace hub clean entry, environment filter behavior, detail access, DB-only rendering, and OperationRun authorization.
Known current productization gap:
- The page is still monitoring/table-first. It does not yet consistently promote an attention-needed operation with outcome, reason, impact, environment, progress/proof state, and one primary next action ahead of the table.
## Existing Repository Surfaces Likely Affected
Runtime files, only during later implementation:
- `apps/platform/app/Filament/Pages/Monitoring/Operations.php`
- `apps/platform/resources/views/filament/pages/monitoring/operations.blade.php`
- `apps/platform/app/Filament/Resources/OperationRunResource.php`
- `apps/platform/app/Filament/Widgets/Operations/OperationsKpiHeader.php` remains existing widget code, but it must not render before the Spec 328 workbench when the decision-first summary cards own the first viewport.
- `apps/platform/resources/lang/en/*` and `apps/platform/resources/lang/de/*` only if current project pattern requires localized strings for new stable copy.
Tests, only during later implementation:
- `apps/platform/tests/Feature/Monitoring/MonitoringOperationsTest.php`
- `apps/platform/tests/Feature/Monitoring/OperationsWorkspaceHubContractTest.php`
- `apps/platform/tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php`
- `apps/platform/tests/Feature/Monitoring/OperationsHeaderHierarchyTest.php`
- `apps/platform/tests/Feature/Monitoring/OperationsTenantScopeTest.php`
- `apps/platform/tests/Feature/Monitoring/OperationLifecycleFreshnessPresentationTest.php`
- `apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
- `apps/platform/tests/Feature/Operations/OperationRunBlockedExecutionPresentationTest.php`
- `apps/platform/tests/Unit/Support/OpsUx/OperationRunProgressContractTest.php`
- `apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php`
- `apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php`
- `apps/platform/tests/Browser/Spec328OperationsHubProductizationSmokeTest.php`
Spec/UI artifacts:
- `specs/328-operations-hub-decision-first-workbench-productization/repo-truth-map.md`
- screenshot artifacts under `specs/328-operations-hub-decision-first-workbench-productization/artifacts/screenshots/`
- optional UI coverage registry updates only if implementation materially changes route/archetype/coverage state.
## Domain / Model Implications
- No new model, table, migration, enum, status family, source of truth, or persisted display state.
- Workbench item state must derive from existing OperationRun truth:
- `status`: queued, running, completed.
- `outcome`: pending, succeeded, partially_succeeded, blocked, failed, reserved cancelled.
- `problemClass()`: active stale attention, terminal follow-up, none.
- `freshnessState()` and lifecycle reconciliation payload.
- `summary_counts` through `OperationRunProgressContract` and `SummaryCountsNormalizer`.
- `OperationCatalog::label()` for operation type labels.
- `managed_environment_id` / `tenant` relation for affected environment.
- `started_at`, `completed_at`, `created_at` for timing.
- `context`, `failure_summary`, and related models only behind safe proof/diagnostic paths.
- If exact reason, impact, artifact, evidence, or next action is missing, render explicit unavailable/missing state.
## UI / Filament Implications
- Filament v5 and Livewire v4.0+ compliance must be preserved.
- Panel providers remain in `apps/platform/bootstrap/providers.php`; no panel provider changes expected.
- No globally searchable resource is added or changed. `OperationRunResource` remains `protected static bool $isGloballySearchable = false`.
- The layout should use Filament sections/cards/badges/buttons and Tailwind utility classes consistent with existing pages; no heavy one-off CSS.
- Header must stay short:
- `Operations Hub`
- workspace label
- scope label: workspace-wide or filtered environment
- visible filter chip and clear action when filtered
- subtitle explaining execution truth and follow-up
- Main workbench must render before the operations table.
- Summary cards should be compact and action-relevant.
- Follow-up summary-card alignment: the four workbench summary cards should use native Filament `StatsOverviewWidget` / `Stat` rendering while remaining execution/attention signals. Use title, large value, concise subline, optional description/status icon, and semantic `Stat::color(...)`. Do not reintroduce custom Blade/Tailwind stat-card markup, explicit custom accent bars, generic `Total Operations` / `Avg Duration` cards, fake trends, fake sparklines, decorative charts, or health claims.
- Card emphasis should reflect execution risk: `Needs attention` strongest when actionable, `Active operations` neutral when zero, `Failed or blocked` danger/warning, and `Completed recently` muted success/secondary.
- Right-side detail/proof panel should be desktop aside and mobile stack.
- Diagnostics must be collapsed by default.
## Livewire / Page State Implications
- Existing internal methods/properties may keep legacy names only as implementation detail; runtime URL/filter language must be `environment_id` and `Environment`.
- Existing `activeTab` and `problemClass` query filters can remain if they do not conflict with `environment_id` contract.
- Highest-priority operation selection should be deterministic on page load. If interactive selection is implemented, it must not introduce persisted state or break reload/back/forward behavior.
- Clear filter must remove `environment_id` and any environment-like table/session filter state through existing helpers.
## OperationRun / Monitoring Implications
- No new `OperationRun` creation or lifecycle transition.
- No queued/running/terminal DB notification changes.
- No new summary-count writer, progress key, operation type, status, or outcome.
- Any operation proof link must use existing `OperationRunLinks`, `OperationRunResource`, related routes, and authorization.
- Raw `OperationRun.context`, `failure_summary`, stack traces, and payload-like data must not be default-visible.
## RBAC / Policy Implications
Reuse existing authorization:
- Workspace page access through current workspace membership checks and `WorkspaceCapabilityResolver::isMember()`.
- Environment access through `ManagedEnvironmentAccessScopeResolver` and current accessible environments.
- Run visibility through `OperationRunPolicy`.
- Run capability mapping through `OperationRunCapabilityResolver`.
- Evidence and artifacts through existing source policies/capabilities where links are shown.
- Support diagnostics through `Capabilities::SUPPORT_DIAGNOSTICS_VIEW` where diagnostics are exposed.
- Existing detail actions such as support diagnostics, support request, resume capture, retry/resume, or related links remain owned by the detail/source surfaces.
No new permission semantics should be added unless implementation proves existing capabilities cannot express the action and spec/plan are updated first.
## Audit / Evidence / Disclosure Implications
- No new audit event is required for read-only page rendering unless current page-open audit conventions are extended repo-wide.
- Evidence/proof should appear as state:
- operation record available
- artifact link available
- evidence linked
- proof unavailable
- diagnostics hidden
- Do not show raw provider payloads, debug metadata, internal exception traces, provider secrets, raw OperationRun payloads, or stack traces by default.
- If diagnostics disclosure is present, it must be collapsed and capability-aware.
## Data / Migration Implications
Expected outcome:
- No migrations.
- No seeders.
- No data backfills.
- No packages.
- No env vars.
- No queues/scheduler/storage changes.
- No deployment asset changes.
- No backwards compatibility layer.
- No legacy tenant query alias support.
If implementation discovers an actual schema need, stop and update spec/plan/tasks/repo-truth-map first. Default decision remains no migration.
## Localization / Copy Implications
- Runtime copy must be concise and operator-safe.
- Avoid platform-context `tenant` wording. Use `Workspace` and `Environment` for shell/filter/product context.
- Provider-bound tenant wording may remain only when describing an external Microsoft/Entra tenant identifier or provider payload outside the default decision view.
- Add EN/DE localization only if the surrounding files already route stable page copy through language files; otherwise keep localized scope as implementation-local and document the decision.
## Implementation Phases
### Phase 1 - Repo Truth And Current UI Audit
- Re-read this spec, plan, tasks, and `repo-truth-map.md`.
- Inspect current `Operations`, Blade view, `OperationRunResource`, `TenantlessOperationRunViewer`, progress contract, links, policies, and tests.
- Update `repo-truth-map.md` before runtime changes if implementation discovers new source truth or gaps.
- Confirm no migration/package/env/queue/storage/deployment asset need.
### Phase 2 - Tests First
- Add tests for repo truth map existence.
- Add Feature/Livewire/HTTP tests for decision-first workbench, non-empty attention state, empty/no-attention state, right proof panel, table secondary context, diagnostics hidden, progress/terminal semantics, RBAC action visibility, environment filter, legacy aliases, cross-workspace guard, and tenant-copy guard.
### Phase 3 - Layout Productization
- Refactor the existing page into:
- header/scope
- main operations workbench
- compact execution summary cards
- right operation/proof panel
- operations queue/history table
- collapsed diagnostics disclosure
- Keep existing tabs/table filters and canonical row links.
### Phase 4 - Data Binding
- Bind workbench and panel to repo-verified sources.
- Render unavailable states for missing reason, impact, proof, artifact, evidence, or environment data.
- Do not create synthetic success, health, artifact, or governance claims.
### Phase 5 - Progress / Outcome Semantics
- Use existing progress contract semantics.
- Show determinate progress only when active counted progress is trustworthy.
- Show activity/status only for active uncounted runs.
- Show outcome guidance for terminal failed/blocked/partial/succeeded runs.
- Do not show terminal progress bars.
### Phase 6 - Action Hierarchy And RBAC
- Show one primary next action for the selected/highest-priority operation.
- Link only to existing, authorized operation/detail/artifact/evidence/source routes/actions.
- Keep rare/debug/source actions secondary.
- Do not introduce destructive actions.
### Phase 7 - Scope / Filter Integration
- Preserve clean workspace-wide entry.
- Preserve `?environment_id=` filter, visible chip, clear filter, reload/back/forward behavior.
- Preserve legacy alias rejection and cross-workspace guard.
### Phase 8 - Browser Smoke And Screenshots
- Add targeted Browser smoke for clean, filtered, clear/reload, non-empty workbench, empty/no-attention state, right detail/proof panel, diagnostics hidden, table secondary, light mode if feasible, and no platform-context tenant wording.
- Save screenshots under the spec artifacts path when generated.
### Phase 9 - Validation And Close-Out
- Run targeted Feature/navigation tests, Browser smoke, filtered guard tests, `pint --dirty`, and `git diff --check`.
- Report full suite status honestly if not run.
- Record no migrations/seeders/packages/env/queues/scheduler/storage/deployment asset/backcompat/legacy alias support.
## Testing Strategy
Required tests:
- `it('documents_operations_hub_repo_truth_map')`
- `it('renders_operations_hub_decision_first_workbench')`
- `it('renders_highest_priority_operation_needing_attention')`
- `it('renders_operations_hub_empty_attention_state')`
- `it('renders_operations_hub_operation_detail_proof_panel')`
- `it('keeps_operations_table_available_as_secondary_context')`
- `it('operations_hub_hides_raw_diagnostics_by_default')`
- `it('operations_hub_does_not_show_progress_for_terminal_outcomes')`
- `it('operations_hub_uses_determinate_progress_only_with_trustworthy_counts')`
- `it('operations_hub_supports_canonical_environment_filter')`
- `it('operations_hub_rejects_legacy_environment_aliases')`
- `it('operations_hub_rejects_cross_workspace_environment_filter')`
- `it('operations_hub_does_not_use_tenant_as_platform_context_copy')`
- `it('operations_hub_respects_capabilities_for_primary_actions')`
Required Browser smoke:
- `tests/Browser/Spec328OperationsHubProductizationSmokeTest.php`
Browser flows:
1. Clean workspace Operations Hub.
2. Environment-filtered Operations Hub.
3. Clear filter and reload.
4. Non-empty highest-priority operation needing attention.
5. Empty/no-attention state.
6. Right operation/proof panel visible.
7. Diagnostics hidden by default.
8. Operations table/history remains secondary.
9. No platform-context tenant wording.
10. Light mode readability if supported.
## Rollout Considerations
- No feature flag expected.
- No schema rollout.
- No queue or scheduler rollout.
- No env vars.
- No new Filament asset registration expected. If implementation registers assets unexpectedly, deployment must include `cd apps/platform && php artisan filament:assets`, but the expected path is no asset change.
- Staging validation should run the targeted feature/navigation/browser lanes before production promotion.
## Risk Controls
- Use `repo-truth-map.md` as the runtime claim gate.
- Keep priority derived and feature-local.
- Keep OperationRun detail/source surfaces as owners of deep diagnostics and mutations.
- Keep progress semantics delegated to the existing progress contract.
- Keep environment filtering delegated to existing workspace hub helpers.
- Keep all raw/support information collapsed and capability-aware.
- Keep final report explicit about unavailable states and full-suite status.
## Implementation Close-Out
- Implemented a page-local decision workbench in `Operations::decisionWorkbench()` and `operations.blade.php`.
- Follow-up: replaced the custom summary-card Blade loop with a narrow native `OperationsWorkbenchStats` Filament `StatsOverviewWidget` using four `Stat::make(...)` records, descriptions, description icons, semantic colors, disabled polling, and scoped OperationRun queries.
- Preserved existing route, table, tabs, row links, scope filter, and canonical detail route.
- Added focused feature coverage and a Spec 328 browser smoke with clean, filtered, clear/reload, and empty/no-attention flows.
- Adjusted canonical operation detail shell behavior so current/remembered environment context can be displayed as context while the canonical detail return action remains workspace-oriented.
- No public backend abstraction, persisted state, migration, env var, package, queue, scheduler, storage, deployment asset, or legacy alias support was added.
- Validation passed:
- `./vendor/bin/sail artisan test tests/Feature/Monitoring tests/Feature/Operations tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`
- `./vendor/bin/sail artisan test tests/Browser/Spec328OperationsHubProductizationSmokeTest.php --compact`
- `./vendor/bin/sail artisan test --filter='Operations|OperationRun|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`
- `./vendor/bin/sail pint --dirty`
- `git diff --check`

View File

@ -0,0 +1,105 @@
# Spec 328 Repo Truth Map
Status: implemented / validated
Created: 2026-05-18
Purpose: classify each Operations Hub decision-first workbench element before runtime implementation. This map is based on repository inspection only; implementation must update it before runtime changes if new source gaps appear.
## Classification Legend
- `repo-verified`: exact runtime source exists and was inspected.
- `foundation-real`: backend model/service/policy exists, but exact page binding still needs implementation verification.
- `derived from existing model`: display value can be derived from existing persisted/domain truth.
- `empty state / unavailable`: no safe source/action exists for v1; show explicit unavailable state or omit.
- `deferred future capability`: outside Spec 328 and must not be shown as live runtime truth.
## Required Data Areas
| Data area | Repo source | Preparation finding |
|---|---|---|
| Operations route | `apps/platform/routes/web.php`, route `admin.operations.index` | repo-real path is `/admin/workspaces/{workspace}/operations` |
| Operations page | `Operations` Filament page and `operations.blade.php` | repo-real current layout is decision-first workbench with secondary history table |
| OperationRun table | `OperationRunResource::table()` | repo-real status/outcome/type/timing table with filters, row URL, empty state |
| OperationRun detail | `TenantlessOperationRunViewer`, `OperationRunResource::infolist()` | repo-real detail/proof/diagnostic destination |
| OperationRun statuses | `OperationRunStatus` | repo-real `queued`, `running`, `completed` |
| OperationRun outcomes | `OperationRunOutcome` | repo-real `pending`, `succeeded`, `partially_succeeded`, `blocked`, `failed`; `cancelled` is reserved |
| OperationRun problem classes | `OperationRun::problemClass()`, scopes `activeStaleAttention()`, `terminalFollowUp()`, `dashboardNeedsFollowUp()` | repo-real for attention grouping without new priority engine |
| Summary counts | `summary_counts`, `OperationSummaryKeys`, `SummaryCountsNormalizer` | repo-real numeric keys and normalized rendering |
| Progress | `OperationRunProgressContract` | repo-real active-only progress semantics; terminal runs return `none` |
| Operation labels | `OperationCatalog::label()` | repo-real labels for canonical/legacy operation types |
| Environment relationship | `OperationRun::tenant()` relation to `ManagedEnvironment` | repo-real affected environment where present and accessible |
| Artifact/evidence links | `OperationRunLinks::related()`, `ArtifactTruthPresenter`, related resources | foundation-real/repo-real depending on operation type and linked artifact |
| Diagnostics | run detail technical sections, support diagnostics action, `failure_summary`, `context` | repo-real but default index must not expose raw data |
| RBAC | `OperationRunPolicy`, `OperationRunCapabilityResolver`, source policies/capabilities | repo-real membership, environment entitlement, capability checks |
| Workspace / Environment filter state | `WorkspaceContext`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CanonicalAdminEnvironmentFilterState`, filter chip partial | repo-real canonical `environment_id`, clear filter, alias rejection, cross-workspace guard |
## UI Element Map
| UI element | Source model/service/page | Status source | Authorization/capability | Workspace/Environment scope | OperationRun/evidence/artifact/audit link | Fallback/empty state | Classification |
|---|---|---|---|---|---|---|---|
| Operations Hub route | `admin.operations.index`, `OperationRunLinks::index()` | route + helper | workspace middleware and page access | current workspace route param | none | workspace chooser / 404 per existing middleware | repo-verified |
| Operations Hub title | `Operations` page/view | static copy to update | page access | workspace | none | static title | repo-verified |
| Workspace scope label | `OperateHubShell`, `WorkspaceContext` | current workspace/session | workspace membership | workspace shell | none | omit/404 if unavailable | repo-verified |
| Environment filter chip | `environmentFilterChip()`, shared chip partial | `WorkspaceHubEnvironmentFilter::fromRequest()` and table state | actor must access environment | `?environment_id={id}` only | none | no chip on clean URL | repo-verified |
| Clear filter action | `cleanWorkspaceHubUrl()`, shared resetter | generated clean route | page access | removes canonical filter/table state | none | omit when unfiltered | repo-verified |
| Legacy alias rejection | `WorkspaceHubFilterStateResetter`, navigation tests | forbidden query/session keys | page/source access | aliases do not set filter | none | workspace-wide view or safe 404 | repo-verified |
| Cross-workspace environment guard | `WorkspaceHubEnvironmentFilter::fromRequest()` | environment scoped by workspace and entitlement | workspace and environment entitlement | current workspace only | none | `abort(404)` / safe no-access | repo-verified |
| Main workbench question | `Operations::decisionWorkbench()` and `operations.blade.php` | static stable copy | page access | current scope | none | visible with no-attention state | repo-verified |
| Highest-priority operation | `Operations::selectedWorkbenchOperation()`, `OperationRun` problem class, outcome, status, freshness, timestamps | derived ordering from existing status/outcome/problem class | existing scoped Operations page query and OperationRun visibility | current workspace/filter | canonical detail link | `No operations need attention` | derived from existing model |
| Operation title/type | `OperationCatalog::label($run->type)` | type field | run visibility | current workspace/filter | detail link | fallback raw-safe type label | repo-verified |
| Outcome/status | `OperationRun.status`, `outcome`, badge renderer | status/outcome enums | run visibility | current workspace/filter | detail link | `Outcome unavailable` only if missing | repo-verified |
| Reason | problem class, lifecycle/freshness, outcome, `OperationUxPresenter`, `ReasonPresenter`, `failure_summary` summary where safe | derived from run truth | run/detail/source capabilities | current scope | detail link | `Reason unavailable` | derived from existing model |
| Impact | outcome, problem class, artifact truth, related artifact/review/restore context | derived from existing run/artifact truth | run/source capabilities | current scope | related links if authorized | `Impact unavailable` | derived from existing model |
| Environment | `OperationRun::tenant()` / `ManagedEnvironment` | managed environment relationship | entitlement via `OperationRunPolicy` / scope resolver | workspace and environment | related environment/resource links if existing | `Workspace-level operation` or unavailable | repo-verified |
| Timing | `created_at`, `started_at`, `completed_at`, `RunDurationInsights` | timestamps | run visibility | current scope | detail link | `Timing unavailable` | repo-verified |
| Proof/artifact state | `OperationRunLinks::related()`, `ArtifactTruthPresenter`, related resources | related route presence, artifact truth envelope | source policies/capabilities | current scope | artifact/evidence/review/backup/restore links | `Proof unavailable` or omit | foundation-real |
| Operation detail proof | `OperationRunLinks::tenantlessView()` | run id / route | `OperationRunPolicy` | current workspace/filter | canonical run detail | unavailable only if unauthorized | repo-verified |
| Progress state | `OperationRunProgressContract::forRun()` | summary counts/context/status | run visibility | current scope | detail link | no progress for terminal or unsupported | repo-verified |
| Determinate progress | `summary_counts.processed`, `summary_counts.total` | numeric trustworthy counts | run visibility | current scope | detail link | indeterminate/activity/unavailable | repo-verified |
| Terminal outcome guidance | `OperationRunResource::surfaceGuidance()`, `resolvePrimaryNextStep()`, `OperationUxPresenter` | status/outcome/problem class | run visibility | current scope | detail link/source links | generic review operation | foundation-real |
| Primary next action | `OperationRunLinks::tenantlessView()`, existing detail/source actions | route/action availability | run/source capability | current scope | operation detail link | unavailable when no selected operation | repo-verified for open operation; stronger proof links remain deferred to detail/source surfaces |
| Summary card: needs attention | `dashboardNeedsFollowUp()` / problem class scopes | stale active + terminal follow-up | scoped query | current workspace/filter | operations tab/deep link | 0 count with careful copy | repo-verified |
| Summary card: active operations | `status in queued/running` | status field | scoped query | current workspace/filter | active tab link | 0 count | repo-verified |
| Summary card: failed/blocked | completed failed/blocked outcomes | outcome field | scoped query | current workspace/filter | failed/follow-up tab link | 0 count | repo-verified |
| Summary card: follow-up required | terminal follow-up/problem class | problem class/outcome/reconciliation | scoped query | current workspace/filter | follow-up tab link | 0 count | repo-verified |
| Summary card: completed recently | completed_at within window | timestamp/outcome | scoped query | current workspace/filter | table filter/tab | 0 count | repo-verified |
| Summary card: artifact available | related artifact/evidence/link presence | related route / artifact truth | source capabilities | current workspace/filter | artifact/evidence links | unavailable if no safe aggregate | foundation-real |
| Right operation/proof panel | `operations.blade.php` over selected run payload | selected/highest run payload | run/source capabilities | current scope | operation detail link | no-attention/empty panel | repo-verified |
| Operations table/history | `OperationRunResource::table()` | table query, filters, row URL | run visibility | workspace + environment scoping | canonical detail route | existing `No operations found` | repo-verified |
| Tabs / problemClass links | `activeTab`, `problemClass`, `tabUrl()` | query/livewire state | page access | current scope | active/stale/follow-up/succeeded/partial/failed views | all tab default | repo-verified |
| Diagnostics disclosure | run detail technical sections, support diagnostics action | raw context/failure summary/support bundle | `SUPPORT_DIAGNOSTICS_VIEW` where action exposed | current scope | detail/support diagnostics only | collapsed/hidden | foundation-real |
| Raw provider payloads | raw Graph/provider payloads | not safe default | support-only future | N/A | N/A | never default-visible | deferred future capability |
| Retry/cancel/rerun action | existing detail/source actions only if present | action availability | source capability and confirmation | source scope | existing action owner | hidden/unavailable | foundation-real / deferred if unsupported |
| Platform-context tenant copy guard | runtime copy/tests | string assertions | N/A | page copy | N/A | use Workspace/Environment | repo-verified need; implementation test required |
## Required Runtime Element Decisions
| Element | v1 decision |
|---|---|
| Top summary-card rendering | native Filament `StatsOverviewWidget` / `Stat`; no custom Blade/Tailwind card component or explicit accent-bar system |
| New persisted operations workbench item | deferred future capability; do not build |
| New priority engine | deferred future capability; derive local ordering from existing status/outcome/problem class |
| New OperationRun status/outcome | deferred future capability; do not add |
| New summary-count keys | deferred future capability; do not add |
| AI operation summary | deferred future capability; do not show |
| Owner/due for operation | unavailable unless source operation context already proves it |
| Artifact/evidence where absent | explicit unavailable/missing state; do not invent |
| Green success state | allowed only for exact execution completion copy; never environment/governance health |
| Diagnostics | collapsed/hidden by default and capability-aware if exposed |
| Dangerous/mutating actions | do not add unless spec/plan updated first |
| Legacy query aliases | rejected/neutralized; do not support |
## Implementation Update Rule
If implementation discovers that a planned UI element has no safe source, no authorization path, or would require new persisted truth, the element must become `empty state / unavailable` or `deferred future capability`. Do not create backend foundation inside Spec 328 without updating `spec.md`, `plan.md`, and this map first.
## Implementation Close-Out
- Implemented the decision-first workbench on the existing `/admin/workspaces/{workspace}/operations` page.
- Selected-operation priority is derived locally from existing `OperationRun` status, outcome, problem class, timestamps, and scoped queries; no persisted priority engine was added.
- Determinate progress is delegated to `OperationRunProgressContract` and remains hidden for terminal outcomes.
- Primary next action is `OperationRunLinks::tenantlessView()` only; artifact/evidence/source-specific actions remain on existing detail/source surfaces.
- Summary cards are compact execution/attention signals backed by scoped OperationRun counts; they now render through the narrow native `OperationsWorkbenchStats` Filament `StatsOverviewWidget` using `Stat::make(...)`, descriptions, description icons, semantic colors, and disabled polling. No fake trends, sparklines, chart placeholders, custom Blade/Tailwind stat-card component, explicit custom accent bars, generic total-operation KPI, or average-duration KPI is used.
- `OperationsKpiHeader` remains available as existing widget code, but the Operations page no longer renders it above the workbench because the first viewport is owned by the decision-first summary cards.
- Diagnostics stay collapsed by default and raw provider/debug payloads are not default-visible.
- Clean workspace entry remains workspace-wide; `environment_id` remains the only accepted visible filter key.
- UI coverage registry files were not changed because the route and archetype remain the existing UI-003 Operations strategic surface; screenshots were stored under `artifacts/screenshots/`.

View File

@ -0,0 +1,572 @@
# Feature Specification: Spec 328 - Operations Hub Decision-First Workbench Productization
**Feature Branch**: `328-operations-hub-decision-first-workbench-productization`
**Created**: 2026-05-18
**Status**: Implemented / Validated
**Type**: Runtime UI productization / OperationRun workbench / execution-truth surface
**Runtime posture**: Narrow runtime UI implementation. Repo-based. No invented backend foundation.
**Input**: User-provided full Spec 328 draft.
## Dependencies And Historical Context
Depends on:
- Spec 314 - Workspace Hub Navigation Context Contract.
- Spec 315 - Environment CTA Explicit Filter Contract.
- Spec 316 - Workspace Hub Clear Filter Contract.
- Spec 317 - Legacy Tenant / Environment Context Cleanup.
- Spec 318 - Admin Surface Scope & Shell Context Audit.
- Spec 319 - Environment-Owned Surface Routing & Shell Context Contract.
- Spec 320 - Workspace-Owned Analysis Surface Registration & Shell Cutover.
- Spec 321 - Alerts / Audit Log Environment Filter Contract Decision.
- Spec 322 - Browser No-Drift Regression Guard.
- Spec 325 - Screenshot-Anchored Strategic Target Images.
- Spec 326 - Customer Review Workspace v1 Productization.
- Spec 327 - Governance Inbox Decision-First Workbench Productization.
Repo truth adjustment: the user draft allowed `/admin/operations` or the existing canonical route. Current repository truth is `admin.operations.index` at `/admin/workspaces/{workspace}/operations`, generated through `OperationRunLinks::index()`. Spec 328 productizes that existing route, `Operations` Filament page, and `OperationRunResource` table. It must not create a new operations route, operation engine, queue backend, monitoring backend, or OperationRun status family.
Spec 325 target images are visual calibration only. They are not runtime truth for counts, metrics, actions, artifact links, RBAC, evidence, or health claims.
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: Operations Hub is repo-real and execution-truth rich, but the default surface still reads as a monitoring landing plus chronological run table instead of a decision-first workbench that answers which operation needs attention now.
- **Today's failure**: Failed, blocked, partial, stale, or follow-up runs can compete visually with successful history and diagnostic table controls. Operators must infer reason, impact, affected environment, proof/artifact state, and safe next action from table rows, tabs, and run detail links.
- **User-visible improvement**: Operations responders get a focused workbench that prioritizes attention-needed OperationRuns, explains outcome/reason/impact, keeps progress truthful, shows proof availability, and points to one safe next action while raw diagnostics stay secondary.
- **Smallest enterprise-capable version**: Productize only the existing workspace Operations page and related table payloads. Derive all states from existing `OperationRun`, `OperationRunResource`, `OperationRunLinks`, `OperationRunProgressContract`, badges, related links, capabilities, and workspace environment-filter mechanisms.
- **Explicit non-goals**: No new operation engine, queue system, monitoring platform, alert routing, governance health dashboard, AI summarization, operation type, persisted run item, status/outcome enum, migration, package, env var, scheduler, queue, storage, deployment asset, compatibility route, or legacy query alias.
- **Permanent complexity imported**: Feature-local page composition, feature tests, one browser smoke, screenshot artifacts, and `repo-truth-map.md`. No new persisted truth, public abstraction, enum/status family, cross-domain UI framework, or backend service foundation.
- **Why now**: Specs 314-322 stabilized workspace/environment scope. Specs 326 and 327 established the decision-first productization pattern for strategic surfaces. Operations Hub is the next explicit follow-up and already has the OperationRun foundations needed for a bounded runtime UI pass.
- **Why not local**: A title or column tweak would not solve the first decision problem. A new observability engine would overbuild. The narrow correct slice is a repo-truth-bounded productization pass on the existing Operations page.
- **Approval class**: Workflow Compression.
- **Red flags triggered**: Strategic UI productization and cross-family OperationRun outcome presentation. Defense: scope is bounded to one existing page/table/detail-link ecosystem, uses existing truth sources, and explicitly forbids new backend/state frameworks.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12**
- **Decision**: approve.
## Candidate Source And Completed-Spec Guardrail
- **Candidate source**: Direct user-provided manual promotion for Spec 328, aligned with the OperationRun / execution-truth maturity queue in `docs/product/spec-candidates.md`, the Operations Hub target brief from Spec 325, and the follow-up list in Spec 327.
- **Current package check**: `specs/328-operations-hub-decision-first-workbench-productization/` existed before this preparation with template `spec.md` and `plan.md` only. No completion, implementation close-out, validation, browser smoke, or completed task markers existed in the Spec 328 package.
- **Related completed-spec check**: Specs 314-327 include historical/completed foundation and productization signals. They are dependency context only and must not be rewritten by Spec 328.
- **Close alternatives deferred**: Evidence / Audit Log Disclosure Productization, Environment Dashboard / Baseline Compare Productization, and Restore Safety Workflow Productization remain follow-up specs 329-331.
- **Smallest viable implementation slice**: Existing Operations Hub only: header/scope, main decision workbench, summary cards, right operation/proof panel, operations table as secondary context, diagnostics disclosure, RBAC-aware links/actions, canonical `environment_id` filter behavior, terminal/progress semantics, and tests/browser smoke.
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace canonical-view operations workbench, optionally filtered by canonical `environment_id`.
- **Primary Routes**:
- Existing route: `/admin/workspaces/{workspace}/operations`.
- Existing route name: `admin.operations.index`.
- Existing detail route: `/admin/workspaces/{workspace}/operations/{run}`.
- Existing detail route name: `admin.operations.view`.
- Existing index page class: `apps/platform/app/Filament/Pages/Monitoring/Operations.php`.
- Existing index view: `apps/platform/resources/views/filament/pages/monitoring/operations.blade.php`.
- Existing table/detail resource: `apps/platform/app/Filament/Resources/OperationRunResource.php`.
- Existing detail page class: `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`.
- **Data Ownership**:
- Execution truth: `OperationRun.status`, `outcome`, `type`, `summary_counts`, `failure_summary`, `context`, `started_at`, `completed_at`, `initiator_name`, `workspace_id`, and `managed_environment_id`.
- Status/outcome vocabulary: `OperationRunStatus` and `OperationRunOutcome`.
- Problem class truth: `OperationRun::problemClass()`, `PROBLEM_CLASS_ACTIVE_STALE_ATTENTION`, `PROBLEM_CLASS_TERMINAL_FOLLOW_UP`, `activeStaleAttention()`, `terminalFollowUp()`, and `dashboardNeedsFollowUp()`.
- Progress truth: `OperationRunProgressContract`, `SummaryCountsNormalizer`, `OperationSummaryKeys`.
- Operation labels and related context: `OperationCatalog`, `OperationRunLinks::related()`, canonical run URLs, and related resource routes.
- Artifact/proof truth: existing `ArtifactTruthPresenter`, related evidence/review/backup/restore links where already derived from OperationRun context or relations.
- Workspace/environment scope: `WorkspaceContext`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CanonicalAdminEnvironmentFilterState`, and the workspace hub filter chip partial.
- **RBAC**:
- Workspace membership required.
- Managed-environment entitlement required for environment-bound runs.
- Existing `OperationRunPolicy` and `OperationRunCapabilityResolver` remain authoritative for run visibility.
- Existing source policies/capabilities remain authoritative for evidence, artifact, related record, support diagnostics, retry/resume, support request, and related links.
- Non-member or cross-workspace environment access remains deny-as-not-found.
- Member with missing capability must not see unauthorized details or actionable controls.
For canonical-view specs:
- **Default filter behavior when tenant-context is active**: Clean `OperationRunLinks::index()` / `/admin/workspaces/{workspace}/operations` remains workspace-wide and must not inherit remembered environment context, Filament tenant context, session table filters, or legacy query aliases.
- **Explicit entitlement checks preventing cross-tenant leakage**: `?environment_id=` must resolve through the current workspace and actor entitlement. Cross-workspace or inaccessible IDs return safe no-access / 404.
## UI Surface Impact *(mandatory - UI-COV-001)*
Does this spec add, remove, rename, or materially change any reachable UI surface?
- [ ] No UI surface impact
- [x] Existing page changed
- [ ] New page/route added
- [ ] Navigation changed
- [ ] Filament panel/provider surface changed
- [ ] New modal/drawer/wizard/action added
- [x] New table/form/state added
- [ ] Customer-facing surface changed
- [ ] Dangerous action changed
- [x] Status/evidence/review presentation changed
- [x] Workspace/environment context presentation changed
## UI/Productization Coverage *(mandatory when UI Surface Impact is not "No UI surface impact")*
- **Route/page/surface**: `/admin/workspaces/{workspace}/operations`, `Operations`, `operations.blade.php`, `OperationRunResource` table and canonical run detail links.
- **Current or new page archetype**: Strategic Surface / Operations Monitoring Workbench, matching `docs/ui-ux-enterprise-audit/page-reports/ui-003-operations.md`.
- **Design depth**: Strategic Surface.
- **Repo-truth level**: repo-verified page/table/detail foundation; individual runtime elements are classified in `repo-truth-map.md`.
- **Existing pattern reused**: Filament Page, Filament table, header widgets, badges, shared operation links, workspace hub environment filter chip, EnterpriseDetail run detail, OperationRun progress contract, existing policies/capabilities.
- **New pattern required**: no new runtime framework; page-local workbench composition only.
- **Screenshot required**: yes, Browser smoke screenshots under `specs/328-operations-hub-decision-first-workbench-productization/artifacts/screenshots/`.
- **Page audit required**: no new full audit unless implementation materially changes route inventory; active spec artifacts and screenshots are sufficient if route/archetype remains UI-003.
- **Customer-safe review required**: not customer-facing by default. If any customer-readable copy is introduced later, raw diagnostics and provider payloads remain hidden.
- **Dangerous-action review required**: no new dangerous action expected. If implementation unexpectedly exposes retry/cancel/rerun/restore-related execution, spec/plan must be updated first and the action must use `Action::make(...)->action(...)`, confirmation where relevant, authorization, audit, notification, and tests.
- **Coverage files updated or explicitly not needed**:
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md`
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
- [ ] `docs/ui-ux-enterprise-audit/page-reports/...`
- [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md`
- [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md`
- [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md`
- [ ] `N/A - no reachable UI surface impact`
- [x] No registry update needed; route and archetype remain covered by UI-003 and Spec 325 target artifacts.
- **Coverage decision for implementation**: implementation must either update UI coverage registry artifacts or document in this spec's close-out why UI-003 and Spec 325 target artifacts remain sufficient for the unchanged route/archetype.
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
- **Cross-cutting feature?**: yes.
- **Interaction class(es)**: status messaging, action links, OperationRun links, evidence/artifact proof links, workspace/environment filter chip, summary cards, diagnostics disclosure, navigation/back context.
- **Systems touched**: `Operations`, `operations.blade.php`, `OperationRunResource`, `TenantlessOperationRunViewer`, `OperationsKpiHeader`, `OperationRunLinks`, `OperationRunProgressContract`, `OperationUxPresenter`, `ArtifactTruthPresenter`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CanonicalAdminEnvironmentFilterState`, `OperationRunPolicy`, `OperationRunCapabilityResolver`.
- **Existing pattern(s) to extend**: existing operations table, existing problem-class tabs, existing OperationRun badges, existing run detail EnterpriseDetail sections, existing workspace hub filter chip, existing operation related links and progress contract.
- **Shared contract / presenter / builder / renderer to reuse**: `OperationRunLinks`, `OperationCatalog`, `OperationRunProgressContract`, `OperationUxPresenter`, `BadgeCatalog` / `BadgeRenderer`, `OperationRunPolicy`, `OperationRunCapabilityResolver`, existing related-resource routes.
- **Why the existing shared path is sufficient or insufficient**: Existing paths are sufficient for execution truth, links, badges, progress semantics, authorization, and detail diagnostics. They are insufficient only in first-read page hierarchy on the operations index.
- **Allowed deviation and why**: bounded page-local payload/view helpers are allowed if needed to reduce Blade complexity. New public cross-domain status, priority, presenter, or monitoring frameworks are not allowed.
- **Consistency impact**: Labels, badges, progress language, operation URLs, diagnostics labels, and related links must stay aligned with existing OperationRun and EnterpriseDetail vocabulary.
- **Review focus**: Verify no fake progress, no false green health, no raw diagnostics by default, no unauthorized action, no shell-scope regression, and no duplicate local operation truth.
## OperationRun UX Impact *(mandatory)*
- **Touches OperationRun start/completion/link UX?**: link and presentation semantics only. No new OperationRun creation, queueing, dedupe, lifecycle transition, summary-count writer, or terminal notification behavior.
- **Shared OperationRun UX contract/layer reused**: `OperationRunLinks`, `OperationRunProgressContract`, `OperationUxPresenter`, `OperationStatusNormalizer`, `SummaryCountsNormalizer`, and `OperationSummaryKeys`.
- **Delegated start/completion UX behaviors**: N/A - no operation start.
- **Local surface-owned behavior that remains**: derive index-page focus, reason, impact, proof availability, and primary next action from existing run truth and related links.
- **Queued DB-notification policy**: unchanged / N/A.
- **Terminal notification path**: unchanged.
- **Exception required?**: none.
## Provider Boundary / Platform Core Check *(mandatory)*
- **Shared provider/platform boundary touched?**: no new provider seam.
- **Boundary classification**: platform-core operations workbench over existing provider-backed execution records.
- **Seams affected**: display and routing over OperationRuns, environment-bound related records, evidence/artifact proof, restore/backup/provider operation links, and diagnostic disclosure.
- **Neutral platform terms preserved or introduced**: workspace, environment filter, operation, execution outcome, proof, artifact, diagnostics, follow-up.
- **Provider-specific semantics retained and why**: existing Microsoft/Intune/Entra terms may appear only where the underlying operation type or related resource already uses them. Do not surface raw provider IDs, Graph payloads, or provider diagnostics by default.
- **Why this does not deepen provider coupling accidentally**: no Graph calls, no provider contracts, no provider connection changes, no new provider-shaped persistence, and no new operation taxonomy.
- **Follow-up path**: Evidence/Audit disclosure, Environment Dashboard/Baseline Compare, and Restore Safety Workflow remain separate specs.
## UI / Surface Guardrail Impact
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---:|---|---|---|---:|---|
| Operations Hub page | yes | Native Filament page plus existing Blade composition | operations monitoring, status messaging, proof links | page, URL-query, table state, derived payload | no | Existing route only |
| Header / scope area | yes | Filament header actions / Blade | workspace/environment context presentation | page, URL-query | no | Must keep Workspace shell ownership |
| Main operation workbench | yes | Filament section/cards/badges | execution outcome and next action | page payload | no | Derived labels only |
| Summary cards | yes | Filament widget/cards | execution posture counts | query payload | no | Only repo-backed counts or unavailable states |
| Right operation/proof panel | yes | native layout / Blade | proof, artifact, related links, diagnostics | page payload | no | Desktop aside, stacked on small screens |
| Operations table/history | yes | existing Filament table | run log / secondary context | table state | no | Table remains available |
| Diagnostics disclosure | yes | collapsed/progressive disclosure only | support/raw detail | detail links/action visibility | no | Authorized and collapsed by default |
## Decision-First Surface Role
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|---|---|---|---|---|---|---|---|
| Operations Hub | Primary Decision Surface for execution follow-up | Operator decides which operation needs attention and what is safe to inspect next | highest-priority operation, outcome, reason, impact, progress/proof state, environment, one next action | run detail, artifact/evidence, related resource, diagnostics | Primary because it is the canonical workspace operations workbench | Follows execution follow-up, not raw history | Prevents scanning all runs before first action |
| Operations table/history | Secondary Context | Operator scans remaining runs after the top operation is clear | operation, environment, status/outcome, timing, table filters | canonical detail route and related links | Secondary because table supports investigation/history | Keeps existing monitoring power | Reduces raw-table dominance |
| Diagnostics disclosure | Tertiary Evidence / Diagnostics | Support/operator inspects technical data after decision path | collapsed availability only | raw context, failure summary, support diagnostics where authorized | Not primary; diagnostics support execution truth | Preserves audit/debug depth | Prevents default raw-console experience |
## Audience-Aware Disclosure
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|---|---|---|---|---|---|---|---|
| Operations Hub | operator-MSP, manager, support reviewer, auditor | outcome, reason, impact, environment, timing, proof/artifact availability, active progress only when trustworthy, next action | related run detail and collapsed diagnostic sections | raw context, failure summary, stack traces, provider payloads, provider secrets, debug metadata | Review/open operation unless a stronger authorized proof link exists | raw diagnostics, unsupported proof claims, unauthorized links | top workbench states the execution blocker once; table/panel add proof/source context |
| Right operation/proof panel | operator-MSP, support reviewer where authorized | operation summary, outcome, environment, timing, proof/artifact state, primary next action | operation detail sections and support diagnostics links | raw/support sections stay collapsed or routed to existing authorized surfaces | same selected-operation primary action | raw JSON and support diagnostics | panel expands selected operation rather than creating another decision |
## UI/UX Surface Classification
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Operations Hub | Workbench / Monitoring | Decision-first operations queue | Review highest-priority operation | explicit primary action plus table row detail | existing table row click remains | right/detail panel and related links | none introduced; future retry/cancel in More/detail only | `/admin/workspaces/{workspace}/operations` | `/admin/workspaces/{workspace}/operations/{run}` | active workspace, optional `environment_id` chip | Operations / Operation | outcome, reason, impact, environment, proof/progress, next action | none |
| Operations table/history | List / Table / Read-only run log | Secondary monitoring context | Open operation detail | existing row click | yes | table filters/secondary links | none | same page | canonical run detail route | workspace + environment filter | OperationRun | status/outcome/type/timing | none |
| Diagnostics disclosure | Diagnostics / Support Raw | Collapsed diagnostic context | Expand or open diagnostics if authorized | disclosure/detail action | N/A | below/inside detail panel | none | same page | existing support diagnostics action/run detail | authorized-only label | Diagnostics | collapsed status only | none |
## Operator Surface Contract
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| Operations Hub | Operations responder / MSP operator | Decide which execution record needs attention and open the safest existing proof/action path | Workspace operations workbench | Which operation needs attention now? | operation title/type, outcome/status, reason, impact, environment, started/completed time, proof/artifact state, trustworthy active progress, next action | raw context, raw failure summaries, provider payloads, stack traces, support diagnostic bundle | execution status, terminal outcome, freshness/problem class, progress capability, proof/artifact availability | none on the page by default; existing source surfaces own mutations | review/open operation, open artifact/evidence/related record when authorized | none introduced |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no.
- **New persisted entity/table/artifact?**: no. `repo-truth-map.md` is a Spec Kit preparation artifact, not runtime truth.
- **New abstraction?**: no public abstraction. Page-local private helpers are allowed only when they reduce Blade complexity and stay feature-local.
- **New enum/state/reason family?**: no domain state. Priority and display guidance must derive from existing status, outcome, problem class, progress contract, timestamps, context, related links, and artifact truth.
- **New cross-domain UI framework/taxonomy?**: no.
- **Current operator problem**: The operations index needs to answer the next execution follow-up question without forcing operators through a raw run table.
- **Existing structure is insufficient because**: Current page provides table, tabs, KPI header, scope cards, lifecycle counts, and run detail pages, but does not consistently elevate the highest-priority operation with outcome/reason/impact/proof/next action first.
- **Narrowest correct implementation**: Refactor the existing page layout and derived payloads, bind to existing OperationRun sources, keep diagnostics collapsed, and add targeted tests/browser smoke.
- **Ownership cost**: Feature-local layout/payload tests, one Browser smoke, screenshots, and spec truth map. No durable backend model or new framework cost.
- **Alternative intentionally rejected**: new observability platform, new queue monitor, new operation priority engine, new status enum, new artifact store, AI summary, broad design system work, or raw log viewer.
- **Release truth**: current-release runtime UI productization over existing OperationRun foundations.
### Compatibility posture
This feature assumes pre-production runtime posture. Backward compatibility, historical aliases, migration shims, dual-write logic, legacy route redirects, and legacy query aliases are out of scope. Existing legacy query aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) must not be supported for Operations Hub filtering.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature, Filament/Livewire/HTTP, Browser.
- **Validation lane(s)**: confidence plus browser for critical workspace/environment UI/scope smoke.
- **Why this classification and these lanes are sufficient**: The change is a user-facing Filament page productization with RBAC, operation truth, progress semantics, scope, and disclosure behavior. Feature tests prove data/scope/action rules; Browser smoke proves shell/filter/reload/disclosure/table hierarchy behavior.
- **New or expanded test families**: additions under `tests/Feature/Monitoring/*` and `tests/Browser/Spec328OperationsHubProductizationSmokeTest.php`.
- **Fixture / helper cost impact**: reuse existing `OperationRun` factory, workspace/environment helpers, and current navigation test helpers. Do not widen expensive defaults.
- **Heavy-family visibility / justification**: browser addition is explicit and named for Spec 328.
- **Special surface test profile**: `global-context-shell` plus `monitoring-state-page`.
- **Standard-native relief or required special coverage**: special coverage required for canonical filter, clear/reload, highest-priority operation, right proof panel, progress/terminal semantics, diagnostics default-hidden, and no platform-context tenant copy.
- **Reviewer handoff**: confirm diagnostics are collapsed, RBAC actions hide/disable correctly, no false green, clean workspace entry, canonical filter, clear filter, cross-workspace guard, terminal runs do not show progress, and table/history remains available as secondary context.
- **Budget / baseline / trend impact**: no expected material lane-cost shift beyond one targeted browser smoke.
- **Escalation needed**: document-in-feature if browser coverage becomes too expensive or requires fixture broadening.
- **Active feature PR close-out entry**: Smoke Coverage.
- **Planned validation commands**:
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Monitoring tests/Feature/Operations tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec328OperationsHubProductizationSmokeTest.php --compact`
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='Operations|OperationRun|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
- `git diff --check`
## Summary
Productize the existing Operations Hub into a decision-first OperationRun workbench.
The page must answer:
```text
Which operation needs attention now?
```
It must lead with outcome, reason, impact, trustworthy progress/proof, affected environment, and primary next action. The existing operations table remains available as secondary context. OperationRuns remain execution truth and must not be presented as governance or environment health.
## Product Context
TenantPilot uses OperationRuns as execution truth for queued, running, completed, failed, blocked, partial, stale, and follow-up operational work. The Operations Hub is the workspace-wide place to understand execution posture and inspect operation proof. It should help operators decide which operation requires attention without turning the page into a raw queue monitor or governance health dashboard.
OperationRuns answer what ran, what is running, what completed, what failed, what is blocked, what needs follow-up, and which artifact or proof may exist. They do not prove that an environment is healthy, governance is complete, or customer review evidence is ready.
## Problem Statement
The current Operations page is technically useful but still table-first and monitor-like. It has scope cards, tabs, KPI header, lifecycle warning copy, and the OperationRun table. The current page does not yet consistently elevate the highest-priority operation needing attention with reason, impact, proof state, and next action.
Known repo-verified starting points:
- `Operations` page exists and uses `OperationRunResource::table()`.
- `OperationRunResource` has badge-backed status/outcome columns, table filters, canonical row URL, and empty state.
- `OperationRun` has problem-class helpers for stale active and terminal follow-up states.
- `OperationRunProgressContract` already prevents terminal progress and derives determinate progress only from trustworthy `processed`/`total` counts.
- `TenantlessOperationRunViewer` already provides rich run detail, related links, support diagnostics actions, and collapsed technical sections.
- Workspace/environment filter contracts are already implemented around `environment_id`.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Decide which operation needs attention now (Priority: P1)
As an operations responder, I want Operations Hub to show the highest-priority operation needing attention with outcome, reason, impact, environment, proof state, and one next action so I can act without scanning raw history first.
**Why this priority**: This is the core productization promise and delivers value independently.
**Independent Test**: Seed runs with failed, blocked, partial, stale/running, and successful states; open Operations Hub; verify the top workbench prioritizes attention-needed runs and shows outcome, reason, impact, environment, proof state, and primary action.
**Acceptance Scenarios**:
1. **Given** a blocked or follow-up operation exists, **When** the operator opens clean Operations Hub, **Then** the workbench asks `Which operation needs attention now?` and shows the attention operation above successful history.
2. **Given** reason, impact, artifact, or evidence data is absent, **When** the operation is rendered, **Then** the page shows an honest unavailable state rather than invented proof.
3. **Given** no attention-needed operation exists, **When** the page renders, **Then** the primary state says no operations need attention without claiming environment or governance health.
### User Story 2 - Preserve truthful OperationRun progress and terminal outcomes (Priority: P1)
As an operator, I want active progress to be shown only when trustworthy and terminal outcomes to show guidance instead of progress so I do not misread a failed or completed run as still processing.
**Why this priority**: Misleading progress creates false calm and undermines OperationRun trust.
**Independent Test**: Render active counted, active uncounted, failed, blocked, partial, and completed runs; assert progress displays only for active trustworthy counts and terminal runs show outcome guidance.
**Acceptance Scenarios**:
1. **Given** a running operation has valid `processed` and `total`, **When** the workbench renders, **Then** determinate progress appears from those counts.
2. **Given** a queued/running operation lacks trustworthy counts, **When** the workbench renders, **Then** only activity/status guidance appears.
3. **Given** failed, blocked, partial, or succeeded terminal operations exist, **When** they render, **Then** no progress bar appears for terminal outcomes.
### User Story 3 - Keep workspace/environment scope explicit (Priority: P1)
As a workspace operator, I want Operations Hub to stay workspace-owned while accepting a canonical environment filter so clean, filtered, clear, reload, and cross-workspace behavior remain predictable.
**Why this priority**: Specs 314-322 made this a hard product contract for workspace hub surfaces.
**Independent Test**: Open clean `OperationRunLinks::index()`, filtered `?environment_id=`, clear filter, reload, legacy aliases, and cross-workspace IDs; verify workspace shell and chip behavior.
**Acceptance Scenarios**:
1. **Given** clean Operations Hub URL, **When** the page loads, **Then** no Environment chip or remembered Environment filter appears.
2. **Given** `?environment_id={id}` for an entitled environment, **When** the page loads, **Then** Workspace shell remains active and a visible Environment filter chip appears.
3. **Given** a cross-workspace environment ID or legacy query alias, **When** the page loads, **Then** the cross-workspace ID is denied as not found and aliases do not create filter state.
### User Story 4 - Inspect proof without raw diagnostics by default (Priority: P1)
As an operator or support reviewer, I want proof/artifact links and operation detail to be easy to reach while raw diagnostics remain collapsed and capability-gated.
**Why this priority**: Operations need proof, but raw details should not dominate or leak.
**Independent Test**: Render runs with related artifact/evidence links and diagnostic payloads; assert proof state/link appears where authorized while raw payload, stack trace, provider secret, debug metadata, and internal exception text are absent by default.
**Acceptance Scenarios**:
1. **Given** an operation has a related artifact or evidence route, **When** the detail panel renders, **Then** proof state/link appears only if repo-real and authorized.
2. **Given** diagnostics are available, **When** the page first renders, **Then** raw diagnostics are collapsed or absent by default.
3. **Given** the actor lacks diagnostics/proof capability, **When** the page renders, **Then** protected links/actions are hidden or unavailable without sensitive hints.
### User Story 5 - Preserve operations table as secondary context (Priority: P2)
As an operations responder, I want the existing operations table/history to remain available below or beside the workbench so I can scan history after the top decision is clear.
**Why this priority**: The workbench must not regress existing monitoring and filtering power.
**Independent Test**: Render the existing OperationRun table fixtures and assert rows, filters, tabs, row links, and empty state still appear as secondary context.
**Acceptance Scenarios**:
1. **Given** multiple visible OperationRuns exist, **When** the workbench renders, **Then** existing table context remains available and is not replaced by a generic dashboard.
2. **Given** an operations tab or table filter is selected, **When** the page renders, **Then** the workbench and table align to current scope without losing clear-filter behavior.
## Functional Requirements
- **FR-001**: Operations Hub MUST remain on the existing `admin.operations.index` route.
- **FR-002**: The page MUST render a decision-first workbench before the secondary operations table/history.
- **FR-003**: The workbench MUST include the question `Which operation needs attention now?` or stable equivalent final copy.
- **FR-004**: The selected/highest-priority operation MUST show operation title/type, outcome/status, reason, impact, environment, timing, proof/artifact state, and primary next action.
- **FR-005**: Missing reason, impact, environment, artifact, evidence, or proof data MUST render honest unavailable/missing states or be omitted; it MUST NOT be invented.
- **FR-006**: Priority MUST derive from existing status/outcome/problem-class/freshness truth only. No new persisted priority engine is allowed.
- **FR-007**: Summary cards MUST use only repo-supported execution posture counts/states, such as needs attention, active operations, failed/blocked, follow-up required, completed recently, or artifact available where proven.
- **FR-007A**: Summary cards MUST use native Filament `StatsOverviewWidget` / `Stat` rendering where feasible while remaining Operations execution/attention signals, not generic business KPIs.
- **FR-007B**: The four top workbench summary cards MUST be `Needs attention`, `Active operations`, `Failed or blocked`, and `Completed recently`; no `Total Operations` or `Avg Duration` card may appear before the workbench.
- **FR-007C**: Summary-card visual emphasis MUST follow execution risk: `Needs attention` strongest when non-zero, `Active operations` neutral when zero, `Failed or blocked` danger/warning, and `Completed recently` muted success/secondary without implying environment or governance health.
- **FR-007D**: Summary cards MUST NOT show fake trends, fake sparklines, decorative charts, or chart-like areas unless repo-backed time-series data exists.
- **FR-008**: Existing OperationRun table/history MUST remain available as secondary context.
- **FR-009**: A right-side operation/proof panel MUST be visible on desktop and stack on smaller screens.
- **FR-010**: Proof/artifact/evidence must be shown as proof state/link, not raw payload.
- **FR-011**: Diagnostics/raw details MUST be collapsed, hidden, or capability-gated by default.
- **FR-012**: Primary action MUST be singular and repo-real for the selected/highest-priority operation.
- **FR-013**: Unauthorized actions MUST be hidden, disabled with existing helper text convention, or replaced by unavailable state; UI visibility is not security.
- **FR-014**: No new destructive or high-impact action may be added without updating spec/plan first and applying confirmation, server authorization, audit, notification, and tests.
- **FR-015**: Determinate progress MUST appear only when `OperationRunProgressContract` or equivalent existing trusted-count semantics allow it.
- **FR-016**: Terminal completed, failed, blocked, partial, or follow-up runs MUST NOT show progress bars.
- **FR-017**: Completed successful operations MUST NOT be described as environment health, governance health, or customer-safe evidence readiness.
- **FR-018**: Clean entry MUST remain workspace-wide with no active Environment shell and no remembered Environment fallback.
- **FR-019**: Filtered entry MUST use only `?environment_id={id}` with a visible chip and clear action.
- **FR-020**: Legacy query aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) MUST NOT create filter state.
- **FR-021**: Cross-workspace or unauthorized `environment_id` MUST be rejected as safe not-found/no-access.
- **FR-022**: Runtime copy MUST avoid `tenant` as platform context copy. Provider-specific use remains allowed only when explicitly provider-bound.
- **FR-023**: `repo-truth-map.md` MUST exist before runtime changes and be updated if implementation discovers new source gaps.
- **FR-024**: No migrations, seeders, packages, env vars, queues, scheduler, storage, deployment asset changes, backwards compatibility layer, or legacy alias support are expected.
## Non-Functional Requirements
- **NFR-001**: Page render MUST remain DB-only and MUST NOT perform Microsoft Graph or external provider calls.
- **NFR-002**: The layout MUST remain Filament v5 / Livewire v4 compatible and use native Filament/Tailwind primitives before custom UI.
- **NFR-003**: The workbench MUST remain responsive enough for the Filament shell; desktop gets a right-side panel and smaller viewports stack it below.
- **NFR-004**: Operation priority, status, and proof meaning MUST NOT rely on color alone.
- **NFR-005**: Raw diagnostics, provider payloads, stack traces, provider secrets, raw OperationRun context, and internal exception/debug metadata MUST NOT be default-visible.
- **NFR-006**: Queries and summary counts MUST be scoped by workspace, environment entitlement, and capability before rendering.
- **NFR-007**: Implementation MUST avoid broad fixture/helper defaults or hidden heavy-governance test cost.
## Auditability / Observability Requirements
- **AOR-001**: No new audit event is required for read-only page productization unless implementation introduces a new mutation or extends an existing repo audit convention.
- **AOR-002**: Any unexpected mutating/high-impact action MUST be added only after spec/plan update and must include server-side authorization, confirmation when destructive/high-impact, audit logging, user feedback, and tests.
- **AOR-003**: Operation proof links must use existing OperationRun truth and link helpers; this spec must not create or transition OperationRuns.
- **AOR-004**: Evidence, artifact, and proof states must distinguish available proof from unavailable/missing proof without implying audit success or governance health.
## Data / Truth Requirements
Each visible runtime element must be classified as one of:
- `repo-verified`
- `foundation-real`
- `derived from existing model`
- `empty state / unavailable`
- `deferred future capability`
The authoritative map is `repo-truth-map.md` in this spec directory. If implementation discovers that a planned element has no safe source, no authorization path, or would require new persisted truth, the element must become `empty state / unavailable` or `deferred future capability`.
## RBAC / Capability Requirements
At minimum, implementation must verify existing capabilities/policies for:
- view Operations Hub / workspace membership
- view operation runs
- view operation run detail
- view evidence/artifacts
- view support diagnostics/raw detail
- retry/re-run/resume operation if an existing action is shown
- cancel operation if an existing action is shown
- open related alert/finding/review/restore/backup/provider context if existing links are shown
Do not introduce new permission semantics unless repo analysis proves an existing capability cannot express the action and spec/plan are updated first.
## Assumptions
- The existing `OperationRun` problem-class helpers and freshness scopes can provide enough priority truth for a first implementation without creating a new priority engine.
- Existing `OperationRunResource` and `TenantlessOperationRunViewer` provide enough detail/proof/diagnostic patterns to reuse or link from the index workbench.
- Existing `OperationRunProgressContract` is the authoritative v1 progress semantics path.
- Existing `OperationRunPolicy`, `OperationRunCapabilityResolver`, and source-resource policies are sufficient for v1 action visibility and source-link access.
- Spec 325 target images remain visual calibration and must not be treated as runtime data truth.
## Risks
- Implementation could accidentally create a new priority taxonomy instead of a local derived ordering.
- Completed successful runs could be overclaimed as environment health or governance health.
- Artifact/evidence labels could become false proof claims if unavailable links are not rendered honestly.
- Progress could regress if terminal runs show determinate or indeterminate progress.
- Capability-limited actors could learn hidden run or artifact existence through counts, empty states, or disabled labels if scoping is applied too late.
- Browser coverage could become expensive if fixtures grow beyond the targeted smoke path.
## Open Questions
- None blocking preparation. Implementation must update `repo-truth-map.md` and spec/plan before runtime edits if it discovers a selected UI element requires new persisted truth, a new capability, or an unsupported source route.
## Non-Goals
- No new operation engine, queue engine, or monitoring backend.
- No new observability platform or raw logs page.
- No governance health dashboard.
- No alert routing redesign.
- No Evidence/Audit, Governance Inbox, Customer Review Workspace, Restore, Backup, or Provider Readiness redesign.
- No AI summarization or recommendation engine.
- No new operation types, statuses, outcomes, progress keys, priority enums, persisted workbench items, or backend foundations.
- No migrations by default.
- No packages, env vars, queues, scheduler, storage, or deployment asset changes.
- No backwards compatibility layer or legacy tenant query alias support.
## Acceptance Criteria
### Productization
- [x] Operations Hub has a decision-first layout.
- [x] Main decision question is visible.
- [x] Highest-priority operation needing attention is visible where fixture data exists.
- [x] Empty/no-attention state is clear where no operation needs attention.
- [x] Outcome/status is visible.
- [x] Reason and impact are visible.
- [x] Affected Environment is visible where applicable.
- [x] Artifact/evidence/proof state is visible.
- [x] Primary next action is clear.
- [x] Diagnostics are secondary/collapsed.
- [x] Operations table/history remains available as secondary context.
### OperationRun Semantics
- [x] OperationRun remains execution truth, not governance health.
- [x] Completed successful runs are not overclaimed as environment health.
- [x] Failed/blocked/follow-up runs show outcome guidance.
- [x] Terminal runs do not show misleading progress.
- [x] Determinate progress appears only with trustworthy counts.
- [x] Artifact/evidence links shown only where repo-supported.
### Operator Safety
- [x] Raw diagnostics hidden by default.
- [x] Provider secrets not visible.
- [x] Internal exception/debug text not visible.
- [x] No false green success state.
- [x] Copy avoids tenant as platform context.
- [x] Empty/unavailable states are honest.
### Scope
- [x] Clean URL is workspace-wide.
- [x] Shell is Workspace-only.
- [x] Environment filter uses `environment_id`.
- [x] Visible Environment chip appears when filtered.
- [x] Clear filter works.
- [x] Reload after clear is safe.
- [x] Legacy aliases do not create filter state.
- [x] Cross-workspace Environment is rejected.
### RBAC
- [x] Unauthorized user cannot access protected operation data.
- [x] Unauthorized actions are hidden/disabled.
- [x] Operation detail access respects capability.
- [x] Evidence/artifact access respects capability.
- [x] Diagnostics access respects capability.
- [x] Retry/cancel/resume actions, if present, respect capability and confirmation conventions.
### UI / Visual
- [x] Layout uses premium direction from Spec 325 without treating target images as runtime truth.
- [x] Filament light mode remains readable.
- [x] No heavy one-off CSS.
- [x] Right-side operation/proof panel exists on desktop.
- [x] Operations table is not the only default experience.
- [x] Page remains responsive enough for Filament shell.
- [x] Summary cards visually align with compact Filament KPI/Stats-card density while staying execution/attention signals.
- [x] Summary cards use a clear title, large value, concise subline, and subtle accent line/status color.
- [x] Needs attention is visually strongest when actionable; Active operations is neutral when zero; Completed recently stays muted and does not imply health.
- [x] No fake trends, fake sparklines, or unsupported chart affordances appear.
## Spec 328 Follow-up - Filament Summary Card Alignment
The Operations summary cards should visually read like high-quality compact Filament KPI/Stats cards, but their product semantics remain Operations execution and attention signals. They are not generic business KPIs and must not imply environment health, governance health, revenue, adoption, or customer readiness.
Required cards:
1. `Needs attention`
2. `Active operations`
3. `Failed or blocked`
4. `Completed recently`
Design rules:
- Use compact native Filament Stats Overview hierarchy with clear label, large numeric value, short supporting text, description/status icon where useful, and `Stat::color(...)` semantic emphasis.
- Do not hand-roll a custom Blade/Tailwind stats-card design system or explicit custom accent bars unless native widget embedding is proven infeasible and the reason is documented in `repo-truth-map.md`.
- Use no sparkline or chart area unless the data is repo-backed. No fake trends or decorative chart placeholders.
- `Needs attention` carries the strongest warning emphasis when its value is non-zero.
- `Active operations` must not look stronger than attention cards when the value is `0`.
- `Completed recently` must remain muted success/secondary and must not read as an environment-health claim.
- The old generic pre-workbench cards such as `Total Operations` and `Avg Duration` must not return.
- Implementation should use `Filament\Widgets\StatsOverviewWidget`, `Filament\Widgets\StatsOverviewWidget\Stat`, `Stat::make(...)`, `description(...)`, optional `descriptionIcon(...)`, and `color(...)`; polling stays disabled unless the Operations page requires live updating.
### Tests / Validation
- [x] Repo truth map exists.
- [x] Required Feature tests pass.
- [x] Required Browser smoke passes.
- [x] Relevant Spec 314-322 guards still pass.
- [x] `pint --dirty` passes.
- [x] `git diff --check` passes.
- [x] Full suite status is honestly reported if run/not run.
## Follow-Up Spec Candidates
- Spec 329 - Evidence / Audit Log Disclosure Productization.
- Spec 330 - Environment Dashboard / Baseline Compare Productization.
- Spec 331 - Restore Safety Workflow Productization.
Do not start these inside Spec 328.
## Implementation Close-Out
- **Changed behavior**: the existing Operations Hub now opens with a decision-first execution follow-up workbench before the operations history table.
- **Runtime truth**: selected operation, reason, impact, environment, timing, summary cards, and progress are derived from existing `OperationRun` state, existing problem classes, `OperationCatalog`, `OperationRunLinks`, and `OperationRunProgressContract`.
- **Summary-card follow-up**: the decision-first cards own the first viewport; generic `Total Operations` / `Avg Duration` KPI cards are not rendered before the workbench.
- **Native summary-card follow-up**: the four top summary cards now render through a narrow native Filament `StatsOverviewWidget` using `Stat::make(...)`, descriptions, description icons, and semantic `color(...)` values. The previous custom Blade/Tailwind card loop and explicit bottom accent bars were removed. No fake charts, sparklines, trends, or health overclaims were added.
- **Unavailable/deferred truth**: raw diagnostics, provider payloads, artifact/evidence specifics, retry/cancel/rerun, and stronger proof actions remain on existing authorized detail/source surfaces or unavailable in the index.
- **Scope contract**: clean entry remains workspace-wide; explicit filtering uses only `environment_id`; legacy aliases stay rejected/neutralized; cross-workspace filters return safe not-found/no-access.
- **UI coverage decision**: no registry files were changed because the route and archetype remain the existing UI-003 Operations strategic surface; Spec 328 browser screenshots provide the implementation evidence.
- **Screenshots**: stored under `specs/328-operations-hub-decision-first-workbench-productization/artifacts/screenshots/`, including `operations-hub-premium-summary-cards.png`.
- **Validation**:
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Monitoring tests/Feature/Operations tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact` - passed, 182 tests / 1544 assertions.
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec328OperationsHubProductizationSmokeTest.php --compact` - passed, 3 tests / 67 assertions.
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='Operations|OperationRun|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact` - passed, 482 tests / 4713 assertions.
- `cd apps/platform && ./vendor/bin/sail pint --dirty` - passed.
- `git diff --check` - passed.
- **Deployment impact**: no migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, backwards compatibility layer, or legacy alias support were added.

View File

@ -0,0 +1,196 @@
# Tasks: Spec 328 - Operations Hub Decision-First Workbench Productization
**Input**: Design documents from `/specs/328-operations-hub-decision-first-workbench-productization/`
**Prerequisites**: `spec.md`, `plan.md`, `repo-truth-map.md`
**Tests**: Required. This is a runtime UI/operator workbench Filament page productization with browser smoke.
## Test Governance Checklist
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
- [x] New or changed tests stay in the smallest honest family, and the browser addition is explicit.
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default.
- [x] Planned validation commands cover the change without pulling in unrelated lane cost.
- [x] The declared surface test profile (`global-context-shell` plus `monitoring-state-page`) is explicit.
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
## Phase 1: Preparation And Repo Truth
**Purpose**: Confirm runtime truth and prevent invented claims before page edits.
- [x] T001 Re-read `specs/328-operations-hub-decision-first-workbench-productization/spec.md`, `plan.md`, `tasks.md`, and `repo-truth-map.md`.
- [x] T002 Re-read related completed context only: Specs 314-327. Do not modify their artifacts.
- [x] T003 Verify current `Operations` route/class/view, `OperationRunResource`, `TenantlessOperationRunViewer`, `OperationsKpiHeader`, `OperationRunLinks`, progress contract, policies, and existing tests before editing.
- [x] T004 Update `repo-truth-map.md` with any newly discovered source, capability, fallback, or classification before runtime changes.
- [x] T005 Confirm no migration/package/env/queue/storage/deployment asset change is required; if one appears necessary, stop and update spec/plan first.
- [x] T006 Confirm Filament v5 / Livewire v4.0+ compliance and no Livewire v3/Filament legacy API use.
- [x] T007 Confirm panel provider registration remains `apps/platform/bootstrap/providers.php`.
- [x] T008 Confirm `OperationRunResource` remains non-globally-searchable or has a safe View/Edit path if changed; no global search change is expected.
## Phase 2: Feature Tests First
**Purpose**: Lock decision-first layout, scope, RBAC, progress, proof, and diagnostics behavior before the UI refactor.
- [x] T009 Add or update a feature test asserting `repo-truth-map.md` exists and lists required data areas.
- [x] T010 Add or update a Feature/Livewire/HTTP test for the decision-first layout text: `Operations Hub`, `Which operation needs attention now?`, `Outcome`, `Reason`, `Impact`, and `Next action`.
- [x] T011 Add or update a Feature/Livewire/HTTP test asserting a highest-priority attention operation shows operation type/title, outcome/status, reason, impact, environment, proof/artifact state, timing, and primary next action.
- [x] T012 Add or update a Feature/Livewire/HTTP test asserting the no-attention empty state says no operations need attention and avoids false health claims.
- [x] T013 Add or update a Feature/Livewire/HTTP test asserting the right operation/proof panel contains `Operation summary`, `Outcome`, `Environment`, `Proof` or artifact/evidence state, primary next action, and collapsed diagnostics.
- [x] T014 Add or update a test asserting existing operations table/history rows, tabs, filters, row links, and empty state remain available as secondary context.
- [x] T015 Add or update a test that raw diagnostics are hidden by default: `raw payload`, `stack trace`, `debug metadata`, `provider secret`, `internal exception`, and raw OperationRun context text must not appear.
- [x] T016 Add or update progress tests proving terminal failed/blocked/partial/succeeded runs do not show progress bars.
- [x] T017 Add or update progress tests proving determinate progress appears only for active runs with trustworthy `processed` and `total` counts.
- [x] T018 Add or update RBAC tests covering primary action visibility/unavailability for open operation, open artifact/evidence, open related source, open diagnostics, retry/resume/cancel if any existing action is shown.
- [x] T019 Add or update canonical environment filter tests for `?environment_id=`, visible chip, workspace shell only, clear filter, and provable filtered data.
- [x] T020 Add or update legacy alias rejection tests for `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`.
- [x] T021 Add or update cross-workspace environment filter guard test returning safe 404/no-access.
- [x] T022 Add or update tenant-copy guard asserting platform-context copy such as `current tenant`, `tenant filter`, `entitled tenant`, and `all tenants` is not visible on Operations Hub.
## Phase 3: Page Skeleton Productization
**Purpose**: Refactor existing page layout without new backend foundation.
- [x] T023 Update `apps/platform/app/Filament/Pages/Monitoring/Operations.php` to expose a repo-truth-bounded payload for header/scope, selected/highest-priority operation, summary cards, table context, detail/proof panel, actions, progress state, and diagnostics disclosure.
- [x] T024 Update `apps/platform/resources/views/filament/pages/monitoring/operations.blade.php` to render the decision-first workbench before the secondary operations table.
- [x] T025 Ensure the header/scope area shows workspace-wide vs environment-filtered context, visible environment chip when filtered, and concise execution-truth copy.
- [x] T026 Ensure the main workbench shows the stable question, status/outcome badge, operation title/type, reason, impact, environment, timing, proof/artifact state, and one primary next action.
- [x] T027 Ensure summary cards show only repo-backed posture such as needs attention, active operations, failed/blocked, follow-up required, completed recently, or artifact available; show unavailable or omit unsupported cards.
- [x] T028 Ensure the right-side operation/proof panel shows operation summary, outcome, environment, timing, progress state if active/trustworthy, artifact/evidence/proof state, related links where available, primary next action, and diagnostics disclosure.
- [x] T029 Ensure the right-side detail panel is visible on desktop and stacks below on smaller screens.
- [x] T030 Keep the existing operations table/history as secondary content; it must not be the only default experience.
- [x] T031 Ensure diagnostics/internal details are collapsed, hidden, or capability-gated by default.
## Phase 4: Data Binding And Honest States
**Purpose**: Bind to repo-verified sources and avoid false claims.
- [x] T032 Map selected/highest-priority operation state from existing `OperationRun` status, outcome, problem class, freshness, timestamps, and source links without creating persisted state.
- [x] T033 Bind reason and impact to existing outcome, problem class, lifecycle/freshness guidance, failure summary summaries, `OperationUxPresenter`, `ReasonPresenter`, and artifact truth only where safe; show unavailable state otherwise.
- [x] T034 Bind environment display to the existing `ManagedEnvironment` relation where accessible; show workspace-only or unavailable state for tenantless runs.
- [x] T035 Bind proof/artifact/evidence display to existing `OperationRunLinks::related()`, artifact truth, and related resources only; show unavailable or omit unsupported proof paths.
- [x] T036 Bind operation detail links only through existing `OperationRunLinks` and authorized source routes.
- [x] T037 Ensure completed successful operations are described as execution results, not environment health or governance health.
- [x] T038 Ensure no generic green success state appears without exact repo-backed proof.
## Phase 5: Progress And Outcome Semantics
**Purpose**: Preserve OperationRun progress contract and terminal semantics.
- [x] T039 Use `OperationRunProgressContract` or existing equivalent logic for progress display decisions.
- [x] T040 Show determinate progress only for active runs with valid `processed` and `total` counts.
- [x] T041 Show activity/status-only treatment for queued/running runs without trustworthy counts.
- [x] T042 Show outcome guidance, not progress, for terminal succeeded/failed/blocked/partial/follow-up states.
- [x] T043 Preserve existing `summary_counts` whitelist semantics through `OperationSummaryKeys`; do not add new keys.
- [x] T044 Preserve `OperationRun.status` and `OperationRun.outcome` lifecycle ownership; do not mutate them from the page.
## Phase 6: Actions, RBAC, And Safety
**Purpose**: Show only real, authorized actions and preserve read-first default behavior.
- [x] T045 Keep primary action singular and context-aware for the selected/highest-priority operation.
- [x] T046 Show open operation, open artifact, open evidence, open related source, review failure, open related alert/finding/review, view diagnostics, retry/resume, or cancel only when route/action and authorization are repo-real.
- [x] T047 Ensure unauthorized actions are hidden or unavailable without leaking sensitive details.
- [x] T048 Verify no default action restores, deletes, cancels, retries, reruns, or mutates provider state unless it already exists and is properly secondary.
- [x] T049 If any high-impact action is unexpectedly required, update spec/plan first, then implement it with `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, audit, notification, and tests.
## Phase 7: Workspace / Environment Scope Contract
**Purpose**: Preserve Specs 314-322.
- [x] T050 Verify clean `OperationRunLinks::index()` does not read remembered environment shell state or persisted table filters.
- [x] T051 Verify `OperationRunLinks::index(... environment_id ...)` filters only page data, shows visible chip, and keeps Workspace shell ownership.
- [x] T052 Verify clear filter redirects to clean workspace URL and remains safe after reload.
- [x] T053 Verify legacy aliases are removed/neutralized and do not set filter state.
- [x] T054 Verify cross-workspace or unauthorized `environment_id` returns safe no-access/404.
- [x] T055 Verify back/forward/reload behavior does not resurrect cleared environment filter state.
## Phase 8: Browser Smoke And Screenshots
**Purpose**: Prove the user-facing contract in the integrated browser lane.
- [x] T056 Create `apps/platform/tests/Browser/Spec328OperationsHubProductizationSmokeTest.php` using existing Pest Browser conventions.
- [x] T057 Browser Flow A: clean workspace entry; assert Workspace shell only, no Environment chip, main decision question, right proof panel, diagnostics collapsed, screenshot.
- [x] T058 Browser Flow B: filtered environment entry; assert Workspace shell only, visible chip, clear filter action, filtered scope copy, screenshot.
- [x] T059 Browser Flow C: clear filter and reload; assert clean URL, chip does not return, no active Environment shell.
- [x] T060 Browser Flow D: non-empty operation needing attention; assert outcome, reason, impact, environment, proof/artifact state, primary action, and diagnostics absent by default.
- [x] T061 Browser Flow E: empty/no-attention state; assert clear empty state and no false success/health claim.
- [x] T062 Browser Flow F: operations table/history remains visible lower/secondary and no platform-context tenant wording appears.
- [x] T063 Browser Flow G: light mode readability check if supported; capture optional screenshot.
- [x] T064 Save screenshots under `specs/328-operations-hub-decision-first-workbench-productization/artifacts/screenshots/` when generated and ensure they contain no secrets.
## Phase 9: UI Coverage And Documentation Artifacts
**Purpose**: Satisfy UI-COV without unrelated docs churn.
- [x] T065 Decide after runtime diff whether `docs/ui-ux-enterprise-audit/route-inventory.md` or `design-coverage-matrix.md` needs an update.
- [x] T066 If coverage docs are not changed, add a close-out note explaining why existing UI-003 report plus Spec 325 target artifacts remain sufficient for the unchanged route/archetype.
- [x] T067 Update `repo-truth-map.md` final classifications for implemented/empty/deferred elements.
- [x] T068 Do not create general documentation files outside required Spec Kit/UI coverage artifacts.
## Phase 10: Validation
**Purpose**: Run narrow proof and report honestly.
- [x] T069 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Monitoring tests/Feature/Operations tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`.
- [x] T070 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec328OperationsHubProductizationSmokeTest.php --compact`.
- [x] T071 Run `cd apps/platform && ./vendor/bin/sail artisan test --filter='Operations|OperationRun|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`.
- [x] T072 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
- [x] T073 Run `git diff --check`.
- [x] T074 Report full-suite status honestly if not run.
- [x] T075 Confirm no migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, backwards compatibility layer, or legacy tenant alias support were added.
## Phase 11: Follow-Up Summary Card Alignment
**Purpose**: Align the four Operations Workbench summary cards with compact Filament KPI/Stats-card visual density without changing execution-truth semantics.
- [x] T076 Update `spec.md` and `plan.md` with the follow-up rule that summary cards are execution/attention signals, not generic business KPIs.
- [x] T077 Refine the four summary cards to use compact native Filament Stats Overview hierarchy: title, large value, concise subline, optional description icon, and semantic status color.
- [x] T078 Ensure `Needs attention` has the strongest warning emphasis only when actionable.
- [x] T079 Ensure `Active operations` is neutral when its value is `0`.
- [x] T080 Ensure `Completed recently` remains muted success/secondary and does not imply environment or governance health.
- [x] T081 Confirm no fake trends, fake sparklines, decorative charts, `Total Operations`, or `Avg Duration` cards appear before the workbench.
- [x] T082 Verify the refined cards in the Integrated Browser in light and dark mode where supported.
- [x] T083 Re-run the requested Feature, Browser, Pint, and diff validation commands.
## Phase 12: Native Filament Stats Follow-Up
**Purpose**: Replace the custom Blade/Tailwind summary card rebuild with native Filament Stats Overview cards while preserving Spec 328 execution-truth semantics.
- [x] T084 Add a narrow `OperationsWorkbenchStats` widget using `StatsOverviewWidget` and `Stat::make(...)`.
- [x] T085 Scope widget counts to the current workspace and canonical optional `environment_id`.
- [x] T086 Render the widget above the decision workbench and remove the custom summary card loop/accent bars.
- [x] T087 Update Feature and Browser smoke tests to assert the four native Stat labels/values and no fake charts/trends.
- [x] T088 Update spec close-out and repo truth map to document native widget rendering.
- [x] T089 Re-run requested Feature, Browser, navigation contract, Pint, and diff validation.
- [x] T090 Refresh `operations-hub-premium-summary-cards.png`.
## Non-Goals Checklist
- [x] NT001 Do not rebuild OperationRun backend.
- [x] NT002 Do not build a new queue engine or observability platform.
- [x] NT003 Do not turn Operations Hub into a governance health dashboard.
- [x] NT004 Do not redesign Governance Inbox, Customer Review Workspace, Evidence Overview, Environment Dashboard, Baseline Compare, Restore, Backup, or Provider Readiness.
- [x] NT005 Do not add AI prioritization or summarization.
- [x] NT006 Do not add migrations unless spec/plan are updated first with proof.
- [x] NT007 Do not add new operation types, statuses, outcomes, priority enums, or summary-count keys.
- [x] NT008 Do not rewrite completed Specs 314-327.
- [x] NT009 Do not add legacy tenant query alias support.
## Required Final Report Content
When implementation later completes, report:
- Changed behavior.
- Decision-first operations workbench details.
- OperationRun outcome/progress/proof coverage.
- Files changed.
- Repo truth map status.
- Tests run and results.
- Browser verification and screenshots path.
- Known gaps.
- Remaining follow-ups.
- Diagnostics default state.
- RBAC-visible/hidden actions.
- Repo-verified vs unavailable states.
- Full suite run/not run.
- Explicit no migrations/seeders/packages/env/queues/scheduler/storage/deployment assets/backcompat/legacy aliases statement.