feat: retrofit deferred operator surfaces #203

Merged
ahmido merged 1 commits from 172-deferred-operator-surfaces-retrofit into dev 2026-04-02 09:22:45 +00:00
27 changed files with 1716 additions and 235 deletions
Showing only changes of commit 12a812825f - Show all commits

View File

@ -122,6 +122,7 @@ ## Active Technologies
- PostgreSQL with existing `operation_runs` and `audit_logs` tables; no schema changes (170-system-operations-surface-alignment)
- PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest` (171-operations-naming-consolidation)
- PostgreSQL with existing `operation_runs`, notification payloads, workspace records, and tenant records; no schema changes (171-operations-naming-consolidation)
- PostgreSQL with existing `operation_runs`, `managed_tenant_onboarding_sessions`, tenant records, and workspace records; no schema changes (172-deferred-operator-surfaces-retrofit)
- PHP 8.4.15 (feat/005-bulk-operations)
@ -141,8 +142,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions
## Recent Changes
- 172-deferred-operator-surfaces-retrofit: Added PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest`
- 171-operations-naming-consolidation: Added PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest`
- 170-system-operations-surface-alignment: Added PHP 8.4, Laravel 12, Livewire v4, Filament v5 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest`
- 169-action-surface-v11: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `ActionSurfaceDeclaration`, `ActionSurfaceValidator`, `ActionSurfaceDiscovery`, `ActionSurfaceExemptions`, and Filament Tables / Actions APIs
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

View File

@ -87,6 +87,7 @@
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\ValidationException;
use InvalidArgumentException;
use Livewire\Attributes\Locked;
@ -564,7 +565,7 @@ public function content(Schema $schema): Schema
->default(null)
->view('filament.forms.components.managed-tenant-onboarding-verification-report')
->viewData(fn (): array => $this->verificationReportViewData())
->visible(fn (): bool => $this->verificationRunUrl() !== null),
->visible(fn (): bool => $this->managedTenant instanceof Tenant),
]),
])
->beforeValidation(function (): void {
@ -1708,27 +1709,24 @@ private function verificationStatusColor(): string
private function verificationRunUrl(): ?string
{
if (! $this->managedTenant instanceof Tenant) {
$run = $this->verificationRun();
if (! $run instanceof OperationRun) {
return null;
}
if (! $this->onboardingSession instanceof TenantOnboardingSession) {
if (! $this->canInspectOperationRun($run)) {
return null;
}
$runId = $this->onboardingSession->state['verification_operation_run_id'] ?? null;
if (! is_int($runId)) {
return null;
}
return $this->tenantlessOperationRunUrl($runId);
return $this->tenantlessOperationRunUrl((int) $run->getKey());
}
/**
* @return array{
* run: array<string, mixed>|null,
* runUrl: string|null,
* advancedRunUrl: string|null,
* report: array<string, mixed>|null,
* fingerprint: string|null,
* changeIndicator: array{state: 'no_changes'|'changed', previous_report_id: int}|null,
@ -1742,7 +1740,9 @@ private function verificationRunUrl(): ?string
* acknowledged_by: array{id: int, name: string}|null
* }>,
* assistVisibility: array{is_visible: bool, reason: 'permission_blocked'|'permission_attention'|'hidden_ready'|'hidden_irrelevant'},
* assistActionName: string
* assistActionName: string,
* technicalDetailsActionName: string,
* runState: 'no_run'|'active'|'completed'
* }
*/
private function verificationReportViewData(): array
@ -1755,6 +1755,7 @@ private function verificationReportViewData(): array
return [
'run' => null,
'runUrl' => $runUrl,
'advancedRunUrl' => null,
'report' => null,
'fingerprint' => null,
'changeIndicator' => null,
@ -1763,6 +1764,8 @@ private function verificationReportViewData(): array
'acknowledgements' => [],
'assistVisibility' => $assistVisibility,
'assistActionName' => 'wizardVerificationRequiredPermissionsAssist',
'technicalDetailsActionName' => 'wizardVerificationTechnicalDetails',
'runState' => 'no_run',
];
}
@ -1770,9 +1773,7 @@ private function verificationReportViewData(): array
$fingerprint = is_array($report) ? VerificationReportViewer::fingerprint($report) : null;
$changeIndicator = VerificationReportChangeIndicator::forRun($run);
$previousRunUrl = $changeIndicator === null
? null
: $this->tenantlessOperationRunUrl((int) $changeIndicator['previous_report_id']);
$previousRunUrl = $this->verificationPreviousRunUrl($changeIndicator);
$user = auth()->user();
$canAcknowledge = $user instanceof User && $this->managedTenant instanceof Tenant
@ -1820,11 +1821,13 @@ private function verificationReportViewData(): array
'outcome' => (string) $run->outcome,
'initiator_name' => (string) $run->initiator_name,
'started_at' => $run->started_at?->toJSON(),
'updated_at' => $run->updated_at?->toJSON(),
'completed_at' => $run->completed_at?->toJSON(),
'target_scope' => $targetScope,
'failures' => $failures,
],
'runUrl' => $runUrl,
'advancedRunUrl' => $runUrl,
'report' => $report,
'fingerprint' => $fingerprint,
'changeIndicator' => $changeIndicator,
@ -1833,6 +1836,8 @@ private function verificationReportViewData(): array
'acknowledgements' => $acknowledgements,
'assistVisibility' => $assistVisibility,
'assistActionName' => 'wizardVerificationRequiredPermissionsAssist',
'technicalDetailsActionName' => 'wizardVerificationTechnicalDetails',
'runState' => (string) $run->status === OperationRunStatus::Completed->value ? 'completed' : 'active',
];
}
@ -1855,6 +1860,26 @@ public function wizardVerificationRequiredPermissionsAssistAction(): Action
->visible(fn (): bool => $this->verificationAssistVisibility()['is_visible']);
}
public function wizardVerificationTechnicalDetailsAction(): Action
{
return Action::make('wizardVerificationTechnicalDetails')
->label('Technical details')
->icon('heroicon-m-information-circle')
->color('gray')
->modal()
->slideOver()
->stickyModalHeader()
->modalHeading('Verification technical details')
->modalDescription('Diagnostics-only details for the current verification run.')
->modalSubmitAction(false)
->modalCancelAction(fn (Action $action): Action => $action->label('Close'))
->modalContent(fn (): View => view(
'filament.modals.onboarding-verification-technical-details',
$this->verificationTechnicalDetailsViewData(),
))
->visible(fn (): bool => $this->verificationRun() instanceof OperationRun);
}
public function acknowledgeVerificationCheckAction(): Action
{
return Action::make('acknowledgeVerificationCheck')
@ -3229,6 +3254,89 @@ private function tenantlessOperationRunUrl(int $runId): string
return OperationRunLinks::tenantlessView($runId);
}
/**
* @param array{state: 'no_changes'|'changed', previous_report_id: int}|null $changeIndicator
*/
private function verificationPreviousRunUrl(?array $changeIndicator): ?string
{
if (! is_array($changeIndicator)) {
return null;
}
$previousRunId = $changeIndicator['previous_report_id'] ?? null;
if (! is_int($previousRunId)) {
return null;
}
$previousRun = OperationRun::query()->whereKey($previousRunId)->first();
if (! $previousRun instanceof OperationRun) {
return null;
}
if (! $this->canInspectOperationRun($previousRun)) {
return null;
}
return $this->tenantlessOperationRunUrl((int) $previousRun->getKey());
}
/**
* @return array{
* run: array<string, mixed>|null,
* runUrl: string|null,
* previousRunUrl: string|null,
* hasReport: bool
* }
*/
private function verificationTechnicalDetailsViewData(): array
{
$run = $this->verificationRun();
if (! $run instanceof OperationRun) {
return [
'run' => null,
'runUrl' => null,
'previousRunUrl' => null,
'hasReport' => false,
];
}
$report = VerificationReportViewer::report($run);
$changeIndicator = VerificationReportChangeIndicator::forRun($run);
$context = is_array($run->context ?? null) ? $run->context : [];
$targetScope = $context['target_scope'] ?? [];
$targetScope = is_array($targetScope) ? $targetScope : [];
return [
'run' => [
'id' => (int) $run->getKey(),
'type' => (string) $run->type,
'status' => (string) $run->status,
'outcome' => (string) $run->outcome,
'started_at' => $run->started_at?->toJSON(),
'updated_at' => $run->updated_at?->toJSON(),
'completed_at' => $run->completed_at?->toJSON(),
'target_scope' => $targetScope,
],
'runUrl' => $this->verificationRunUrl(),
'previousRunUrl' => $this->verificationPreviousRunUrl($changeIndicator),
'hasReport' => is_array($report),
];
}
private function canInspectOperationRun(OperationRun $run): bool
{
$user = auth()->user();
if (! $user instanceof User) {
return false;
}
return Gate::forUser($user)->allows('view', $run);
}
public function verificationSucceeded(): bool
{
return $this->verificationHasSucceeded();

View File

@ -29,6 +29,16 @@ class ViewTenant extends ViewRecord
{
protected static string $resource = TenantResource::class;
public static function verificationHeaderActionLabel(): string
{
return 'Verify configuration';
}
public static function verificationHeaderActionHint(): string
{
return 'Use "'.self::verificationHeaderActionLabel().'" in the tenant header to run verification again after you inspect the current operation.';
}
public function getHeaderWidgetsColumns(): int|array
{
return 1;
@ -83,7 +93,7 @@ protected function getHeaderActions(): array
->openUrlInNewTab(),
UiEnforcement::forAction(
Actions\Action::make('verify')
->label('Verify configuration')
->label(self::verificationHeaderActionLabel())
->icon('heroicon-o-check-badge')
->color('primary')
->requiresConfirmation()

View File

@ -6,6 +6,7 @@
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\OperationRunLinks;
use Filament\Facades\Filament;
use Filament\Widgets\Widget;
use Illuminate\Database\Eloquent\Collection;
@ -41,6 +42,8 @@ protected function getViewData(): array
'tenant' => null,
'runs' => collect(),
'operationsIndexUrl' => route('admin.operations.index'),
'operationsIndexLabel' => OperationRunLinks::openCollectionLabel(),
'operationsIndexDescription' => OperationRunLinks::collectionScopeDescription(),
];
}
@ -64,6 +67,8 @@ protected function getViewData(): array
'tenant' => $tenant,
'runs' => $runs,
'operationsIndexUrl' => route('admin.operations.index'),
'operationsIndexLabel' => OperationRunLinks::openCollectionLabel(),
'operationsIndexDescription' => OperationRunLinks::collectionScopeDescription(),
];
}
}

View File

@ -4,6 +4,7 @@
namespace App\Filament\Widgets\Tenant;
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
use App\Filament\Support\VerificationReportViewer;
use App\Models\OperationRun;
use App\Models\Tenant;
@ -175,6 +176,7 @@ protected function getViewData(): array
'isInProgress' => false,
'canStart' => false,
'startTooltip' => null,
'rerunHint' => null,
];
}
@ -230,10 +232,13 @@ protected function getViewData(): array
'report' => $report,
'redactionNotes' => VerificationReportViewer::redactionNotes($report),
'isInProgress' => $isInProgress,
'showStartAction' => $isTenantMember && $canOperate,
'showStartAction' => ! ($run instanceof OperationRun) && $isTenantMember && $canOperate,
'canStart' => $canStart,
'startTooltip' => $isTenantMember && $canOperate && ! $canStart ? UiTooltips::insufficientPermission() : null,
'lifecycleNotice' => $lifecycleNotice,
'rerunHint' => $run instanceof OperationRun && $canStart
? ViewTenant::verificationHeaderActionHint()
: null,
];
}
}

View File

@ -33,6 +33,11 @@ public static function openCollectionLabel(): string
return 'Open operations';
}
public static function collectionScopeDescription(): string
{
return 'Broader admin view across recent and historical operations.';
}
public static function viewInCollectionLabel(): string
{
return 'View in Operations';
@ -48,6 +53,16 @@ public static function openLabel(): string
return 'Open operation';
}
public static function advancedMonitoringLabel(): string
{
return 'Open operation in Monitoring (advanced)';
}
public static function advancedMonitoringDescription(): string
{
return 'Diagnostics-only link to the canonical admin operation viewer.';
}
public static function identifierLabel(): string
{
return 'Operation ID';

View File

@ -27,7 +27,7 @@ public static function baseline(): self
'App\\Filament\\Pages\\Monitoring\\Alerts' => 'Monitoring alerts remains exempt because the active admin alerts surface resolves through the cluster entry at /admin/alerts, not this page-class route.',
'App\\Filament\\Pages\\Tenancy\\RegisterTenant' => 'Tenant onboarding route is covered by onboarding/RBAC specs.',
'App\\Filament\\Pages\\TenantDashboard' => 'Dashboard retrofit deferred; widget and summary surfaces are excluded from this contract.',
'App\\Filament\\Pages\\Workspaces\\ManagedTenantOnboardingWizard' => 'Onboarding wizard has dedicated conformance tests and remains exempt in spec 082.',
'App\\Filament\\Pages\\Workspaces\\ManagedTenantOnboardingWizard' => 'Onboarding wizard has dedicated conformance tests in spec 172 (OnboardingVerificationTest, OnboardingVerificationClustersTest, OnboardingVerificationV1_5UxTest) and remains exempt from blanket discovery.',
'App\\Filament\\Pages\\Workspaces\\ManagedTenantsLanding' => 'Managed-tenant landing retrofit deferred to workspace feature track.',
], TenantOwnedModelFamilies::actionSurfaceBaselineExemptions()));
}

View File

@ -19,6 +19,9 @@
$previousRunUrl = $previousRunUrl ?? null;
$previousRunUrl = is_string($previousRunUrl) && $previousRunUrl !== '' ? $previousRunUrl : null;
$advancedRunUrl = $advancedRunUrl ?? null;
$advancedRunUrl = is_string($advancedRunUrl) && $advancedRunUrl !== '' ? $advancedRunUrl : null;
$canAcknowledge = (bool) ($canAcknowledge ?? false);
$acknowledgements = $acknowledgements ?? [];
@ -32,6 +35,11 @@
? trim($assistActionName)
: 'wizardVerificationRequiredPermissionsAssist';
$technicalDetailsActionName = $technicalDetailsActionName ?? 'wizardVerificationTechnicalDetails';
$technicalDetailsActionName = is_string($technicalDetailsActionName) && trim($technicalDetailsActionName) !== ''
? trim($technicalDetailsActionName)
: 'wizardVerificationTechnicalDetails';
$showAssist = (bool) ($assistVisibility['is_visible'] ?? false);
$assistReason = $assistVisibility['reason'] ?? 'hidden_irrelevant';
$assistReason = is_string($assistReason) ? $assistReason : 'hidden_irrelevant';
@ -149,6 +157,15 @@
}
$linkBehavior = app(\App\Support\Verification\VerificationLinkBehavior::class);
$runState = $runState ?? null;
$runState = is_string($runState) ? $runState : null;
if (! in_array($runState, ['no_run', 'active', 'completed'], true)) {
$runState = $run === null
? 'no_run'
: ($status === 'completed' ? 'completed' : 'active');
}
@endphp
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
@ -157,14 +174,41 @@
heading="Verification report"
:description="$completedAtLabel ? ('Completed: ' . $completedAtLabel) : 'Stored details for the latest verification operation.'"
>
@if ($run === null)
<div class="text-sm text-gray-600 dark:text-gray-300">
No verification operation has been started yet.
@if ($runState === 'no_run')
<div class="space-y-2 rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
<div class="font-medium text-gray-900 dark:text-white">
No verification operation has been started yet.
</div>
<div>
Use the workflow action above to start verification for this tenant.
</div>
</div>
@elseif ($status !== 'completed')
<div class="text-sm text-gray-600 dark:text-gray-300">
@elseif ($runState === 'active')
<div class="rounded-lg border border-gray-200 bg-white p-4 text-sm text-gray-600 shadow-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-300">
Report unavailable while the operation is in progress. Stored status updates automatically about every 5 seconds. Use “Refresh” to re-check immediately.
</div>
<div class="mt-4 flex flex-wrap items-center gap-2">
@if ($runUrl)
<x-filament::button
tag="a"
:href="$runUrl"
color="primary"
size="sm"
>
{{ \App\Support\OperationRunLinks::openLabel() }}
</x-filament::button>
@endif
<x-filament::button
color="gray"
size="sm"
wire:click="mountAction('{{ $technicalDetailsActionName }}')"
>
Technical details
</x-filament::button>
</div>
@else
<div class="space-y-4">
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
@ -226,6 +270,27 @@
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
@if ($runUrl)
<x-filament::button
tag="a"
:href="$runUrl"
color="primary"
size="sm"
>
{{ \App\Support\OperationRunLinks::openLabel() }}
</x-filament::button>
@endif
<x-filament::button
color="gray"
size="sm"
wire:click="mountAction('{{ $technicalDetailsActionName }}')"
>
Technical details
</x-filament::button>
</div>
@if ($showAssist)
<div class="rounded-lg border border-warning-300 bg-warning-50 p-4 shadow-sm dark:border-warning-700 dark:bg-warning-950/40">
<div class="flex flex-wrap items-start justify-between gap-3">
@ -279,13 +344,6 @@ class="space-y-4"
>
Passed
</x-filament::tabs.item>
<x-filament::tabs.item
:active="false"
alpine-active="tab === 'technical'"
x-on:click="tab = 'technical'"
>
Technical details
</x-filament::tabs.item>
</x-filament::tabs>
<div x-show="tab === 'issues'">
@ -587,85 +645,6 @@ class="inline-flex items-center gap-2 text-primary-600 hover:underline dark:text
</div>
@endif
</div>
<div x-show="tab === 'technical'" style="display: none;">
<div class="space-y-4 text-sm text-gray-700 dark:text-gray-200">
<div class="space-y-1">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Identifiers
</div>
<div class="flex flex-col gap-1">
<div>
<span class="text-gray-500 dark:text-gray-400">Operation ID:</span>
<span class="font-mono">{{ (int) ($run['id'] ?? 0) }}</span>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Flow:</span>
<span class="font-mono">{{ (string) ($run['type'] ?? '') }}</span>
</div>
@if ($fingerprint)
<div>
<span class="text-gray-500 dark:text-gray-400">Fingerprint:</span>
<span class="font-mono text-xs break-all">{{ $fingerprint }}</span>
</div>
@endif
</div>
</div>
@if ($previousRunUrl !== null)
<div>
<a
href="{{ $previousRunUrl }}"
class="font-medium text-primary-600 hover:underline dark:text-primary-400"
>
Open previous operation
</a>
</div>
@endif
@if ($runUrl !== null)
<div>
<a
href="{{ $runUrl }}"
class="font-medium text-primary-600 hover:underline dark:text-primary-400"
>
Open operation details
</a>
</div>
@endif
@if ($targetScope !== [])
<div class="space-y-1">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Target scope
</div>
<div class="flex flex-col gap-1">
@php
$entraTenantId = $targetScope['entra_tenant_id'] ?? null;
$entraTenantName = $targetScope['entra_tenant_name'] ?? null;
$entraTenantId = is_string($entraTenantId) && $entraTenantId !== '' ? $entraTenantId : null;
$entraTenantName = is_string($entraTenantName) && $entraTenantName !== '' ? $entraTenantName : null;
@endphp
@if ($entraTenantName !== null)
<div>
<span class="text-gray-500 dark:text-gray-400">Entra tenant:</span>
<span class="font-medium text-gray-900 dark:text-gray-100">{{ $entraTenantName }}</span>
</div>
@endif
@if ($entraTenantId !== null)
<div>
<span class="text-gray-500 dark:text-gray-400">Entra tenant ID:</span>
<span class="font-mono text-xs break-all text-gray-900 dark:text-gray-100">{{ $entraTenantId }}</span>
</div>
@endif
</div>
</div>
@endif
</div>
</div>
</div>
@endif
</div>

View File

@ -5,6 +5,9 @@
$runUrl = $runUrl ?? null;
$runUrl = is_string($runUrl) && $runUrl !== '' ? $runUrl : null;
$previousRunUrl = $previousRunUrl ?? null;
$previousRunUrl = is_string($previousRunUrl) && $previousRunUrl !== '' ? $previousRunUrl : null;
$status = $run['status'] ?? null;
$status = is_string($status) ? $status : null;
@ -142,16 +145,47 @@
</div>
</div>
@if ($runUrl)
<div>
<a
href="{{ $runUrl }}"
class="text-sm font-medium text-gray-600 hover:underline dark:text-gray-300"
target="_blank"
rel="noreferrer"
>
Open operation in Monitoring (advanced)
</a>
@if ($previousRunUrl || $runUrl)
<div class="rounded-lg border border-gray-200 bg-white p-3 dark:border-gray-800 dark:bg-gray-900">
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Diagnostics links
</div>
<div class="mt-2 space-y-3">
@if ($previousRunUrl)
<div class="space-y-1">
<a
href="{{ $previousRunUrl }}"
class="text-sm font-medium text-gray-600 hover:underline dark:text-gray-300"
target="_blank"
rel="noreferrer"
>
Open previous operation
</a>
<div class="text-xs text-gray-500 dark:text-gray-400">
Compare against the prior verification run without changing the onboarding workflow.
</div>
</div>
@endif
@if ($runUrl)
<div class="space-y-1">
<a
href="{{ $runUrl }}"
class="text-sm font-medium text-gray-600 hover:underline dark:text-gray-300"
target="_blank"
rel="noreferrer"
>
{{ \App\Support\OperationRunLinks::advancedMonitoringLabel() }}
</a>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ \App\Support\OperationRunLinks::advancedMonitoringDescription() }}
</div>
</div>
@endif
</div>
</div>
@endif
@endif

View File

@ -2,18 +2,11 @@
/** @var ?\App\Models\Tenant $tenant */
/** @var \Illuminate\Support\Collection<int, \App\Models\OperationRun> $runs */
/** @var string $operationsIndexUrl */
/** @var string $operationsIndexLabel */
/** @var string $operationsIndexDescription */
@endphp
<x-filament::section heading="Recent operations">
<x-slot name="afterHeader">
<a
href="{{ $operationsIndexUrl }}"
class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
>
View all operations
</a>
</x-slot>
@if ($runs->isEmpty())
<div class="text-sm text-gray-500 dark:text-gray-400">
No operations yet.
@ -69,5 +62,20 @@ class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-pri
</li>
@endforeach
</ul>
@if (! empty($operationsIndexUrl))
<div class="mt-4 border-t border-gray-200 pt-3 dark:border-gray-800">
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $operationsIndexDescription }}
</div>
<a
href="{{ $operationsIndexUrl }}"
class="mt-2 inline-flex items-center gap-2 text-xs font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
>
{{ $operationsIndexLabel }}
</a>
</div>
@endif
@endif
</x-filament::section>

View File

@ -20,6 +20,9 @@
$lifecycleNotice = $lifecycleNotice ?? null;
$lifecycleNotice = is_string($lifecycleNotice) && trim($lifecycleNotice) !== '' ? trim($lifecycleNotice) : null;
$rerunHint = $rerunHint ?? null;
$rerunHint = is_string($rerunHint) && trim($rerunHint) !== '' ? trim($rerunHint) : null;
@endphp
<x-filament::section
@ -76,46 +79,19 @@
<x-filament::button
tag="a"
:href="$runUrl"
color="gray"
color="primary"
size="sm"
>
Open operation
</x-filament::button>
@endif
@if ($showStartAction)
@if ($canStart)
<x-filament::button
color="primary"
size="sm"
wire:click="startVerification"
>
Start verification
</x-filament::button>
@else
<div class="flex flex-col gap-1">
<x-filament::button
color="gray"
size="sm"
disabled
:title="$startTooltip"
>
Start verification
</x-filament::button>
@if ($startTooltip)
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $startTooltip }}
</div>
@endif
</div>
@endif
@elseif ($lifecycleNotice)
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $lifecycleNotice }}
</div>
@endif
</div>
@if ($rerunHint || $startTooltip || $lifecycleNotice)
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $rerunHint ?? $startTooltip ?? $lifecycleNotice }}
</div>
@endif
@else
@include('filament.components.verification-report-viewer', [
'run' => $runData,
@ -128,46 +104,19 @@
<x-filament::button
tag="a"
:href="$runUrl"
color="gray"
color="primary"
size="sm"
>
Open operation
</x-filament::button>
@endif
@if ($showStartAction)
@if ($canStart)
<x-filament::button
color="primary"
size="sm"
wire:click="startVerification"
>
Start verification
</x-filament::button>
@else
<div class="flex flex-col gap-1">
<x-filament::button
color="gray"
size="sm"
disabled
:title="$startTooltip"
>
Start verification
</x-filament::button>
@if ($startTooltip)
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $startTooltip }}
</div>
@endif
</div>
@endif
@elseif ($lifecycleNotice)
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $lifecycleNotice }}
</div>
@endif
</div>
@if ($rerunHint || $startTooltip || $lifecycleNotice)
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $rerunHint ?? $startTooltip ?? $lifecycleNotice }}
</div>
@endif
@endif
</div>
</x-filament::section>

View File

@ -33,3 +33,4 @@ ## Notes
- Validation pass 1: complete
- This spec intentionally targets only deferred non-table surfaces that already expose operation affordances; unrelated deferred pages remain explicit non-goals unless a later spec enrolls them.
- Validation pass 2: aligned route and surface naming with the current implementation. The tenant detail view and onboarding verification surfaces are in scope; the table-based tenant dashboard operations widget remains out of scope for this deferred-surface retrofit.

View File

@ -0,0 +1,138 @@
openapi: 3.1.0
info:
title: Deferred Embedded Operation Surface Contract
version: 1.0.0
summary: CTA hierarchy and scope contract for tenant-detail and onboarding surfaces that reference existing OperationRun records.
paths:
/admin/tenants/{record}:
get:
operationId: renderTenantDetailEmbeddedOperationSurfaces
summary: Render tenant-detail embedded widgets that may drill into the canonical operations viewers.
parameters:
- name: record
in: path
required: true
schema:
type: string
responses:
'200':
description: Tenant detail surface rendered successfully.
'403':
description: Authenticated tenant member lacks the required capability within the established tenant scope.
'404':
description: Wrong plane, missing workspace or tenant membership, or inaccessible tenant detail record.
x-surface-rules:
recentOperationsSummary:
canonicalCollectionRoute: /admin/operations
canonicalDetailRoute: /admin/operations/{run}
primaryInspectModel: Row-level Open operation links for displayed records.
collectionAffordance:
allowed: true
prominence: secondary
scopeRequirement: Any remaining collection affordance must make broader admin scope explicit through nearby copy or placement.
forbiddenPatterns:
- A header-level collection CTA with equal emphasis to row-level inspect links.
tenantVerificationWidget:
primaryCtaByState:
noRun: Start verification
activeRun: Open operation
completedRun: Open operation
archivedOrInactive: none
rerunPath:
owner: Tenant detail header action
label: Verify configuration
inlineSecondaryCtasAllowed: []
x-unchanged-behavior:
- Existing authorization, capability checks, and tenant/workspace isolation remain authoritative.
- Existing OperationRun lifecycle, notification timing, and route helpers remain unchanged.
/admin/onboarding:
get:
operationId: renderOnboardingVerificationOperationSurfaces
summary: Render onboarding verification workflow controls plus embedded report and technical-details surfaces.
responses:
'200':
description: Onboarding verification surface rendered successfully.
'403':
description: Authenticated workspace member lacks the required capability within the established workspace scope.
'404':
description: Wrong plane, missing workspace membership, or inaccessible onboarding context.
x-surface-rules:
workflowControls:
primaryCtaByState:
noRun: Start verification
activeRun: Refresh
completedRun: none
reportSurface:
currentRunInspect:
allowed: true
prominence: primary
labelFamily:
- Open operation
previousRunInspect:
allowed: true
prominence: secondary
placement: diagnostics only
technicalDetails:
advancedMonitoringLink:
allowed: true
prominence: secondary
visibilityRule: Only when the operator can access the destination and the link is explicitly labeled as advanced.
x-unchanged-behavior:
- Existing onboarding workflow semantics, session fields, and verification execution behavior remain unchanged.
- Existing step progression and permission-assist behavior remain unchanged.
/admin/operations:
get:
operationId: listAdminOperations
summary: Canonical admin-plane operations collection used by embedded drill-ins.
responses:
'200':
description: Admin operations collection rendered successfully.
'403':
description: Authenticated member lacks the required capability within an established scope.
'404':
description: Wrong plane, missing scope membership, or inaccessible workspace or tenant context.
x-canonical-role:
role: collection-destination
visibleNoun: Operations
unchangedBehavior:
- Existing route helper remains authoritative.
- This feature only changes how embedded surfaces explain navigation into this collection.
/admin/operations/{run}:
get:
operationId: viewAdminOperation
summary: Canonical admin-plane operation detail used by embedded drill-ins.
parameters:
- name: run
in: path
required: true
schema:
type: integer
responses:
'200':
description: Admin operation detail rendered successfully.
'403':
description: Authenticated member lacks the required capability within an established scope.
'404':
description: Wrong plane, missing scope membership, or inaccessible operation record.
x-canonical-role:
role: detail-destination
visibleNoun: Operation
unchangedBehavior:
- Existing route helper remains authoritative.
- Existing membership and capability checks remain unchanged.
/admin/t/{tenant}:
get:
operationId: tenantDashboardReference
summary: Reference route for the table-based tenant dashboard operations widget.
parameters:
- name: tenant
in: path
required: true
schema:
type: string
responses:
'200':
description: Tenant dashboard rendered successfully.
x-scope-status:
status: out-of-scope
rationale: The table-based recent-operations widget on the tenant dashboard is already declaration-backed and is not part of the deferred embedded-surface retrofit.

View File

@ -0,0 +1,180 @@
# Data Model: Deferred Operator Surfaces Retrofit
## Overview
This feature introduces no new persisted entity, no new table, and no new state family. It reuses existing `OperationRun` truth plus existing tenant-detail and onboarding presentation state to enforce clearer CTA hierarchy and scope signals.
## Entity: OperationRun
- **Type**: Existing persisted model
- **Purpose in this feature**: Canonical record whose existing admin-plane collection/detail routes remain the inspect targets for tenant-detail and onboarding embedded surfaces.
### Relevant Fields
| Field | Type | Notes |
|-------|------|-------|
| `id` | integer | Existing operation identifier used by embedded links and labels. |
| `type` | string | Continues to determine the operation label and UX guidance. |
| `workspace_id` | integer nullable | Preserves workspace-context routing and entitlement checks. |
| `tenant_id` | integer nullable | Preserves tenant-context entitlement and embedded-surface filtering. |
| `status` | string | Drives whether the current run is still active. |
| `outcome` | string | Drives blocked/failed/succeeded summary presentation. |
| `context` | array/json | Already carries target-scope, verification-report, and next-step metadata used by the affected surfaces. |
| `created_at` | timestamp | Used for recency display on recent-operations surfaces. |
| `started_at` / `completed_at` | timestamp nullable | Used for in-progress vs completed display and technical details. |
### Relationships
| Relationship | Target | Purpose |
|--------------|--------|---------|
| `tenant` | `Tenant` | Keeps tenant entitlement and tenant detail context explicit. |
| `workspace` | `Workspace` | Preserves workspace-context authorization for onboarding and admin viewers. |
| `initiator` | `User` / platform initiator context | Remains unchanged; no notification or lifecycle behavior changes in this feature. |
### Feature-Specific Invariants
- `/admin/operations` and `/admin/operations/{run}` remain the canonical collection/detail destinations.
- No new `OperationRun` type, state transition, notification timing, or summary-count behavior is introduced.
- Embedded surfaces may change CTA hierarchy and nearby scope copy, but not the underlying destination semantics.
### State Transitions Used By This Feature
| Transition | Preconditions | Result |
|------------|---------------|--------|
| Render inspect link for existing record | A covered surface already has a current `OperationRun` reference | No state change; surface exposes a single primary inspect path for that record. |
| Render workflow-start action | Covered surface has no current `OperationRun` reference and the operator can start the workflow | No state change; surface exposes one next-step CTA such as `Start verification`. |
## Derived Surface State: Tenant Detail Recent Operations Summary
- **Type**: Existing derived widget state, not persisted
- **Sources**:
- `app/Filament/Resources/TenantResource/Pages/ViewTenant.php`
- `app/Filament/Widgets/Tenant/RecentOperationsSummary.php`
- `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php`
- `app/Support/OperationRunLinks.php`
### Relevant Fields
| Field | Type | Purpose |
|-------|------|---------|
| `tenant` | `Tenant` | Provides the current tenant context for filtering and copy. |
| `runs` | collection of `OperationRun` | Existing recent operation records rendered in the embedded summary. |
| `operationsIndexUrl` | string | Existing admin-plane collection destination. |
| `rowOperationUrl` | string derived | Existing admin-plane detail destination per rendered run. |
| `hasRuns` | boolean derived | Distinguishes empty vs populated summary state. |
### Feature-Specific Invariants
- Row-level `Open operation` remains the primary inspect affordance for displayed records.
- Any collection drill-in that remains visible is secondary and must make the broader admin scope explicit through placement or nearby helper text.
- The table-based recent-operations widget on `/admin/t/{tenant}` remains out of scope for this model.
### State Rules
| State | Preconditions | Primary CTA | Secondary CTA |
|-------|---------------|-------------|---------------|
| Empty summary | `runs` is empty | None or a single next-step/collection affordance if retained | Admin-scope collection link only if clearly secondary |
| Populated summary | `runs` is not empty | Per-row `Open operation` for each visible record | One secondary collection affordance at most |
## Derived Surface State: Tenant Verification Widget
- **Type**: Existing derived widget state, not persisted
- **Sources**:
- `app/Filament/Resources/TenantResource/Pages/ViewTenant.php`
- `app/Filament/Widgets/Tenant/TenantVerificationReport.php`
- `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php`
### Relevant Fields
| Field | Type | Purpose |
|-------|------|---------|
| `run` | `OperationRun` nullable | Current verification-backed operation, if one exists. |
| `runUrl` | string nullable | Canonical inspect path for the current run. |
| `report` | array nullable | Stored verification report payload displayed read-only. |
| `isInProgress` | boolean | Distinguishes active vs completed run state. |
| `showStartAction` | boolean | Whether the embedded surface may expose a workflow-start CTA in empty-state conditions. |
| `canStart` | boolean | Whether the current actor can start verification. |
| `startTooltip` | string nullable | Existing permission helper text for disabled start states. |
| `lifecycleNotice` | string nullable | Existing archived/inactive-tenant explanation when starting work is not allowed. |
### Feature-Specific Invariants
- When no run exists and the tenant can be operated, the widget exposes one primary `Start verification` CTA.
- When a run exists, the widget exposes one primary inspect CTA for that run and does not compete with an inline rerun CTA.
- The existing tenant-detail header action remains the rerun/start path when the page still needs one outside the embedded widget.
- Archived or inoperable tenants may show explanation text, but no new inspect or start path is introduced by this feature.
### State Matrix
| State | Preconditions | Primary CTA | Secondary Inline CTA |
|-------|---------------|-------------|----------------------|
| No run / start allowed | `run` is null and `showStartAction && canStart` | `Start verification` | None |
| No run / cannot start | `run` is null and `! canStart` | None | None |
| Active run | `run` exists and `isInProgress` | `Open operation` | None |
| Completed or stale run | `run` exists and `! isInProgress` | `Open operation` | None |
| Archived / inactive tenant | `showStartAction` is false and `lifecycleNotice` is present | None | None |
## Derived Surface State: Onboarding Verification Surface
- **Type**: Existing guided-flow report state, not persisted
- **Sources**:
- `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`
- `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php`
- `resources/views/filament/modals/onboarding-verification-technical-details.blade.php`
- `managed_tenant_onboarding_sessions.state[verification_operation_run_id]`
### Relevant Fields
| Field | Type | Purpose |
|-------|------|---------|
| `verification_operation_run_id` | integer nullable | Existing onboarding-session pointer to the current verification run. |
| `run` | array nullable | Read-only operation data rendered in the onboarding report. |
| `runUrl` | string nullable | Canonical inspect path for the current run. |
| `previousRunUrl` | string nullable | Secondary link to the previously relevant run, when retained. |
| `report` | array nullable | Stored verification report payload rendered in the onboarding step. |
| `workflowPrimaryAction` | derived string nullable | Existing step-level CTA such as `Start verification` or `Refresh`. |
| `technicalDetailsVisible` | boolean derived | Controls whether advanced monitoring/context affordances are available. |
### Feature-Specific Invariants
- The wizard step owns workflow-next-step controls such as `Start verification` and `Refresh`.
- The embedded report/technical-details surfaces may expose one inspect CTA for the current run, but previous-run and monitoring links remain diagnostics-secondary only.
- Any advanced monitoring/admin destination is visible only when the destination is legitimate for the current operator and remains explicitly labeled as advanced.
- No new onboarding state, session field, or workflow branch is introduced.
### State Matrix
| State | Preconditions | Workflow CTA | Inspect CTA | Diagnostics CTA |
|-------|---------------|--------------|-------------|-----------------|
| No run | onboarding session has no current verification run | `Start verification` | None | None |
| Active run | current verification run exists and is not completed | `Refresh` | One current-run inspect link if retained on the report surface | Advanced monitoring only in technical details |
| Completed run | current verification run exists and is completed | None or existing step progression controls | One current-run inspect link | Previous-run and monitoring links remain secondary |
## Governance Artifact: Deferred Surface Exemption And Conformance Coverage
- **Type**: Existing registry and test coverage, not persisted
- **Sources**:
- `app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`
- `tests/Feature/Guards/ActionSurfaceContractTest.php`
- focused widget/onboarding feature tests listed in the plan
### Relevant Fields
| Field | Type | Purpose |
|-------|------|---------|
| `className` | string | Exempted page/component class still outside declaration-backed discovery. |
| `reason` | string | Concrete justification for the current exemption. |
| `focusedTests` | derived list | Dedicated conformance coverage that protects the exempted surface behavior. |
### Feature-Specific Invariants
- `ManagedTenantOnboardingWizard` may remain baseline-exempt if the reason continues to point to dedicated conformance tests.
- This feature does not introduce a new widget declaration system or a new validator mode.
- Governance for the retrofitted surfaces should become narrower and more explicit, not broader.
## Persistence Impact
- **Schema changes**: None
- **Data migration**: None
- **New indexes**: None
- **Retention impact**: None

View File

@ -0,0 +1,134 @@
# Implementation Plan: Deferred Operator Surfaces Retrofit
**Branch**: `172-deferred-operator-surfaces-retrofit` | **Date**: 2026-03-31 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/172-deferred-operator-surfaces-retrofit/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/172-deferred-operator-surfaces-retrofit/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
Retrofit the existing embedded operation-bearing surfaces on the tenant detail page and onboarding verification flow so each state exposes one clear primary action, keeps scope truthful before navigation, and preserves the current canonical `OperationRun` destinations. The implementation stays narrow: reuse the existing Filament widgets, Blade partials, route helpers, and page-level actions; do not add routes, persistence, capabilities, assets, or a new embedded-surface framework.
## Technical Context
**Language/Version**: PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4
**Primary Dependencies**: `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest`
**Storage**: PostgreSQL with existing `operation_runs`, `managed_tenant_onboarding_sessions`, tenant records, and workspace records; no schema changes
**Testing**: Pest feature, Livewire, and browser-style UI coverage executed through Laravel Sail
**Target Platform**: Laravel web application running in Sail locally and containerized Linux environments for staging and production
**Project Type**: Laravel monolith with admin, tenant, and system Filament panels plus shared Blade partials
**Performance Goals**: Preserve current DB-only render paths for tenant detail widgets and onboarding verification reports; add no remote calls, no new queued work, no new polling cadence, and no broader query fan-out than the current run lookups
**Constraints**: `/admin/operations` and `/admin/operations/{run}` remain the canonical inspect destinations; no new route family, capability, persistence artifact, or `OperationRun` lifecycle change is allowed; tenant detail widgets must not render equal-weight competing CTAs; advanced monitoring links stay secondary and access-aware; no provider-registration, global-search, or asset-pipeline change is in scope
**Scale/Scope**: Two tenant-detail embedded widgets, one onboarding verification report surface plus technical-details modal, one baseline exemption/governance note, and a focused set of Pest/Livewire regression tests
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- `PASS` Inventory-first / snapshots-second: the feature does not change inventory truth, snapshot truth, or backup behavior; it only reorders and relabels derived inspection affordances.
- `PASS` Read/write separation: no new write path or long-running workflow is introduced; existing verification-start actions remain the same and only embedded presentation around existing `OperationRun` records is retrofitted.
- `PASS` Graph contract path: no Microsoft Graph contract or outbound-call path is changed.
- `PASS` Deterministic capabilities: no capability registry, role mapping, or authorization primitive is introduced or modified.
- `PASS` RBAC-UX plane separation: tenant-detail surfaces may continue linking to canonical admin-plane operation viewers, but they must do so without widening access; non-members remain 404 and member-but-missing-capability remains 403 on the existing destinations.
- `PASS` Workspace and tenant isolation: no new tenantless shortcut is introduced; current workspace context, tenant membership, and canonical route guards remain authoritative.
- `PASS` Destructive confirmation standard: no destructive action is added or altered in this slice.
- `PASS` Run observability / Ops-UX lifecycle: the plan reuses existing `OperationRun` records and route helpers only; no status/outcome transition logic, summary-count semantics, notification timing, or run-creation rules are changed.
- `PASS` Proportionality / abstraction / persistence / state (`PROP-001`, `ABSTR-001`, `PERSIST-001`, `STATE-001`, `BLOAT-001`): the feature adds no persistence, abstraction, enum, reason family, or semantic framework and instead narrows drift on existing current-release surfaces.
- `PASS` UI taxonomy and inspect model (`UI-CONST-001`, `UI-SURF-001`, `UI-HARD-001`): the affected surfaces remain embedded widgets and guided-flow report surfaces; the work reduces competing affordances instead of introducing new surface types or inspect models.
- `PASS` Operator surface rules (`OPSURF-001`): default-visible content stays operator-first, while diagnostics and advanced monitoring links remain secondary and explicitly revealed.
- `PASS` UI naming and Filament-native UI (`UI-NAMING-001`, `UI-FIL-001`): the feature reuses existing Filament sections, buttons, widgets, and Blade views, and keeps canonical `Operations` / `Operation` nouns without inventing a local presentation layer.
- `PASS` Testing truth (`TEST-TRUTH-001`): the design will extend focused widget, onboarding, and guard tests rather than introducing a broad string-ban or framework-only conformance layer.
- `PASS` Filament v5 / Livewire v4 guardrails: all touched surfaces already run on Filament v5 and Livewire v4; panel provider registration remains unchanged in `bootstrap/providers.php`; no new globally searchable resource is introduced; no asset strategy changes are needed, so deployment requirements for `filament:assets` are unchanged.
## Project Structure
### Documentation (this feature)
```text
specs/172-deferred-operator-surfaces-retrofit/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ └── embedded-operation-surface-contract.yaml
└── tasks.md
```
### Source Code (repository root)
```text
app/
├── Filament/
│ ├── Pages/
│ │ └── Workspaces/
│ │ └── ManagedTenantOnboardingWizard.php
│ ├── Resources/
│ │ └── TenantResource/
│ │ └── Pages/
│ │ └── ViewTenant.php
│ └── Widgets/
│ └── Tenant/
│ ├── RecentOperationsSummary.php
│ └── TenantVerificationReport.php
├── Support/
│ ├── OperationRunLinks.php
│ └── Ui/
│ └── ActionSurface/
│ └── ActionSurfaceExemptions.php
resources/
└── views/
└── filament/
├── forms/
│ └── components/
│ └── managed-tenant-onboarding-verification-report.blade.php
├── modals/
│ └── onboarding-verification-technical-details.blade.php
└── widgets/
└── tenant/
├── recent-operations-summary.blade.php
└── tenant-verification-report.blade.php
tests/
└── Feature/
├── Filament/
│ ├── RecentOperationsSummaryWidgetTest.php
│ └── TenantVerificationReportWidgetTest.php
├── Guards/
│ └── ActionSurfaceContractTest.php
└── Onboarding/
├── OnboardingVerificationClustersTest.php
├── OnboardingVerificationTest.php
└── OnboardingVerificationV1_5UxTest.php
```
**Structure Decision**: This is a single Laravel application. The implementation stays inside existing tenant-detail widgets, onboarding report views, shared route helpers, and guard tests. No new panel, route family, base directory, or abstraction layer is needed.
**Focused test inventory (authoritative)**: `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`, `tests/Feature/Onboarding/OnboardingVerificationTest.php`, `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`, `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`, and `tests/Feature/Guards/ActionSurfaceContractTest.php` are the core regression surfaces for this slice.
## Complexity Tracking
No constitution waiver is expected. This slice intentionally avoids new persistence, abstractions, and UI frameworks.
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| None | Not applicable | Not applicable |
## Proportionality Review
- **Current operator problem**: tenant-detail widgets and onboarding verification surfaces currently expose operation drill-ins with ambiguous scope and competing inline calls to action, which makes it unclear whether the operator should inspect existing execution truth or start new work.
- **Existing structure is insufficient because**: the current behavior is split across Blade branches, widget view-data helpers, and broad deferred-surface exemptions, so one-off wording fixes would not reliably enforce CTA hierarchy or scope-truth across the covered surfaces.
- **Narrowest correct implementation**: retrofit the existing tenant-detail widgets and onboarding verification views in place, reuse the canonical admin operation routes, and rely on the existing tenant-detail header action and onboarding step controls for rerun/start flows instead of adding a new embedded-action framework.
- **Ownership cost created**: low. The feature adds a surface contract artifact plus a focused set of widget/onboarding/governance assertions, but no schema, queue, routing, or abstraction maintenance burden.
- **Alternative intentionally rejected**: creating tenant-scoped operations routes, inventing a widget-specific action-surface declaration framework, or redesigning the entire tenant dashboard/tenant detail experience was rejected because the current-release need is limited to a few embedded operation-bearing surfaces.
- **Release truth**: current-release truth. The covered surfaces already ship today and already expose operation affordances that need clearer hierarchy and scope semantics.
## Post-Design Constitution Re-check
- `PASS` `UI-CONST-001` / `UI-SURF-001` / `UI-HARD-001`: the design keeps embedded summaries and guided-flow reports in their existing surface classes and narrows each to one primary inline action model instead of adding a new inspect mechanism.
- `PASS` `OPSURF-001`: tenant-detail widgets answer what happened and where to inspect it next, while onboarding keeps workflow controls (`Start verification`, `Refresh`) separate from diagnostics-only operation links.
- `PASS` `RBAC-UX-001` through `RBAC-UX-005`: existing server-side authorization, 404 vs 403 semantics, and confirmation rules remain unchanged because the design only changes CTA hierarchy and visible scope cues.
- `PASS` `UI-NAMING-001`: canonical `Operations` / `Operation` nouns remain stable across tenant-detail and onboarding inspect affordances without introducing scope-first labels.
- `PASS` `TEST-TRUTH-001`: the design expands focused Pest/Livewire coverage for CTA count, explicit scope cues, and advanced-link visibility rather than codifying a new cross-widget framework.
- `PASS` `BLOAT-001`: no new persistence, abstraction, state family, taxonomy, or presenter layer was added during design.
- `PASS` Filament v5 / Livewire v4 implementation contract: the plan touches existing Filament v5 and Livewire v4 widgets/pages only; panel providers remain registered in `bootstrap/providers.php`; no globally searchable resource changes are introduced; no destructive actions are added or modified; no asset registration changes are needed, so the deployment `filament:assets` step is unaffected.
- `PASS` Testing plan: implementation coverage will target `RecentOperationsSummaryWidgetTest`, `TenantVerificationReportWidgetTest`, `OnboardingVerificationTest`, `OnboardingVerificationClustersTest`, `OnboardingVerificationV1_5UxTest`, and `ActionSurfaceContractTest`.

View File

@ -0,0 +1,85 @@
# Quickstart: Deferred Operator Surfaces Retrofit
## Goal
Retrofit the tenant-detail and onboarding embedded operation surfaces so they present one clear primary action per state, keep scope truthful before navigation, and leave existing `OperationRun` routes, authorization, and lifecycle behavior unchanged.
## Prerequisites
1. Start the local stack:
```bash
vendor/bin/sail up -d
```
2. Work on branch `172-deferred-operator-surfaces-retrofit`.
## Implementation Steps
1. Align the tenant-detail embedded surfaces first:
- `app/Filament/Resources/TenantResource/Pages/ViewTenant.php`
- `app/Filament/Widgets/Tenant/RecentOperationsSummary.php`
- `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php`
- `app/Filament/Widgets/Tenant/TenantVerificationReport.php`
- `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php`
- `app/Support/OperationRunLinks.php` only if a secondary collection affordance needs clearer admin-scope copy
2. Make the CTA hierarchy explicit on tenant detail:
- recent-operations summary keeps row-level inspect links primary and any collection link secondary
- verification widget uses `Start verification` only when no run exists and relies on the existing tenant-detail header action for reruns once a run exists
3. Align the onboarding verification surfaces without changing workflow semantics:
- `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`
- `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php`
- `resources/views/filament/modals/onboarding-verification-technical-details.blade.php`
- keep the wizard's existing next-step controls (`Start verification`, `Refresh`) authoritative
- keep current-run inspect links singular and keep previous-run/monitoring links diagnostics-secondary only
4. Narrow the governance story instead of creating a new framework:
- update `app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php` only if the baseline reason needs to become more specific
- update `tests/Feature/Guards/ActionSurfaceContractTest.php` if the exemption or dedicated-conformance expectation changes
5. Re-run a final pass over the covered views to ensure there is no equal-weight combination of:
- collection CTA plus row/detail CTA on the same embedded summary
- `Start verification` plus `Open operation` inside the same tenant-detail widget state
- `Open operation`, `Open previous operation`, and `Open operation in Monitoring (advanced)` all appearing as peer actions on the onboarding report surface
## Tests To Update
1. Tenant-detail embedded surface coverage:
- `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`
- `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`
2. Onboarding verification coverage:
- `tests/Feature/Onboarding/OnboardingVerificationTest.php`
- `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`
- `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`
3. Governance coverage:
- `tests/Feature/Guards/ActionSurfaceContractTest.php`
## Focused Verification
Run the narrow regression set first:
```bash
vendor/bin/sail artisan test --compact tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php
vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantVerificationReportWidgetTest.php
vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingVerificationTest.php
vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingVerificationClustersTest.php
vendor/bin/sail artisan test --compact tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php
vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php
```
If the implementation touches shared operation-link wording or helper text used outside the immediate surfaces, run the smallest additional focused tests that render those helpers.
## Formatting
After code changes, run:
```bash
vendor/bin/sail bin pint --dirty --format agent
```
## Manual Review Checklist
1. The tenant-detail recent-operations summary does not present a header-level collection link with the same visual weight as row-level inspect links.
2. The tenant verification widget shows `Start verification` only when no current run exists and otherwise shows one primary inspect link for the current run.
3. The onboarding flow keeps one workflow-next-step control per state and does not present current-run, previous-run, and advanced-monitoring links as peer primary actions.
4. Any remaining broader-scope operations collection link makes that broader admin scope explicit through context or nearby copy.
5. Any remaining advanced monitoring/admin link is visibly secondary and only appears when the operator can access that destination.
6. No route, capability, persistence artifact, provider registration, or `OperationRun` lifecycle behavior changed.

View File

@ -0,0 +1,49 @@
# Research: Deferred Operator Surfaces Retrofit
## Decision 1: Treat the tenant detail view, not `/admin/t/{tenant}`, as the primary tenant-plane retrofit surface
- **Decision**: Scope the tenant-plane retrofit to the embedded widgets on `ViewTenant` and keep the table-based recent-operations widget on `/admin/t/{tenant}` out of scope.
- **Rationale**: Repo inspection shows the deferred embedded operation surfaces live on `app/Filament/Resources/TenantResource/Pages/ViewTenant.php`, while `app/Filament/Pages/TenantDashboard.php` already uses a declaration-backed table widget with row-click inspection. Mixing both into one slice would blur two different action-surface models.
- **Alternatives considered**: Retrofit the tenant dashboard table widget in the same spec. Rejected because it is already governed as a table surface and would expand the slice beyond the deferred embedded-surface problem.
## Decision 2: Keep `/admin/operations` and `/admin/operations/{run}` as the canonical inspect destinations
- **Decision**: Reuse the existing admin-plane operations collection and detail routes for embedded drill-ins rather than creating tenant-scoped operations routes or a parallel tenant viewer.
- **Rationale**: `OperationRunLinks` already centralizes these destinations, and the spec explicitly forbids route or lifecycle changes. The missing behavior is scope-truth before navigation, not a missing destination.
- **Alternatives considered**: Add tenant-scoped operations routes or query-prefiltered viewers. Rejected because that would introduce new routing, new navigation semantics, and more RBAC surface area than this retrofit requires.
## Decision 3: Use a state-driven CTA hierarchy instead of adding new embedded controls
- **Decision**: Model each embedded surface around a small CTA matrix: no run means one workflow-start action, an existing run means one primary inspect action, and any broader collection or monitoring links become explicitly secondary.
- **Rationale**: The current drift comes from equal-weight CTAs such as `View all operations` alongside per-row `Open operation` or inline `Start verification` beside an existing `Open operation`. A state matrix solves that with minimal code churn.
- **Alternatives considered**: Keep all current links and only rename them. Rejected because naming alone would not remove the competing-action problem described by Spec 172.
## Decision 4: Let the owning page or wizard keep rerun controls while embedded surfaces focus on inspection
- **Decision**: On tenant detail, the existing page-level `Verify configuration` header action remains the rerun/start path when a verification run already exists, while the embedded widget focuses on inspecting the current run. On onboarding, the step-level workflow controls (`Start verification` or `Refresh`) remain the next-step controls, while report/technical-details links stay inspection-oriented.
- **Rationale**: The rerun affordance already exists on the owning surfaces. Reusing it avoids adding a second equal-weight CTA inside the embedded report widgets.
- **Alternatives considered**: Keep rerun/start controls inline inside every embedded surface state. Rejected because it produces the same CTA competition the feature is meant to eliminate.
## Decision 5: Keep advanced monitoring links in diagnostics-only slots and gate them by existing access
- **Decision**: Retain any broader monitoring/admin operation link only in low-emphasis technical-details or diagnostics areas, and only when the operator can access that destination.
- **Rationale**: The technical-details modal already has an explicit advanced-monitoring affordance. Keeping it secondary preserves operator clarity without removing legitimate escalation paths for power users.
- **Alternatives considered**: Show advanced monitoring links beside every primary inspect CTA. Rejected because it weakens the single-primary-action rule and increases scope ambiguity.
## Decision 6: Narrow governance through focused tests and baseline-exemption notes, not through a new widget framework
- **Decision**: Reuse the existing `ActionSurfaceExemptions` and `ActionSurfaceContractTest` model, tighten the relevant exemption wording if needed, and add focused widget/onboarding coverage instead of introducing widget-specific `actionSurfaceDeclaration()` plumbing.
- **Rationale**: Embedded widgets and report partials do not fit the existing table/page declaration model cleanly, and the codebase already recognizes that `ManagedTenantOnboardingWizard` is covered through dedicated conformance tests.
- **Alternatives considered**: Add a new generic embedded-surface declaration framework. Rejected because one concrete retrofit does not justify a new abstraction under `ABSTR-001` and `BLOAT-001`.
## Decision 7: Verify behavior with focused Pest and Livewire tests instead of broad string guards
- **Decision**: Extend the existing widget and onboarding feature tests to assert CTA count, scope cues, and advanced-link visibility directly on rendered surfaces.
- **Rationale**: The business truth here is UI hierarchy and scope behavior, not just string presence. Focused rendered-surface tests are more precise than grep-style bans and align with `TEST-TRUTH-001`.
- **Alternatives considered**: Add a repo-wide architecture or grep-style rule forbidding specific link combinations. Rejected because valid out-of-scope surfaces and diagnostics would require an exception list that encodes the same complexity this slice is avoiding.
## Decision 8: Make scope explicit through placement and nearby copy rather than scope-first labels
- **Decision**: Any remaining collection drill-in from a tenant-detail surface should communicate broader admin scope through placement or helper text, not by inventing scope-first primary labels.
- **Rationale**: `UI-NAMING-001` forbids making scope the primary verb-object label. The operator still needs to understand when a link leaves the tenant-local shell, but that should come from context and secondary explanation.
- **Alternatives considered**: Rename the main CTA to `Open admin operations`. Rejected because it over-rotates toward implementation language and conflicts with the repo's operator-facing naming rules.

View File

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

View File

@ -0,0 +1,190 @@
# Tasks: Deferred Operator Surfaces Retrofit
**Input**: Design documents from `specs/172-deferred-operator-surfaces-retrofit/`
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `contracts/embedded-operation-surface-contract.yaml`, `quickstart.md`
**Tests**: Required. Update the focused Pest coverage listed in `specs/172-deferred-operator-surfaces-retrofit/quickstart.md`.
**Operations**: Reuse the existing `OperationRun` lifecycle and canonical `/admin/operations` routes; this feature must not add new run semantics.
**RBAC**: Authorization planes and 404/403 behavior stay unchanged; covered surfaces still need access-aware advanced links.
## Phase 1: Setup (Shared Context)
**Purpose**: Confirm the retrofit boundary, affected surfaces, and focused verification targets before implementation starts.
- [X] T001 Review the implementation boundary in `specs/172-deferred-operator-surfaces-retrofit/spec.md`, `specs/172-deferred-operator-surfaces-retrofit/plan.md`, `specs/172-deferred-operator-surfaces-retrofit/research.md`, and `specs/172-deferred-operator-surfaces-retrofit/contracts/embedded-operation-surface-contract.yaml`
- [X] T002 Baseline the current tenant-detail and onboarding behavior in `app/Filament/Resources/TenantResource/Pages/ViewTenant.php`, `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`, `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, and `tests/Feature/Onboarding/OnboardingVerificationTest.php`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Put the shared route and CTA handoff in place before changing individual surfaces.
**⚠️ CRITICAL**: No user story work should start until this phase is complete.
- [X] T003 Update shared destination metadata in `app/Support/OperationRunLinks.php` so retrofitted surfaces can keep canonical collection/detail routes while making any broader admin scope explicit
- [X] T004 Establish page-level workflow-action ownership in `app/Filament/Resources/TenantResource/Pages/ViewTenant.php` and `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php` so embedded surfaces consume inspect-oriented inputs instead of deciding workflow CTAs themselves
**Checkpoint**: Shared helpers and page-level state handoff are ready, so story work can proceed safely.
---
## Phase 3: User Story 1 - Tenant Detail Drill-Ins Preserve Tenant Context (Priority: P1) 🎯 MVP
**Goal**: Make the tenant-detail recent-operations and verification widgets expose one clear primary CTA per state without silently broadening scope.
**Independent Test**: Render the tenant-detail widgets and verify that row-level or current-run inspection stays primary, any collection drill-in is clearly secondary, and the visible scope remains truthful before navigation.
### Tests for User Story 1
- [X] T005 [P] [US1] Update `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php` to assert row-level `Open operation` links stay primary and any collection drill-in is secondary and scope-explicit
- [X] T006 [P] [US1] Update `tests/Feature/Filament/TenantVerificationReportWidgetTest.php` to assert `Start verification` and `Open operation` never appear as competing peer CTAs on the widget
### Implementation for User Story 1
- [X] T007 [US1] Refine state assembly in `app/Filament/Widgets/Tenant/RecentOperationsSummary.php` and `app/Filament/Widgets/Tenant/TenantVerificationReport.php` so each tenant-detail surface emits one primary CTA contract per state
- [X] T008 [P] [US1] Update `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php` to demote any collection affordance and make broader admin scope explicit before navigation
- [X] T009 [P] [US1] Update `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php` to render one primary CTA per state and rely on the tenant-detail header action for reruns
**Checkpoint**: Tenant-detail embedded operation surfaces are independently testable and preserve tenant context truth.
---
## Phase 4: User Story 2 - Onboarding And Verification Surfaces Expose One Clear Operation Path (Priority: P1)
**Goal**: Keep onboarding verification workflow controls authoritative while report and technical-details surfaces expose one primary inspect path plus diagnostics-secondary links only.
**Independent Test**: Render onboarding verification surfaces in no-run, active-run, and completed-run states and verify that each state exposes exactly one primary CTA while previous-run or advanced monitoring links remain secondary and access-aware.
### Tests for User Story 2
- [X] T010 [P] [US2] Update `tests/Feature/Onboarding/OnboardingVerificationTest.php` to assert one primary verification CTA per onboarding state
- [X] T011 [P] [US2] Update `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php` and `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php` to assert previous-run and advanced monitoring links stay diagnostics-secondary and access-aware
### Implementation for User Story 2
- [X] T012 [US2] Refine onboarding verification report payload assembly in `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php` so current-run, previous-run, and advanced-link data reach the embedded surfaces without introducing inline workflow CTAs
- [X] T013 [P] [US2] Update `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php` to show exactly one primary current-run inspect CTA when a run exists and otherwise render explanatory empty-state content without inline workflow CTAs
- [X] T014 [P] [US2] Update `resources/views/filament/modals/onboarding-verification-technical-details.blade.php` to keep previous-run and advanced monitoring links secondary, explicitly labeled, and diagnostics-only
**Checkpoint**: Onboarding verification surfaces are independently testable and expose a single clear operator path per state.
---
## Phase 5: User Story 3 - Retrofitted Deferred Surfaces Gain Explicit Governance (Priority: P2)
**Goal**: Replace blanket deferred-surface treatment for the retrofitted operation-bearing surfaces with explicit governance and representative automated coverage.
**Independent Test**: Run the guard suite and confirm that tenant-detail and onboarding verification surfaces are covered explicitly, while unrelated deferred surfaces remain intentional non-goals.
### Tests for User Story 3
- [X] T015 [P] [US3] Update `tests/Feature/Guards/ActionSurfaceContractTest.php` to assert tenant-detail and onboarding verification surfaces are covered explicitly rather than by blanket deferred exemptions
### Implementation for User Story 3
- [X] T016 [US3] Narrow retrofit governance in `app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php` so only true non-goal deferred surfaces remain exempt and dedicated coverage is documented for `ManagedTenantOnboardingWizard`
**Checkpoint**: Governance now protects the retrofitted surfaces without sweeping unrelated deferred pages into scope.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Run the focused regression and cleanup steps needed to ship the retrofit safely.
- [X] T017 Run the focused regression suite in `tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php`, `tests/Feature/Filament/TenantVerificationReportWidgetTest.php`, `tests/Feature/Onboarding/OnboardingVerificationTest.php`, `tests/Feature/Onboarding/OnboardingVerificationClustersTest.php`, `tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`, and `tests/Feature/Guards/ActionSurfaceContractTest.php`
- [X] T018 Run formatting for `app/Support/OperationRunLinks.php`, `app/Filament/Resources/TenantResource/Pages/ViewTenant.php`, `app/Filament/Widgets/Tenant/RecentOperationsSummary.php`, `app/Filament/Widgets/Tenant/TenantVerificationReport.php`, `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`, and `app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`
- [X] T019 Execute the manual validation checklist in `specs/172-deferred-operator-surfaces-retrofit/quickstart.md` against `resources/views/filament/widgets/tenant/recent-operations-summary.blade.php`, `resources/views/filament/widgets/tenant/tenant-verification-report.blade.php`, `resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php`, and `resources/views/filament/modals/onboarding-verification-technical-details.blade.php`
- [X] T020 Verify render-path constraints in `app/Filament/Widgets/Tenant/RecentOperationsSummary.php`, `app/Filament/Widgets/Tenant/TenantVerificationReport.php`, `app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`, and `app/Support/OperationRunLinks.php` so the retrofit stays DB-only at render time, adds no remote calls or queued work, does not alter polling cadence, and does not broaden query fan-out
---
## Dependencies & Execution Order
### Phase Dependencies
- **Phase 1: Setup** has no dependencies and starts immediately.
- **Phase 2: Foundational** depends on Phase 1 and blocks all story work.
- **Phase 3: User Story 1** depends on Phase 2.
- **Phase 4: User Story 2** depends on Phase 2.
- **Phase 5: User Story 3** depends on the relevant US1 and US2 implementation and test coverage being in place.
- **Phase 6: Polish** depends on the desired story phases being complete.
### User Story Dependencies
- **US1** can start as soon as T003-T004 are complete.
- **US2** can start as soon as T003-T004 are complete.
- **US3** should start after US1 and US2 have landed their representative surface behavior, because the governance guard needs the final retrofit boundary.
### Parallel Opportunities
- **US1**: T005 and T006 can run in parallel; after T007, T008 and T009 can run in parallel.
- **US2**: T010 and T011 can run in parallel; after T012, T013 and T014 can run in parallel.
- **US3**: Parallelism is intentionally limited; create the failing guard in T015 first, then narrow the exemption set in T016.
---
## Parallel Example: User Story 1
```bash
# Run the tenant-detail widget tests together:
Task: "T005 [US1] Update tests/Feature/Filament/RecentOperationsSummaryWidgetTest.php"
Task: "T006 [US1] Update tests/Feature/Filament/TenantVerificationReportWidgetTest.php"
# After widget state assembly is updated, align both Blade views together:
Task: "T008 [US1] Update resources/views/filament/widgets/tenant/recent-operations-summary.blade.php"
Task: "T009 [US1] Update resources/views/filament/widgets/tenant/tenant-verification-report.blade.php"
```
## Parallel Example: User Story 2
```bash
# Update onboarding coverage together:
Task: "T010 [US2] Update tests/Feature/Onboarding/OnboardingVerificationTest.php"
Task: "T011 [US2] Update tests/Feature/Onboarding/OnboardingVerificationClustersTest.php and tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php"
# After the wizard state contract is ready, update both onboarding views together:
Task: "T013 [US2] Update resources/views/filament/forms/components/managed-tenant-onboarding-verification-report.blade.php"
Task: "T014 [US2] Update resources/views/filament/modals/onboarding-verification-technical-details.blade.php"
```
## Parallel Example: User Story 3
```bash
# No safe implementation pair is recommended here; land the failing guard first, then narrow the exemption baseline:
Task: "T015 [US3] Update tests/Feature/Guards/ActionSurfaceContractTest.php"
Task: "T016 [US3] Update app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php"
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete T001-T004.
2. Complete T005-T009.
3. Validate tenant-detail behavior with the focused US1 tests before moving on.
### Incremental Delivery
1. Complete Setup and Foundational work to lock the shared route and CTA contract.
2. Deliver US1 and validate tenant-detail scope truth.
3. Deliver US2 and validate onboarding CTA hierarchy.
4. Deliver US3 to replace blanket exemptions with explicit governance.
5. Finish with T017-T020.
### Parallel Team Strategy
1. One developer completes T001-T004.
2. After Phase 2, one developer can take US1 while another takes US2.
3. Once both surfaces are stable, finish US3 governance and the shared regression pass.
---
## Notes
- `[P]` tasks touch different files and can run in parallel.
- User story labels map directly to the priorities and acceptance criteria in `specs/172-deferred-operator-surfaces-retrofit/spec.md`.
- Keep the implementation narrow: no new routes, persistence, capabilities, assets, or `OperationRun` lifecycle changes.

View File

@ -105,10 +105,10 @@
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Verify access')
->assertSee('Status: Needs attention')
->assertSee('The selected provider connection has changed since this verification run. Start verification again to validate the current connection.')
->assertSee('The selected provider connection has changed since this verification operation. Start verification again to validate the current connection.')
->assertSee('Start verification')
->refresh()
->waitForText('The selected provider connection has changed since this verification run. Start verification again to validate the current connection.')
->waitForText('The selected provider connection has changed since this verification operation. Start verification again to validate the current connection.')
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Status: Needs attention')

View File

@ -0,0 +1,318 @@
<?php
declare(strict_types=1);
use App\Filament\Resources\TenantResource;
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\Tenant;
use App\Models\TenantOnboardingSession;
use App\Models\User;
use App\Models\Workspace;
use App\Support\OperationRunLinks;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Verification\VerificationReportWriter;
use App\Support\Workspaces\WorkspaceContext;
pest()->browser()->timeout(15_000);
it('smokes tenant detail with existing operation surfaces and canonical operations routes', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$report = VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'provider.connection.check',
'title' => 'Provider connection preflight',
'status' => 'fail',
'severity' => 'critical',
'blocking' => true,
'reason_code' => 'provider_connection_missing',
'message' => 'No provider connection configured.',
'evidence' => [],
'next_steps' => [],
],
]);
$run = OperationRun::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Blocked->value,
'context' => [
'target_scope' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
],
'verification_report' => $report,
],
]);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
$page = visit(TenantResource::getUrl('view', ['record' => $tenant->getRouteKey()], panel: 'admin'));
$page
->assertNoJavaScriptErrors()
->assertSee((string) $tenant->name)
->assertSee('Recent operations')
->assertSee('Verification report')
->assertSee(OperationRunLinks::openCollectionLabel())
->assertSee(OperationRunLinks::collectionScopeDescription())
->assertSee(OperationRunLinks::openLabel())
->assertSee(ViewTenant::verificationHeaderActionLabel())
->assertDontSee('Start verification')
->click(OperationRunLinks::openCollectionLabel())
->assertNoJavaScriptErrors()
->assertRoute('admin.operations.index');
visit(OperationRunLinks::tenantlessView($run))
->assertNoJavaScriptErrors()
->assertRoute('admin.operations.view', ['run' => (int) $run->getKey()])
->assertSee(OperationRunLinks::identifier((int) $run->getKey()));
});
it('smokes tenant detail empty verification state without an inline inspect action', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
visit(TenantResource::getUrl('view', ['record' => $tenant->getRouteKey()], panel: 'admin'))
->assertNoJavaScriptErrors()
->assertSee('Verification report')
->assertSee('No verification operation has been started yet.')
->assertSee('Start verification')
->assertDontSee(OperationRunLinks::openLabel());
});
it('smokes onboarding verify step without a verification run', function (): void {
$workspace = Workspace::factory()->create();
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => Tenant::STATUS_ONBOARDING,
]);
$user = User::factory()->create(['name' => 'Spec172 Browser Owner']);
createUserWithTenant(
tenant: $tenant,
user: $user,
role: 'owner',
workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
);
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => (string) $tenant->tenant_id,
'is_default' => true,
'status' => 'connected',
]);
TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => (string) $tenant->tenant_id,
'current_step' => 'verify',
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
visit('/admin/onboarding')
->assertNoJavaScriptErrors()
->assertSee('Verify access')
->assertSee('Run a queued verification check (Operation).')
->assertSee('Start verification')
->assertDontSee('Refresh')
->assertDontSee(OperationRunLinks::openLabel());
});
it('smokes onboarding active verification state with refresh and current-run inspection', function (): void {
$workspace = Workspace::factory()->create();
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => Tenant::STATUS_ONBOARDING,
]);
$user = User::factory()->create(['name' => 'Spec172 Active Browser Owner']);
createUserWithTenant(
tenant: $tenant,
user: $user,
role: 'owner',
workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
);
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => (string) $tenant->tenant_id,
'is_default' => true,
'status' => 'connected',
]);
$run = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => OperationRunStatus::Running->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
'entra_tenant_name' => (string) $tenant->name,
],
],
]);
TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => (string) $tenant->tenant_id,
'current_step' => 'verify',
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $run->getKey(),
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
visit('/admin/onboarding')
->assertNoJavaScriptErrors()
->assertSee('Verify access')
->assertSee('Refresh')
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Start verification');
});
it('smokes onboarding completed verification details with secondary links revealed only in technical details', function (): void {
$workspace = Workspace::factory()->create();
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => '17217217-2172-4172-9172-172172172172',
'external_id' => 'browser-spec172-complete',
'status' => Tenant::STATUS_ONBOARDING,
]);
$user = User::factory()->create(['name' => 'Spec172 Completed Browser Owner']);
createUserWithTenant(
tenant: $tenant,
user: $user,
role: 'owner',
workspaceRole: 'owner',
ensureDefaultMicrosoftProviderConnection: false,
);
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => (string) $tenant->tenant_id,
'display_name' => 'Spec172 completed connection',
'is_default' => true,
'status' => 'connected',
]);
$previousRun = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Succeeded->value,
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
],
'verification_report' => VerificationReportWriter::build('provider.connection.check', []),
],
]);
$report = VerificationReportWriter::build('provider.connection.check', [
[
'key' => 'permissions.admin_consent',
'title' => 'Required application permissions',
'status' => 'fail',
'severity' => 'critical',
'blocking' => true,
'reason_code' => 'permission_denied',
'message' => 'Missing required Graph permissions.',
'evidence' => [],
'next_steps' => [],
],
]);
$report['previous_report_id'] = (int) $previousRun->getKey();
$currentRun = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => OperationRunStatus::Completed->value,
'outcome' => OperationRunOutcome::Failed->value,
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
'target_scope' => [
'entra_tenant_id' => (string) $tenant->tenant_id,
'entra_tenant_name' => (string) $tenant->name,
],
'verification_report' => $report,
],
]);
TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => (string) $tenant->tenant_id,
'current_step' => 'verify',
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $currentRun->getKey(),
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$page = visit('/admin/onboarding');
$page
->assertNoJavaScriptErrors()
->assertSee('Verify access')
->assertSee('Technical details')
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Open previous operation')
->assertDontSee(OperationRunLinks::advancedMonitoringLabel())
->click('Technical details')
->waitForText('Verification technical details')
->assertNoJavaScriptErrors()
->assertSee('Open previous operation')
->assertSee(OperationRunLinks::advancedMonitoringLabel());
});

View File

@ -4,6 +4,7 @@
use App\Filament\Widgets\Tenant\RecentOperationsSummary;
use App\Models\OperationRun;
use App\Support\OperationRunLinks;
use Livewire\Livewire;
it('renders recent operations from the record tenant in admin panel context', function (): void {
@ -25,6 +26,8 @@
->assertSee('Provider connection check')
->assertSee('Operation finished')
->assertSee('Open operation')
->assertSee(OperationRunLinks::openCollectionLabel())
->assertSee(OperationRunLinks::collectionScopeDescription())
->assertSee('No action needed.')
->assertDontSee('No operations yet.');
});

View File

@ -111,12 +111,41 @@
Livewire::actingAs($user)
->test(TenantVerificationReport::class)
->assertSee('Provider connection preflight')
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Start verification')
->assertSee(OperationRunLinks::identifierLabel().':')
->assertSee('Read-only:')
->assertSee('Insufficient permission — ask a tenant Owner.');
});
});
it('keeps existing verification runs inspect-first on the widget surface', function (string $status, string $outcome): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
Filament::setTenant($tenant, true);
OperationRun::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => $status,
'outcome' => $outcome,
'context' => [],
]);
$component = Livewire::actingAs($user)
->test(TenantVerificationReport::class, ['record' => $tenant])
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Start verification')
->assertSee(ViewTenant::verificationHeaderActionLabel());
expect(substr_count($component->html(), OperationRunLinks::openLabel()))->toBe(1)
->and(substr_count($component->html(), 'Start verification'))->toBe(0);
})->with([
'active run' => ['running', 'pending'],
'completed run' => ['completed', 'blocked'],
]);
it('renders tenant detail without invoking synchronous verification or permission persistence services', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');

View File

@ -13,6 +13,7 @@
use App\Filament\Pages\Reviews\ReviewRegister;
use App\Filament\Pages\TenantDiagnostics;
use App\Filament\Pages\TenantRequiredPermissions;
use App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard;
use App\Filament\Resources\AlertDeliveryResource;
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
use App\Filament\Resources\AlertDestinationResource;
@ -749,8 +750,9 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
->and($baselineExemptions->hasClass(Alerts::class))->toBeTrue()
->and((string) $baselineExemptions->reasonForClass(Alerts::class))->toContain('cluster entry');
expect($baselineExemptions->hasClass(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class))->toBeTrue()
->and((string) $baselineExemptions->reasonForClass(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class))->toContain('dedicated conformance tests');
expect($baselineExemptions->hasClass(ManagedTenantOnboardingWizard::class))->toBeTrue()
->and((string) $baselineExemptions->reasonForClass(ManagedTenantOnboardingWizard::class))->toContain('dedicated conformance tests')
->toContain('spec 172');
expect(method_exists(\App\Filament\System\Pages\Ops\Runbooks::class, 'actionSurfaceDeclaration'))->toBeFalse()
->and($baselineExemptions->hasClass(\App\Filament\System\Pages\Ops\Runbooks::class))->toBeFalse();
@ -759,6 +761,18 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
->and($baselineExemptions->hasClass(\App\Filament\System\Pages\RepairWorkspaceOwners::class))->toBeFalse();
});
it('keeps spec 172 retrofit surfaces covered without broad baseline exemptions', function (): void {
$baselineExemptions = ActionSurfaceExemptions::baseline();
expect($baselineExemptions->hasClass(ViewTenant::class))->toBeFalse()
->and($baselineExemptions->hasClass(ManagedTenantOnboardingWizard::class))->toBeTrue()
->and((string) $baselineExemptions->reasonForClass(ManagedTenantOnboardingWizard::class))
->toContain('spec 172')
->toContain('OnboardingVerificationTest')
->toContain('OnboardingVerificationClustersTest')
->toContain('OnboardingVerificationV1_5UxTest');
});
it('keeps enrolled system panel pages declaration-backed without stale baseline exemptions', function (): void {
$baselineExemptions = ActionSurfaceExemptions::baseline();

View File

@ -119,11 +119,12 @@
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Technical details')
->assertSee(OperationRunLinks::identifierLabel())
->assertSee('Open operation details')
->assertSee(OperationRunLinks::openLabel())
->assertSee('Required application permissions')
->assertSee('Open required permissions')
->assertSee('Issues')
->assertDontSee('Open previous operation')
->assertDontSee('Open operation in Monitoring (advanced)')
->assertSee($entraTenantId);
});
@ -187,7 +188,95 @@
'onboardingDraft' => (int) $session->getKey(),
])
->mountAction('wizardVerificationTechnicalDetails')
->assertSuccessful();
->assertMountedActionModalSee('Verification technical details')
->assertMountedActionModalSee(OperationRunLinks::advancedMonitoringLabel())
->assertMountedActionModalSee(OperationRunLinks::identifier((int) $run->getKey()));
});
it('keeps previous-run and advanced monitoring links inside technical details only', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$entraTenantId = 'dddddddd-dddd-dddd-dddd-dddddddddddd';
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => $entraTenantId,
'external_id' => 'tenant-clusters-d',
'status' => 'onboarding',
]);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
$previousReport = VerificationReportWriter::build('provider.connection.check', []);
$previousRun = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => 'completed',
'outcome' => 'succeeded',
'context' => [
'target_scope' => [
'entra_tenant_id' => $entraTenantId,
],
'verification_report' => $previousReport,
],
]);
$currentReport = VerificationReportWriter::build('provider.connection.check', []);
$currentReport['previous_report_id'] = (int) $previousRun->getKey();
$currentRun = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => 'completed',
'outcome' => 'failed',
'context' => [
'target_scope' => [
'entra_tenant_id' => $entraTenantId,
],
'verification_report' => $currentReport,
],
]);
$session = TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => $entraTenantId,
'current_step' => 'verify',
'state' => [
'verification_operation_run_id' => (int) $currentRun->getKey(),
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$this->actingAs($user)
->followingRedirects()
->get('/admin/onboarding')
->assertSuccessful()
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Open previous operation')
->assertDontSee(OperationRunLinks::advancedMonitoringLabel());
Livewire::test(ManagedTenantOnboardingWizard::class, [
'onboardingDraft' => (int) $session->getKey(),
])
->mountAction('wizardVerificationTechnicalDetails')
->assertMountedActionModalSee('Open previous operation')
->assertMountedActionModalSee(OperationRunLinks::advancedMonitoringLabel());
});
it('routes permission-related verification next steps through the required permissions assist', function (): void {

View File

@ -327,13 +327,118 @@
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Status: Blocked')
->assertSee(OperationRunLinks::identifierLabel())
->assertSee('Open operation details')
->assertSee(OperationRunLinks::openLabel())
->assertSee('Technical details')
->assertDontSee('Open operation in Monitoring (advanced)')
->assertDontSee('Open previous operation')
->assertSee('Missing required Graph permissions.')
->assertSee('Graph permissions')
->assertSee($entraTenantId);
});
it('keeps one onboarding verification path per state while leaving workflow actions on the wizard step', function (): void {
$workspace = Workspace::factory()->create();
$user = User::factory()->create();
WorkspaceMembership::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'user_id' => (int) $user->getKey(),
'role' => 'owner',
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$tenant = Tenant::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'status' => Tenant::STATUS_ONBOARDING,
]);
$user->tenants()->syncWithoutDetaching([
$tenant->getKey() => ['role' => 'owner'],
]);
$connection = ProviderConnection::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'is_default' => true,
'status' => 'connected',
'consent_status' => 'granted',
]);
$session = TenantOnboardingSession::query()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'entra_tenant_id' => (string) $tenant->tenant_id,
'current_step' => 'verify',
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
],
'started_by_user_id' => (int) $user->getKey(),
'updated_by_user_id' => (int) $user->getKey(),
]);
$this->actingAs($user)
->followingRedirects()
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Start verification')
->assertSee('Use the workflow action above to start verification for this tenant.')
->assertDontSee(OperationRunLinks::openLabel())
->assertDontSee('Refresh');
$activeRun = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => 'running',
'outcome' => 'pending',
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
],
]);
$session->forceFill([
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $activeRun->getKey(),
],
])->save();
$this->actingAs($user)
->followingRedirects()
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Refresh')
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Start verification');
$completedRun = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => 'completed',
'outcome' => 'succeeded',
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_report' => VerificationReportWriter::build('provider.connection.check', []),
],
]);
$session->forceFill([
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $completedRun->getKey(),
],
])->save();
$this->actingAs($user)
->followingRedirects()
->get('/admin/onboarding')
->assertSuccessful()
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Refresh');
});
it('clears the stored verification run id when switching provider connections', function (): void {
Queue::fake();

View File

@ -10,6 +10,7 @@
use App\Models\VerificationCheckAcknowledgement;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Support\OperationRunLinks;
use App\Support\Verification\VerificationReportWriter;
use App\Support\Workspaces\WorkspaceContext;
@ -86,7 +87,37 @@
->get('/admin/onboarding')
->assertSuccessful()
->assertSee('Refresh')
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Start verification');
$completedRun = OperationRun::factory()->create([
'workspace_id' => (int) $workspace->getKey(),
'tenant_id' => (int) $tenant->getKey(),
'type' => 'provider.connection.check',
'status' => 'completed',
'outcome' => 'succeeded',
'context' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_report' => VerificationReportWriter::build('provider.connection.check', []),
],
]);
TenantOnboardingSession::query()
->where('workspace_id', (int) $workspace->getKey())
->where('tenant_id', (int) $tenant->getKey())
->update([
'state' => [
'provider_connection_id' => (int) $connection->getKey(),
'verification_operation_run_id' => (int) $completedRun->getKey(),
],
]);
$this->actingAs($user)
->followingRedirects()
->get('/admin/onboarding')
->assertSuccessful()
->assertSee(OperationRunLinks::openLabel())
->assertDontSee('Refresh');
});
it('orders issues deterministically and groups acknowledged issues', function (): void {