feat: retrofit deferred operator surfaces #203
3
.github/agents/copilot-instructions.md
vendored
3
.github/agents/copilot-instructions.md
vendored
@ -122,6 +122,7 @@ ## Active Technologies
|
|||||||
- PostgreSQL with existing `operation_runs` and `audit_logs` tables; no schema changes (170-system-operations-surface-alignment)
|
- PostgreSQL with existing `operation_runs` and `audit_logs` tables; no schema changes (170-system-operations-surface-alignment)
|
||||||
- PHP 8.4, Laravel 12, Livewire v4, Filament v5, Tailwind CSS v4 + `laravel/framework`, `filament/filament`, `livewire/livewire`, `pestphp/pest` (171-operations-naming-consolidation)
|
- 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`, 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)
|
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||||
|
|
||||||
@ -141,8 +142,8 @@ ## Code Style
|
|||||||
PHP 8.4.15: Follow standard conventions
|
PHP 8.4.15: Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## 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`
|
- 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`
|
- 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 START -->
|
||||||
<!-- MANUAL ADDITIONS END -->
|
<!-- MANUAL ADDITIONS END -->
|
||||||
|
|||||||
@ -87,6 +87,7 @@
|
|||||||
use Illuminate\Database\QueryException;
|
use Illuminate\Database\QueryException;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Livewire\Attributes\Locked;
|
use Livewire\Attributes\Locked;
|
||||||
@ -564,7 +565,7 @@ public function content(Schema $schema): Schema
|
|||||||
->default(null)
|
->default(null)
|
||||||
->view('filament.forms.components.managed-tenant-onboarding-verification-report')
|
->view('filament.forms.components.managed-tenant-onboarding-verification-report')
|
||||||
->viewData(fn (): array => $this->verificationReportViewData())
|
->viewData(fn (): array => $this->verificationReportViewData())
|
||||||
->visible(fn (): bool => $this->verificationRunUrl() !== null),
|
->visible(fn (): bool => $this->managedTenant instanceof Tenant),
|
||||||
]),
|
]),
|
||||||
])
|
])
|
||||||
->beforeValidation(function (): void {
|
->beforeValidation(function (): void {
|
||||||
@ -1708,27 +1709,24 @@ private function verificationStatusColor(): string
|
|||||||
|
|
||||||
private function verificationRunUrl(): ?string
|
private function verificationRunUrl(): ?string
|
||||||
{
|
{
|
||||||
if (! $this->managedTenant instanceof Tenant) {
|
$run = $this->verificationRun();
|
||||||
|
|
||||||
|
if (! $run instanceof OperationRun) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! $this->onboardingSession instanceof TenantOnboardingSession) {
|
if (! $this->canInspectOperationRun($run)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$runId = $this->onboardingSession->state['verification_operation_run_id'] ?? null;
|
return $this->tenantlessOperationRunUrl((int) $run->getKey());
|
||||||
|
|
||||||
if (! is_int($runId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->tenantlessOperationRunUrl($runId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* @return array{
|
||||||
* run: array<string, mixed>|null,
|
* run: array<string, mixed>|null,
|
||||||
* runUrl: string|null,
|
* runUrl: string|null,
|
||||||
|
* advancedRunUrl: string|null,
|
||||||
* report: array<string, mixed>|null,
|
* report: array<string, mixed>|null,
|
||||||
* fingerprint: string|null,
|
* fingerprint: string|null,
|
||||||
* changeIndicator: array{state: 'no_changes'|'changed', previous_report_id: int}|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
|
* acknowledged_by: array{id: int, name: string}|null
|
||||||
* }>,
|
* }>,
|
||||||
* assistVisibility: array{is_visible: bool, reason: 'permission_blocked'|'permission_attention'|'hidden_ready'|'hidden_irrelevant'},
|
* 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
|
private function verificationReportViewData(): array
|
||||||
@ -1755,6 +1755,7 @@ private function verificationReportViewData(): array
|
|||||||
return [
|
return [
|
||||||
'run' => null,
|
'run' => null,
|
||||||
'runUrl' => $runUrl,
|
'runUrl' => $runUrl,
|
||||||
|
'advancedRunUrl' => null,
|
||||||
'report' => null,
|
'report' => null,
|
||||||
'fingerprint' => null,
|
'fingerprint' => null,
|
||||||
'changeIndicator' => null,
|
'changeIndicator' => null,
|
||||||
@ -1763,6 +1764,8 @@ private function verificationReportViewData(): array
|
|||||||
'acknowledgements' => [],
|
'acknowledgements' => [],
|
||||||
'assistVisibility' => $assistVisibility,
|
'assistVisibility' => $assistVisibility,
|
||||||
'assistActionName' => 'wizardVerificationRequiredPermissionsAssist',
|
'assistActionName' => 'wizardVerificationRequiredPermissionsAssist',
|
||||||
|
'technicalDetailsActionName' => 'wizardVerificationTechnicalDetails',
|
||||||
|
'runState' => 'no_run',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1770,9 +1773,7 @@ private function verificationReportViewData(): array
|
|||||||
$fingerprint = is_array($report) ? VerificationReportViewer::fingerprint($report) : null;
|
$fingerprint = is_array($report) ? VerificationReportViewer::fingerprint($report) : null;
|
||||||
|
|
||||||
$changeIndicator = VerificationReportChangeIndicator::forRun($run);
|
$changeIndicator = VerificationReportChangeIndicator::forRun($run);
|
||||||
$previousRunUrl = $changeIndicator === null
|
$previousRunUrl = $this->verificationPreviousRunUrl($changeIndicator);
|
||||||
? null
|
|
||||||
: $this->tenantlessOperationRunUrl((int) $changeIndicator['previous_report_id']);
|
|
||||||
|
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
$canAcknowledge = $user instanceof User && $this->managedTenant instanceof Tenant
|
$canAcknowledge = $user instanceof User && $this->managedTenant instanceof Tenant
|
||||||
@ -1820,11 +1821,13 @@ private function verificationReportViewData(): array
|
|||||||
'outcome' => (string) $run->outcome,
|
'outcome' => (string) $run->outcome,
|
||||||
'initiator_name' => (string) $run->initiator_name,
|
'initiator_name' => (string) $run->initiator_name,
|
||||||
'started_at' => $run->started_at?->toJSON(),
|
'started_at' => $run->started_at?->toJSON(),
|
||||||
|
'updated_at' => $run->updated_at?->toJSON(),
|
||||||
'completed_at' => $run->completed_at?->toJSON(),
|
'completed_at' => $run->completed_at?->toJSON(),
|
||||||
'target_scope' => $targetScope,
|
'target_scope' => $targetScope,
|
||||||
'failures' => $failures,
|
'failures' => $failures,
|
||||||
],
|
],
|
||||||
'runUrl' => $runUrl,
|
'runUrl' => $runUrl,
|
||||||
|
'advancedRunUrl' => $runUrl,
|
||||||
'report' => $report,
|
'report' => $report,
|
||||||
'fingerprint' => $fingerprint,
|
'fingerprint' => $fingerprint,
|
||||||
'changeIndicator' => $changeIndicator,
|
'changeIndicator' => $changeIndicator,
|
||||||
@ -1833,6 +1836,8 @@ private function verificationReportViewData(): array
|
|||||||
'acknowledgements' => $acknowledgements,
|
'acknowledgements' => $acknowledgements,
|
||||||
'assistVisibility' => $assistVisibility,
|
'assistVisibility' => $assistVisibility,
|
||||||
'assistActionName' => 'wizardVerificationRequiredPermissionsAssist',
|
'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']);
|
->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
|
public function acknowledgeVerificationCheckAction(): Action
|
||||||
{
|
{
|
||||||
return Action::make('acknowledgeVerificationCheck')
|
return Action::make('acknowledgeVerificationCheck')
|
||||||
@ -3229,6 +3254,89 @@ private function tenantlessOperationRunUrl(int $runId): string
|
|||||||
return OperationRunLinks::tenantlessView($runId);
|
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
|
public function verificationSucceeded(): bool
|
||||||
{
|
{
|
||||||
return $this->verificationHasSucceeded();
|
return $this->verificationHasSucceeded();
|
||||||
|
|||||||
@ -29,6 +29,16 @@ class ViewTenant extends ViewRecord
|
|||||||
{
|
{
|
||||||
protected static string $resource = TenantResource::class;
|
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
|
public function getHeaderWidgetsColumns(): int|array
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
@ -83,7 +93,7 @@ protected function getHeaderActions(): array
|
|||||||
->openUrlInNewTab(),
|
->openUrlInNewTab(),
|
||||||
UiEnforcement::forAction(
|
UiEnforcement::forAction(
|
||||||
Actions\Action::make('verify')
|
Actions\Action::make('verify')
|
||||||
->label('Verify configuration')
|
->label(self::verificationHeaderActionLabel())
|
||||||
->icon('heroicon-o-check-badge')
|
->icon('heroicon-o-check-badge')
|
||||||
->color('primary')
|
->color('primary')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Support\OperationRunLinks;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Filament\Widgets\Widget;
|
use Filament\Widgets\Widget;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
@ -41,6 +42,8 @@ protected function getViewData(): array
|
|||||||
'tenant' => null,
|
'tenant' => null,
|
||||||
'runs' => collect(),
|
'runs' => collect(),
|
||||||
'operationsIndexUrl' => route('admin.operations.index'),
|
'operationsIndexUrl' => route('admin.operations.index'),
|
||||||
|
'operationsIndexLabel' => OperationRunLinks::openCollectionLabel(),
|
||||||
|
'operationsIndexDescription' => OperationRunLinks::collectionScopeDescription(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +67,8 @@ protected function getViewData(): array
|
|||||||
'tenant' => $tenant,
|
'tenant' => $tenant,
|
||||||
'runs' => $runs,
|
'runs' => $runs,
|
||||||
'operationsIndexUrl' => route('admin.operations.index'),
|
'operationsIndexUrl' => route('admin.operations.index'),
|
||||||
|
'operationsIndexLabel' => OperationRunLinks::openCollectionLabel(),
|
||||||
|
'operationsIndexDescription' => OperationRunLinks::collectionScopeDescription(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
namespace App\Filament\Widgets\Tenant;
|
namespace App\Filament\Widgets\Tenant;
|
||||||
|
|
||||||
|
use App\Filament\Resources\TenantResource\Pages\ViewTenant;
|
||||||
use App\Filament\Support\VerificationReportViewer;
|
use App\Filament\Support\VerificationReportViewer;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
@ -175,6 +176,7 @@ protected function getViewData(): array
|
|||||||
'isInProgress' => false,
|
'isInProgress' => false,
|
||||||
'canStart' => false,
|
'canStart' => false,
|
||||||
'startTooltip' => null,
|
'startTooltip' => null,
|
||||||
|
'rerunHint' => null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,10 +232,13 @@ protected function getViewData(): array
|
|||||||
'report' => $report,
|
'report' => $report,
|
||||||
'redactionNotes' => VerificationReportViewer::redactionNotes($report),
|
'redactionNotes' => VerificationReportViewer::redactionNotes($report),
|
||||||
'isInProgress' => $isInProgress,
|
'isInProgress' => $isInProgress,
|
||||||
'showStartAction' => $isTenantMember && $canOperate,
|
'showStartAction' => ! ($run instanceof OperationRun) && $isTenantMember && $canOperate,
|
||||||
'canStart' => $canStart,
|
'canStart' => $canStart,
|
||||||
'startTooltip' => $isTenantMember && $canOperate && ! $canStart ? UiTooltips::insufficientPermission() : null,
|
'startTooltip' => $isTenantMember && $canOperate && ! $canStart ? UiTooltips::insufficientPermission() : null,
|
||||||
'lifecycleNotice' => $lifecycleNotice,
|
'lifecycleNotice' => $lifecycleNotice,
|
||||||
|
'rerunHint' => $run instanceof OperationRun && $canStart
|
||||||
|
? ViewTenant::verificationHeaderActionHint()
|
||||||
|
: null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,11 @@ public static function openCollectionLabel(): string
|
|||||||
return 'Open operations';
|
return 'Open operations';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function collectionScopeDescription(): string
|
||||||
|
{
|
||||||
|
return 'Broader admin view across recent and historical operations.';
|
||||||
|
}
|
||||||
|
|
||||||
public static function viewInCollectionLabel(): string
|
public static function viewInCollectionLabel(): string
|
||||||
{
|
{
|
||||||
return 'View in Operations';
|
return 'View in Operations';
|
||||||
@ -48,6 +53,16 @@ public static function openLabel(): string
|
|||||||
return 'Open operation';
|
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
|
public static function identifierLabel(): string
|
||||||
{
|
{
|
||||||
return 'Operation ID';
|
return 'Operation ID';
|
||||||
|
|||||||
@ -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\\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\\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\\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.',
|
'App\\Filament\\Pages\\Workspaces\\ManagedTenantsLanding' => 'Managed-tenant landing retrofit deferred to workspace feature track.',
|
||||||
], TenantOwnedModelFamilies::actionSurfaceBaselineExemptions()));
|
], TenantOwnedModelFamilies::actionSurfaceBaselineExemptions()));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,6 +19,9 @@
|
|||||||
$previousRunUrl = $previousRunUrl ?? null;
|
$previousRunUrl = $previousRunUrl ?? null;
|
||||||
$previousRunUrl = is_string($previousRunUrl) && $previousRunUrl !== '' ? $previousRunUrl : null;
|
$previousRunUrl = is_string($previousRunUrl) && $previousRunUrl !== '' ? $previousRunUrl : null;
|
||||||
|
|
||||||
|
$advancedRunUrl = $advancedRunUrl ?? null;
|
||||||
|
$advancedRunUrl = is_string($advancedRunUrl) && $advancedRunUrl !== '' ? $advancedRunUrl : null;
|
||||||
|
|
||||||
$canAcknowledge = (bool) ($canAcknowledge ?? false);
|
$canAcknowledge = (bool) ($canAcknowledge ?? false);
|
||||||
|
|
||||||
$acknowledgements = $acknowledgements ?? [];
|
$acknowledgements = $acknowledgements ?? [];
|
||||||
@ -32,6 +35,11 @@
|
|||||||
? trim($assistActionName)
|
? trim($assistActionName)
|
||||||
: 'wizardVerificationRequiredPermissionsAssist';
|
: 'wizardVerificationRequiredPermissionsAssist';
|
||||||
|
|
||||||
|
$technicalDetailsActionName = $technicalDetailsActionName ?? 'wizardVerificationTechnicalDetails';
|
||||||
|
$technicalDetailsActionName = is_string($technicalDetailsActionName) && trim($technicalDetailsActionName) !== ''
|
||||||
|
? trim($technicalDetailsActionName)
|
||||||
|
: 'wizardVerificationTechnicalDetails';
|
||||||
|
|
||||||
$showAssist = (bool) ($assistVisibility['is_visible'] ?? false);
|
$showAssist = (bool) ($assistVisibility['is_visible'] ?? false);
|
||||||
$assistReason = $assistVisibility['reason'] ?? 'hidden_irrelevant';
|
$assistReason = $assistVisibility['reason'] ?? 'hidden_irrelevant';
|
||||||
$assistReason = is_string($assistReason) ? $assistReason : 'hidden_irrelevant';
|
$assistReason = is_string($assistReason) ? $assistReason : 'hidden_irrelevant';
|
||||||
@ -149,6 +157,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$linkBehavior = app(\App\Support\Verification\VerificationLinkBehavior::class);
|
$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
|
@endphp
|
||||||
|
|
||||||
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
<x-dynamic-component :component="$fieldWrapperView" :field="$field">
|
||||||
@ -157,14 +174,41 @@
|
|||||||
heading="Verification report"
|
heading="Verification report"
|
||||||
:description="$completedAtLabel ? ('Completed: ' . $completedAtLabel) : 'Stored details for the latest verification operation.'"
|
:description="$completedAtLabel ? ('Completed: ' . $completedAtLabel) : 'Stored details for the latest verification operation.'"
|
||||||
>
|
>
|
||||||
@if ($run === null)
|
@if ($runState === 'no_run')
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
<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.
|
No verification operation has been started yet.
|
||||||
</div>
|
</div>
|
||||||
@elseif ($status !== 'completed')
|
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
<div>
|
||||||
|
Use the workflow action above to start verification for this tenant.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@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.
|
Report unavailable while the operation is in progress. Stored status updates automatically about every 5 seconds. Use “Refresh” to re-check immediately.
|
||||||
</div>
|
</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
|
@else
|
||||||
<div class="space-y-4">
|
<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">
|
<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>
|
</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)
|
@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="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">
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
@ -279,13 +344,6 @@ class="space-y-4"
|
|||||||
>
|
>
|
||||||
Passed
|
Passed
|
||||||
</x-filament::tabs.item>
|
</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>
|
</x-filament::tabs>
|
||||||
|
|
||||||
<div x-show="tab === 'issues'">
|
<div x-show="tab === 'issues'">
|
||||||
@ -587,85 +645,6 @@ class="inline-flex items-center gap-2 text-primary-600 hover:underline dark:text
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</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>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,6 +5,9 @@
|
|||||||
$runUrl = $runUrl ?? null;
|
$runUrl = $runUrl ?? null;
|
||||||
$runUrl = is_string($runUrl) && $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 = $run['status'] ?? null;
|
||||||
$status = is_string($status) ? $status : null;
|
$status = is_string($status) ? $status : null;
|
||||||
|
|
||||||
@ -142,16 +145,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@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)
|
@if ($runUrl)
|
||||||
<div>
|
<div class="space-y-1">
|
||||||
<a
|
<a
|
||||||
href="{{ $runUrl }}"
|
href="{{ $runUrl }}"
|
||||||
class="text-sm font-medium text-gray-600 hover:underline dark:text-gray-300"
|
class="text-sm font-medium text-gray-600 hover:underline dark:text-gray-300"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Open operation in Monitoring (advanced)
|
{{ \App\Support\OperationRunLinks::advancedMonitoringLabel() }}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ \App\Support\OperationRunLinks::advancedMonitoringDescription() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@ -2,18 +2,11 @@
|
|||||||
/** @var ?\App\Models\Tenant $tenant */
|
/** @var ?\App\Models\Tenant $tenant */
|
||||||
/** @var \Illuminate\Support\Collection<int, \App\Models\OperationRun> $runs */
|
/** @var \Illuminate\Support\Collection<int, \App\Models\OperationRun> $runs */
|
||||||
/** @var string $operationsIndexUrl */
|
/** @var string $operationsIndexUrl */
|
||||||
|
/** @var string $operationsIndexLabel */
|
||||||
|
/** @var string $operationsIndexDescription */
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<x-filament::section heading="Recent operations">
|
<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())
|
@if ($runs->isEmpty())
|
||||||
<div class="text-sm text-gray-500 dark:text-gray-400">
|
<div class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
No operations yet.
|
No operations yet.
|
||||||
@ -69,5 +62,20 @@ class="text-sm font-medium text-primary-600 hover:text-primary-500 dark:text-pri
|
|||||||
</li>
|
</li>
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</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
|
@endif
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|||||||
@ -20,6 +20,9 @@
|
|||||||
|
|
||||||
$lifecycleNotice = $lifecycleNotice ?? null;
|
$lifecycleNotice = $lifecycleNotice ?? null;
|
||||||
$lifecycleNotice = is_string($lifecycleNotice) && trim($lifecycleNotice) !== '' ? trim($lifecycleNotice) : null;
|
$lifecycleNotice = is_string($lifecycleNotice) && trim($lifecycleNotice) !== '' ? trim($lifecycleNotice) : null;
|
||||||
|
|
||||||
|
$rerunHint = $rerunHint ?? null;
|
||||||
|
$rerunHint = is_string($rerunHint) && trim($rerunHint) !== '' ? trim($rerunHint) : null;
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<x-filament::section
|
<x-filament::section
|
||||||
@ -76,46 +79,19 @@
|
|||||||
<x-filament::button
|
<x-filament::button
|
||||||
tag="a"
|
tag="a"
|
||||||
:href="$runUrl"
|
:href="$runUrl"
|
||||||
color="gray"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
Open operation
|
Open operation
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
@endif
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
@if ($showStartAction)
|
@if ($rerunHint || $startTooltip || $lifecycleNotice)
|
||||||
@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">
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{{ $startTooltip }}
|
{{ $rerunHint ?? $startTooltip ?? $lifecycleNotice }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@elseif ($lifecycleNotice)
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ $lifecycleNotice }}
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@else
|
@else
|
||||||
@include('filament.components.verification-report-viewer', [
|
@include('filament.components.verification-report-viewer', [
|
||||||
'run' => $runData,
|
'run' => $runData,
|
||||||
@ -128,46 +104,19 @@
|
|||||||
<x-filament::button
|
<x-filament::button
|
||||||
tag="a"
|
tag="a"
|
||||||
:href="$runUrl"
|
:href="$runUrl"
|
||||||
color="gray"
|
color="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
>
|
>
|
||||||
Open operation
|
Open operation
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
@endif
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
@if ($showStartAction)
|
@if ($rerunHint || $startTooltip || $lifecycleNotice)
|
||||||
@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">
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{{ $startTooltip }}
|
{{ $rerunHint ?? $startTooltip ?? $lifecycleNotice }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
@elseif ($lifecycleNotice)
|
|
||||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{{ $lifecycleNotice }}
|
|
||||||
</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|||||||
@ -33,3 +33,4 @@ ## Notes
|
|||||||
|
|
||||||
- Validation pass 1: complete
|
- 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.
|
- 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.
|
||||||
@ -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.
|
||||||
180
specs/172-deferred-operator-surfaces-retrofit/data-model.md
Normal file
180
specs/172-deferred-operator-surfaces-retrofit/data-model.md
Normal 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
|
||||||
134
specs/172-deferred-operator-surfaces-retrofit/plan.md
Normal file
134
specs/172-deferred-operator-surfaces-retrofit/plan.md
Normal 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`.
|
||||||
85
specs/172-deferred-operator-surfaces-retrofit/quickstart.md
Normal file
85
specs/172-deferred-operator-surfaces-retrofit/quickstart.md
Normal 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.
|
||||||
49
specs/172-deferred-operator-surfaces-retrofit/research.md
Normal file
49
specs/172-deferred-operator-surfaces-retrofit/research.md
Normal 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.
|
||||||
@ -9,14 +9,14 @@ ## Spec Scope Fields *(mandatory)*
|
|||||||
|
|
||||||
- **Scope**: workspace + tenant + canonical-view
|
- **Scope**: workspace + tenant + canonical-view
|
||||||
- **Primary Routes**:
|
- **Primary Routes**:
|
||||||
- `/admin/t/{tenant}`
|
- `/admin/tenants/{record}`
|
||||||
- `/admin/operations`
|
- `/admin/operations`
|
||||||
- `/admin/operations/{run}`
|
- `/admin/operations/{run}`
|
||||||
- managed-tenant onboarding flow routes that expose verification operation reports
|
- `/admin/onboarding` and related onboarding verification technical-details surfaces
|
||||||
- **Data Ownership**:
|
- **Data Ownership**:
|
||||||
- No new platform-owned, workspace-owned, or tenant-owned records are introduced
|
- 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
|
- 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**:
|
- **RBAC**:
|
||||||
- No new capability family is introduced
|
- No new capability family is introduced
|
||||||
- Existing tenant, workspace, and admin access rules remain authoritative for every destination that a retrofitted surface may open
|
- 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 abstraction?**: No
|
||||||
- **New enum/state/reason family?**: No
|
- **New enum/state/reason family?**: No
|
||||||
- **New cross-domain UI framework/taxonomy?**: 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.
|
- **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.
|
- **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.
|
- **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 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**:
|
**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.
|
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 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.
|
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.
|
**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**:
|
**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.
|
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
|
### 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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)*
|
## 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 (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.
|
**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
|
### 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-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-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-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-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-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-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-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 dashboard page, a new onboarding flow, or a new operations capability.
|
- **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)*
|
## 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 |
|
| 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 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 | 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 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 |
|
| 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)*
|
### 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.
|
- **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.
|
- **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)*
|
## 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-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-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
|
## Assumptions
|
||||||
|
|
||||||
@ -163,7 +164,7 @@ ## Assumptions
|
|||||||
|
|
||||||
## Non-Goals
|
## 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
|
- 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
|
- 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
|
- 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 169 deferred-surface exemption baseline
|
||||||
- Spec 170 system operations surface alignment
|
- Spec 170 system operations surface alignment
|
||||||
- Spec 171 operations naming consolidation
|
- 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
|
## 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.
|
||||||
190
specs/172-deferred-operator-surfaces-retrofit/tasks.md
Normal file
190
specs/172-deferred-operator-surfaces-retrofit/tasks.md
Normal 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.
|
||||||
@ -105,10 +105,10 @@
|
|||||||
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
||||||
->assertSee('Verify access')
|
->assertSee('Verify access')
|
||||||
->assertSee('Status: Needs attention')
|
->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')
|
->assertSee('Start verification')
|
||||||
->refresh()
|
->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()
|
->assertNoJavaScriptErrors()
|
||||||
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
|
||||||
->assertSee('Status: Needs attention')
|
->assertSee('Status: Needs attention')
|
||||||
|
|||||||
318
tests/Browser/Spec172DeferredOperatorSurfacesSmokeTest.php
Normal file
318
tests/Browser/Spec172DeferredOperatorSurfacesSmokeTest.php
Normal 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());
|
||||||
|
});
|
||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Filament\Widgets\Tenant\RecentOperationsSummary;
|
use App\Filament\Widgets\Tenant\RecentOperationsSummary;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
|
use App\Support\OperationRunLinks;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
it('renders recent operations from the record tenant in admin panel context', function (): void {
|
it('renders recent operations from the record tenant in admin panel context', function (): void {
|
||||||
@ -25,6 +26,8 @@
|
|||||||
->assertSee('Provider connection check')
|
->assertSee('Provider connection check')
|
||||||
->assertSee('Operation finished')
|
->assertSee('Operation finished')
|
||||||
->assertSee('Open operation')
|
->assertSee('Open operation')
|
||||||
|
->assertSee(OperationRunLinks::openCollectionLabel())
|
||||||
|
->assertSee(OperationRunLinks::collectionScopeDescription())
|
||||||
->assertSee('No action needed.')
|
->assertSee('No action needed.')
|
||||||
->assertDontSee('No operations yet.');
|
->assertDontSee('No operations yet.');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -111,12 +111,41 @@
|
|||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(TenantVerificationReport::class)
|
->test(TenantVerificationReport::class)
|
||||||
->assertSee('Provider connection preflight')
|
->assertSee('Provider connection preflight')
|
||||||
|
->assertSee(OperationRunLinks::openLabel())
|
||||||
|
->assertDontSee('Start verification')
|
||||||
->assertSee(OperationRunLinks::identifierLabel().':')
|
->assertSee(OperationRunLinks::identifierLabel().':')
|
||||||
->assertSee('Read-only:')
|
->assertSee('Read-only:')
|
||||||
->assertSee('Insufficient permission — ask a tenant Owner.');
|
->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 {
|
it('renders tenant detail without invoking synchronous verification or permission persistence services', function (): void {
|
||||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@
|
|||||||
use App\Filament\Pages\Reviews\ReviewRegister;
|
use App\Filament\Pages\Reviews\ReviewRegister;
|
||||||
use App\Filament\Pages\TenantDiagnostics;
|
use App\Filament\Pages\TenantDiagnostics;
|
||||||
use App\Filament\Pages\TenantRequiredPermissions;
|
use App\Filament\Pages\TenantRequiredPermissions;
|
||||||
|
use App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard;
|
||||||
use App\Filament\Resources\AlertDeliveryResource;
|
use App\Filament\Resources\AlertDeliveryResource;
|
||||||
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
|
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
|
||||||
use App\Filament\Resources\AlertDestinationResource;
|
use App\Filament\Resources\AlertDestinationResource;
|
||||||
@ -749,8 +750,9 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
|
|||||||
->and($baselineExemptions->hasClass(Alerts::class))->toBeTrue()
|
->and($baselineExemptions->hasClass(Alerts::class))->toBeTrue()
|
||||||
->and((string) $baselineExemptions->reasonForClass(Alerts::class))->toContain('cluster entry');
|
->and((string) $baselineExemptions->reasonForClass(Alerts::class))->toContain('cluster entry');
|
||||||
|
|
||||||
expect($baselineExemptions->hasClass(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class))->toBeTrue()
|
expect($baselineExemptions->hasClass(ManagedTenantOnboardingWizard::class))->toBeTrue()
|
||||||
->and((string) $baselineExemptions->reasonForClass(\App\Filament\Pages\Workspaces\ManagedTenantOnboardingWizard::class))->toContain('dedicated conformance tests');
|
->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()
|
expect(method_exists(\App\Filament\System\Pages\Ops\Runbooks::class, 'actionSurfaceDeclaration'))->toBeFalse()
|
||||||
->and($baselineExemptions->hasClass(\App\Filament\System\Pages\Ops\Runbooks::class))->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();
|
->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 {
|
it('keeps enrolled system panel pages declaration-backed without stale baseline exemptions', function (): void {
|
||||||
$baselineExemptions = ActionSurfaceExemptions::baseline();
|
$baselineExemptions = ActionSurfaceExemptions::baseline();
|
||||||
|
|
||||||
|
|||||||
@ -119,11 +119,12 @@
|
|||||||
->get('/admin/onboarding')
|
->get('/admin/onboarding')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Technical details')
|
->assertSee('Technical details')
|
||||||
->assertSee(OperationRunLinks::identifierLabel())
|
->assertSee(OperationRunLinks::openLabel())
|
||||||
->assertSee('Open operation details')
|
|
||||||
->assertSee('Required application permissions')
|
->assertSee('Required application permissions')
|
||||||
->assertSee('Open required permissions')
|
->assertSee('Open required permissions')
|
||||||
->assertSee('Issues')
|
->assertSee('Issues')
|
||||||
|
->assertDontSee('Open previous operation')
|
||||||
|
->assertDontSee('Open operation in Monitoring (advanced)')
|
||||||
->assertSee($entraTenantId);
|
->assertSee($entraTenantId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -187,7 +188,95 @@
|
|||||||
'onboardingDraft' => (int) $session->getKey(),
|
'onboardingDraft' => (int) $session->getKey(),
|
||||||
])
|
])
|
||||||
->mountAction('wizardVerificationTechnicalDetails')
|
->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 {
|
it('routes permission-related verification next steps through the required permissions assist', function (): void {
|
||||||
|
|||||||
@ -327,13 +327,118 @@
|
|||||||
->get('/admin/onboarding')
|
->get('/admin/onboarding')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Status: Blocked')
|
->assertSee('Status: Blocked')
|
||||||
->assertSee(OperationRunLinks::identifierLabel())
|
->assertSee(OperationRunLinks::openLabel())
|
||||||
->assertSee('Open operation details')
|
->assertSee('Technical details')
|
||||||
|
->assertDontSee('Open operation in Monitoring (advanced)')
|
||||||
|
->assertDontSee('Open previous operation')
|
||||||
->assertSee('Missing required Graph permissions.')
|
->assertSee('Missing required Graph permissions.')
|
||||||
->assertSee('Graph permissions')
|
->assertSee('Graph permissions')
|
||||||
->assertSee($entraTenantId);
|
->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 {
|
it('clears the stored verification run id when switching provider connections', function (): void {
|
||||||
Queue::fake();
|
Queue::fake();
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
use App\Models\VerificationCheckAcknowledgement;
|
use App\Models\VerificationCheckAcknowledgement;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
use App\Models\WorkspaceMembership;
|
use App\Models\WorkspaceMembership;
|
||||||
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\Verification\VerificationReportWriter;
|
use App\Support\Verification\VerificationReportWriter;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
|
||||||
@ -86,7 +87,37 @@
|
|||||||
->get('/admin/onboarding')
|
->get('/admin/onboarding')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Refresh')
|
->assertSee('Refresh')
|
||||||
|
->assertSee(OperationRunLinks::openLabel())
|
||||||
->assertDontSee('Start verification');
|
->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 {
|
it('orders issues deterministically and groups acknowledged issues', function (): void {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user