feat: implement spec 193 monitoring action hierarchy #227
4
.github/agents/copilot-instructions.md
vendored
4
.github/agents/copilot-instructions.md
vendored
@ -171,6 +171,8 @@ ## Active Technologies
|
|||||||
- PostgreSQL via existing baseline, assignment, compare-run, and finding tables; no new persistence planned (191-baseline-compare-operator-mode)
|
- PostgreSQL via existing baseline, assignment, compare-run, and finding tables; no new persistence planned (191-baseline-compare-operator-mode)
|
||||||
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `UiEnforcement`, `RelatedNavigationResolver`, `ActionSurfaceValidator`, and page-local Filament action builders (192-record-header-discipline)
|
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `UiEnforcement`, `RelatedNavigationResolver`, `ActionSurfaceValidator`, and page-local Filament action builders (192-record-header-discipline)
|
||||||
- PostgreSQL through existing workspace-owned and tenant-owned resource models; no schema change planned (192-record-header-discipline)
|
- PostgreSQL through existing workspace-owned and tenant-owned resource models; no schema change planned (192-record-header-discipline)
|
||||||
|
- PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `OperateHubShell`, `CanonicalNavigationContext`, `CanonicalAdminTenantFilterState`, `UiEnforcement`, `ActionSurfaceValidator`, and Filament page or resource action builders (193-monitoring-action-hierarchy)
|
||||||
|
- PostgreSQL through existing workspace-owned and tenant-owned models; no schema change planned (193-monitoring-action-hierarchy)
|
||||||
|
|
||||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||||
|
|
||||||
@ -205,8 +207,8 @@ ## Code Style
|
|||||||
PHP 8.4.15: Follow standard conventions
|
PHP 8.4.15: Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 193-monitoring-action-hierarchy: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `OperateHubShell`, `CanonicalNavigationContext`, `CanonicalAdminTenantFilterState`, `UiEnforcement`, `ActionSurfaceValidator`, and Filament page or resource action builders
|
||||||
- 192-record-header-discipline: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `UiEnforcement`, `RelatedNavigationResolver`, `ActionSurfaceValidator`, and page-local Filament action builders
|
- 192-record-header-discipline: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `UiEnforcement`, `RelatedNavigationResolver`, `ActionSurfaceValidator`, and page-local Filament action builders
|
||||||
- 191-baseline-compare-operator-mode: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns
|
- 191-baseline-compare-operator-mode: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `BaselineCompareMatrixBuilder`, `BadgeCatalog`, `CanonicalNavigationContext`, and `UiEnforcement` patterns
|
||||||
- 190-baseline-compare-matrix: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, existing `BaselineCompareService`, `BaselineSnapshotTruthResolver`, `BaselineCompareStats`, `RelatedNavigationResolver`, `CanonicalNavigationContext`, `BadgeCatalog`, and `UiEnforcement` patterns
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
<!-- MANUAL ADDITIONS END -->
|
<!-- MANUAL ADDITIONS END -->
|
||||||
|
|||||||
@ -12,6 +12,10 @@
|
|||||||
use App\Support\Auth\Capabilities;
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\Navigation\CanonicalNavigationContext;
|
use App\Support\Navigation\CanonicalNavigationContext;
|
||||||
use App\Support\OperateHub\OperateHubShell;
|
use App\Support\OperateHub\OperateHubShell;
|
||||||
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||||
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||||
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
||||||
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
@ -37,6 +41,16 @@ class Alerts extends Page
|
|||||||
|
|
||||||
protected string $view = 'filament.pages.monitoring.alerts';
|
protected string $view = 'filament.pages.monitoring.alerts';
|
||||||
|
|
||||||
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||||
|
{
|
||||||
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly, ActionSurfaceType::ReadOnlyRegistryReport)
|
||||||
|
->satisfy(ActionSurfaceSlot::ListHeader, 'Header keeps alerts scope and origin navigation quiet on the page-level overview.')
|
||||||
|
->exempt(ActionSurfaceSlot::InspectAffordance, 'The alerts overview is a page-level monitoring summary and does not inspect records inline.')
|
||||||
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The alerts overview does not render row-level secondary actions.')
|
||||||
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The alerts overview does not expose bulk actions.')
|
||||||
|
->exempt(ActionSurfaceSlot::ListEmptyState, 'The overview always renders KPI widgets and downstream drilldown navigation instead of a list-style empty state.');
|
||||||
|
}
|
||||||
|
|
||||||
public static function canAccess(): bool
|
public static function canAccess(): bool
|
||||||
{
|
{
|
||||||
if (Filament::getCurrentPanel()?->getId() !== 'admin') {
|
if (Filament::getCurrentPanel()?->getId() !== 'admin') {
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
use App\Support\Filament\FilterOptionCatalog;
|
use App\Support\Filament\FilterOptionCatalog;
|
||||||
use App\Support\Filament\FilterPresets;
|
use App\Support\Filament\FilterPresets;
|
||||||
use App\Support\Filament\TablePaginationProfiles;
|
use App\Support\Filament\TablePaginationProfiles;
|
||||||
|
use App\Support\Navigation\CanonicalNavigationContext;
|
||||||
use App\Support\Navigation\RelatedNavigationResolver;
|
use App\Support\Navigation\RelatedNavigationResolver;
|
||||||
use App\Support\OperateHub\OperateHubShell;
|
use App\Support\OperateHub\OperateHubShell;
|
||||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||||
@ -93,7 +94,6 @@ public function mount(): void
|
|||||||
if ($requestedEventId !== null) {
|
if ($requestedEventId !== null) {
|
||||||
$this->resolveAuditLog($requestedEventId);
|
$this->resolveAuditLog($requestedEventId);
|
||||||
$this->selectedAuditLogId = $requestedEventId;
|
$this->selectedAuditLogId = $requestedEventId;
|
||||||
$this->mountTableAction('inspect', (string) $requestedEventId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,10 +102,24 @@ public function mount(): void
|
|||||||
*/
|
*/
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return app(OperateHubShell::class)->headerActions(
|
$actions = app(OperateHubShell::class)->headerActions(
|
||||||
scopeActionName: 'operate_hub_scope_audit_log',
|
scopeActionName: 'operate_hub_scope_audit_log',
|
||||||
returnActionName: 'operate_hub_return_audit_log',
|
returnActionName: 'operate_hub_return_audit_log',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$navigationContext = CanonicalNavigationContext::fromRequest(request());
|
||||||
|
|
||||||
|
if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) {
|
||||||
|
array_splice($actions, 1, 0, [
|
||||||
|
Action::make('operate_hub_back_to_origin_audit_log')
|
||||||
|
->label($navigationContext->backLinkLabel)
|
||||||
|
->icon('heroicon-o-arrow-left')
|
||||||
|
->color('gray')
|
||||||
|
->url($navigationContext->backLinkUrl),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function table(Table $table): Table
|
public function table(Table $table): Table
|
||||||
|
|||||||
@ -28,6 +28,7 @@
|
|||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Actions\ActionGroup;
|
||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Filament\Forms\Components\DateTimePicker;
|
use Filament\Forms\Components\DateTimePicker;
|
||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
@ -164,30 +165,32 @@ protected function getHeaderActions(): array
|
|||||||
return FindingExceptionResource::getUrl('index', panel: 'tenant', tenant: $tenant);
|
return FindingExceptionResource::getUrl('index', panel: 'tenant', tenant: $tenant);
|
||||||
});
|
});
|
||||||
|
|
||||||
$actions[] = Action::make('clear_selected_exception')
|
$selectedContextActions = [
|
||||||
|
Action::make('clear_selected_exception')
|
||||||
->label('Close details')
|
->label('Close details')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->visible(fn (): bool => $this->selectedFindingExceptionId !== null)
|
->visible(fn (): bool => $this->selectedFindingExceptionId !== null)
|
||||||
->action(function (): void {
|
->action(function (): void {
|
||||||
$this->selectedFindingExceptionId = null;
|
$this->clearSelectedException();
|
||||||
$this->showSelectedExceptionSummary = false;
|
}),
|
||||||
});
|
|
||||||
|
|
||||||
$actions[] = Action::make('open_selected_exception')
|
Action::make('open_selected_exception')
|
||||||
->label('Open tenant detail')
|
->label('Open tenant detail')
|
||||||
->icon('heroicon-o-arrow-top-right-on-square')
|
->icon('heroicon-o-arrow-top-right-on-square')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->visible(fn (): bool => $this->selectedFindingExceptionId !== null)
|
->visible(fn (): bool => $this->selectedFindingExceptionId !== null)
|
||||||
->url(fn (): ?string => $this->selectedExceptionUrl());
|
->url(fn (): ?string => $this->selectedExceptionUrl()),
|
||||||
|
|
||||||
$actions[] = Action::make('open_selected_finding')
|
Action::make('open_selected_finding')
|
||||||
->label('Open finding')
|
->label('Open finding')
|
||||||
->icon('heroicon-o-arrow-top-right-on-square')
|
->icon('heroicon-o-arrow-top-right-on-square')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->visible(fn (): bool => $this->selectedFindingExceptionId !== null)
|
->visible(fn (): bool => $this->selectedFindingExceptionId !== null)
|
||||||
->url(fn (): ?string => $this->selectedFindingUrl());
|
->url(fn (): ?string => $this->selectedFindingUrl()),
|
||||||
|
];
|
||||||
|
|
||||||
$actions[] = Action::make('approve_selected_exception')
|
$selectedDecisionActions = [
|
||||||
|
Action::make('approve_selected_exception')
|
||||||
->label('Approve exception')
|
->label('Approve exception')
|
||||||
->color('success')
|
->color('success')
|
||||||
->visible(fn (): bool => $this->selectedFindingException()?->isPending() ?? false)
|
->visible(fn (): bool => $this->selectedFindingException()?->isPending() ?? false)
|
||||||
@ -223,9 +226,9 @@ protected function getHeaderActions(): array
|
|||||||
->title($wasRenewalRequest ? 'Exception renewed' : 'Exception approved')
|
->title($wasRenewalRequest ? 'Exception renewed' : 'Exception approved')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
});
|
}),
|
||||||
|
|
||||||
$actions[] = Action::make('reject_selected_exception')
|
Action::make('reject_selected_exception')
|
||||||
->label('Reject exception')
|
->label('Reject exception')
|
||||||
->color('danger')
|
->color('danger')
|
||||||
->visible(fn (): bool => $this->selectedFindingException()?->isPending() ?? false)
|
->visible(fn (): bool => $this->selectedFindingException()?->isPending() ?? false)
|
||||||
@ -254,7 +257,20 @@ protected function getHeaderActions(): array
|
|||||||
->title($wasRenewalRequest ? 'Renewal rejected' : 'Exception rejected')
|
->title($wasRenewalRequest ? 'Renewal rejected' : 'Exception rejected')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
});
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
$actions[] = ActionGroup::make($selectedContextActions)
|
||||||
|
->label('Selected context')
|
||||||
|
->icon('heroicon-o-rectangle-stack')
|
||||||
|
->color('gray')
|
||||||
|
->visible(fn (): bool => $this->selectedFindingExceptionId !== null);
|
||||||
|
|
||||||
|
$actions[] = ActionGroup::make($selectedDecisionActions)
|
||||||
|
->label('Review selected')
|
||||||
|
->icon('heroicon-o-shield-check')
|
||||||
|
->color('primary')
|
||||||
|
->visible(fn (): bool => $this->selectedFindingException()?->isPending() ?? false);
|
||||||
|
|
||||||
return $actions;
|
return $actions;
|
||||||
}
|
}
|
||||||
@ -409,6 +425,12 @@ public function selectedFindingUrl(): ?string
|
|||||||
return FindingResource::getUrl('view', ['record' => $record->finding], panel: 'tenant', tenant: $record->tenant);
|
return FindingResource::getUrl('view', ['record' => $record->finding], panel: 'tenant', tenant: $record->tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function clearSelectedException(): void
|
||||||
|
{
|
||||||
|
$this->selectedFindingExceptionId = null;
|
||||||
|
$this->showSelectedExceptionSummary = false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, Tenant>
|
* @return array<int, Tenant>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -142,6 +142,49 @@ protected function getHeaderActions(): array
|
|||||||
return $actions;
|
return $actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* scope_label: string,
|
||||||
|
* scope_body: string,
|
||||||
|
* return_label: ?string,
|
||||||
|
* return_body: ?string,
|
||||||
|
* scope_reset_label: ?string,
|
||||||
|
* scope_reset_body: ?string,
|
||||||
|
* inspect_body: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function landingHierarchySummary(): array
|
||||||
|
{
|
||||||
|
$operateHubShell = app(OperateHubShell::class);
|
||||||
|
$navigationContext = $this->navigationContext();
|
||||||
|
$activeTenant = $operateHubShell->activeEntitledTenant(request());
|
||||||
|
|
||||||
|
$returnLabel = null;
|
||||||
|
$returnBody = null;
|
||||||
|
|
||||||
|
if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) {
|
||||||
|
$returnLabel = $navigationContext->backLinkLabel;
|
||||||
|
$returnBody = 'Return to the originating monitoring surface without competing with the current tab, filters, or row inspection flow.';
|
||||||
|
} elseif ($activeTenant instanceof Tenant) {
|
||||||
|
$returnLabel = 'Back to '.$activeTenant->name;
|
||||||
|
$returnBody = 'Return to the tenant dashboard when you need tenant-specific context outside this workspace monitoring landing.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'scope_label' => $operateHubShell->scopeLabel(request()),
|
||||||
|
'scope_body' => $activeTenant instanceof Tenant
|
||||||
|
? 'The landing is currently narrowed to one tenant inside the active workspace.'
|
||||||
|
: 'The landing is currently showing workspace-wide monitoring across all entitled tenants.',
|
||||||
|
'return_label' => $returnLabel,
|
||||||
|
'return_body' => $returnBody,
|
||||||
|
'scope_reset_label' => $activeTenant instanceof Tenant ? 'Show all tenants' : null,
|
||||||
|
'scope_reset_body' => $activeTenant instanceof Tenant
|
||||||
|
? 'Reset the landing back to workspace-wide monitoring when tenant-specific context is no longer needed.'
|
||||||
|
: null,
|
||||||
|
'inspect_body' => 'Open a run from the table to enter the canonical monitoring detail viewer.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function navigationContext(): ?CanonicalNavigationContext
|
private function navigationContext(): ?CanonicalNavigationContext
|
||||||
{
|
{
|
||||||
if (! is_array($this->navigationContextPayload)) {
|
if (! is_array($this->navigationContextPayload)) {
|
||||||
|
|||||||
@ -123,7 +123,7 @@ protected function getHeaderActions(): array
|
|||||||
$actions[] = Action::make('refresh')
|
$actions[] = Action::make('refresh')
|
||||||
->label('Refresh')
|
->label('Refresh')
|
||||||
->icon('heroicon-o-arrow-path')
|
->icon('heroicon-o-arrow-path')
|
||||||
->color('gray')
|
->color('primary')
|
||||||
->url(fn (): string => isset($this->run)
|
->url(fn (): string => isset($this->run)
|
||||||
? OperationRunLinks::tenantlessView($this->run, $navigationContext)
|
? OperationRunLinks::tenantlessView($this->run, $navigationContext)
|
||||||
: route('admin.operations.index'));
|
: route('admin.operations.index'));
|
||||||
@ -155,6 +155,57 @@ protected function getHeaderActions(): array
|
|||||||
return $actions;
|
return $actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* scope_label: string,
|
||||||
|
* scope_body: string,
|
||||||
|
* navigation_label: string,
|
||||||
|
* navigation_body: string,
|
||||||
|
* utility_body: string,
|
||||||
|
* related_body: string,
|
||||||
|
* follow_up_body: string,
|
||||||
|
* follow_up_label: ?string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function monitoringDetailSummary(): array
|
||||||
|
{
|
||||||
|
$operateHubShell = app(OperateHubShell::class);
|
||||||
|
$navigationContext = $this->navigationContext();
|
||||||
|
$activeTenant = $operateHubShell->activeEntitledTenant(request());
|
||||||
|
$runTenantId = isset($this->run) ? (int) ($this->run->tenant_id ?? 0) : 0;
|
||||||
|
|
||||||
|
$navigationLabel = 'Back to Operations';
|
||||||
|
$navigationBody = 'Return to the operations landing when this review is complete.';
|
||||||
|
|
||||||
|
if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) {
|
||||||
|
$navigationLabel = $navigationContext->backLinkLabel;
|
||||||
|
$navigationBody = 'Return to the originating surface while keeping refresh and follow-up work separate from navigation.';
|
||||||
|
} elseif ($activeTenant instanceof Tenant && (int) $activeTenant->getKey() === $runTenantId) {
|
||||||
|
$navigationLabel = 'Back to '.$activeTenant->name;
|
||||||
|
$navigationBody = 'Return to the active tenant dashboard, then widen back to the workspace view only when you need broader monitoring context.';
|
||||||
|
}
|
||||||
|
|
||||||
|
$relatedLabels = array_values(array_keys($this->relatedLinks()));
|
||||||
|
$relatedBody = $relatedLabels === []
|
||||||
|
? 'Open keeps secondary drilldowns grouped under one control when downstream context exists.'
|
||||||
|
: 'Open keeps secondary drilldowns grouped under one control: '.implode(', ', $relatedLabels).'.';
|
||||||
|
|
||||||
|
$followUpLabel = $this->canResumeCapture() ? 'Resume capture' : null;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'scope_label' => $operateHubShell->scopeLabel(request()),
|
||||||
|
'scope_body' => 'The current workspace or tenant scope remains visible without behaving like a primary task action.',
|
||||||
|
'navigation_label' => $navigationLabel,
|
||||||
|
'navigation_body' => $navigationBody,
|
||||||
|
'utility_body' => 'Refresh keeps the current run state accurate without changing scope.',
|
||||||
|
'related_body' => $relatedBody,
|
||||||
|
'follow_up_body' => $followUpLabel !== null
|
||||||
|
? 'Resume capture only appears when this run supports additional evidence collection.'
|
||||||
|
: 'No run-specific follow-up is currently available.',
|
||||||
|
'follow_up_label' => $followUpLabel,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
public function mount(OperationRun $run): void
|
public function mount(OperationRun $run): void
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
@ -364,6 +415,7 @@ private function resumeCaptureAction(): Action
|
|||||||
return Action::make('resumeCapture')
|
return Action::make('resumeCapture')
|
||||||
->label('Resume capture')
|
->label('Resume capture')
|
||||||
->icon('heroicon-o-forward')
|
->icon('heroicon-o-forward')
|
||||||
|
->color('primary')
|
||||||
->requiresConfirmation()
|
->requiresConfirmation()
|
||||||
->modalHeading('Resume capture')
|
->modalHeading('Resume capture')
|
||||||
->modalDescription('This will start a follow-up operation to capture remaining baseline evidence for this scope.')
|
->modalDescription('This will start a follow-up operation to capture remaining baseline evidence for this scope.')
|
||||||
@ -532,9 +584,16 @@ private function relatedLinks(bool $fresh = false): array
|
|||||||
|
|
||||||
$resolver = app(RelatedNavigationResolver::class);
|
$resolver = app(RelatedNavigationResolver::class);
|
||||||
|
|
||||||
return $fresh
|
$links = $fresh
|
||||||
? $resolver->operationLinksFresh($this->run, $this->relatedLinksTenant())
|
? $resolver->operationLinksFresh($this->run, $this->relatedLinksTenant())
|
||||||
: $resolver->operationLinks($this->run, $this->relatedLinksTenant());
|
: $resolver->operationLinks($this->run, $this->relatedLinksTenant());
|
||||||
|
|
||||||
|
unset(
|
||||||
|
$links[OperationRunLinks::collectionLabel()],
|
||||||
|
$links[OperationRunLinks::openCollectionLabel()],
|
||||||
|
);
|
||||||
|
|
||||||
|
return $links;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function lifecycleAttentionSummary(bool $fresh = false): ?string
|
private function lifecycleAttentionSummary(bool $fresh = false): ?string
|
||||||
|
|||||||
@ -94,7 +94,7 @@ protected function getHeaderActions(): array
|
|||||||
->color('gray')
|
->color('gray')
|
||||||
->visible(fn (): bool => $this->hasActiveFilters())
|
->visible(fn (): bool => $this->hasActiveFilters())
|
||||||
->action(function (): void {
|
->action(function (): void {
|
||||||
$this->resetTable();
|
$this->clearRegisterFilters();
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -209,7 +209,7 @@ public function table(Table $table): Table
|
|||||||
->label('Clear filters')
|
->label('Clear filters')
|
||||||
->icon('heroicon-o-x-mark')
|
->icon('heroicon-o-x-mark')
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->action(fn (): mixed => $this->resetTable()),
|
->action(fn (): mixed => $this->clearRegisterFilters()),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,9 +311,29 @@ private function applyRequestedTenantPrefilter(): void
|
|||||||
|
|
||||||
private function hasActiveFilters(): bool
|
private function hasActiveFilters(): bool
|
||||||
{
|
{
|
||||||
$filters = array_filter((array) $this->tableFilters);
|
return $this->currentTenantFilterId() !== null
|
||||||
|
|| is_string(data_get($this->tableFilters, 'status.value'))
|
||||||
|
|| is_string(data_get($this->tableFilters, 'completeness_state.value'))
|
||||||
|
|| is_string(data_get($this->tableFilters, 'published_state.value'))
|
||||||
|
|| filled(data_get($this->tableFilters, 'review_date.from'))
|
||||||
|
|| filled(data_get($this->tableFilters, 'review_date.until'));
|
||||||
|
}
|
||||||
|
|
||||||
return $filters !== [];
|
private function clearRegisterFilters(): void
|
||||||
|
{
|
||||||
|
app(WorkspaceContext::class)->clearLastTenantId(request());
|
||||||
|
$this->removeTableFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentTenantFilterId(): ?int
|
||||||
|
{
|
||||||
|
$tenantFilter = data_get($this->tableFilters, 'tenant_id.value');
|
||||||
|
|
||||||
|
if (! is_numeric($tenantFilter)) {
|
||||||
|
$tenantFilter = data_get(session()->get($this->getTableFiltersSessionKey(), []), 'tenant_id.value');
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_numeric($tenantFilter) ? (int) $tenantFilter : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function workspace(): ?Workspace
|
private function workspace(): ?Workspace
|
||||||
|
|||||||
@ -6,7 +6,9 @@
|
|||||||
|
|
||||||
use App\Filament\Resources\AlertDeliveryResource;
|
use App\Filament\Resources\AlertDeliveryResource;
|
||||||
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
use App\Support\Filament\CanonicalAdminTenantFilterState;
|
||||||
|
use App\Support\Navigation\CanonicalNavigationContext;
|
||||||
use App\Support\OperateHub\OperateHubShell;
|
use App\Support\OperateHub\OperateHubShell;
|
||||||
|
use Filament\Actions\Action;
|
||||||
use Filament\Resources\Pages\ListRecords;
|
use Filament\Resources\Pages\ListRecords;
|
||||||
|
|
||||||
class ListAlertDeliveries extends ListRecords
|
class ListAlertDeliveries extends ListRecords
|
||||||
@ -22,9 +24,23 @@ public function mount(): void
|
|||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return app(OperateHubShell::class)->headerActions(
|
$actions = app(OperateHubShell::class)->headerActions(
|
||||||
scopeActionName: 'operate_hub_scope_alerts',
|
scopeActionName: 'operate_hub_scope_alerts',
|
||||||
returnActionName: 'operate_hub_return_alerts',
|
returnActionName: 'operate_hub_return_alerts',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$navigationContext = CanonicalNavigationContext::fromRequest(request());
|
||||||
|
|
||||||
|
if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) {
|
||||||
|
array_splice($actions, 1, 0, [
|
||||||
|
Action::make('operate_hub_back_to_origin_alert_deliveries')
|
||||||
|
->label($navigationContext->backLinkLabel)
|
||||||
|
->icon('heroicon-o-arrow-left')
|
||||||
|
->color('gray')
|
||||||
|
->url($navigationContext->backLinkUrl),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $actions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,17 @@
|
|||||||
|
|
||||||
namespace App\Support\Ui\ActionSurface;
|
namespace App\Support\Ui\ActionSurface;
|
||||||
|
|
||||||
|
use App\Filament\Pages\BaselineCompareLanding;
|
||||||
|
use App\Filament\Pages\BaselineCompareMatrix;
|
||||||
|
use App\Filament\Pages\Monitoring\Alerts;
|
||||||
|
use App\Filament\Pages\Monitoring\AuditLog;
|
||||||
|
use App\Filament\Pages\Monitoring\EvidenceOverview;
|
||||||
|
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
||||||
|
use App\Filament\Pages\Monitoring\Operations;
|
||||||
|
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
||||||
|
use App\Filament\Pages\Reviews\ReviewRegister;
|
||||||
|
use App\Filament\Pages\TenantDiagnostics;
|
||||||
|
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
|
||||||
use App\Filament\Resources\AlertDestinationResource\Pages\ViewAlertDestination;
|
use App\Filament\Resources\AlertDestinationResource\Pages\ViewAlertDestination;
|
||||||
use App\Filament\Resources\BackupSetResource\Pages\ViewBackupSet;
|
use App\Filament\Resources\BackupSetResource\Pages\ViewBackupSet;
|
||||||
use App\Filament\Resources\BaselineProfileResource\Pages\ViewBaselineProfile;
|
use App\Filament\Resources\BaselineProfileResource\Pages\ViewBaselineProfile;
|
||||||
@ -38,7 +49,6 @@ public static function baseline(): self
|
|||||||
'App\\Filament\\Pages\\BreakGlassRecovery' => 'Break-glass flow is governed by dedicated security specs and tests.',
|
'App\\Filament\\Pages\\BreakGlassRecovery' => 'Break-glass flow is governed by dedicated security specs and tests.',
|
||||||
'App\\Filament\\Pages\\ChooseTenant' => 'Tenant chooser has no contract-style table action surface.',
|
'App\\Filament\\Pages\\ChooseTenant' => 'Tenant chooser has no contract-style table action surface.',
|
||||||
'App\\Filament\\Pages\\ChooseWorkspace' => 'Workspace chooser has no contract-style table action surface.',
|
'App\\Filament\\Pages\\ChooseWorkspace' => 'Workspace chooser has no contract-style table action surface.',
|
||||||
'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 in spec 172 (OnboardingVerificationTest, OnboardingVerificationClustersTest, OnboardingVerificationV1_5UxTest) and remains exempt from blanket discovery.',
|
'App\\Filament\\Pages\\Workspaces\\ManagedTenantOnboardingWizard' => 'Onboarding wizard has dedicated conformance tests in spec 172 (OnboardingVerificationTest, OnboardingVerificationClustersTest, OnboardingVerificationV1_5UxTest) and remains exempt from blanket discovery.',
|
||||||
@ -312,6 +322,182 @@ public static function spec192RecordPageInventory(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array{
|
||||||
|
* surfaceKey: string,
|
||||||
|
* classification: string,
|
||||||
|
* canonicalNoun: string,
|
||||||
|
* panelScope: string,
|
||||||
|
* ownerScope: string,
|
||||||
|
* surfaceKind: string,
|
||||||
|
* primaryInspectModel: string,
|
||||||
|
* sharedPattern: string,
|
||||||
|
* requiresHeaderRemediation: bool,
|
||||||
|
* requiresExplicitDeclaration: bool,
|
||||||
|
* exceptionReason: ?string,
|
||||||
|
* browserSmokeRequired: bool
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
public static function spec193MonitoringSurfaceInventory(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
FindingExceptionsQueue::class => [
|
||||||
|
'surfaceKey' => 'finding_exceptions_queue',
|
||||||
|
'classification' => 'remediation_required',
|
||||||
|
'canonicalNoun' => 'Finding exceptions',
|
||||||
|
'panelScope' => 'admin',
|
||||||
|
'ownerScope' => 'workspace-visible-tenant-owned',
|
||||||
|
'surfaceKind' => 'queue_workbench',
|
||||||
|
'primaryInspectModel' => 'explicit_inspect_action',
|
||||||
|
'sharedPattern' => 'operate_hub_shell',
|
||||||
|
'requiresHeaderRemediation' => true,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => true,
|
||||||
|
],
|
||||||
|
TenantlessOperationRunViewer::class => [
|
||||||
|
'surfaceKey' => 'tenantless_operation_run_viewer',
|
||||||
|
'classification' => 'remediation_required',
|
||||||
|
'canonicalNoun' => 'Operation run',
|
||||||
|
'panelScope' => 'admin',
|
||||||
|
'ownerScope' => 'workspace-owned',
|
||||||
|
'surfaceKind' => 'monitoring_detail',
|
||||||
|
'primaryInspectModel' => 'singleton_detail_surface',
|
||||||
|
'sharedPattern' => 'operate_hub_shell',
|
||||||
|
'requiresHeaderRemediation' => true,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => true,
|
||||||
|
],
|
||||||
|
Operations::class => [
|
||||||
|
'surfaceKey' => 'operations',
|
||||||
|
'classification' => 'remediation_required',
|
||||||
|
'canonicalNoun' => 'Operations',
|
||||||
|
'panelScope' => 'admin',
|
||||||
|
'ownerScope' => 'workspace-owned',
|
||||||
|
'surfaceKind' => 'monitoring_landing',
|
||||||
|
'primaryInspectModel' => 'clickable_row',
|
||||||
|
'sharedPattern' => 'operate_hub_shell',
|
||||||
|
'requiresHeaderRemediation' => true,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => true,
|
||||||
|
],
|
||||||
|
Alerts::class => [
|
||||||
|
'surfaceKey' => 'alerts',
|
||||||
|
'classification' => 'minor_alignment_only',
|
||||||
|
'canonicalNoun' => 'Alerts',
|
||||||
|
'panelScope' => 'admin',
|
||||||
|
'ownerScope' => 'workspace-owned',
|
||||||
|
'surfaceKind' => 'monitoring_landing',
|
||||||
|
'primaryInspectModel' => 'page_level_overview',
|
||||||
|
'sharedPattern' => 'cluster_entry',
|
||||||
|
'requiresHeaderRemediation' => false,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => false,
|
||||||
|
],
|
||||||
|
AuditLog::class => [
|
||||||
|
'surfaceKey' => 'audit_log',
|
||||||
|
'classification' => 'minor_alignment_only',
|
||||||
|
'canonicalNoun' => 'Audit log',
|
||||||
|
'panelScope' => 'admin',
|
||||||
|
'ownerScope' => 'workspace-visible-tenant-owned',
|
||||||
|
'surfaceKind' => 'read_only_report',
|
||||||
|
'primaryInspectModel' => 'explicit_inspect_action',
|
||||||
|
'sharedPattern' => 'operate_hub_shell',
|
||||||
|
'requiresHeaderRemediation' => false,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => false,
|
||||||
|
],
|
||||||
|
ListAlertDeliveries::class => [
|
||||||
|
'surfaceKey' => 'alert_deliveries',
|
||||||
|
'classification' => 'minor_alignment_only',
|
||||||
|
'canonicalNoun' => 'Alert deliveries',
|
||||||
|
'panelScope' => 'admin',
|
||||||
|
'ownerScope' => 'workspace-owned',
|
||||||
|
'surfaceKind' => 'read_only_report',
|
||||||
|
'primaryInspectModel' => 'clickable_row',
|
||||||
|
'sharedPattern' => 'operate_hub_shell',
|
||||||
|
'requiresHeaderRemediation' => false,
|
||||||
|
'requiresExplicitDeclaration' => false,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => false,
|
||||||
|
],
|
||||||
|
EvidenceOverview::class => [
|
||||||
|
'surfaceKey' => 'evidence_overview',
|
||||||
|
'classification' => 'compliant_no_op',
|
||||||
|
'canonicalNoun' => 'Evidence overview',
|
||||||
|
'panelScope' => 'admin',
|
||||||
|
'ownerScope' => 'workspace-visible-tenant-owned',
|
||||||
|
'surfaceKind' => 'read_only_report',
|
||||||
|
'primaryInspectModel' => 'clickable_row',
|
||||||
|
'sharedPattern' => 'none',
|
||||||
|
'requiresHeaderRemediation' => false,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => true,
|
||||||
|
],
|
||||||
|
BaselineCompareLanding::class => [
|
||||||
|
'surfaceKey' => 'baseline_compare_landing',
|
||||||
|
'classification' => 'compliant_no_op',
|
||||||
|
'canonicalNoun' => 'Baseline compare',
|
||||||
|
'panelScope' => 'tenant',
|
||||||
|
'ownerScope' => 'tenant-owned',
|
||||||
|
'surfaceKind' => 'monitoring_landing',
|
||||||
|
'primaryInspectModel' => 'page_level_overview',
|
||||||
|
'sharedPattern' => 'none',
|
||||||
|
'requiresHeaderRemediation' => false,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => true,
|
||||||
|
],
|
||||||
|
BaselineCompareMatrix::class => [
|
||||||
|
'surfaceKey' => 'baseline_compare_matrix',
|
||||||
|
'classification' => 'compliant_no_op',
|
||||||
|
'canonicalNoun' => 'Baseline compare matrix',
|
||||||
|
'panelScope' => 'tenant',
|
||||||
|
'ownerScope' => 'tenant-owned',
|
||||||
|
'surfaceKind' => 'read_only_report',
|
||||||
|
'primaryInspectModel' => 'matrix_itself',
|
||||||
|
'sharedPattern' => 'none',
|
||||||
|
'requiresHeaderRemediation' => false,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => true,
|
||||||
|
],
|
||||||
|
ReviewRegister::class => [
|
||||||
|
'surfaceKey' => 'review_register',
|
||||||
|
'classification' => 'compliant_no_op',
|
||||||
|
'canonicalNoun' => 'Review register',
|
||||||
|
'panelScope' => 'admin',
|
||||||
|
'ownerScope' => 'workspace-visible-tenant-owned',
|
||||||
|
'surfaceKind' => 'read_only_report',
|
||||||
|
'primaryInspectModel' => 'clickable_row',
|
||||||
|
'sharedPattern' => 'none',
|
||||||
|
'requiresHeaderRemediation' => false,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => null,
|
||||||
|
'browserSmokeRequired' => true,
|
||||||
|
],
|
||||||
|
TenantDiagnostics::class => [
|
||||||
|
'surfaceKey' => 'tenant_diagnostics',
|
||||||
|
'classification' => 'special_type_acceptable',
|
||||||
|
'canonicalNoun' => 'Tenant diagnostics',
|
||||||
|
'panelScope' => 'tenant',
|
||||||
|
'ownerScope' => 'tenant-owned',
|
||||||
|
'surfaceKind' => 'diagnostic_exception',
|
||||||
|
'primaryInspectModel' => 'singleton_detail_surface',
|
||||||
|
'sharedPattern' => 'none',
|
||||||
|
'requiresHeaderRemediation' => false,
|
||||||
|
'requiresExplicitDeclaration' => true,
|
||||||
|
'exceptionReason' => 'Tenant diagnostics is already the focused diagnostic surface for the active tenant and may expose repair actions only when a real defect exists.',
|
||||||
|
'browserSmokeRequired' => true,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* @return array{
|
||||||
* surfaceKey: string,
|
* surfaceKey: string,
|
||||||
@ -334,4 +520,25 @@ public static function spec192RecordPageSurface(string $className): ?array
|
|||||||
{
|
{
|
||||||
return self::spec192RecordPageInventory()[$className] ?? null;
|
return self::spec192RecordPageInventory()[$className] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* surfaceKey: string,
|
||||||
|
* classification: string,
|
||||||
|
* canonicalNoun: string,
|
||||||
|
* panelScope: string,
|
||||||
|
* ownerScope: string,
|
||||||
|
* surfaceKind: string,
|
||||||
|
* primaryInspectModel: string,
|
||||||
|
* sharedPattern: string,
|
||||||
|
* requiresHeaderRemediation: bool,
|
||||||
|
* requiresExplicitDeclaration: bool,
|
||||||
|
* exceptionReason: ?string,
|
||||||
|
* browserSmokeRequired: bool
|
||||||
|
* }|null
|
||||||
|
*/
|
||||||
|
public static function spec193MonitoringSurface(string $className): ?array
|
||||||
|
{
|
||||||
|
return self::spec193MonitoringSurfaceInventory()[$className] ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,7 @@ public function validateComponents(array $components): ActionSurfaceValidationRe
|
|||||||
{
|
{
|
||||||
$issues = [];
|
$issues = [];
|
||||||
|
|
||||||
|
$this->validateSpec193MonitoringSurfaceInventory($issues);
|
||||||
$this->validateSpec192RecordPageInventory($issues);
|
$this->validateSpec192RecordPageInventory($issues);
|
||||||
|
|
||||||
foreach ($components as $component) {
|
foreach ($components as $component) {
|
||||||
@ -108,6 +109,146 @@ className: $component->className,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, ActionSurfaceValidationIssue> $issues
|
||||||
|
*/
|
||||||
|
private function validateSpec193MonitoringSurfaceInventory(array &$issues): void
|
||||||
|
{
|
||||||
|
$allowedClassifications = [
|
||||||
|
'remediation_required',
|
||||||
|
'minor_alignment_only',
|
||||||
|
'compliant_no_op',
|
||||||
|
'special_type_acceptable',
|
||||||
|
];
|
||||||
|
$allowedPanelScopes = ['admin', 'tenant'];
|
||||||
|
$allowedOwnerScopes = ['workspace-owned', 'workspace-visible-tenant-owned', 'tenant-owned'];
|
||||||
|
$allowedSurfaceKinds = ['queue_workbench', 'monitoring_detail', 'monitoring_landing', 'read_only_report', 'diagnostic_exception'];
|
||||||
|
$allowedPrimaryInspectModels = ['explicit_inspect_action', 'clickable_row', 'page_level_overview', 'matrix_itself', 'singleton_detail_surface'];
|
||||||
|
$allowedSharedPatterns = ['operate_hub_shell', 'cluster_entry', 'none'];
|
||||||
|
$surfaceKeys = [];
|
||||||
|
|
||||||
|
foreach (ActionSurfaceExemptions::spec193MonitoringSurfaceInventory() as $className => $surface) {
|
||||||
|
if (! class_exists($className)) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 inventory references a surface class that does not exist.',
|
||||||
|
hint: 'Keep ActionSurfaceExemptions::spec193MonitoringSurfaceInventory() aligned with the in-scope monitoring surface classes.',
|
||||||
|
);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$surfaceKey = (string) ($surface['surfaceKey'] ?? '');
|
||||||
|
|
||||||
|
if ($surfaceKey === '') {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 inventory entry is missing a non-empty surface key.',
|
||||||
|
hint: 'Provide the stable spec surface key for this monitoring surface.',
|
||||||
|
);
|
||||||
|
} elseif (isset($surfaceKeys[$surfaceKey])) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: sprintf('Spec 193 surface key "%s" is declared more than once.', $surfaceKey),
|
||||||
|
hint: 'Each in-scope monitoring surface must have a unique surface key.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$surfaceKeys[$surfaceKey] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($surface['classification'] ?? null, $allowedClassifications, true)) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 classification is invalid or missing.',
|
||||||
|
hint: 'Use remediation_required, minor_alignment_only, compliant_no_op, or special_type_acceptable.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($surface['panelScope'] ?? null, $allowedPanelScopes, true)) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 panel scope is invalid or missing.',
|
||||||
|
hint: 'Use admin or tenant for each monitoring surface inventory entry.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($surface['ownerScope'] ?? null, $allowedOwnerScopes, true)) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 owner scope is invalid or missing.',
|
||||||
|
hint: 'Use workspace-owned, workspace-visible-tenant-owned, or tenant-owned.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($surface['surfaceKind'] ?? null, $allowedSurfaceKinds, true)) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 surface kind is invalid or missing.',
|
||||||
|
hint: 'Use queue_workbench, monitoring_detail, monitoring_landing, read_only_report, or diagnostic_exception.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($surface['primaryInspectModel'] ?? null, $allowedPrimaryInspectModels, true)) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 primary inspect model is invalid or missing.',
|
||||||
|
hint: 'Use an allowed inspect model such as explicit_inspect_action, clickable_row, or page_level_overview.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! in_array($surface['sharedPattern'] ?? null, $allowedSharedPatterns, true)) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 shared pattern is invalid or missing.',
|
||||||
|
hint: 'Use operate_hub_shell, cluster_entry, or none.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($surface['canonicalNoun'] ?? null) || trim((string) $surface['canonicalNoun']) === '') {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 canonical noun must be non-empty.',
|
||||||
|
hint: 'Use the stable operator-facing noun for the monitoring surface.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$classification = (string) ($surface['classification'] ?? '');
|
||||||
|
$exceptionReason = $surface['exceptionReason'] ?? null;
|
||||||
|
|
||||||
|
if ($classification === 'special_type_acceptable') {
|
||||||
|
if (! is_string($exceptionReason) || trim($exceptionReason) === '') {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Special-type acceptable Spec 193 surfaces require an explicit exception reason.',
|
||||||
|
hint: 'Document why this surface intentionally differs from the standard monitoring hierarchy.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} elseif ($exceptionReason !== null && trim((string) $exceptionReason) !== '') {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Only special-type acceptable Spec 193 surfaces may carry an exception reason.',
|
||||||
|
hint: 'Clear the exception reason for remediation, minor-alignment, and compliant surfaces.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($classification === 'remediation_required' && ($surface['requiresHeaderRemediation'] ?? false) !== true) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Remediation-required Spec 193 surfaces must mark header remediation as required.',
|
||||||
|
hint: 'Set requiresHeaderRemediation to true for remediation_required surfaces.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($surface['requiresExplicitDeclaration'] ?? false) === true && ! method_exists($className, 'actionSurfaceDeclaration')) {
|
||||||
|
$issues[] = new ActionSurfaceValidationIssue(
|
||||||
|
className: $className,
|
||||||
|
message: 'Spec 193 surface requires an explicit action-surface declaration, but the class does not define one.',
|
||||||
|
hint: 'Add actionSurfaceDeclaration() to the page class or clear requiresExplicitDeclaration if the surface is intentionally declaration-free.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, ActionSurfaceValidationIssue> $issues
|
* @param array<int, ActionSurfaceValidationIssue> $issues
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1,4 +1,20 @@
|
|||||||
<x-filament-panels::page>
|
<x-filament-panels::page>
|
||||||
|
@php($navigationContext = \App\Support\Navigation\CanonicalNavigationContext::fromRequest(request()))
|
||||||
|
|
||||||
|
@if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null)
|
||||||
|
<x-filament::section class="mb-6">
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Return path stays quiet while this overview remains focused on alert health and downstream drilldowns.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-filament::button tag="a" color="gray" :href="$navigationContext->backLinkUrl">
|
||||||
|
{{ $navigationContext->backLinkLabel }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@endif
|
||||||
|
|
||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
|||||||
@ -14,10 +14,51 @@
|
|||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|
||||||
@if ($this->showSelectedExceptionSummary && $selectedException)
|
@if ($this->showSelectedExceptionSummary && $selectedException)
|
||||||
<x-filament::section>
|
<x-filament::section heading="Focused review lane">
|
||||||
|
<x-slot name="description">
|
||||||
|
Selection-bound decisions now define the active work lane. Scope, filters, and drilldowns stay visible without competing with the current review step.
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="grid gap-6 xl:grid-cols-[minmax(0,2fr)_minmax(22rem,26rem)]">
|
||||||
@include('filament.pages.monitoring.partials.finding-exception-queue-sidebar', [
|
@include('filament.pages.monitoring.partials.finding-exception-queue-sidebar', [
|
||||||
'selectedException' => $selectedException,
|
'selectedException' => $selectedException,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<div class="rounded-2xl border border-primary-200 bg-primary-50/80 p-4 shadow-sm dark:border-primary-500/30 dark:bg-primary-500/10">
|
||||||
|
<div class="text-sm font-semibold text-primary-900 dark:text-primary-100">
|
||||||
|
Decision lane
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 text-sm text-primary-800 dark:text-primary-200">
|
||||||
|
@if ($selectedException->isPending())
|
||||||
|
Approve exception and Reject exception are the only promoted next steps while this request remains pending.
|
||||||
|
@else
|
||||||
|
This exception is no longer decision-ready. Use the selected context group to close details or drill into related records.
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||||
|
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
|
Related drilldown
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
Open tenant detail and Open finding stay available for context, but they no longer share the same semantic lane as the review decision.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
@else
|
||||||
|
<x-filament::section heading="Quiet monitoring mode">
|
||||||
|
<x-slot name="description">
|
||||||
|
Inspect an exception to enter the focused review lane. Scope, filters, and tenant drilldowns stay secondary until one request is actively under review.
|
||||||
|
</x-slot>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
No exception is selected right now. Use Inspect exception from the queue to review one request in context.
|
||||||
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,45 @@
|
|||||||
<x-filament-panels::page>
|
<x-filament-panels::page>
|
||||||
|
@php($landingHierarchy = $this->landingHierarchySummary())
|
||||||
@php($lifecycleSummary = $this->lifecycleVisibilitySummary())
|
@php($lifecycleSummary = $this->lifecycleVisibilitySummary())
|
||||||
@php($staleAttentionTab = \App\Models\OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION)
|
@php($staleAttentionTab = \App\Models\OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION)
|
||||||
@php($terminalFollowUpTab = \App\Models\OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP)
|
@php($terminalFollowUpTab = \App\Models\OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP)
|
||||||
|
|
||||||
|
<x-filament::section heading="Monitoring landing" class="mb-6">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Tabs, filters, and row inspection define the active work lane. Scope context and return navigation stay secondary.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Scope context</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $landingHierarchy['scope_label'] }}</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $landingHierarchy['scope_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($landingHierarchy['return_label'] !== null && $landingHierarchy['return_body'] !== null)
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Return path</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $landingHierarchy['return_label'] }}</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $landingHierarchy['return_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($landingHierarchy['scope_reset_label'] !== null && $landingHierarchy['scope_reset_body'] !== null)
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Scope reset</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $landingHierarchy['scope_reset_label'] }}</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $landingHierarchy['scope_reset_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Inspect flow</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">Open run detail</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $landingHierarchy['inspect_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
<x-filament::tabs label="Operations tabs">
|
<x-filament::tabs label="Operations tabs">
|
||||||
<x-filament::tabs.item
|
<x-filament::tabs.item
|
||||||
:active="$this->activeTab === 'all'"
|
:active="$this->activeTab === 'all'"
|
||||||
@ -57,3 +94,4 @@
|
|||||||
|
|
||||||
{{ $this->table }}
|
{{ $this->table }}
|
||||||
</x-filament-panels::page>
|
</x-filament-panels::page>
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
$blockedBanner = $this->blockedExecutionBanner();
|
$blockedBanner = $this->blockedExecutionBanner();
|
||||||
$lifecycleBanner = $this->lifecycleBanner();
|
$lifecycleBanner = $this->lifecycleBanner();
|
||||||
$restoreContinuationBanner = $this->restoreContinuationBanner();
|
$restoreContinuationBanner = $this->restoreContinuationBanner();
|
||||||
|
$monitoringDetail = $this->monitoringDetailSummary();
|
||||||
$pollInterval = $this->pollInterval();
|
$pollInterval = $this->pollInterval();
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@ -10,6 +11,44 @@
|
|||||||
<div
|
<div
|
||||||
@if ($pollInterval !== null) wire:poll.{{ $pollInterval }} @endif
|
@if ($pollInterval !== null) wire:poll.{{ $pollInterval }} @endif
|
||||||
>
|
>
|
||||||
|
<x-filament::section heading="Monitoring detail" class="mb-6">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Scope context, return navigation, utility, related drilldowns, and run-specific follow-up stay in separate lanes on this viewer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Scope context</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $monitoringDetail['scope_label'] }}</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['scope_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Navigation lane</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $monitoringDetail['navigation_label'] }}</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['navigation_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Utility lane</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">Refresh</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['utility_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Related drilldown</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">Open</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['related_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Follow-up lane</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $monitoringDetail['follow_up_label'] ?? 'No follow-up action' }}</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['follow_up_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
@if ($contextBanner !== null)
|
@if ($contextBanner !== null)
|
||||||
@php
|
@php
|
||||||
$bannerClasses = match ($contextBanner['tone']) {
|
$bannerClasses = match ($contextBanner['tone']) {
|
||||||
|
|||||||
@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
||||||
|
use App\Models\Finding;
|
||||||
|
use App\Models\FindingException;
|
||||||
|
use App\Models\OperationRun;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Models\TenantMembership;
|
||||||
|
use App\Support\OperationRunOutcome;
|
||||||
|
use App\Support\OperationRunStatus;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
pest()->browser()->timeout(15_000);
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('smokes remediated, calm-reference, and explicit-exception monitoring surfaces', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||||
|
|
||||||
|
$finding = Finding::factory()->for($tenant)->create();
|
||||||
|
|
||||||
|
FindingException::query()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'finding_id' => (int) $finding->getKey(),
|
||||||
|
'requested_by_user_id' => (int) $user->getKey(),
|
||||||
|
'owner_user_id' => (int) $user->getKey(),
|
||||||
|
'status' => FindingException::STATUS_PENDING,
|
||||||
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
||||||
|
'request_reason' => 'Browser hierarchy smoke',
|
||||||
|
'requested_at' => now()->subDay(),
|
||||||
|
'review_due_at' => now()->addDay(),
|
||||||
|
'evidence_summary' => ['reference_count' => 0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => null,
|
||||||
|
'type' => 'provider.connection.check',
|
||||||
|
'status' => OperationRunStatus::Completed->value,
|
||||||
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$diagnosticsTenant = Tenant::factory()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
createUserWithTenant(
|
||||||
|
tenant: $diagnosticsTenant,
|
||||||
|
user: $user,
|
||||||
|
role: 'readonly',
|
||||||
|
workspaceRole: 'manager',
|
||||||
|
ensureDefaultMicrosoftProviderConnection: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
TenantMembership::query()
|
||||||
|
->where('tenant_id', (int) $diagnosticsTenant->getKey())
|
||||||
|
->update(['role' => 'readonly']);
|
||||||
|
|
||||||
|
$this->actingAs($user)->withSession([
|
||||||
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||||
|
]);
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
|
visit(FindingExceptionsQueue::getUrl(panel: 'admin'))
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->assertNoConsoleLogs()
|
||||||
|
->assertSee('Quiet monitoring mode');
|
||||||
|
|
||||||
|
visit(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->assertNoConsoleLogs()
|
||||||
|
->assertSee('Monitoring detail')
|
||||||
|
->assertSee('Follow-up lane');
|
||||||
|
|
||||||
|
visit('/admin/alerts')
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->assertNoConsoleLogs()
|
||||||
|
->assertSee('Alert deliveries');
|
||||||
|
|
||||||
|
visit('/admin/t/'.$diagnosticsTenant->external_id.'/diagnostics')
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->assertNoConsoleLogs()
|
||||||
|
->assertSee('Missing owner');
|
||||||
|
});
|
||||||
@ -150,6 +150,55 @@ function alertDeliveryFilterIndicatorLabels($component): array
|
|||||||
->assertCanNotSeeTableRecords([$deliveryB]);
|
->assertCanNotSeeTableRecords([$deliveryB]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps deep-linked delivery filters while surfacing origin context quietly', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
|
$workspaceId = (int) session()->get(WorkspaceContext::SESSION_KEY);
|
||||||
|
|
||||||
|
$destination = AlertDestination::factory()->create([
|
||||||
|
'workspace_id' => $workspaceId,
|
||||||
|
'is_enabled' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rule = AlertRule::factory()->create([
|
||||||
|
'workspace_id' => $workspaceId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$sentDelivery = AlertDelivery::factory()->create([
|
||||||
|
'workspace_id' => $workspaceId,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'alert_rule_id' => (int) $rule->getKey(),
|
||||||
|
'alert_destination_id' => (int) $destination->getKey(),
|
||||||
|
'status' => AlertDelivery::STATUS_SENT,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$failedDelivery = AlertDelivery::factory()->create([
|
||||||
|
'workspace_id' => $workspaceId,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'alert_rule_id' => (int) $rule->getKey(),
|
||||||
|
'alert_destination_id' => (int) $destination->getKey(),
|
||||||
|
'status' => AlertDelivery::STATUS_FAILED,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::withQueryParams([
|
||||||
|
'nav' => [
|
||||||
|
'source_surface' => 'alerts.overview',
|
||||||
|
'canonical_route_name' => 'admin.alert-deliveries.index',
|
||||||
|
'back_label' => 'Back to alerts',
|
||||||
|
'back_url' => \App\Filament\Clusters\Monitoring\AlertsCluster::getUrl(panel: 'admin'),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->actingAs($user)
|
||||||
|
->test(ListAlertDeliveries::class)
|
||||||
|
->assertActionVisible('operate_hub_back_to_origin_alert_deliveries')
|
||||||
|
->filterTable('status', AlertDelivery::STATUS_SENT)
|
||||||
|
->assertCanSeeTableRecords([$sentDelivery])
|
||||||
|
->assertCanNotSeeTableRecords([$failedDelivery]);
|
||||||
|
});
|
||||||
|
|
||||||
it('replaces the persisted tenant filter when canonical tenant context changes', function (): void {
|
it('replaces the persisted tenant filter when canonical tenant context changes', function (): void {
|
||||||
$tenantA = Tenant::factory()->create();
|
$tenantA = Tenant::factory()->create();
|
||||||
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner');
|
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner');
|
||||||
|
|||||||
@ -49,7 +49,10 @@
|
|||||||
->assertSee('Artifact truth')
|
->assertSee('Artifact truth')
|
||||||
->assertSee($tenantA->name)
|
->assertSee($tenantA->name)
|
||||||
->assertSee($tenantB->name)
|
->assertSee($tenantB->name)
|
||||||
->assertDontSee($foreignWorkspaceTenant->name);
|
->assertDontSee($foreignWorkspaceTenant->name)
|
||||||
|
->assertDontSee('Monitoring landing')
|
||||||
|
->assertDontSee('Navigation lane')
|
||||||
|
->assertDontSee('Focused review lane');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 404 for users without workspace membership on the evidence overview', function (): void {
|
it('returns 404 for users without workspace membership on the evidence overview', function (): void {
|
||||||
|
|||||||
@ -136,6 +136,26 @@ function getAlertDeliveryHeaderAction(Testable $component, string $name): ?Actio
|
|||||||
expect(getAlertDeliveryHeaderAction($component, 'view_alert_rules'))->toBeNull();
|
expect(getAlertDeliveryHeaderAction($component, 'view_alert_rules'))->toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows quiet origin navigation on alert deliveries when deep-linked from alerts overview', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
|
||||||
|
Livewire::withQueryParams([
|
||||||
|
'nav' => [
|
||||||
|
'source_surface' => 'alerts.overview',
|
||||||
|
'canonical_route_name' => 'admin.alert-deliveries.index',
|
||||||
|
'back_label' => 'Back to alerts',
|
||||||
|
'back_url' => \App\Filament\Clusters\Monitoring\AlertsCluster::getUrl(panel: 'admin'),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->actingAs($user)
|
||||||
|
->test(ListAlertDeliveries::class)
|
||||||
|
->assertActionVisible('operate_hub_back_to_origin_alert_deliveries');
|
||||||
|
});
|
||||||
|
|
||||||
it('returns 404 when a member from another workspace tries to view a delivery', function (): void {
|
it('returns 404 when a member from another workspace tries to view a delivery', function (): void {
|
||||||
[$user] = createUserWithTenant(role: 'owner');
|
[$user] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,22 @@ function auditLogPageTestRecord(?Tenant $tenant, array $attributes = []): AuditL
|
|||||||
->assertSee('Review governance, operational, and workspace-admin events in reverse chronological order');
|
->assertSee('Review governance, operational, and workspace-admin events in reverse chronological order');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps preselected audit detail subordinate to the summary-first route', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
$audit = auditLogPageTestRecord($tenant, [
|
||||||
|
'summary' => 'Preselected audit detail',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
|
->get(route('admin.monitoring.audit-log', ['event' => (int) $audit->getKey()]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Summary-first audit history')
|
||||||
|
->assertSee('Preselected audit detail')
|
||||||
|
->assertDontSee('Focused review lane');
|
||||||
|
});
|
||||||
|
|
||||||
it('loads the audit page with populated filter options', function (): void {
|
it('loads the audit page with populated filter options', function (): void {
|
||||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
|||||||
@ -61,6 +61,8 @@
|
|||||||
Livewire::test(BaselineCompareLanding::class)
|
Livewire::test(BaselineCompareLanding::class)
|
||||||
->assertActionVisible('compareNow')
|
->assertActionVisible('compareNow')
|
||||||
->assertActionDisabled('compareNow')
|
->assertActionDisabled('compareNow')
|
||||||
|
->assertDontSee('Monitoring landing')
|
||||||
|
->assertDontSee('Navigation lane')
|
||||||
->callAction('compareNow')
|
->callAction('compareNow')
|
||||||
->assertStatus(200);
|
->assertStatus(200);
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,8 @@
|
|||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Visible-set baseline')
|
->assertSee('Visible-set baseline')
|
||||||
->assertSee('Requested: Auto mode. Resolved: Dense mode.')
|
->assertSee('Requested: Auto mode. Resolved: Dense mode.')
|
||||||
|
->assertDontSee('Monitoring landing')
|
||||||
|
->assertDontSee('Focused review lane')
|
||||||
->assertDontSee('fonts/filament/filament/inter/inter-latin-wght-normal', false)
|
->assertDontSee('fonts/filament/filament/inter/inter-latin-wght-normal', false)
|
||||||
->assertDontSee('Passive auto-refresh every 5 seconds')
|
->assertDontSee('Passive auto-refresh every 5 seconds')
|
||||||
->assertSee('Grouped legend')
|
->assertSee('Grouped legend')
|
||||||
|
|||||||
@ -16,6 +16,19 @@
|
|||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
describe('Tenant diagnostics repairs', function () {
|
describe('Tenant diagnostics repairs', function () {
|
||||||
|
it('hides repair actions when no defect is present', function () {
|
||||||
|
[$owner, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
$this->actingAs($owner);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
Livewire::test(TenantDiagnostics::class)
|
||||||
|
->assertSee('All good')
|
||||||
|
->assertActionHidden('bootstrapOwner')
|
||||||
|
->assertActionHidden('mergeDuplicateMemberships');
|
||||||
|
});
|
||||||
|
|
||||||
it('allows an authorized member to bootstrap an owner when a tenant has no owners', function () {
|
it('allows an authorized member to bootstrap an owner when a tenant has no owners', function () {
|
||||||
[$manager, $tenant] = createUserWithTenant(role: 'manager');
|
[$manager, $tenant] = createUserWithTenant(role: 'manager');
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Filament\Pages\InventoryCoverage;
|
use App\Filament\Pages\InventoryCoverage;
|
||||||
|
use App\Filament\Pages\BaselineCompareLanding;
|
||||||
|
use App\Filament\Pages\BaselineCompareMatrix;
|
||||||
use App\Filament\Pages\Monitoring\Alerts;
|
use App\Filament\Pages\Monitoring\Alerts;
|
||||||
use App\Filament\Pages\Monitoring\AuditLog as AuditLogPage;
|
use App\Filament\Pages\Monitoring\AuditLog as AuditLogPage;
|
||||||
use App\Filament\Pages\Monitoring\EvidenceOverview;
|
use App\Filament\Pages\Monitoring\EvidenceOverview;
|
||||||
@ -817,9 +819,8 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
|
|||||||
expect(method_exists(TenantRequiredPermissions::class, 'actionSurfaceDeclaration'))->toBeTrue()
|
expect(method_exists(TenantRequiredPermissions::class, 'actionSurfaceDeclaration'))->toBeTrue()
|
||||||
->and($baselineExemptions->hasClass(TenantRequiredPermissions::class))->toBeFalse();
|
->and($baselineExemptions->hasClass(TenantRequiredPermissions::class))->toBeFalse();
|
||||||
|
|
||||||
expect(method_exists(Alerts::class, 'actionSurfaceDeclaration'))->toBeFalse()
|
expect(method_exists(Alerts::class, 'actionSurfaceDeclaration'))->toBeTrue()
|
||||||
->and($baselineExemptions->hasClass(Alerts::class))->toBeTrue()
|
->and($baselineExemptions->hasClass(Alerts::class))->toBeFalse();
|
||||||
->and((string) $baselineExemptions->reasonForClass(Alerts::class))->toContain('cluster entry');
|
|
||||||
|
|
||||||
expect($baselineExemptions->hasClass(ManagedTenantOnboardingWizard::class))->toBeTrue()
|
expect($baselineExemptions->hasClass(ManagedTenantOnboardingWizard::class))->toBeTrue()
|
||||||
->and((string) $baselineExemptions->reasonForClass(ManagedTenantOnboardingWizard::class))->toContain('dedicated conformance tests')
|
->and((string) $baselineExemptions->reasonForClass(ManagedTenantOnboardingWizard::class))->toContain('dedicated conformance tests')
|
||||||
@ -2021,6 +2022,146 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('documents the spec 193 monitoring hierarchy inventory and explicit exception', function (): void {
|
||||||
|
$inventory = ActionSurfaceExemptions::spec193MonitoringSurfaceInventory();
|
||||||
|
$baselineExemptions = ActionSurfaceExemptions::baseline();
|
||||||
|
|
||||||
|
$remediationRequired = collect($inventory)
|
||||||
|
->filter(fn (array $surface): bool => $surface['classification'] === 'remediation_required')
|
||||||
|
->keys()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
$calmReferences = collect($inventory)
|
||||||
|
->filter(fn (array $surface): bool => $surface['classification'] === 'compliant_no_op')
|
||||||
|
->keys()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
expect(array_keys($inventory))->toEqualCanonicalizing([
|
||||||
|
FindingExceptionsQueue::class,
|
||||||
|
TenantlessOperationRunViewer::class,
|
||||||
|
Operations::class,
|
||||||
|
Alerts::class,
|
||||||
|
AuditLogPage::class,
|
||||||
|
ListAlertDeliveries::class,
|
||||||
|
EvidenceOverview::class,
|
||||||
|
BaselineCompareLanding::class,
|
||||||
|
BaselineCompareMatrix::class,
|
||||||
|
ReviewRegister::class,
|
||||||
|
TenantDiagnostics::class,
|
||||||
|
])
|
||||||
|
->and($baselineExemptions->hasClass(Alerts::class))->toBeFalse()
|
||||||
|
->and(method_exists(Alerts::class, 'actionSurfaceDeclaration'))->toBeTrue()
|
||||||
|
->and($remediationRequired)->toEqualCanonicalizing([
|
||||||
|
FindingExceptionsQueue::class,
|
||||||
|
TenantlessOperationRunViewer::class,
|
||||||
|
Operations::class,
|
||||||
|
])
|
||||||
|
->and($calmReferences)->toEqualCanonicalizing([
|
||||||
|
EvidenceOverview::class,
|
||||||
|
BaselineCompareLanding::class,
|
||||||
|
BaselineCompareMatrix::class,
|
||||||
|
ReviewRegister::class,
|
||||||
|
])
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(TenantDiagnostics::class)['classification'] ?? null)->toBe('special_type_acceptable')
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(TenantDiagnostics::class)['exceptionReason'] ?? null)->toContain('diagnostic');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps spec 193 hierarchy work from expanding confirmation, reason capture, or compare-start semantics', function (): void {
|
||||||
|
[$approver, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||||
|
|
||||||
|
$mountedActionFieldNames = static function (mixed $component): array {
|
||||||
|
$method = new \ReflectionMethod($component->instance(), 'getMountedActionForm');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
$form = $method->invoke($component->instance());
|
||||||
|
|
||||||
|
return collect($form?->getFlatFields(withHidden: true) ?? [])
|
||||||
|
->map(static fn (mixed $field): ?string => method_exists($field, 'getName') ? $field->getName() : null)
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
};
|
||||||
|
|
||||||
|
$finding = Finding::factory()->for($tenant)->create();
|
||||||
|
|
||||||
|
$exception = FindingException::query()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'finding_id' => (int) $finding->getKey(),
|
||||||
|
'requested_by_user_id' => (int) $approver->getKey(),
|
||||||
|
'owner_user_id' => (int) $approver->getKey(),
|
||||||
|
'status' => FindingException::STATUS_PENDING,
|
||||||
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
||||||
|
'request_reason' => 'Guarded spec 193 review',
|
||||||
|
'requested_at' => now()->subDay(),
|
||||||
|
'review_due_at' => now()->addDay(),
|
||||||
|
'evidence_summary' => ['reference_count' => 0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($approver);
|
||||||
|
setAdminPanelContext();
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
|
||||||
|
$approveComponent = Livewire::withQueryParams([
|
||||||
|
'exception' => (int) $exception->getKey(),
|
||||||
|
])
|
||||||
|
->actingAs($approver)
|
||||||
|
->test(FindingExceptionsQueue::class)
|
||||||
|
->assertActionExists('approve_selected_exception', function (Action $action): bool {
|
||||||
|
return $action->isConfirmationRequired();
|
||||||
|
})
|
||||||
|
->mountAction('approve_selected_exception');
|
||||||
|
|
||||||
|
expect($mountedActionFieldNames($approveComponent))->toBe([
|
||||||
|
'effective_from',
|
||||||
|
'expires_at',
|
||||||
|
'approval_reason',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$rejectComponent = Livewire::withQueryParams([
|
||||||
|
'exception' => (int) $exception->getKey(),
|
||||||
|
])
|
||||||
|
->actingAs($approver)
|
||||||
|
->test(FindingExceptionsQueue::class)
|
||||||
|
->assertActionExists('reject_selected_exception', function (Action $action): bool {
|
||||||
|
return $action->isConfirmationRequired();
|
||||||
|
})
|
||||||
|
->mountAction('reject_selected_exception');
|
||||||
|
|
||||||
|
expect($mountedActionFieldNames($rejectComponent))->toBe([
|
||||||
|
'rejection_reason',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
$profile = BaselineProfile::factory()->active()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'capture_mode' => \App\Support\Baselines\BaselineCaptureMode::FullContent->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$snapshot = BaselineSnapshot::factory()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'baseline_profile_id' => (int) $profile->getKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
|
||||||
|
|
||||||
|
BaselineTenantAssignment::factory()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'baseline_profile_id' => (int) $profile->getKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::actingAs($approver)
|
||||||
|
->test(BaselineCompareLanding::class)
|
||||||
|
->assertActionExists('compareNow', function (Action $action): bool {
|
||||||
|
return $action->isConfirmationRequired()
|
||||||
|
&& $action->getModalDescription() === 'This will refresh content evidence on demand (redacted) before comparing the current tenant inventory against the assigned baseline snapshot.';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps spec 192 remediated pages out of the enterprise-detail layout rollout', function (): void {
|
it('keeps spec 192 remediated pages out of the enterprise-detail layout rollout', function (): void {
|
||||||
foreach ([
|
foreach ([
|
||||||
\App\Filament\Resources\BaselineProfileResource::class,
|
\App\Filament\Resources\BaselineProfileResource::class,
|
||||||
|
|||||||
@ -237,3 +237,11 @@ className: $className,
|
|||||||
|
|
||||||
expect($result->hasIssues())->toBeFalse($result->formatForAssertion());
|
expect($result->hasIssues())->toBeFalse($result->formatForAssertion());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('accepts the repository spec 193 monitoring inventory even when only inventory validation runs', function (): void {
|
||||||
|
$validator = ActionSurfaceValidator::withBaselineExemptions();
|
||||||
|
|
||||||
|
$result = $validator->validateComponents([]);
|
||||||
|
|
||||||
|
expect($result->hasIssues())->toBeFalse($result->formatForAssertion());
|
||||||
|
});
|
||||||
|
|||||||
@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\BaselineCompareLanding;
|
||||||
|
use App\Filament\Pages\BaselineCompareMatrix;
|
||||||
|
use App\Filament\Pages\Monitoring\Alerts;
|
||||||
|
use App\Filament\Pages\Monitoring\AuditLog as AuditLogPage;
|
||||||
|
use App\Filament\Pages\Monitoring\EvidenceOverview;
|
||||||
|
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
||||||
|
use App\Filament\Pages\Monitoring\Operations;
|
||||||
|
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
||||||
|
use App\Filament\Pages\Reviews\ReviewRegister;
|
||||||
|
use App\Filament\Pages\TenantDiagnostics;
|
||||||
|
use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries;
|
||||||
|
use App\Support\Ui\ActionSurface\ActionSurfaceExemptions;
|
||||||
|
use App\Support\Ui\ActionSurface\ActionSurfaceValidator;
|
||||||
|
|
||||||
|
it('keeps the spec 193 monitoring inventory complete and explicitly classified', function (): void {
|
||||||
|
$inventory = ActionSurfaceExemptions::spec193MonitoringSurfaceInventory();
|
||||||
|
|
||||||
|
expect(array_keys($inventory))->toEqualCanonicalizing([
|
||||||
|
FindingExceptionsQueue::class,
|
||||||
|
TenantlessOperationRunViewer::class,
|
||||||
|
Operations::class,
|
||||||
|
Alerts::class,
|
||||||
|
AuditLogPage::class,
|
||||||
|
ListAlertDeliveries::class,
|
||||||
|
EvidenceOverview::class,
|
||||||
|
BaselineCompareLanding::class,
|
||||||
|
BaselineCompareMatrix::class,
|
||||||
|
ReviewRegister::class,
|
||||||
|
TenantDiagnostics::class,
|
||||||
|
])
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(FindingExceptionsQueue::class)['classification'] ?? null)->toBe('remediation_required')
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(FindingExceptionsQueue::class)['surfaceKind'] ?? null)->toBe('queue_workbench')
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(FindingExceptionsQueue::class)['primaryInspectModel'] ?? null)->toBe('explicit_inspect_action')
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(Alerts::class)['classification'] ?? null)->toBe('minor_alignment_only')
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(EvidenceOverview::class)['classification'] ?? null)->toBe('compliant_no_op')
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(TenantDiagnostics::class)['classification'] ?? null)->toBe('special_type_acceptable');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps tenant diagnostics as the only explicit spec 193 exception surface', function (): void {
|
||||||
|
$inventory = ActionSurfaceExemptions::spec193MonitoringSurfaceInventory();
|
||||||
|
|
||||||
|
$exceptionPages = collect($inventory)
|
||||||
|
->filter(fn (array $surface): bool => $surface['classification'] === 'special_type_acceptable')
|
||||||
|
->keys()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
expect($exceptionPages)->toBe([
|
||||||
|
TenantDiagnostics::class,
|
||||||
|
])
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(TenantDiagnostics::class)['exceptionReason'] ?? null)->toContain('diagnostic')
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(TenantDiagnostics::class)['surfaceKind'] ?? null)->toBe('diagnostic_exception')
|
||||||
|
->and(ActionSurfaceExemptions::spec193MonitoringSurface(TenantDiagnostics::class)['primaryInspectModel'] ?? null)->toBe('singleton_detail_surface')
|
||||||
|
->and(collect($inventory)
|
||||||
|
->except([TenantDiagnostics::class])
|
||||||
|
->every(fn (array $surface): bool => ($surface['exceptionReason'] ?? null) === null))->toBeTrue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the spec 193 monitoring inventory valid inside the action-surface validator', function (): void {
|
||||||
|
$result = ActionSurfaceValidator::withBaselineExemptions()->validateComponents([]);
|
||||||
|
|
||||||
|
expect($result->hasIssues())->toBeFalse($result->formatForAssertion());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps spec 193 monitoring surfaces out of record-page header layouts', function (): void {
|
||||||
|
foreach (array_keys(ActionSurfaceExemptions::spec193MonitoringSurfaceInventory()) as $className) {
|
||||||
|
$source = file_get_contents((string) (new \ReflectionClass($className))->getFileName()) ?: '';
|
||||||
|
|
||||||
|
expect($source)
|
||||||
|
->not->toContain('EnterpriseDetail')
|
||||||
|
->not->toContain('enterprise-detail/header');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\Monitoring\Alerts;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('keeps alerts as a quiet overview with downstream drilldown entry points', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
|
->followingRedirects()
|
||||||
|
->get('/admin/alerts')
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Alert targets')
|
||||||
|
->assertSee('Alert rules')
|
||||||
|
->assertSee('Alert deliveries')
|
||||||
|
->assertDontSee('Focused review lane')
|
||||||
|
->assertDontSee('Follow-up lane');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('surfaces origin context quietly on the alerts overview', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
setAdminPanelContext();
|
||||||
|
|
||||||
|
Livewire::withQueryParams([
|
||||||
|
'nav' => [
|
||||||
|
'source_surface' => 'backup_set.detail_section',
|
||||||
|
'canonical_route_name' => 'admin.alerts.overview',
|
||||||
|
'back_label' => 'Back to backup set',
|
||||||
|
'back_url' => '/admin/tenant/backup-sets/1',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->actingAs($user)
|
||||||
|
->test(Alerts::class)
|
||||||
|
->assertSee('Back to backup set')
|
||||||
|
->assertSee('/admin/tenant/backup-sets/1', false);
|
||||||
|
});
|
||||||
@ -165,3 +165,39 @@
|
|||||||
->assertDontSee('Close details')
|
->assertDontSee('Close details')
|
||||||
->assertDontSee('Open operation');
|
->assertDontSee('Open operation');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('surfaces origin context quietly when deep-linked to a selected audit event', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
$audit = AuditLog::query()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'actor_email' => 'owner@example.com',
|
||||||
|
'actor_name' => 'Owner',
|
||||||
|
'actor_type' => 'human',
|
||||||
|
'action' => 'workspace.selected',
|
||||||
|
'status' => 'success',
|
||||||
|
'resource_type' => 'workspace',
|
||||||
|
'resource_id' => (string) $tenant->workspace_id,
|
||||||
|
'target_label' => 'Workspace 1',
|
||||||
|
'summary' => 'Workspace selected for Workspace 1',
|
||||||
|
'recorded_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
|
Livewire::withQueryParams([
|
||||||
|
'event' => (int) $audit->getKey(),
|
||||||
|
'nav' => [
|
||||||
|
'source_surface' => 'alerts.overview',
|
||||||
|
'canonical_route_name' => 'admin.monitoring.audit-log',
|
||||||
|
'back_label' => 'Back to alerts',
|
||||||
|
'back_url' => '/admin/alerts',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->actingAs($user)
|
||||||
|
->test(AuditLogPage::class)
|
||||||
|
->assertSet('selectedAuditLogId', (int) $audit->getKey())
|
||||||
|
->assertActionVisible('operate_hub_back_to_origin_audit_log');
|
||||||
|
});
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
||||||
|
use App\Models\Finding;
|
||||||
|
use App\Models\FindingException;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('renders a quiet monitoring state when no exception is selected', function (): void {
|
||||||
|
[$approver, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||||
|
|
||||||
|
$finding = Finding::factory()->for($tenant)->create();
|
||||||
|
|
||||||
|
FindingException::query()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'finding_id' => (int) $finding->getKey(),
|
||||||
|
'requested_by_user_id' => (int) $approver->getKey(),
|
||||||
|
'owner_user_id' => (int) $approver->getKey(),
|
||||||
|
'status' => FindingException::STATUS_PENDING,
|
||||||
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
||||||
|
'request_reason' => 'Queue hierarchy review lane',
|
||||||
|
'requested_at' => now()->subDay(),
|
||||||
|
'review_due_at' => now()->addDay(),
|
||||||
|
'evidence_summary' => ['reference_count' => 0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($approver);
|
||||||
|
setAdminPanelContext();
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
|
||||||
|
Livewire::test(FindingExceptionsQueue::class)
|
||||||
|
->assertSee('Quiet monitoring mode')
|
||||||
|
->assertSee('Inspect an exception to enter the focused review lane.')
|
||||||
|
->assertDontSee('Focused review lane')
|
||||||
|
->assertActionHidden('approve_selected_exception')
|
||||||
|
->assertActionHidden('reject_selected_exception');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a focused review lane when a pending exception is selected', function (): void {
|
||||||
|
[$approver, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||||
|
|
||||||
|
$finding = Finding::factory()->for($tenant)->create();
|
||||||
|
|
||||||
|
$exception = FindingException::query()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'finding_id' => (int) $finding->getKey(),
|
||||||
|
'requested_by_user_id' => (int) $approver->getKey(),
|
||||||
|
'owner_user_id' => (int) $approver->getKey(),
|
||||||
|
'status' => FindingException::STATUS_PENDING,
|
||||||
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
||||||
|
'request_reason' => 'Focused review lane request',
|
||||||
|
'requested_at' => now()->subDay(),
|
||||||
|
'review_due_at' => now()->addDay(),
|
||||||
|
'evidence_summary' => ['reference_count' => 0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($approver);
|
||||||
|
setAdminPanelContext();
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
|
||||||
|
Livewire::withQueryParams([
|
||||||
|
'exception' => (int) $exception->getKey(),
|
||||||
|
])
|
||||||
|
->test(FindingExceptionsQueue::class)
|
||||||
|
->assertSee('Focused review lane')
|
||||||
|
->assertSee('Decision lane')
|
||||||
|
->assertSee('Related drilldown')
|
||||||
|
->assertDontSee('Quiet monitoring mode')
|
||||||
|
->assertActionVisible('approve_selected_exception')
|
||||||
|
->assertActionVisible('reject_selected_exception');
|
||||||
|
});
|
||||||
@ -202,6 +202,8 @@
|
|||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get(OperationRunLinks::index($tenant, $context))
|
->get(OperationRunLinks::index($tenant, $context))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
|
->assertSee('Monitoring landing')
|
||||||
|
->assertSee('Return path')
|
||||||
->assertSee('Back to backup set')
|
->assertSee('Back to backup set')
|
||||||
->assertSee('/admin/tenant/backup-sets/1', false);
|
->assertSee('/admin/tenant/backup-sets/1', false);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\OperationRun;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Support\Navigation\CanonicalNavigationContext;
|
||||||
|
use App\Support\OperationRunLinks;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('renders the operations landing as a quiet monitoring surface', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
|
||||||
|
|
||||||
|
OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'inventory_sync',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
|
->get(route('admin.operations.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Monitoring landing')
|
||||||
|
->assertSee('Tabs, filters, and row inspection define the active work lane.')
|
||||||
|
->assertSee('Scope context')
|
||||||
|
->assertSee('Scope reset')
|
||||||
|
->assertSee('Inspect flow')
|
||||||
|
->assertSee('Show all tenants');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('surfaces canonical return context separately from the operations work lane', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner');
|
||||||
|
|
||||||
|
$context = new CanonicalNavigationContext(
|
||||||
|
sourceSurface: 'backup_set.detail_section',
|
||||||
|
canonicalRouteName: 'admin.operations.index',
|
||||||
|
tenantId: (int) $tenant->getKey(),
|
||||||
|
backLinkLabel: 'Back to backup set',
|
||||||
|
backLinkUrl: '/admin/tenant/backup-sets/1',
|
||||||
|
);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
|
->get(OperationRunLinks::index($tenant, $context))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Monitoring landing')
|
||||||
|
->assertSee('Return path')
|
||||||
|
->assertSee('Back to backup set')
|
||||||
|
->assertSee('/admin/tenant/backup-sets/1', false)
|
||||||
|
->assertSee('Inspect flow');
|
||||||
|
});
|
||||||
@ -37,6 +37,8 @@
|
|||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get(OperationRunLinks::tenantlessView($run, $context))
|
->get(OperationRunLinks::tenantlessView($run, $context))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
|
->assertSee('Monitoring detail')
|
||||||
|
->assertSee('Related drilldown')
|
||||||
->assertSee('Back to backup set')
|
->assertSee('Back to backup set')
|
||||||
->assertSee(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant), false)
|
->assertSee(BackupSetResource::getUrl('view', ['record' => $backupSet], tenant: $tenant), false)
|
||||||
->assertSee('Related context')
|
->assertSee('Related context')
|
||||||
|
|||||||
@ -2,6 +2,9 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
||||||
|
use App\Models\BaselineProfile;
|
||||||
|
use App\Models\BaselineSnapshot;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
@ -559,6 +562,117 @@
|
|||||||
->assertActionVisible('operate_hub_back_to_origin_run_detail');
|
->assertActionVisible('operate_hub_back_to_origin_run_detail');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders the canonical tenantless viewer as a layered monitoring detail surface', 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()->forget(WorkspaceContext::SESSION_KEY);
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'tenant_id' => null,
|
||||||
|
'type' => 'provider.connection.check',
|
||||||
|
'status' => OperationRunStatus::Completed->value,
|
||||||
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Monitoring detail')
|
||||||
|
->assertSee('Navigation lane')
|
||||||
|
->assertSee('Utility lane')
|
||||||
|
->assertSee('Related drilldown')
|
||||||
|
->assertSee('Follow-up lane')
|
||||||
|
->assertSee('Refresh keeps the current run state accurate without changing scope.')
|
||||||
|
->assertSee('No run-specific follow-up is currently available.')
|
||||||
|
->assertDontSee('Resume capture');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('surfaces resumable follow-up separately from navigation and drilldown lanes', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'baseline_compare',
|
||||||
|
'status' => OperationRunStatus::Completed->value,
|
||||||
|
'outcome' => OperationRunOutcome::PartiallySucceeded->value,
|
||||||
|
'context' => [
|
||||||
|
'baseline_compare' => [
|
||||||
|
'resume_token' => 'resume-spec-193',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([
|
||||||
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||||
|
])
|
||||||
|
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Monitoring detail')
|
||||||
|
->assertSee('Follow-up lane')
|
||||||
|
->assertSee('Resume capture')
|
||||||
|
->assertSee('Resume capture only appears when this run supports additional evidence collection.')
|
||||||
|
->assertSee('Related drilldown');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps operations-list navigation out of the related drilldown lane', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$profile = BaselineProfile::factory()->active()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$snapshot = BaselineSnapshot::factory()->complete()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'baseline_profile_id' => (int) $profile->getKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'baseline_compare',
|
||||||
|
'status' => OperationRunStatus::Completed->value,
|
||||||
|
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||||
|
'context' => [
|
||||||
|
'baseline_profile_id' => (int) $profile->getKey(),
|
||||||
|
'baseline_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
Livewire::test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||||
|
->assertActionVisible('view_baseline_profile')
|
||||||
|
->assertActionVisible('view_snapshot')
|
||||||
|
->assertActionDoesNotExist('operations')
|
||||||
|
->assertActionDoesNotExist('open_operations');
|
||||||
|
|
||||||
|
$this
|
||||||
|
->withSession([
|
||||||
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||||
|
])
|
||||||
|
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Monitoring detail')
|
||||||
|
->assertSee('Open keeps secondary drilldowns grouped under one control: View baseline profile, View snapshot.');
|
||||||
|
});
|
||||||
|
|
||||||
it('renders shared polling markup for active tenantless runs', function (string $status, int $ageSeconds): void {
|
it('renders shared polling markup for active tenantless runs', function (string $status, int $ageSeconds): void {
|
||||||
$workspace = Workspace::factory()->create();
|
$workspace = Workspace::factory()->create();
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|||||||
@ -170,6 +170,37 @@
|
|||||||
->assertDontSee('Show all operations');
|
->assertDontSee('Show all operations');
|
||||||
})->group('ops-ux');
|
})->group('ops-ux');
|
||||||
|
|
||||||
|
it('renders shared scope and return copy as secondary monitoring context on operations surfaces', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'policy.sync',
|
||||||
|
'status' => 'queued',
|
||||||
|
'outcome' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
$this->withSession([
|
||||||
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||||
|
])->get(route('admin.operations.index'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Monitoring landing')
|
||||||
|
->assertSee('Scope context')
|
||||||
|
->assertSee('Scope reset');
|
||||||
|
|
||||||
|
$this->withSession([
|
||||||
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||||
|
])->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Monitoring detail')
|
||||||
|
->assertSee('Navigation lane')
|
||||||
|
->assertSee('Follow-up lane');
|
||||||
|
})->group('ops-ux');
|
||||||
|
|
||||||
it('returns 404 for non-member workspace access on /admin/operations', function (): void {
|
it('returns 404 for non-member workspace access on /admin/operations', function (): void {
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
$workspace = Workspace::factory()->create();
|
$workspace = Workspace::factory()->create();
|
||||||
|
|||||||
@ -2,11 +2,16 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
|
||||||
|
use App\Models\Finding;
|
||||||
|
use App\Models\FindingException;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
use App\Models\WorkspaceMembership;
|
use App\Models\WorkspaceMembership;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Livewire\Livewire;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
|
||||||
it('returns 404 for non-members on representative action-surface route', function (): void {
|
it('returns 404 for non-members on representative action-surface route', function (): void {
|
||||||
@ -38,3 +43,45 @@
|
|||||||
->get(route('admin.operations.view', ['run' => (int) $runB->getKey()]))
|
->get(route('admin.operations.view', ['run' => (int) $runB->getKey()]))
|
||||||
->assertNotFound();
|
->assertNotFound();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps queue approval and rejection actions behind the approval capability', function (): void {
|
||||||
|
[$approver, $tenant] = createUserWithTenant(role: 'owner', workspaceRole: 'manager');
|
||||||
|
|
||||||
|
$readonly = User::factory()->create();
|
||||||
|
createUserWithTenant(tenant: $tenant, user: $readonly, role: 'readonly', workspaceRole: 'readonly');
|
||||||
|
|
||||||
|
$finding = Finding::factory()->for($tenant)->create();
|
||||||
|
|
||||||
|
$exception = FindingException::query()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'finding_id' => (int) $finding->getKey(),
|
||||||
|
'requested_by_user_id' => (int) $approver->getKey(),
|
||||||
|
'owner_user_id' => (int) $approver->getKey(),
|
||||||
|
'status' => FindingException::STATUS_PENDING,
|
||||||
|
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
||||||
|
'request_reason' => 'Authorization continuity test',
|
||||||
|
'requested_at' => now()->subDay(),
|
||||||
|
'review_due_at' => now()->addDay(),
|
||||||
|
'evidence_summary' => ['reference_count' => 0],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($approver);
|
||||||
|
Filament::setCurrentPanel('admin');
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
Filament::bootCurrentPanel();
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
|
||||||
|
Livewire::withQueryParams([
|
||||||
|
'exception' => (int) $exception->getKey(),
|
||||||
|
])
|
||||||
|
->actingAs($approver)
|
||||||
|
->test(FindingExceptionsQueue::class)
|
||||||
|
->assertActionVisible('approve_selected_exception')
|
||||||
|
->assertActionVisible('reject_selected_exception');
|
||||||
|
|
||||||
|
$this->actingAs($readonly)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
|
->get(FindingExceptionsQueue::getUrl(panel: 'admin'))
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|||||||
@ -1,8 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Pages\TenantDiagnostics;
|
||||||
|
use App\Models\TenantMembership;
|
||||||
|
use App\Support\Rbac\UiTooltips;
|
||||||
|
use Filament\Actions\Action;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
@ -22,3 +28,21 @@
|
|||||||
->get("/admin/t/{$tenant->external_id}/diagnostics")
|
->get("/admin/t/{$tenant->external_id}/diagnostics")
|
||||||
->assertNotFound();
|
->assertNotFound();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows disabled repair affordances to readonly members when a defect exists', function () {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
TenantMembership::query()
|
||||||
|
->where('tenant_id', (int) $tenant->getKey())
|
||||||
|
->update(['role' => 'readonly']);
|
||||||
|
|
||||||
|
Livewire::test(TenantDiagnostics::class)
|
||||||
|
->assertActionVisible('bootstrapOwner')
|
||||||
|
->assertActionDisabled('bootstrapOwner')
|
||||||
|
->assertActionExists('bootstrapOwner', function (Action $action): bool {
|
||||||
|
return $action->getTooltip() === UiTooltips::INSUFFICIENT_PERMISSION;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -41,6 +41,8 @@
|
|||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(ReviewRegister::class)
|
->test(ReviewRegister::class)
|
||||||
->assertSee('Artifact truth')
|
->assertSee('Artifact truth')
|
||||||
|
->assertDontSee('Monitoring landing')
|
||||||
|
->assertDontSee('Navigation lane')
|
||||||
->assertCanSeeTableRecords([$reviewA, $reviewB])
|
->assertCanSeeTableRecords([$reviewA, $reviewB])
|
||||||
->assertCanNotSeeTableRecords([$reviewC])
|
->assertCanNotSeeTableRecords([$reviewC])
|
||||||
->filterTable('tenant_id', (string) $tenantB->getKey())
|
->filterTable('tenant_id', (string) $tenantB->getKey())
|
||||||
@ -65,6 +67,42 @@
|
|||||||
->assertSee('Clear filters');
|
->assertSee('Clear filters');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('clears the remembered tenant prefilter from the review register', function (): void {
|
||||||
|
$tenantA = Tenant::factory()->create(['name' => 'Alpha Tenant']);
|
||||||
|
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner');
|
||||||
|
|
||||||
|
$tenantB = Tenant::factory()->create([
|
||||||
|
'workspace_id' => (int) $tenantA->workspace_id,
|
||||||
|
'name' => 'Beta Tenant',
|
||||||
|
]);
|
||||||
|
createUserWithTenant(tenant: $tenantB, user: $user, role: 'owner');
|
||||||
|
|
||||||
|
$reviewA = composeTenantReviewForTest($tenantA, $user);
|
||||||
|
$reviewB = composeTenantReviewForTest($tenantB, $user);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
setAdminPanelContext();
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id);
|
||||||
|
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
|
||||||
|
(string) $tenantA->workspace_id => (int) $tenantA->getKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = Livewire::actingAs($user)
|
||||||
|
->test(ReviewRegister::class)
|
||||||
|
->assertActionVisible('clear_filters')
|
||||||
|
->assertCanSeeTableRecords([$reviewA])
|
||||||
|
->assertCanNotSeeTableRecords([$reviewB]);
|
||||||
|
|
||||||
|
expect(app(WorkspaceContext::class)->lastTenantId())->toBe((int) $tenantA->getKey());
|
||||||
|
|
||||||
|
$component
|
||||||
|
->callAction('clear_filters')
|
||||||
|
->assertActionHidden('clear_filters')
|
||||||
|
->assertCanSeeTableRecords([$reviewA, $reviewB]);
|
||||||
|
|
||||||
|
expect(app(WorkspaceContext::class)->lastTenantId())->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it('keeps stale and partial review rows aligned with tenant review detail trust', function (): void {
|
it('keeps stale and partial review rows aligned with tenant review detail trust', function (): void {
|
||||||
$staleTenant = Tenant::factory()->create(['name' => 'Stale Tenant']);
|
$staleTenant = Tenant::factory()->create(['name' => 'Stale Tenant']);
|
||||||
[$user, $staleTenant] = createUserWithTenant(tenant: $staleTenant, role: 'owner');
|
[$user, $staleTenant] = createUserWithTenant(tenant: $staleTenant, role: 'owner');
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
# Specification Quality Checklist: Monitoring Surface Action Hierarchy and Workbench Semantics
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-04-11
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Validation completed in one pass.
|
||||||
|
- No clarification markers remain in the specification.
|
||||||
|
- Required operator-surface contract and UI action matrix sections are present and bounded to action hierarchy semantics rather than implementation design.
|
||||||
@ -0,0 +1,318 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Monitoring Surface Action Hierarchy Internal Contract
|
||||||
|
version: 0.1.0
|
||||||
|
summary: Internal logical contract for Spec 193 monitoring and workbench surface hierarchy
|
||||||
|
description: |
|
||||||
|
This contract is an internal planning artifact for Spec 193. The affected
|
||||||
|
surfaces continue to render HTML through Filament and Livewire. The schemas
|
||||||
|
below define the bounded render contract and regression expectations for
|
||||||
|
monitoring/workbench action layers, selection-aware prominence, calm
|
||||||
|
bounded-scope references, and the explicit diagnostic exception.
|
||||||
|
servers:
|
||||||
|
- url: /internal
|
||||||
|
x-monitoring-action-hierarchy-consumers:
|
||||||
|
- surface: remediation-required-workbench-pages
|
||||||
|
sourceFiles:
|
||||||
|
- apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php
|
||||||
|
- apps/platform/app/Filament/Pages/Monitoring/Operations.php
|
||||||
|
- apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php
|
||||||
|
mustRender:
|
||||||
|
- explicit_action_layers
|
||||||
|
- quiet_scope_and_navigation_layers
|
||||||
|
- selection_or_focus_actions_only_when_active
|
||||||
|
- no_selection_quiet_state_when_applicable
|
||||||
|
mustNotRender:
|
||||||
|
- flat_scope_navigation_selection_strip
|
||||||
|
- scope_as_peer_cta
|
||||||
|
- mixed_global_and_selection_actions_in_one_lane
|
||||||
|
- surface: shared-pattern-audit-pages
|
||||||
|
sourceFiles:
|
||||||
|
- apps/platform/app/Filament/Pages/Monitoring/Alerts.php
|
||||||
|
- apps/platform/app/Filament/Pages/Monitoring/AuditLog.php
|
||||||
|
- apps/platform/app/Filament/Resources/AlertDeliveryResource/Pages/ListAlertDeliveries.php
|
||||||
|
mustRender:
|
||||||
|
- explicit_inventory_classification
|
||||||
|
- quiet_operate_hub_scope_usage
|
||||||
|
mustNotRender:
|
||||||
|
- undocumented_exemption
|
||||||
|
- surface: calm-reference-pages
|
||||||
|
sourceFiles:
|
||||||
|
- apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php
|
||||||
|
- apps/platform/app/Filament/Pages/BaselineCompareLanding.php
|
||||||
|
- apps/platform/app/Filament/Pages/BaselineCompareMatrix.php
|
||||||
|
- apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php
|
||||||
|
mustRender:
|
||||||
|
- bounded_scope_semantics
|
||||||
|
- no_forced_extra_layers
|
||||||
|
mustNotRender:
|
||||||
|
- cosmetic_normalization_without_finding
|
||||||
|
- surface: special-type-exception
|
||||||
|
sourceFiles:
|
||||||
|
- apps/platform/app/Filament/Pages/TenantDiagnostics.php
|
||||||
|
mustRender:
|
||||||
|
- explicit_exception_reason
|
||||||
|
- repair_actions_only_when_defect_exists
|
||||||
|
mustNotRender:
|
||||||
|
- silent_exception
|
||||||
|
- surface: regression-guards
|
||||||
|
sourceFiles:
|
||||||
|
- apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php
|
||||||
|
- apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php
|
||||||
|
- apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php
|
||||||
|
- apps/platform/tests/Feature/Guards/ActionSurfaceValidatorTest.php
|
||||||
|
paths:
|
||||||
|
/internal/action-surfaces/monitoring/{surface}:
|
||||||
|
get:
|
||||||
|
summary: Return the logical action-layer contract for an in-scope monitoring surface
|
||||||
|
operationId: getMonitoringSurfaceActionHierarchyContract
|
||||||
|
parameters:
|
||||||
|
- name: surface
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SurfaceKey'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Logical render contract and regression expectations for the requested surface
|
||||||
|
content:
|
||||||
|
application/vnd.tenantpilot.monitoring-action-hierarchy+json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/MonitoringSurfaceContract'
|
||||||
|
'404':
|
||||||
|
description: Requested surface is not in the Spec 193 inventory
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
SurfaceKey:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- finding_exceptions_queue
|
||||||
|
- tenantless_operation_run_viewer
|
||||||
|
- operations
|
||||||
|
- alerts
|
||||||
|
- audit_log
|
||||||
|
- alert_deliveries
|
||||||
|
- evidence_overview
|
||||||
|
- baseline_compare_matrix
|
||||||
|
- baseline_compare_landing
|
||||||
|
- review_register
|
||||||
|
- tenant_diagnostics
|
||||||
|
SurfaceClassification:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- remediation_required
|
||||||
|
- minor_alignment_only
|
||||||
|
- compliant_no_op
|
||||||
|
- special_type_acceptable
|
||||||
|
SurfaceKind:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- queue_workbench
|
||||||
|
- monitoring_detail
|
||||||
|
- monitoring_landing
|
||||||
|
- read_only_report
|
||||||
|
- diagnostic_exception
|
||||||
|
ActionLayer:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- scope_context
|
||||||
|
- navigation
|
||||||
|
- surface_utility
|
||||||
|
- selection_focused
|
||||||
|
- related_drilldown
|
||||||
|
SurfaceState:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- no_selection_monitoring
|
||||||
|
- focused_selection
|
||||||
|
- global_monitoring
|
||||||
|
- related_drilldown
|
||||||
|
- diagnostic_exception
|
||||||
|
ActionKind:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- context
|
||||||
|
- navigation
|
||||||
|
- utility
|
||||||
|
- mutation
|
||||||
|
- drilldown
|
||||||
|
- repair
|
||||||
|
- governance
|
||||||
|
MutationScope:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- TenantPilot only
|
||||||
|
- Microsoft tenant
|
||||||
|
- simulation only
|
||||||
|
- read-only
|
||||||
|
ScopeSignal:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- label
|
||||||
|
- source
|
||||||
|
- isContextOnly
|
||||||
|
- changesSurfaceScope
|
||||||
|
properties:
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
source:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- OperateHubShell
|
||||||
|
- CanonicalNavigationContext
|
||||||
|
- tenant_route
|
||||||
|
- local_filter_state
|
||||||
|
isContextOnly:
|
||||||
|
type: boolean
|
||||||
|
changesSurfaceScope:
|
||||||
|
type: boolean
|
||||||
|
leaksScopeIfMisplaced:
|
||||||
|
type: boolean
|
||||||
|
MonitoringSurfaceAction:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- actionKey
|
||||||
|
- label
|
||||||
|
- actionKind
|
||||||
|
- layer
|
||||||
|
- visibleInStates
|
||||||
|
- requiresConfirmation
|
||||||
|
- usesUiEnforcement
|
||||||
|
- mutationScope
|
||||||
|
properties:
|
||||||
|
actionKey:
|
||||||
|
type: string
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
actionKind:
|
||||||
|
$ref: '#/components/schemas/ActionKind'
|
||||||
|
layer:
|
||||||
|
$ref: '#/components/schemas/ActionLayer'
|
||||||
|
visibleInStates:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/SurfaceState'
|
||||||
|
requiresConfirmation:
|
||||||
|
type: boolean
|
||||||
|
usesUiEnforcement:
|
||||||
|
type: boolean
|
||||||
|
capabilityKey:
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- 'null'
|
||||||
|
writesAuditLog:
|
||||||
|
type: boolean
|
||||||
|
mutationScope:
|
||||||
|
$ref: '#/components/schemas/MutationScope'
|
||||||
|
MonitoringLayerContract:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- layer
|
||||||
|
- isPresent
|
||||||
|
- isPrimaryWorkLayer
|
||||||
|
- visibilityRule
|
||||||
|
properties:
|
||||||
|
layer:
|
||||||
|
$ref: '#/components/schemas/ActionLayer'
|
||||||
|
isPresent:
|
||||||
|
type: boolean
|
||||||
|
isPrimaryWorkLayer:
|
||||||
|
type: boolean
|
||||||
|
mustRemainQuiet:
|
||||||
|
type: boolean
|
||||||
|
visibilityRule:
|
||||||
|
type: string
|
||||||
|
StateContract:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- stateKey
|
||||||
|
- dominantQuestion
|
||||||
|
- prominentActionKeys
|
||||||
|
- allowsNoProminentAction
|
||||||
|
properties:
|
||||||
|
stateKey:
|
||||||
|
$ref: '#/components/schemas/SurfaceState'
|
||||||
|
dominantQuestion:
|
||||||
|
type: string
|
||||||
|
prominentActionKeys:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
quietLayerKeys:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ActionLayer'
|
||||||
|
allowsNoProminentAction:
|
||||||
|
type: boolean
|
||||||
|
MonitoringSurfaceRegressionExpectation:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- forbidsScopeAsPeerCta
|
||||||
|
- forbidsFlatGlobalSelectionMix
|
||||||
|
- requiresExplicitExceptionReason
|
||||||
|
- browserSmokeRequired
|
||||||
|
properties:
|
||||||
|
forbidsScopeAsPeerCta:
|
||||||
|
type: boolean
|
||||||
|
forbidsFlatGlobalSelectionMix:
|
||||||
|
type: boolean
|
||||||
|
requiresNoSelectionQuietState:
|
||||||
|
type: boolean
|
||||||
|
requiresExplicitExceptionReason:
|
||||||
|
type: boolean
|
||||||
|
allowsMinorAlignmentOnly:
|
||||||
|
type: boolean
|
||||||
|
browserSmokeRequired:
|
||||||
|
type: boolean
|
||||||
|
MonitoringSurfaceContract:
|
||||||
|
type: object
|
||||||
|
additionalProperties: false
|
||||||
|
required:
|
||||||
|
- surfaceKey
|
||||||
|
- surfaceKind
|
||||||
|
- classification
|
||||||
|
- canonicalNoun
|
||||||
|
- primaryQuestion
|
||||||
|
- scopeSignals
|
||||||
|
- layers
|
||||||
|
- actions
|
||||||
|
- states
|
||||||
|
- regressionExpectation
|
||||||
|
properties:
|
||||||
|
surfaceKey:
|
||||||
|
$ref: '#/components/schemas/SurfaceKey'
|
||||||
|
surfaceKind:
|
||||||
|
$ref: '#/components/schemas/SurfaceKind'
|
||||||
|
classification:
|
||||||
|
$ref: '#/components/schemas/SurfaceClassification'
|
||||||
|
canonicalNoun:
|
||||||
|
type: string
|
||||||
|
primaryQuestion:
|
||||||
|
type: string
|
||||||
|
scopeSignals:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/ScopeSignal'
|
||||||
|
layers:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/MonitoringLayerContract'
|
||||||
|
actions:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/MonitoringSurfaceAction'
|
||||||
|
states:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/StateContract'
|
||||||
|
explicitExceptionReason:
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- 'null'
|
||||||
|
regressionExpectation:
|
||||||
|
$ref: '#/components/schemas/MonitoringSurfaceRegressionExpectation'
|
||||||
158
specs/193-monitoring-action-hierarchy/data-model.md
Normal file
158
specs/193-monitoring-action-hierarchy/data-model.md
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
# Data Model: Monitoring Surface Action Hierarchy and Workbench Semantics
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This feature introduces no new persisted entity, table, enum, or long-lived artifact. It reuses existing Filament pages, existing action definitions, existing authorization helpers, and existing run or audit truth, while adding a derived planning model for how monitoring and workbench surfaces are inventoried, layered, and regression-tested.
|
||||||
|
|
||||||
|
## Existing Source Truths Reused Without Change
|
||||||
|
|
||||||
|
The following truths remain authoritative and are not redefined by this feature:
|
||||||
|
|
||||||
|
- existing page and resource routes
|
||||||
|
- existing model ownership and scope semantics
|
||||||
|
- existing capability checks and `UiEnforcement` behavior
|
||||||
|
- existing confirmation, audit, and `OperationRun` behavior for underlying actions
|
||||||
|
- existing `OperateHubShell`, `CanonicalNavigationContext`, and tenant-filter state behavior
|
||||||
|
- existing page-local visibility rules for selected-object actions and run follow-up behavior
|
||||||
|
|
||||||
|
This feature changes action hierarchy and placement only.
|
||||||
|
|
||||||
|
## New Derived Planning Models
|
||||||
|
|
||||||
|
### MonitoringSurfaceInventoryEntry
|
||||||
|
|
||||||
|
**Type**: spec and guard inventory entry
|
||||||
|
**Source**: explicit Spec 193 classification matrix + action-surface regression guard
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| `surfaceKey` | string | Stable identifier such as `finding_exceptions_queue` or `tenantless_operation_run_viewer` |
|
||||||
|
| `pageClass` | string | Concrete Filament page or resource page class under review |
|
||||||
|
| `panelScope` | string | `admin` or `tenant` |
|
||||||
|
| `ownerScope` | string | `workspace-owned`, `workspace-visible-tenant-owned`, or `tenant-owned` |
|
||||||
|
| `surfaceKind` | string | `queue_workbench`, `monitoring_detail`, `monitoring_landing`, `read_only_report`, or `diagnostic_exception` |
|
||||||
|
| `classification` | string | `remediation_required`, `minor_alignment_only`, `compliant_no_op`, or `special_type_acceptable` |
|
||||||
|
| `sharedPattern` | string or null | e.g. `OperateHubShell`, `cluster_entry`, or `none` |
|
||||||
|
| `requiresHeaderRemediation` | boolean | Whether the surface must change under Spec 193 |
|
||||||
|
| `requiresExplicitDeclaration` | boolean | Whether the page must carry an explicit `actionSurfaceDeclaration()` |
|
||||||
|
| `exceptionReason` | string or null | Required only for the special-type exception |
|
||||||
|
| `browserSmokeRequired` | boolean | Whether browser smoke must cover the surface |
|
||||||
|
|
||||||
|
### ActionLayerDescriptor
|
||||||
|
|
||||||
|
**Type**: derived page render contract
|
||||||
|
**Source**: existing page action methods + explicit Spec 193 rules
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| `surfaceKey` | string | Links the layer state back to the inventory entry |
|
||||||
|
| `layerKey` | string | `scope_context`, `navigation`, `surface_utility`, `selection_focused`, or `related_drilldown` |
|
||||||
|
| `isPresent` | boolean | Whether the layer exists on this surface |
|
||||||
|
| `isPrimaryWorkLayer` | boolean | True when the layer represents the current next-action lane |
|
||||||
|
| `mustRemainQuiet` | boolean | True for scope and navigation layers when work actions exist |
|
||||||
|
| `visibilityRule` | string | Human-readable rule for when the layer is shown or emphasized |
|
||||||
|
|
||||||
|
### MonitoringSurfaceActionDescriptor
|
||||||
|
|
||||||
|
**Type**: derived action classification entry
|
||||||
|
**Source**: existing Filament action definitions on the target page
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| `actionKey` | string | Action name such as `approve_selected_exception` or `refresh` |
|
||||||
|
| `label` | string | Visible operator-facing label |
|
||||||
|
| `actionKind` | string | `context`, `navigation`, `utility`, `mutation`, `drilldown`, `repair`, or `governance` |
|
||||||
|
| `layer` | string | One of the Spec 193 layers |
|
||||||
|
| `visibleInStates` | array<string> | Surface states where the action may be visible |
|
||||||
|
| `requiresConfirmation` | boolean | Mirrors existing confirmation behavior |
|
||||||
|
| `usesUiEnforcement` | boolean | Whether the action is wrapped with a central enforcement helper |
|
||||||
|
| `capabilityKey` | string or null | Canonical capability requirement when applicable |
|
||||||
|
| `writesAuditLog` | boolean | Whether the underlying mutation writes audit truth |
|
||||||
|
| `mutationScope` | string | `TenantPilot only`, `Microsoft tenant`, `simulation only`, or `read-only` |
|
||||||
|
|
||||||
|
### WorkbenchStateContract
|
||||||
|
|
||||||
|
**Type**: derived work-state entry
|
||||||
|
**Source**: explicit queue or viewer state rules in the spec
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| `surfaceKey` | string | The workbench or monitoring surface |
|
||||||
|
| `stateKey` | string | `no_selection_monitoring`, `focused_selection`, `global_monitoring`, `related_drilldown`, or `diagnostic_exception` |
|
||||||
|
| `dominantQuestion` | string | The operator question the state must answer |
|
||||||
|
| `prominentActionKeys` | array<string> | Actions allowed to read as the current next step |
|
||||||
|
| `quietLayerKeys` | array<string> | Layers that must remain visible but subordinate |
|
||||||
|
| `allowsNoProminentAction` | boolean | True for calm reference or exception states |
|
||||||
|
|
||||||
|
### ScopeSignalContract
|
||||||
|
|
||||||
|
**Type**: derived context entry
|
||||||
|
**Source**: `OperateHubShell`, route-bound tenant context, and canonical navigation helpers
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| `surfaceKey` | string | The surface that shows the scope signal |
|
||||||
|
| `label` | string | Operator-facing scope label |
|
||||||
|
| `source` | string | `OperateHubShell`, `CanonicalNavigationContext`, `tenant_route`, or `local_filter_state` |
|
||||||
|
| `isContextOnly` | boolean | True when the signal must not read as a CTA |
|
||||||
|
| `changesSurfaceScope` | boolean | True only when interacting with the signal resets or broadens scope |
|
||||||
|
| `leaksScopeIfMisplaced` | boolean | True when wrong placement could imply broader access or actionability |
|
||||||
|
|
||||||
|
### MonitoringSurfaceRegressionExpectation
|
||||||
|
|
||||||
|
**Type**: guard and test expectation entry
|
||||||
|
**Source**: Spec 193 regression-protection requirements
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|------|------|-------|
|
||||||
|
| `surfaceKey` | string | The page under regression protection |
|
||||||
|
| `forbidsScopeAsPeerCta` | boolean | Scope must not read as a peer CTA |
|
||||||
|
| `forbidsFlatGlobalSelectionMix` | boolean | Global and selected-object actions must not flatten into one lane |
|
||||||
|
| `requiresNoSelectionQuietState` | boolean | Workbench pages must render a calm state when no object is selected |
|
||||||
|
| `requiresExplicitExceptionReason` | boolean | True only for `TenantDiagnostics` |
|
||||||
|
| `allowsMinorAlignmentOnly` | boolean | True for audit-only surfaces that should not be rebuilt without a specific finding |
|
||||||
|
| `browserSmokeRequired` | boolean | Whether browser smoke must cover this surface |
|
||||||
|
|
||||||
|
## Resolution Rules
|
||||||
|
|
||||||
|
### Monitoring and workbench remediation rules
|
||||||
|
|
||||||
|
1. A remediation-required monitoring or workbench surface resolves actions into explicit layers rather than a single flat header strip.
|
||||||
|
2. Scope and context signals resolve to `scope_context` and must remain visibly subordinate to live work actions.
|
||||||
|
3. Back, return, show-all, and origin links resolve to `navigation`, not to the active work lane.
|
||||||
|
4. Refresh, clear filters, and other page controls resolve to `surface_utility`.
|
||||||
|
5. Selection-bound or focused-object actions resolve to `selection_focused` and may become prominent only in states where a valid selection exists.
|
||||||
|
6. Drilldowns and related opens resolve to `related_drilldown`, not to the same peer level as scope or work actions.
|
||||||
|
|
||||||
|
### Work-state rules
|
||||||
|
|
||||||
|
- `finding_exceptions_queue` resolves to `no_selection_monitoring` when no exception is selected and to `focused_selection` when a pending exception is selected.
|
||||||
|
- `tenantless_operation_run_viewer` resolves to `global_monitoring` plus optional `related_drilldown` or `focused follow-up` states depending on run context and resumable behavior.
|
||||||
|
- `operations` resolves to `global_monitoring` even when tenant-prefiltered; scope reset remains utility, not primary work.
|
||||||
|
|
||||||
|
### Bounded-scope reference rules
|
||||||
|
|
||||||
|
1. A compliant or no-op surface may keep one narrow utility or drilldown affordance without being forced into extra layers.
|
||||||
|
2. Reference surfaces must not be rebuilt only to mimic the remediated workbench pages.
|
||||||
|
|
||||||
|
### Special-type exception rules
|
||||||
|
|
||||||
|
1. `tenant_diagnostics` may expose repair actions only when the corresponding diagnostic defect exists.
|
||||||
|
2. `tenant_diagnostics` must always carry an explicit exception reason in inventory and regression expectations.
|
||||||
|
3. The exception does not create a general allowance for other monitoring pages to promote repair or mutation actions in the same way.
|
||||||
|
|
||||||
|
## Relationships
|
||||||
|
|
||||||
|
- One `MonitoringSurfaceInventoryEntry` maps to one or more `ActionLayerDescriptor` entries.
|
||||||
|
- One `MonitoringSurfaceInventoryEntry` may contain many `MonitoringSurfaceActionDescriptor` entries.
|
||||||
|
- A workbench or viewer surface may contain multiple `WorkbenchStateContract` entries.
|
||||||
|
- Every surface may contain zero or many `ScopeSignalContract` entries.
|
||||||
|
- Every in-scope surface must map to one `MonitoringSurfaceRegressionExpectation`.
|
||||||
|
|
||||||
|
## Safety Rules
|
||||||
|
|
||||||
|
- No derived model may widen tenant or workspace visibility beyond existing route and helper semantics.
|
||||||
|
- No action may lose `UiEnforcement`, confirmation, audit, or `OperationRun` behavior when it changes layer.
|
||||||
|
- No scope signal may be promoted into a peer CTA when it is informational only.
|
||||||
|
- No selection-focused lane may remain prominent when the selected object is absent or no longer valid.
|
||||||
|
- No exception may remain undocumented in the inventory and regression layer.
|
||||||
302
specs/193-monitoring-action-hierarchy/plan.md
Normal file
302
specs/193-monitoring-action-hierarchy/plan.md
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
# Implementation Plan: Monitoring Surface Action Hierarchy and Workbench Semantics
|
||||||
|
|
||||||
|
**Branch**: `193-monitoring-action-hierarchy` | **Date**: 2026-04-11 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/193-monitoring-action-hierarchy/spec.md`
|
||||||
|
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/193-monitoring-action-hierarchy/spec.md`
|
||||||
|
|
||||||
|
**Note**: This plan keeps the work inside the existing Filament v5 / Livewire v4 page layer, existing `OperateHubShell` and `CanonicalNavigationContext` scope helpers, and the current action-surface guard infrastructure. It explicitly avoids adding a new monitoring-action framework.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Codify one bounded action-layer contract for monitoring, queue, operations, and workbench surfaces in the admin panel. Reuse existing Filament header actions, `ActionGroup`, `UiEnforcement`, `OperateHubShell`, `CanonicalAdminTenantFilterState`, and the existing `ActionSurfaceValidator` extension path to inventory all in-scope surfaces, remediate the three clearly problematic workbench pages, convert the alerts overview into an explicitly declared in-scope monitoring surface, preserve calm bounded-scope pages as references, document `TenantDiagnostics` as the only special-type exception, and extend guard plus browser regression layers so mixed monitoring headers do not return.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4.15
|
||||||
|
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `OperateHubShell`, `CanonicalNavigationContext`, `CanonicalAdminTenantFilterState`, `UiEnforcement`, `ActionSurfaceValidator`, and Filament page or resource action builders
|
||||||
|
**Storage**: PostgreSQL through existing workspace-owned and tenant-owned models; no schema change planned
|
||||||
|
**Testing**: Pest feature tests, existing guard tests, and Pest browser smoke tests run through Laravel Sail
|
||||||
|
**Target Platform**: Laravel monolith web application under `apps/platform`, with canonical workspace-context monitoring routes under `/admin`, tenant-context routes under `/admin/t/{tenant}/...`, and cluster-backed monitoring routes under `/admin/alerts`
|
||||||
|
**Project Type**: web application
|
||||||
|
**Performance Goals**: Preserve the 5-second scan rule for monitoring surfaces, keep monitoring renders DB-only with no outbound HTTP or queued jobs at render time, avoid adding new polling beyond existing run-detail behavior, and avoid query churn when separating action layers
|
||||||
|
**Constraints**: No new action framework, no new persistence, no route or panel changes, no authorization-plane changes, no new badge taxonomy, no silent surface exemptions, no record-page header rules copied directly onto workbench pages, and no expansion of confirmation depth, reason capture, or run semantics beyond existing actions
|
||||||
|
**Scale/Scope**: 11 in-scope surfaces, 3 remediation-required core pages, 3 minor-alignment audits, 4 compliant or no-op bounded-scope references, 1 special-type exception, existing Blade views for monitoring pages, and focused guard plus feature plus browser regression coverage
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Passed before Phase 0 research. Re-checked after Phase 1 design and still passing.*
|
||||||
|
|
||||||
|
| Principle | Pre-Research | Post-Design | Notes |
|
||||||
|
|-----------|--------------|-------------|-------|
|
||||||
|
| Inventory-first / snapshots-second | PASS | PASS | The feature does not alter inventory, snapshots, or backup truth. It only reorganizes monitoring surfaces. |
|
||||||
|
| Read/write separation | PASS | PASS | Existing write actions such as approve, reject, and tenant repair keep their current confirmation, audit, and test behavior. No new writes are introduced. |
|
||||||
|
| Graph contract path | N/A | N/A | No new Microsoft Graph call path or contract-registry change is planned. |
|
||||||
|
| Deterministic capabilities | PASS | PASS | Capability checks remain in the canonical registries plus `UiEnforcement`; regrouping actions does not change entitlement logic. |
|
||||||
|
| Workspace + tenant isolation | PASS | PASS | Existing route scopes, `OperateHubShell` resolution, and tenant-safe drilldown rules remain authoritative. |
|
||||||
|
| RBAC-UX authorization semantics | PASS | PASS | Non-members remain `404`, member-without-capability remains `403`, and server-side checks remain unchanged. |
|
||||||
|
| Run observability / Ops-UX | PASS | PASS | Existing `OperationRun`-backed actions keep their current queued-toast, monitoring-detail, and terminal-status semantics. |
|
||||||
|
| Data minimization | PASS | PASS | No new persistence, caches, or cross-surface helper artifacts are introduced. |
|
||||||
|
| Proportionality / anti-bloat | PASS | PASS | The work extends the existing inventory and validation layers rather than introducing a new monitoring-action engine. |
|
||||||
|
| UI semantics / few layers | PASS | PASS | The feature uses direct action placement and explicit classification, not a new presenter or interpretation layer. |
|
||||||
|
| Filament-native UI | PASS | PASS | Native Filament actions, action groups, pages, tables, and shared context bars remain the implementation path. |
|
||||||
|
| Surface taxonomy / monitoring-specific hierarchy | PASS | PASS | The plan explicitly distinguishes monitoring/workbench surfaces from Spec 192 record pages and documents the allowed exception. |
|
||||||
|
| Filament v5 / Livewire v4 compliance | PASS | PASS | All touched surfaces stay inside the existing Filament v5 + Livewire v4 stack. |
|
||||||
|
| Provider registration location | PASS | PASS | No provider change is needed; Laravel 11+ provider registration remains in `bootstrap/providers.php`. |
|
||||||
|
| Global search hard rule | PASS | PASS | No new globally searchable resource is introduced and no resource search settings are changed. Existing searchable resources already have View/Edit pages where needed. |
|
||||||
|
| Destructive action safety | PASS | PASS | Existing confirmed actions such as exception approval or rejection and tenant repair actions keep `->requiresConfirmation()` plus current authorization. |
|
||||||
|
| Asset strategy | PASS | PASS | No new global or on-demand asset registration is needed. Existing deployment handling of `cd apps/platform && php artisan filament:assets` remains unchanged. |
|
||||||
|
|
||||||
|
## Filament-Specific Compliance Notes
|
||||||
|
|
||||||
|
- **Livewire v4.0+ compliance**: The plan remains on Filament v5 + Livewire v4 and introduces no legacy or mixed-version API usage.
|
||||||
|
- **Provider registration location**: No panel or provider changes are required; Laravel 11+ panel providers remain registered in `bootstrap/providers.php`.
|
||||||
|
- **Global search**: The feature does not add a new globally searchable resource and does not alter global-search visibility for existing resources. Touched monitoring pages remain page-level surfaces or reuse existing resource detail pages.
|
||||||
|
- **Destructive actions**: `Approve exception`, `Reject exception`, `Bootstrap owner`, and `Merge duplicate memberships` remain routed through `Action::make(...)->action(...)` with `->requiresConfirmation()` plus existing authorization and audit semantics.
|
||||||
|
- **Asset strategy**: No new global or lazy-loaded assets are planned. Existing deployment handling of `cd apps/platform && php artisan filament:assets` remains sufficient.
|
||||||
|
- **Testing plan**: Extend the existing action-surface guard layer, add focused Pest tests for the remediated and exception surfaces, and add a browser smoke suite that proves visible monitoring hierarchy on the remediated pages and no-regression behavior on reference pages.
|
||||||
|
|
||||||
|
## Phase 0 Research
|
||||||
|
|
||||||
|
Research outcomes are captured in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/193-monitoring-action-hierarchy/research.md`.
|
||||||
|
|
||||||
|
Key decisions:
|
||||||
|
|
||||||
|
- Reuse the existing `ActionSurfaceExemptions` plus `ActionSurfaceValidator` inventory pattern from Spec 192 instead of creating a new monitoring-action framework.
|
||||||
|
- Keep `OperateHubShell` and `CanonicalNavigationContext` as the single scope and return-context sources for canonical `/admin` monitoring pages.
|
||||||
|
- Express hierarchy through native Filament actions, `ActionGroup`, existing context bars, and targeted Blade adjustments rather than custom header components.
|
||||||
|
- Retire the blanket baseline exemption for `Alerts` and bring it into explicit Spec 193 declaration plus inventory coverage.
|
||||||
|
- Build regression protection on top of the existing guard tests, `OperateHubShellTest`, focused page tests, and one dedicated browser smoke suite.
|
||||||
|
|
||||||
|
## Phase 1 Design
|
||||||
|
|
||||||
|
Design artifacts are created under `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/193-monitoring-action-hierarchy/`:
|
||||||
|
|
||||||
|
- `research.md`: decisions and rejected alternatives for monitoring/workbench hierarchy
|
||||||
|
- `data-model.md`: derived surface inventory, action-layer, and regression expectation models
|
||||||
|
- `contracts/monitoring-action-hierarchy.logical.openapi.yaml`: internal logical contract for monitoring-surface action layers and exception handling
|
||||||
|
- `quickstart.md`: implementation and verification sequence for the feature
|
||||||
|
|
||||||
|
Design highlights:
|
||||||
|
|
||||||
|
- Keep all classification and action-layer rules derived, not persisted.
|
||||||
|
- Represent each in-scope surface through one explicit inventory entry and one explicit regression expectation.
|
||||||
|
- Extend the existing action-surface validation layer with a second bounded inventory for monitoring/workbench surfaces.
|
||||||
|
- Keep selection-state logic local to the affected pages instead of moving it into a shared runtime resolver.
|
||||||
|
- Treat `TenantDiagnostics` as the only explicit special-type exception and require an exception reason in the guard layer.
|
||||||
|
|
||||||
|
## Phase 1 — Agent Context Update
|
||||||
|
|
||||||
|
Planned command:
|
||||||
|
|
||||||
|
- `.specify/scripts/bash/update-agent-context.sh copilot`
|
||||||
|
|
||||||
|
This feature does not introduce a new technology stack, but the required agent-context refresh still runs after the technical context and design artifacts are complete.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/193-monitoring-action-hierarchy/
|
||||||
|
├── plan.md
|
||||||
|
├── research.md
|
||||||
|
├── data-model.md
|
||||||
|
├── quickstart.md
|
||||||
|
├── spec.md
|
||||||
|
├── contracts/
|
||||||
|
│ └── monitoring-action-hierarchy.logical.openapi.yaml
|
||||||
|
└── checklists/
|
||||||
|
└── requirements.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/
|
||||||
|
├── app/
|
||||||
|
│ ├── Filament/
|
||||||
|
│ │ ├── Pages/
|
||||||
|
│ │ │ ├── Monitoring/
|
||||||
|
│ │ │ │ ├── FindingExceptionsQueue.php # MODIFY
|
||||||
|
│ │ │ │ ├── Operations.php # MODIFY
|
||||||
|
│ │ │ │ ├── Alerts.php # MODIFY (add declaration + minor alignment)
|
||||||
|
│ │ │ │ ├── AuditLog.php # AUDIT / possible minor alignment
|
||||||
|
│ │ │ │ └── EvidenceOverview.php # REFERENCE only
|
||||||
|
│ │ │ ├── Operations/
|
||||||
|
│ │ │ │ └── TenantlessOperationRunViewer.php # MODIFY
|
||||||
|
│ │ │ ├── Reviews/
|
||||||
|
│ │ │ │ └── ReviewRegister.php # REFERENCE only
|
||||||
|
│ │ │ ├── BaselineCompareLanding.php # REFERENCE only
|
||||||
|
│ │ │ ├── BaselineCompareMatrix.php # REFERENCE only
|
||||||
|
│ │ │ └── TenantDiagnostics.php # AUDIT / special-type exception
|
||||||
|
│ │ └── Resources/
|
||||||
|
│ │ ├── AlertDeliveryResource.php # REUSE / possible declaration notes
|
||||||
|
│ │ └── AlertDeliveryResource/
|
||||||
|
│ │ └── Pages/
|
||||||
|
│ │ └── ListAlertDeliveries.php # AUDIT / possible minor alignment
|
||||||
|
│ ├── Support/
|
||||||
|
│ │ ├── OperateHub/
|
||||||
|
│ │ │ └── OperateHubShell.php # REUSE
|
||||||
|
│ │ ├── Navigation/
|
||||||
|
│ │ │ └── CanonicalNavigationContext.php # REUSE
|
||||||
|
│ │ ├── Filament/
|
||||||
|
│ │ │ └── CanonicalAdminTenantFilterState.php # REUSE
|
||||||
|
│ │ ├── Rbac/
|
||||||
|
│ │ │ └── UiEnforcement.php # REUSE
|
||||||
|
│ │ └── Ui/
|
||||||
|
│ │ └── ActionSurface/
|
||||||
|
│ │ ├── ActionSurfaceExemptions.php # MODIFY
|
||||||
|
│ │ ├── ActionSurfaceValidator.php # MODIFY
|
||||||
|
│ │ └── ActionSurfaceProfileDefinition.php # POSSIBLE MODIFY
|
||||||
|
├── resources/
|
||||||
|
│ └── views/
|
||||||
|
│ └── filament/
|
||||||
|
│ ├── partials/
|
||||||
|
│ │ └── context-bar.blade.php # REUSE / possible minor alignment
|
||||||
|
│ └── pages/
|
||||||
|
│ ├── monitoring/
|
||||||
|
│ │ ├── finding-exceptions-queue.blade.php # MODIFY
|
||||||
|
│ │ ├── operations.blade.php # POSSIBLE MODIFY
|
||||||
|
│ │ ├── alerts.blade.php # POSSIBLE MODIFY
|
||||||
|
│ │ └── audit-log.blade.php # POSSIBLE MODIFY
|
||||||
|
│ ├── operations/
|
||||||
|
│ │ └── tenantless-operation-run-viewer.blade.php # MODIFY
|
||||||
|
│ ├── reviews/
|
||||||
|
│ │ └── review-register.blade.php # REFERENCE only
|
||||||
|
│ ├── baseline-compare-landing.blade.php # REFERENCE only
|
||||||
|
│ ├── baseline-compare-matrix.blade.php # REFERENCE only
|
||||||
|
│ └── tenant-diagnostics.blade.php # REFERENCE / special-type audit
|
||||||
|
└── tests/
|
||||||
|
├── Feature/
|
||||||
|
│ ├── Guards/
|
||||||
|
│ │ ├── ActionSurfaceContractTest.php # MODIFY
|
||||||
|
│ │ ├── ActionSurfaceValidatorTest.php # MODIFY
|
||||||
|
│ │ └── Spec193MonitoringSurfaceHierarchyGuardTest.php # NEW
|
||||||
|
│ ├── Monitoring/
|
||||||
|
│ │ ├── AuditLogInspectFlowTest.php # REUSE / possible extend
|
||||||
|
│ │ ├── OperationsCanonicalUrlsTest.php # MODIFY or REUSE
|
||||||
|
│ │ ├── OperationsDashboardDrillthroughTest.php # MODIFY or REUSE
|
||||||
|
│ │ ├── OperationsRelatedNavigationTest.php # MODIFY or REUSE
|
||||||
|
│ │ ├── FindingExceptionsQueueHierarchyTest.php # NEW
|
||||||
|
│ │ └── OperationsHeaderHierarchyTest.php # NEW
|
||||||
|
│ ├── Operations/
|
||||||
|
│ │ └── TenantlessOperationRunViewerTest.php # MODIFY
|
||||||
|
│ ├── OpsUx/
|
||||||
|
│ │ └── OperateHubShellTest.php # MODIFY
|
||||||
|
│ └── Rbac/
|
||||||
|
│ ├── ActionSurfaceRbacSemanticsTest.php # REUSE / possible extend
|
||||||
|
│ └── TenantActionSurfaceConsistencyTest.php # REUSE / possible extend
|
||||||
|
└── Browser/
|
||||||
|
├── Spec192RecordPageHeaderDisciplineSmokeTest.php # REUSE for patterns
|
||||||
|
└── Spec193MonitoringSurfaceHierarchySmokeTest.php # NEW
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Keep the work entirely inside the existing Laravel/Filament monolith under `apps/platform`. Modify only the affected page classes, a small set of monitoring Blade views, the existing action-surface validation layer, and focused tests. Do not create a new runtime resolver or header-framework layer.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|-----------|------------|-------------------------------------|
|
||||||
|
| Cross-page monitoring/workbench taxonomy and explicit exception catalog (BLOAT-001 trigger) | The feature must distinguish remediation-required workbench pages, shared-pattern audit pages, calm bounded-scope references, and the single acceptable diagnostic exception in a way CI can validate. | Pure local cleanup would reduce visible clutter but would not prevent future drift or explain why some monitoring surfaces are preserved while others are remediated. |
|
||||||
|
|
||||||
|
## Proportionality Review
|
||||||
|
|
||||||
|
- **Current operator problem**: Monitoring and workbench pages still mix scope, navigation, utility, and active work actions in one header lane, slowing queue review and making next-step semantics ambiguous.
|
||||||
|
- **Existing structure is insufficient because**: The constitution and Spec 192 now govern other action-surface classes, but the repo lacks a bounded implementation inventory and regression hook for this monitoring/workbench surface class.
|
||||||
|
- **Narrowest correct implementation**: Extend the existing action-surface inventory and validation layer with one additional monitoring/workbench inventory, remediate only the three clearly problematic pages, explicitly classify the others, and add focused feature plus browser regression coverage.
|
||||||
|
- **Ownership cost created**: A small extension to the validator and exemption inventory, one new guard test, several focused page tests, one browser smoke suite, and ongoing review discipline for future monitoring surfaces.
|
||||||
|
- **Alternative intentionally rejected**: A new monitoring action-placement engine or registry was rejected because the current repo already has sufficient primitives through Filament actions, `OperateHubShell`, and the existing action-surface validator.
|
||||||
|
- **Release truth**: current-release operator clarity and action-surface discipline
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Phase A — Codify the monitoring inventory and guard contract
|
||||||
|
|
||||||
|
Goal: turn the spec inventory into an enforceable project-level contract without introducing a new framework.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Extend `ActionSurfaceExemptions` with a `spec193MonitoringSurfaceInventory()` and per-surface lookup helper.
|
||||||
|
- Extend `ActionSurfaceValidator` with explicit validation of the Spec 193 inventory and its allowed classifications.
|
||||||
|
- Add `Spec193MonitoringSurfaceHierarchyGuardTest.php` to assert completeness, explicit exception reasoning, and bounded reference preservation.
|
||||||
|
- Remove the blanket baseline exemption for `Alerts` and replace it with explicit monitoring-surface coverage.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- Extend `ActionSurfaceContractTest.php` and `ActionSurfaceValidatorTest.php` with Spec 193 expectations.
|
||||||
|
- Add `Spec193MonitoringSurfaceHierarchyGuardTest.php`.
|
||||||
|
|
||||||
|
### Phase B — Remediate the highest-noise workbench surfaces
|
||||||
|
|
||||||
|
Goal: implement the core hierarchy wins first on the pages with the clearest mixed-header problem.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Refactor `FindingExceptionsQueue` so scope, return, filters, drilldowns, and selected-exception decisions no longer render as flat peers.
|
||||||
|
- Refactor `TenantlessOperationRunViewer` so scope, return, show-all, refresh, related links, and resumable actions render as distinct layers.
|
||||||
|
- Refactor `Operations` so scope and show-all context stay visible but no longer read as the page’s primary work surface.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- Add focused page tests for no-selection vs selected-workbench behavior on `FindingExceptionsQueue`.
|
||||||
|
- Extend `TenantlessOperationRunViewerTest.php` and relevant Monitoring tests with hierarchy and navigation assertions.
|
||||||
|
|
||||||
|
### Phase C — Tighten shared patterns and classify bounded-scope references
|
||||||
|
|
||||||
|
Goal: bring shared monitoring patterns under the same contract without rebuilding calm pages.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Add an explicit declaration to `Alerts` and audit its origin-context placement.
|
||||||
|
- Review `AuditLog` and `ListAlertDeliveries` for minor alignment only, changing them only where real action-layer ambiguity exists.
|
||||||
|
- Confirm `EvidenceOverview`, `BaselineCompareLanding`, `BaselineCompareMatrix`, and `ReviewRegister` remain compliant or no-op references.
|
||||||
|
- Keep `TenantDiagnostics` as the single special-type acceptable exception and codify why it is allowed.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- Extend `OperateHubShellTest.php` for scope-label and return-affordance expectations that remain relevant after the monitoring refactor.
|
||||||
|
- Add focused assertions for the Alerts overview in `AlertsHierarchyTest.php`, keep alert-delivery minor-alignment coverage in the existing alert-delivery suites, and verify that minor-alignment pages remain calm or declaration-complete.
|
||||||
|
|
||||||
|
### Phase D — Browser verification and final regression protection
|
||||||
|
|
||||||
|
Goal: prove the new hierarchy in a real browser and prevent future mixed monitoring headers from re-entering the repo.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
|
||||||
|
- Add `Spec193MonitoringSurfaceHierarchySmokeTest.php` covering the three remediated pages, the `TenantDiagnostics` exception surface, and a no-regression subset of calm reference pages.
|
||||||
|
- Ensure the guard layer fails on scope-as-CTA regressions, mixed selection and global header lanes, and undocumented exceptions.
|
||||||
|
- Re-run formatting and the focused Sail test pack.
|
||||||
|
|
||||||
|
Tests:
|
||||||
|
|
||||||
|
- Browser smoke coverage for visible hierarchy and no JavaScript errors.
|
||||||
|
- Focused guard and feature tests for each remediated or exception surface.
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
| Risk | Impact | Likelihood | Mitigation |
|
||||||
|
|------|--------|------------|------------|
|
||||||
|
| Cleanup grows into a new monitoring-action framework | Medium | Low | Keep all work inside existing pages, views, and validator layers. |
|
||||||
|
| Shared-pattern loyalty masks real hierarchy issues | High | Medium | Treat `OperateHubShell` pages as review targets, not automatic exemptions. |
|
||||||
|
| Selection-aware workbench pages become too quiet when no selection exists | Medium | Medium | Add explicit no-selection vs selected-state tests on the queue and viewer surfaces. |
|
||||||
|
| Alerts stays outside regression coverage because of the old cluster exemption | Medium | Medium | Convert it to explicit declaration plus inventory coverage in Phase A. |
|
||||||
|
| Calm bounded-scope pages get unnecessary churn | Medium | Low | Maintain an explicit compliant/no-op reference set and cover it in browser smoke. |
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
|
||||||
|
- Extend `ActionSurfaceContractTest.php` and `ActionSurfaceValidatorTest.php` so Spec 193 becomes an explicit CI-enforced rule instead of a manual review note.
|
||||||
|
- Add `Spec193MonitoringSurfaceHierarchyGuardTest.php` to validate remediation-required pages, minor-alignment pages, calm references, and the explicit diagnostic exception.
|
||||||
|
- Add focused feature tests for `FindingExceptionsQueue`, `Operations`, and `TenantlessOperationRunViewer` covering state-driven hierarchy, scope behavior, and preserved authorization semantics.
|
||||||
|
- Reuse and extend `OperateHubShellTest.php` to keep DB-only rendering, tenant-context resolution, and scope-label behavior correct on canonical monitoring pages.
|
||||||
|
- Add `Spec193MonitoringSurfaceHierarchySmokeTest.php` using the existing browser-smoke patterns already present in Spec 192 and other surface smoke tests.
|
||||||
|
- Add explicit regression assertions that this feature does not expand confirmation depth, reason capture, provider-dispatch semantics, or record-page header rules.
|
||||||
|
- Run the focused Sail verification commands from `quickstart.md`, then run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`.
|
||||||
|
|
||||||
|
## Constitution Check (Post-Design)
|
||||||
|
|
||||||
|
Re-check result: PASS.
|
||||||
|
|
||||||
|
- Livewire v4.0+ compliance remains intact because all touched surfaces stay inside the existing Filament v5 + Livewire v4 stack.
|
||||||
|
- Provider registration remains unchanged in `bootstrap/providers.php`.
|
||||||
|
- The plan changes no global-search semantics; affected surfaces are pages or existing resource-backed pages whose search behavior already satisfies the Filament hard rule.
|
||||||
|
- Destructive and governance-changing actions keep `->requiresConfirmation()` plus existing authorization.
|
||||||
|
- No new assets are introduced; existing `filament:assets` deployment behavior remains sufficient.
|
||||||
94
specs/193-monitoring-action-hierarchy/quickstart.md
Normal file
94
specs/193-monitoring-action-hierarchy/quickstart.md
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Quickstart: Monitoring Surface Action Hierarchy and Workbench Semantics
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Bring the in-scope monitoring, queue, operations, and workbench surfaces under one bounded hierarchy: scope reads as context, navigation reads as navigation, utilities stay utility-level, selected-object or focused actions become prominent only in active work states, calm bounded-scope pages remain calm, and `TenantDiagnostics` remains the only explicit exception.
|
||||||
|
|
||||||
|
## Implementation Sequence
|
||||||
|
|
||||||
|
1. Confirm the in-scope inventory in code.
|
||||||
|
- Add the Spec 193 inventory to the existing `ActionSurfaceExemptions` layer.
|
||||||
|
- Validate which surfaces are remediation-required, minor-alignment only, compliant or no-op, or special-type acceptable.
|
||||||
|
- Retire the blanket `Alerts` baseline exemption and replace it with explicit declaration plus inventory coverage.
|
||||||
|
|
||||||
|
2. Remediate the core workbench pages first.
|
||||||
|
- Refactor `FindingExceptionsQueue` so queue scope, utilities, drilldowns, and selected-exception decisions no longer render as flat peers.
|
||||||
|
- Refactor `TenantlessOperationRunViewer` so scope, return navigation, refresh, related links, and resumable actions render as distinct layers.
|
||||||
|
- Refactor `Operations` so scope and show-all context stay visible but quieter than the list’s actual work controls.
|
||||||
|
|
||||||
|
3. Tighten shared monitoring patterns and classify the rest.
|
||||||
|
- Add an explicit declaration and audit pass to `Alerts`.
|
||||||
|
- Review `AuditLog` and `ListAlertDeliveries` for minor alignment only.
|
||||||
|
- Confirm `EvidenceOverview`, `BaselineCompareLanding`, `BaselineCompareMatrix`, and `ReviewRegister` remain calm references.
|
||||||
|
- Keep `TenantDiagnostics` as the explicit special-type exception and verify its exception reason in code.
|
||||||
|
|
||||||
|
4. Add regression protection.
|
||||||
|
- Extend the existing action-surface validator with Spec 193 inventory validation.
|
||||||
|
- Add a dedicated guard test for Spec 193 inventory and exception semantics.
|
||||||
|
- Add focused feature tests for the remediated pages and the diagnostic exception.
|
||||||
|
- Add one browser smoke suite covering remediated, exception, and reference surfaces.
|
||||||
|
|
||||||
|
5. Run focused verification.
|
||||||
|
- Run the guard tests, focused feature tests, browser smoke suite, and formatting through Sail.
|
||||||
|
|
||||||
|
## Suggested Source Files
|
||||||
|
|
||||||
|
- `apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/Monitoring/Operations.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/Monitoring/Alerts.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php`
|
||||||
|
- `apps/platform/app/Filament/Resources/AlertDeliveryResource/Pages/ListAlertDeliveries.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/BaselineCompareLanding.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/TenantDiagnostics.php`
|
||||||
|
- `apps/platform/app/Support/OperateHub/OperateHubShell.php`
|
||||||
|
- `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`
|
||||||
|
- `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`
|
||||||
|
|
||||||
|
## Suggested Test Files
|
||||||
|
|
||||||
|
- `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Guards/ActionSurfaceValidatorTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Guards/Spec193MonitoringSurfaceHierarchyGuardTest.php`
|
||||||
|
- `apps/platform/tests/Feature/OpsUx/OperateHubShellTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Monitoring/OperationsRelatedNavigationTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Monitoring/FindingExceptionsQueueHierarchyTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Monitoring/OperationsHeaderHierarchyTest.php`
|
||||||
|
- `apps/platform/tests/Browser/Spec193MonitoringSurfaceHierarchySmokeTest.php`
|
||||||
|
|
||||||
|
## Minimum Verification Commands
|
||||||
|
|
||||||
|
Run all commands through Sail from `apps/platform`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceValidatorTest.php
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/Spec193MonitoringSurfaceHierarchyGuardTest.php
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx/OperateHubShellTest.php
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Operations/TenantlessOperationRunViewerTest.php
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Monitoring/FindingExceptionsQueueHierarchyTest.php
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec193MonitoringSurfaceHierarchySmokeTest.php
|
||||||
|
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Acceptance Checklist
|
||||||
|
|
||||||
|
1. Open `FindingExceptionsQueue` with and without a selected exception and confirm the page visibly changes from quiet monitoring mode to focused workbench mode.
|
||||||
|
2. Open `TenantlessOperationRunViewer` from Operations and confirm scope, return, refresh, related links, and follow-up actions no longer read as one flat header strip.
|
||||||
|
3. Open `Operations` and confirm scope reset is visible but quieter than tabs, filters, and row drilldown.
|
||||||
|
4. Open `Alerts`, `AuditLog`, and `ListAlertDeliveries` and confirm they remain calm or only receive documented minor alignment.
|
||||||
|
5. Open `EvidenceOverview`, `BaselineCompareLanding`, `BaselineCompareMatrix`, and `ReviewRegister` and confirm they remain calm bounded-scope references.
|
||||||
|
6. Open `TenantDiagnostics` with and without an active defect state and confirm repair actions appear only when justified and remain confirmed.
|
||||||
|
7. Confirm browser smoke checks show no JavaScript errors on remediated, exception, and reference surfaces.
|
||||||
|
|
||||||
|
## Deployment Notes
|
||||||
|
|
||||||
|
- No migration is expected.
|
||||||
|
- No new provider registration is expected; Laravel 11+ provider registration remains in `bootstrap/providers.php`.
|
||||||
|
- No new asset registration is expected. Existing deploy handling of `cd apps/platform && php artisan filament:assets` remains sufficient.
|
||||||
99
specs/193-monitoring-action-hierarchy/research.md
Normal file
99
specs/193-monitoring-action-hierarchy/research.md
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
# Research: Monitoring Surface Action Hierarchy and Workbench Semantics
|
||||||
|
|
||||||
|
## Decision: Reuse the existing `ActionSurfaceExemptions` plus `ActionSurfaceValidator` inventory pattern instead of introducing a monitoring-specific action-placement framework
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
Spec 192 already proved that the repo can encode a bounded surface-class contract through `ActionSurfaceExemptions`, `ActionSurfaceValidator`, and a dedicated guard test. Spec 193 needs the same kind of explicit inventory and CI protection for monitoring and workbench pages, not a second runtime engine for action placement.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- Add a `MonitoringActionHierarchyResolver` or `WorkbenchActionRegistry`: rejected because the placement rules remain page- and state-sensitive, and the repo already has enough validation infrastructure.
|
||||||
|
- Keep the rules only in the spec with manual review: rejected because mixed monitoring headers are exactly the sort of drift CI should catch.
|
||||||
|
|
||||||
|
## Decision: Keep `OperateHubShell` and `CanonicalNavigationContext` as the canonical scope and return-context sources for `/admin` monitoring pages
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
The affected canonical workspace routes already rely on `OperateHubShell` for entitled tenant context and on `CanonicalNavigationContext` for back-link semantics. The narrowest correct move is to preserve those sources and only change how their outputs are layered, rather than letting each page improvise its own scope or return logic.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- Resolve tenant and return context ad hoc in each page: rejected because it would duplicate authorization-sensitive context logic and risk canonical-route drift.
|
||||||
|
- Remove remembered-tenant context from canonical monitoring pages: rejected because the current product truth intentionally supports tenant-prefiltered monitoring views.
|
||||||
|
|
||||||
|
## Decision: Express the hierarchy with native Filament actions, `ActionGroup`, context bars, and targeted Blade adjustments
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
The codebase already uses native Filament header actions, grouped actions, page views, and context-bar partials. The problem is semantic weight and layering, not a missing UI toolkit. Using those existing primitives keeps the feature local and avoids importing a new presentation framework.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- Add custom header components or a page-level action-slot DSL: rejected because the existing Filament action primitives already cover the needed hierarchy.
|
||||||
|
- Solve the issue only by button restyling: rejected because the problem is semantic competition, not color alone.
|
||||||
|
|
||||||
|
## Decision: Bring `Alerts` under explicit Spec 193 coverage instead of leaving it as a blanket baseline exemption
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
`Alerts` is in scope for minor alignment and currently carries a baseline exemption because the active route resolves through the cluster entry. That exemption is too coarse for Spec 193, which needs an explicit verdict for every in-scope surface. The clean approach is to add an explicit declaration and inventory entry while keeping the page minor-alignment only unless a real hierarchy issue is found.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- Keep the existing exemption and rely on browser smoke only: rejected because the spec requires explicit classification and regression protection for all in-scope surfaces.
|
||||||
|
- Fully remediate Alerts as if it were a noisy workbench: rejected because the current surface is probably already calm enough for minor alignment only.
|
||||||
|
|
||||||
|
## Decision: Model selection-heavy workbench behavior locally on the affected pages instead of in a shared state resolver
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
`FindingExceptionsQueue` and `TenantlessOperationRunViewer` already have page-local state and action visibility rules. The narrowest correct implementation is to keep that local state and only classify actions into explicit layers. That preserves current authorization, notifications, and run semantics while avoiding a premature shared workbench state machine.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- Add a shared `WorkbenchState` resolver or enum family: rejected because the feature does not yet have enough truly shared runtime behavior to justify it.
|
||||||
|
- Collapse all non-global actions into a single `More` menu: rejected because the spec requires visible state transitions between calm monitoring mode and active work mode.
|
||||||
|
|
||||||
|
## Decision: Preserve bounded-scope monitoring pages as explicit references
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
`EvidenceOverview`, `BaselineCompareLanding`, `BaselineCompareMatrix`, and `ReviewRegister` already read as bounded-scope monitoring pages with one primary question and limited header complexity. The spec should name them as calm references and use them as a regression baseline, rather than rebuilding them to mimic the remediated workbench pages.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- Normalize every in-scope page to the same visible action-layer structure: rejected because it would create churn without additional operator value.
|
||||||
|
- Ignore calm pages and only document the noisy ones: rejected because the spec explicitly requires full-scope classification.
|
||||||
|
|
||||||
|
## Decision: Keep `TenantDiagnostics` as the only special-type acceptable exception
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
`TenantDiagnostics` is not a generic monitoring list; it is a focused diagnostic repair surface whose actions are meaningful only when a broken membership state exists. It should remain allowed as a special type, with explicit documentation and regression expectations, rather than being forced into the same pattern as a queue or a read-only registry report.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- Force diagnostics into a quiet read-only monitoring pattern: rejected because it would obscure legitimate repair actions.
|
||||||
|
- Let diagnostics stay different without explicit cataloging: rejected because silent exceptions produce review drift.
|
||||||
|
|
||||||
|
## Decision: Build regression protection with one extra inventory, focused feature tests, and one browser smoke suite
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
The repo already has the right three test layers for this kind of change: inventory and validator guards, focused feature tests around page state and authorization, and browser smoke for visible hierarchy. Extending those layers gives the feature durable protection without overbuilding.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- Browser-test every state permutation of every monitoring page: rejected because it is expensive and redundant with feature tests.
|
||||||
|
- Add only static guard coverage: rejected because no-selection vs active-selection behavior still needs runtime assertions.
|
||||||
|
|
||||||
|
## Decision: No new assets, provider changes, or route changes are needed
|
||||||
|
|
||||||
|
### Rationale
|
||||||
|
|
||||||
|
The work stays within existing Filament pages and resource pages. No panel/provider registration, asset registration, or route family change is required. Existing deployment handling of `cd apps/platform && php artisan filament:assets` remains unchanged.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- Introduce a custom asset or component library for layered monitoring headers: rejected because native Filament surfaces already provide the necessary primitives.
|
||||||
333
specs/193-monitoring-action-hierarchy/spec.md
Normal file
333
specs/193-monitoring-action-hierarchy/spec.md
Normal file
@ -0,0 +1,333 @@
|
|||||||
|
# Feature Specification: Monitoring Surface Action Hierarchy and Workbench Semantics
|
||||||
|
|
||||||
|
**Feature Branch**: `193-monitoring-action-hierarchy`
|
||||||
|
**Created**: 2026-04-11
|
||||||
|
**Status**: Proposed
|
||||||
|
**Input**: User description: "Spec 193 - Monitoring Surface Action Hierarchy and Workbench Semantics"
|
||||||
|
|
||||||
|
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||||
|
|
||||||
|
- **Problem**: Several monitoring, queue, operations, and workbench surfaces still present scope context, return navigation, utility controls, and selection-bound work actions as one flat header strip.
|
||||||
|
- **Today's failure**: Operators can reach the right surface, but the header often fails to answer four questions quickly enough: where am I, what scope am I in, what is selected, and what action is actually next. On queue and monitoring pages, scope chips read like calls to action, related navigation competes with work actions, and selection logic appears at the same visual weight as global surface controls.
|
||||||
|
- **User-visible improvement**: Monitoring and workbench surfaces become easier to scan. Scope reads as context, navigation reads as navigation, utilities read as utilities, and selected-object actions only become prominent when there is an active object or selection.
|
||||||
|
- **Smallest enterprise-capable version**: Inventory the in-scope monitoring and workbench surfaces, classify each one, remediate the three clearly problematic core surfaces, lightly align shared-pattern neighbors only where needed, explicitly preserve already calm bounded-scope pages, document the one special-type diagnostic surface, and add lightweight regression protection.
|
||||||
|
- **Explicit non-goals**: No record-page header rewrite, no new danger or reason-capture policy, no dispatch or preflight redesign, no general bulk-action framework, no full monitoring redesign, and no forced normalization of already calm pages.
|
||||||
|
- **Permanent complexity imported**: A narrow monitoring-surface action hierarchy contract, an explicit classification matrix for this surface class, a documented special-type exception, and focused regression coverage for monitoring and workbench action-layer drift.
|
||||||
|
- **Why now**: Spec 192 intentionally excludes this surface class. The repo already contains repeated mixed-header patterns on the canonical monitoring surfaces, so leaving the gap open would keep creating inconsistent operator semantics precisely where operations and queue review need the clearest hierarchy.
|
||||||
|
- **Why not local**: Page-by-page cleanup would reduce clutter on one screen at a time but would not define the repo-wide rule for separating scope, navigation, utility, and selection layers on monitoring and workbench surfaces, nor would it explain why some pages are calm references while others need layered remediation.
|
||||||
|
- **Approval class**: Core Enterprise
|
||||||
|
- **Red flags triggered**: Cross-surface UI taxonomy risk and multi-surface cleanup breadth risk. Defense: the spec is limited to one explicit surface class, introduces no new engine or persisted truth, and preserves no-op pages instead of forcing broad redesign.
|
||||||
|
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||||
|
- **Decision**: approve
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: canonical-view
|
||||||
|
- **Primary Routes**:
|
||||||
|
- Existing workspace queue route for Finding Exceptions Queue
|
||||||
|
- Existing workspace Operations landing and tenantless operation detail routes
|
||||||
|
- Existing workspace Monitoring routes for Alerts, Audit Log, Evidence Overview, and alert deliveries
|
||||||
|
- Existing tenant-bound Baseline Compare landing and compare matrix routes
|
||||||
|
- Existing workspace Review Register route and tenant review detail route it opens
|
||||||
|
- Existing tenant diagnostics route
|
||||||
|
- **Data Ownership**:
|
||||||
|
- Workspace-scoped monitoring views remain workspace-scoped and continue to display existing workspace-owned or workspace-visible operational records.
|
||||||
|
- Tenant-owned records surfaced through these pages remain tenant-owned: finding exceptions, operation runs, evidence snapshots, tenant reviews, and tenant diagnostics context.
|
||||||
|
- This spec introduces no new tables, persisted entities, or route truth. It changes only surface classification, action hierarchy, and visible action placement on existing pages.
|
||||||
|
- **RBAC**:
|
||||||
|
- Existing workspace membership and capability checks continue to govern workspace monitoring surfaces.
|
||||||
|
- Existing tenant membership and tenant capability checks continue to govern tenant-bound monitoring surfaces.
|
||||||
|
- Regrouping actions does not change authorization semantics: non-members remain `404`, members lacking capability remain `403`, and destructive or repair actions keep confirmation plus server-side authorization.
|
||||||
|
|
||||||
|
For canonical-view specs, the spec MUST define:
|
||||||
|
|
||||||
|
- **Default filter behavior when tenant-context is active**: Workspace monitoring surfaces may keep an entitled remembered tenant context as a prefilter or scope signal, but the tenant-wide view must remain explicit and reversible. Tenant-bound monitoring pages remain bound to the active tenant and do not broaden scope. Selection-aware work actions must never imply a broader scope than the currently filtered or active tenant context.
|
||||||
|
- **Explicit entitlement checks preventing cross-tenant leakage**: Scope labels, return links, row drilldowns, and related-navigation affordances must continue to derive from existing capability-aware helpers and entitled-tenant resolution. Moving actions between layers must not expose inaccessible tenants, inaccessible related records, or cross-tenant operation details.
|
||||||
|
|
||||||
|
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Surface Type | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Finding Exceptions Queue | Workspace queue workbench | Explicit inspect action selects one exception and opens the in-page review state | forbidden | Scope, return, clear filters, and drilldown links move into distinct context, utility, and related layers outside the selected-action lane | Selection-bound review decisions stay in the selected-object action layer only when an exception is selected and pending | Existing Finding Exceptions Queue route | Same queue page with selected exception state and tenant detail drilldown | Workspace or remembered-tenant scope, selected exception summary, queue filters | Finding exceptions / exception request | Whether the queue is scoped, whether an exception is selected, and whether a decision is currently available | remediation required |
|
||||||
|
| Tenantless Operation Run Viewer | Workspace monitoring detail viewer | Canonical tenantless operation detail page is the only inspect destination for a selected run | forbidden | Scope, back navigation, show-all, refresh, and related links split into context, navigation, utility, and drilldown layers | No destructive action on the viewer; resumable actions stay isolated from navigation and only appear when actually applicable | Existing Operations landing route | Existing tenantless operation detail route | Scope label, origin context, current tenant context mismatch banner, related-run context | Operations / operation run | What run is being viewed, what scope it belongs to, and whether the current context differs from the run tenant | remediation required |
|
||||||
|
| Operations | Workspace monitoring landing | Clickable row opens the tenantless operation detail viewer | required | Scope label, return navigation, and show-all behavior separate from tabs, filters, and list interaction | none | Existing Operations route | Existing tenantless operation detail route | Workspace scope, remembered tenant scope, active tab, explicit show-all reset | Operations / operation run | Whether the view is tenant-prefiltered, which operational state tab is active, and what class of runs needs attention | remediation required |
|
||||||
|
| Alerts | Workspace monitoring landing | Page-level overview with drilldown into related alert destinations and deliveries | not applicable | Scope and return navigation stay quiet and distinct from alert overview content and related navigation | none | Existing Alerts overview route | Existing alert delivery and destination drilldowns | Workspace scope, origin context | Alerts | Current alert health and KPI context | minor alignment only |
|
||||||
|
| Audit Log | Workspace audit history | Explicit inspect action opens selected event detail without changing the page type | forbidden | Scope and return remain in the header; selected-event inspection and related links remain subordinate to audit history | none | Existing Audit Log route | Same page with selected event inspection state | Workspace or tenant prefilter, audit filters, selected event identity | Audit log / audit event | Which event is selected and whether the list is filtered | minor alignment only |
|
||||||
|
| List Alert Deliveries | Workspace delivery history resource list | Existing alert-delivery inspect flow remains the primary open model | allowed | Shared OperateHubShell scope and return actions stay quiet; list-level utilities remain separate from resource inspection | none | Existing alert deliveries resource index | Existing alert-delivery resource detail or inspect path | Workspace or remembered-tenant scope | Alert deliveries / alert delivery | Delivery history and current scope | minor alignment only |
|
||||||
|
| Evidence Overview | Workspace bounded-scope monitoring report | Clickable row opens the tenant-scoped evidence snapshot detail | required | Single clear-filters utility stays separate from the report body; no additional layering required | none | Existing Evidence Overview route | Existing tenant evidence snapshot view route | Optional tenant prefilter only | Evidence overview / evidence snapshot | Current active evidence freshness and next step by tenant | compliant / no-op |
|
||||||
|
| Baseline Compare Matrix | Tenant focused monitoring matrix | Matrix page itself is the focused inspect surface for a selected baseline profile and subject | forbidden | Existing focused compare actions remain local to the matrix context; no extra header hierarchy is required | none | Existing Baseline Compare landing or profile compare entry | Existing compare matrix route | Active tenant, baseline profile, selected subject | Baseline compare matrix | Compare results for one focused profile and subject | compliant / no-op |
|
||||||
|
| Baseline Compare Landing | Tenant bounded-scope monitoring landing | Page-level landing remains the canonical monitoring entry for baseline compare status | not applicable | Existing landing actions remain tied to compare state and diagnostics without introducing competing header layers | none | Existing Baseline Compare landing route | Existing compare matrix and finding drilldowns | Active tenant, current compare state, profile context | Baseline compare | Compare coverage, evidence gaps, and next step | compliant / no-op |
|
||||||
|
| Review Register | Workspace bounded-scope review register | Clickable row opens the tenant review detail | required | Single clear-filters utility remains sufficient; register actions stay list-scoped and calm | none | Existing Review Register route | Existing tenant review detail route | Tenant filter and review-state filters | Review register / tenant review | Review truth, publication readiness, and next step | compliant / no-op |
|
||||||
|
| Tenant Diagnostics | Tenant singleton diagnostic workbench | The diagnostics page itself is the focused diagnostic surface for the current tenant | forbidden | No quiet navigation layer is required beyond normal tenant context; repair actions remain visible only when inconsistency exists | Repair actions remain capability-gated, confirmed, and isolated to the diagnostics exception surface | Existing tenant diagnostics route | Same page | Active tenant, missing-owner state, duplicate-membership state | Tenant diagnostics / tenant repair | Whether the tenant has repair-needed membership inconsistencies | special-type acceptable |
|
||||||
|
|
||||||
|
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Primary Persona | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Finding Exceptions Queue | Workspace approver | Queue workbench | What scope am I reviewing, is anything selected, and what decision is available right now? | Queue scope, filters, selected exception summary, request status, governance validity | Deep related finding context and tenant detail drilldowns | queue state, validity, selection state | `TenantPilot only` | `Approve exception` or `Reject exception` only when a pending exception is selected | Review decisions are destructive-like governance actions and stay confirmed in the selected-object layer |
|
||||||
|
| Tenantless Operation Run Viewer | Operator or manager monitoring one run | Monitoring detail viewer | What run is this, how does it relate to my current scope, and do I need to refresh, resume, or open something related? | Run identity, current outcome, canonical context banner, lifecycle banner, restore continuation context | Related links, redaction integrity detail, deeper failure explanations | execution outcome, freshness, lifecycle attention, context mismatch | Existing run-linked scopes only | `Refresh` stays utility; resumable action appears only when applicable | none |
|
||||||
|
| Operations | Workspace operator | Monitoring landing | Which run class needs attention, and am I looking at one tenant or all tenants? | KPI widgets, active tab, tenant scope, run table | Deep run detail after drilldown | execution status, outcome, problem class, scope | read-only landing | Tab switch and inspect flow only | none |
|
||||||
|
| Alerts | Workspace operator | Monitoring landing | Are alerts healthy, and where should I drill down next? | Alert KPIs and current scope | Delivery-detail drilldowns and destination management live downstream | alert health, delivery activity | Existing alert-management scopes only | Existing quiet overview and related drilldowns | none |
|
||||||
|
| Audit Log | Workspace operator or auditor | History and inspection surface | What happened, in which scope, and which event needs inspection? | Audit history, filters, selected event context | Event detail body and related target drilldown | audit outcome, actor type, target type, scope | read-only history | `Inspect event` remains the inspect affordance | none |
|
||||||
|
| List Alert Deliveries | Workspace operator | Delivery history list | Which deliveries succeeded or failed, and what scope am I in? | Delivery history and current scope | Delivery-specific inspection downstream | delivery outcome and scope | read-only history | Existing inspect flow only | none |
|
||||||
|
| Evidence Overview | Workspace operator | Read-only registry report | Which tenants have current usable evidence, and what is the next step? | Tenant rows, artifact truth, freshness, next step | Downstream evidence snapshot details | artifact truth, freshness | read-only landing | Clear filters and row drilldown only | none |
|
||||||
|
| Baseline Compare Matrix | Tenant operator | Focused monitoring matrix | What does this compare show for the currently focused profile and subject? | Compare matrix content and focused subject | Deeper compare diagnostics already inside the surface | coverage and drift | read-only focused analysis | Existing focused compare affordances only | none |
|
||||||
|
| Baseline Compare Landing | Tenant operator | Bounded-scope monitoring landing | Is compare current, trustworthy, and what should I inspect next? | Compare state, profile identity, findings count, evidence-gap summary | Deep diagnostics and matrix/finding drilldowns | compare state, coverage, fidelity, evidence gaps | Existing compare and drilldown scopes only | Existing compare-state actions only | none |
|
||||||
|
| Review Register | Workspace reviewer | Read-only register | Which reviews are current, complete, and ready for the next lifecycle step? | Review truth, completeness, publication readiness, next step | Downstream review detail | lifecycle, completeness, publication readiness | Existing review export scope when present | Row drilldown and existing scoped export affordance | none |
|
||||||
|
| Tenant Diagnostics | Tenant owner or manager | Diagnostic exception surface | Is this tenant structurally broken, and what repair action is justified right now? | Missing-owner and duplicate-membership state | None beyond the diagnostic evidence already on the page | repair-needed state | `TenantPilot only` | Repair actions only when a defect exists | `Bootstrap owner` and `Merge duplicate memberships` remain confirmed and capability-gated |
|
||||||
|
|
||||||
|
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||||
|
|
||||||
|
- **New source of truth?**: no
|
||||||
|
- **New persisted entity/table/artifact?**: no
|
||||||
|
- **New abstraction?**: no
|
||||||
|
- **New enum/state/reason family?**: no
|
||||||
|
- **New cross-domain UI framework/taxonomy?**: yes
|
||||||
|
- **Current operator problem**: Monitoring and workbench surfaces currently hide action meaning inside one flat header lane, which slows queue review, weakens scope awareness, and makes it harder to tell whether an action is global, contextual, or selection-bound.
|
||||||
|
- **Existing structure is insufficient because**: Spec 192 intentionally governs classic record pages, but monitoring and workbench surfaces have a different interaction model with scope context, selected-object state, and read-mostly utilities. Applying record-page rules directly would either flatten legitimate workbench behavior or leave the current ambiguity untouched.
|
||||||
|
- **Narrowest correct implementation**: Define the rule only for the named monitoring and workbench surfaces, remediate only the three clearly problematic pages, allow minor alignment on shared-pattern neighbors, preserve already calm bounded-scope surfaces, and document the single acceptable special type.
|
||||||
|
- **Ownership cost**: Ongoing review discipline for this surface class, modest browser and regression-test maintenance, and explicit documentation of exceptions and shared patterns.
|
||||||
|
- **Alternative intentionally rejected**: Treating these pages as ordinary record headers or doing only local one-off cleanup was rejected because both options would leave the surface-class semantics undocumented and would not protect new monitoring pages from drifting back into mixed header patterns.
|
||||||
|
- **Release truth**: current-release operator clarity and workbench surface discipline
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Review a queue without header ambiguity (Priority: P1)
|
||||||
|
|
||||||
|
As a workspace approver using a monitoring queue, I want the page to separate scope, utilities, selected-record state, and decision actions so I can immediately see whether there is something actionable right now.
|
||||||
|
|
||||||
|
**Why this priority**: Finding Exceptions Queue is the clearest workbench failure in the current scope. If it remains a flat mixed header, the spec has not solved its primary operator problem.
|
||||||
|
|
||||||
|
**Independent Test**: Open the queue with and without a selected exception and confirm that the page visibly changes from quiet monitoring mode to active workbench mode without leaving scope or navigation actions at the same level as approve or reject.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** the queue has no selected exception, **When** the page renders, **Then** scope and utility actions remain visible while selected-object decision actions are not prominent.
|
||||||
|
2. **Given** a pending exception is selected, **When** the page renders, **Then** selection-bound decision actions become the prominent work actions and scope or drilldown links do not read as peer actions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Read one operation run without mixed context signals (Priority: P1)
|
||||||
|
|
||||||
|
As an operator opening a tenantless operation run, I want return navigation, scope context, refresh, related links, and resumable actions to be visibly separated so the viewer reads as a monitoring detail surface instead of a record-page button bar.
|
||||||
|
|
||||||
|
**Why this priority**: The run viewer is a canonical monitoring detail surface and a common drilldown from Operations. If it keeps mixing navigation and run actions, the repo still lacks a valid pattern for monitoring detail pages.
|
||||||
|
|
||||||
|
**Independent Test**: Open the viewer from Operations and from another origin context, then verify that back navigation stays quieter than refresh or resumable actions and that related links do not crowd the main action lane.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** the viewer has an origin context, **When** the page renders, **Then** the back affordance is visually distinct from utility and related work actions.
|
||||||
|
2. **Given** the viewed run has no resumable action, **When** the page renders, **Then** the header does not reserve equal prominence for a missing action and remains calm.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Preserve calm monitoring pages without forced churn (Priority: P2)
|
||||||
|
|
||||||
|
As a product reviewer, I want already calm monitoring pages to be explicitly confirmed as compliant or bounded-scope no-ops so the cleanup does not turn into cosmetic standardization.
|
||||||
|
|
||||||
|
**Why this priority**: The spec should sharpen meaningful hierarchy, not create churn on pages that already convey one clear monitoring question.
|
||||||
|
|
||||||
|
**Independent Test**: Review the bounded-scope reference pages and confirm that they either remain unchanged or receive only documented minor alignment, with no artificial new header structure added.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a bounded-scope report page such as Evidence Overview or Review Register, **When** the feature is reviewed, **Then** it remains calm and is not forced into extra action layers it does not need.
|
||||||
|
2. **Given** a page is already sufficiently quiet, **When** the spec is applied, **Then** the page is classified as compliant or minor alignment only rather than remediated by default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 4 - Keep special diagnostic surfaces explicit (Priority: P3)
|
||||||
|
|
||||||
|
As a reviewer, I want special-type monitoring surfaces to be explicitly marked so necessary diagnostic repair actions can exist without weakening the broader monitoring hierarchy rule.
|
||||||
|
|
||||||
|
**Why this priority**: The spec must allow narrow exceptions without creating silent inconsistency.
|
||||||
|
|
||||||
|
**Independent Test**: Review Tenant Diagnostics and verify that it remains a documented exception whose repair actions appear only when the diagnostic condition exists.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** Tenant Diagnostics shows no repair-needed condition, **When** the page renders, **Then** no repair action is promoted.
|
||||||
|
2. **Given** a repair-needed state exists, **When** the page renders, **Then** the required repair action is available with confirmation and explicit exception documentation.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- If a monitoring page is in workspace scope with no remembered tenant context, the scope layer must still read clearly and must not imply tenant-specific actionability.
|
||||||
|
- If a workspace monitoring page is filtered to one tenant, the operator must still be able to understand whether the surface is tenant-prefiltered or truly tenant-bound.
|
||||||
|
- If no selection exists on a queue or workbench surface, selection-bound actions must not keep placeholder prominence.
|
||||||
|
- If a selected object becomes unavailable after filters change, the surface must fall back to a calm no-selection state instead of keeping stale focused-object actions visible.
|
||||||
|
- If a run viewer opens a tenant-owned run while a different tenant is active in context, the page must show the context mismatch clearly without treating the scope banner as a call to action.
|
||||||
|
- If a shared pattern like OperateHubShell supplies scope and return affordances, those affordances must still be subordinate to the actual work state on the page.
|
||||||
|
- If a special-type diagnostic surface has no active inconsistency, it must not manufacture work actions just to match other workbench pages.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
**Constitution alignment (required):** This feature introduces no new Microsoft Graph contract, no new persistence, and no new queued work. It reorganizes action hierarchy and surface semantics only. Existing mutations such as exception approval, exception rejection, diagnostics repair, compare actions, and run-related resumes keep their current confirmation, audit, and run-observability behavior.
|
||||||
|
|
||||||
|
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** The feature introduces only a bounded surface-class contract and classification matrix for monitoring and workbench pages. It adds no new persistence, no new state family, and no new action framework. The proportionality review above explains why a local-only cleanup is too weak and why a broader framework is intentionally rejected.
|
||||||
|
|
||||||
|
**Constitution alignment (OPS-UX):** Existing operations visible from these surfaces continue to use their current run lifecycle, queued toast, progress surfaces, and terminal notification rules. This spec does not create or rename any operation type, does not alter service-owned `OperationRun` transitions, and does not change summary count semantics.
|
||||||
|
|
||||||
|
**Constitution alignment (RBAC-UX):** The feature spans workspace monitoring pages and tenant-bound monitoring pages but does not change authorization logic. Non-members remain `404`, members lacking capability remain `403`, selection-bound or repair actions still enforce server-side authorization, and destructive-like actions continue to require confirmation. At least one positive and one negative authorization regression must verify that moving actions between layers does not loosen access.
|
||||||
|
|
||||||
|
**Constitution alignment (OPS-EX-AUTH-001):** Not applicable. No authentication handshake behavior changes.
|
||||||
|
|
||||||
|
**Constitution alignment (BADGE-001):** This feature does not add or change badge semantics. Existing badge mappings remain centralized and unchanged.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-FIL-001):** The feature must use native Filament actions, action groups, page sections, and existing shared primitives such as OperateHubShell. It must avoid creating page-local button frameworks, ad-hoc status language, or custom visual taxonomies for scope or selection state. The only approved exception is keeping Tenant Diagnostics as a focused diagnostic repair surface with its existing capability-gated actions.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-NAMING-001):** Operator-facing action labels must stay domain-first and consistent while their placement changes. Examples include `Approve exception`, `Reject exception`, `Refresh`, `Open`, `Close details`, `Show all tenants`, `Bootstrap owner`, and `Merge duplicate memberships`. Scope labels must read as context rather than as equivalent action verbs.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001):** This spec classifies each affected surface, defines its single inspect or open model, states whether row click is required, allowed, or forbidden, and assigns secondary, related, and destructive actions to explicit layers. Monitoring and workbench pages must not silently inherit record-page action assumptions.
|
||||||
|
|
||||||
|
**Constitution alignment (OPSURF-001):** Default-visible content remains operator-first. Scope, navigation, selection state, and next action must be legible without revealing raw implementation detail. Diagnostics stay secondary or downstream, dangerous actions keep confirmation and mutation-scope language, and workspace or tenant context remains explicit in navigation and page semantics.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** The feature does not introduce a new presenter or semantic interpretation layer. It uses direct action placement, state-driven visibility, and explicit page classification instead of a new UI meta-framework. Tests must validate user-visible action hierarchy and authorization continuity rather than thin indirection.
|
||||||
|
|
||||||
|
**Constitution alignment (Filament Action Surfaces):** The Action Surface Contract is satisfied for remediated monitoring and workbench pages when each affected surface has one inspect or open model, no redundant peer navigation buttons, no empty action groups, and destructive-like actions only in the appropriate selected-object or exception layer. Any page that intentionally differs must be catalogued as a documented exception.
|
||||||
|
|
||||||
|
**Constitution alignment (UX-001 — Layout & Information Architecture):** This feature changes action hierarchy and contextual placement only. It does not justify any regression in existing table filters, empty states, infolists, or form layouts. Monitoring tables must keep their current search, sort, filter, and empty-state contracts while the header hierarchy becomes calmer.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-193-001 Surface inventory**: The repo MUST maintain an explicit inventory of every in-scope monitoring, queue, operations, and workbench surface covered by this spec.
|
||||||
|
- **FR-193-002 Explicit classification**: Every in-scope surface MUST be assigned exactly one classification: `remediation required`, `minor alignment only`, `compliant / no-op`, or `special-type acceptable`.
|
||||||
|
- **FR-193-003 Monitoring surface action layers**: Remediated monitoring and workbench surfaces MUST separate actions into clearly distinguishable layers drawn from scope or context, navigation, surface utility, selection or focused-object actions, and related or drilldown actions.
|
||||||
|
- **FR-193-004 Scope-is-context rule**: Scope labels, remembered-tenant context, origin context, and active workbench context MUST not appear as peer call-to-action buttons when they are semantically contextual only.
|
||||||
|
- **FR-193-005 Navigation separation rule**: Back, return, show-all, and related-open navigation MUST not share the same primary action lane as live work actions when doing so makes the next step ambiguous.
|
||||||
|
- **FR-193-006 Selection prominence rule**: Selection-bound or focused-object actions MUST become prominent only when a valid selection or focused object exists.
|
||||||
|
- **FR-193-007 No-selection quiet state**: Queue and workbench surfaces MUST render a calm monitoring state when no selection or focused object is active.
|
||||||
|
- **FR-193-008 Work-state transition rule**: Remediated workbench surfaces MUST visibly change hierarchy between no-selection, selected-object, global monitoring, and related drilldown states instead of keeping one static header layout.
|
||||||
|
- **FR-193-009 Finding Exceptions Queue hierarchy**: Finding Exceptions Queue MUST stop presenting scope, clear filters, close details, drilldown links, and exception decisions as one flat peer row. Scope and utility actions remain globally visible, drilldown links move to a related layer, and `Approve exception` or `Reject exception` become prominent only when a pending exception is selected.
|
||||||
|
- **FR-193-010 Queue decision visibility**: Finding Exceptions Queue MUST not promote exception decision actions when the selected exception is absent, resolved, or otherwise not decision-ready.
|
||||||
|
- **FR-193-011 Tenantless run viewer hierarchy**: Tenantless Operation Run Viewer MUST separate scope context, back navigation, show-all navigation, refresh utility, related links, and resumable run actions into distinct layers so the page reads as a monitoring viewer rather than a record-page action strip.
|
||||||
|
- **FR-193-012 Viewer navigation discipline**: Tenantless Operation Run Viewer MUST keep origin or return navigation visually subordinate to refresh and any run-specific follow-up action, and related links MUST not appear as equal peers to scope or refresh.
|
||||||
|
- **FR-193-013 Operations landing hierarchy**: Operations MUST stop using the header as a mixed context-navigation strip. Scope context and show-all behavior remain visible but quieter than list filters, tab state, and row inspect flow.
|
||||||
|
- **FR-193-014 Shared-pattern tightening**: Shared OperateHubShell patterns may remain in use, but any page using them MUST still subordinate scope and return affordances to the actual work or monitoring state of that page.
|
||||||
|
- **FR-193-015 Minor alignment audit**: Alerts, Audit Log, and List Alert Deliveries MUST be reviewed against the same hierarchy but changed only where real action-layer ambiguity exists.
|
||||||
|
- **FR-193-016 Compliant bounded-scope preservation**: Evidence Overview, Baseline Compare Matrix, Baseline Compare Landing, and Review Register MUST remain unchanged or receive only minimal documented alignment when their current bounded-scope semantics are already clear.
|
||||||
|
- **FR-193-017 Special-type exception contract**: Tenant Diagnostics MUST be explicitly marked as a special-type acceptable surface whose repair actions are allowed because the page is itself a focused diagnostic exception surface, not a general monitoring list.
|
||||||
|
- **FR-193-018 No record-header leakage**: Monitoring and workbench surfaces MUST NOT silently inherit classic record-page rules from Spec 192 where those rules would hide workbench state or selection semantics.
|
||||||
|
- **FR-193-019 No governance-friction expansion**: This feature MUST NOT change confirmation depth, reason-capture behavior, danger-language policy, dispatch semantics, or provider-start semantics beyond what underlying actions already own.
|
||||||
|
- **FR-193-020 Authorization continuity**: Moving, regrouping, or relabeling actions MUST NOT change route scope, capability enforcement, deny-as-not-found behavior, or audit obligations.
|
||||||
|
- **FR-193-021 Vocabulary continuity**: Scope, navigation, utility, and work actions MUST keep consistent domain vocabulary across buttons, modal titles, notifications, and audit prose while the hierarchy changes.
|
||||||
|
- **FR-193-022 Regression guard**: The repo MUST add a lightweight project-wide guard that prevents future monitoring or workbench surfaces from reintroducing scope-as-CTA patterns, flat global-plus-selection header mixes, or undocumented surface-class exceptions.
|
||||||
|
- **FR-193-023 Browser verification**: Browser or UI smoke checks MUST cover every remediated surface, the documented special-type exception, and a no-regression subset of the compliant bounded-scope pages.
|
||||||
|
|
||||||
|
## Surface Decision Matrix
|
||||||
|
|
||||||
|
- **Remediation required**:
|
||||||
|
- Finding Exceptions Queue
|
||||||
|
- Tenantless Operation Run Viewer
|
||||||
|
- Operations
|
||||||
|
- **Minor alignment only**:
|
||||||
|
- Alerts
|
||||||
|
- Audit Log
|
||||||
|
- List Alert Deliveries
|
||||||
|
- **Compliant / no-op**:
|
||||||
|
- Evidence Overview
|
||||||
|
- Baseline Compare Matrix
|
||||||
|
- Baseline Compare Landing
|
||||||
|
- Review Register
|
||||||
|
- **Special-type acceptable**:
|
||||||
|
- Tenant Diagnostics
|
||||||
|
|
||||||
|
## Target Outcomes by Key Surface
|
||||||
|
|
||||||
|
- **Finding Exceptions Queue**: The page no longer reads as one flat strip of scope, close, open, and decision actions. Without a selected exception it behaves like a quiet monitoring queue. With a selected pending exception it becomes a focused review workbench.
|
||||||
|
- **Tenantless Operation Run Viewer**: Scope and return context no longer compete with refresh, open-related, or resumable actions. The header reflects a monitoring viewer instead of a record-detail hybrid.
|
||||||
|
- **Operations**: Scope and origin context no longer dominate the header. The page reads as a monitoring landing surface where tab state, filters, and inspect flow carry the work.
|
||||||
|
- **Alerts, Audit Log, and List Alert Deliveries**: Shared monitoring patterns are checked and tightened only where hierarchy is still ambiguous.
|
||||||
|
- **Evidence Overview, Baseline Compare Matrix, Baseline Compare Landing, and Review Register**: Calm bounded-scope monitoring pages are explicitly preserved as references, not cosmetically rebuilt.
|
||||||
|
- **Tenant Diagnostics**: Diagnostic repair actions remain available only as a documented exception surface rather than as evidence that every monitoring page may promote repairs in the same way.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Applying record-page header rules to monitoring and workbench surfaces
|
||||||
|
- Introducing a new global danger, confirmation, or reason-capture policy
|
||||||
|
- Redesigning dispatch, preflight, provider-start, or queue semantics
|
||||||
|
- Reworking list, row, or bulk action contracts outside this surface class
|
||||||
|
- Forcing a full monitoring redesign across every page in the admin panel
|
||||||
|
- Rebuilding calm bounded-scope pages for cosmetic consistency alone
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- Spec 192 remains the rule set for classic record detail and edit pages, and this spec is the companion rule set for monitoring and workbench surfaces.
|
||||||
|
- Existing authorization, audit logging, and run-observability behavior on the underlying actions is already correct and will be preserved.
|
||||||
|
- OperateHubShell remains a valid shared pattern, but not a blanket exemption from action-hierarchy review.
|
||||||
|
- The named compliant pages are bounded enough that a stronger multi-layer header would add noise rather than clarity.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- The constitution rule for action-surface discipline and the separation already established by Spec 192
|
||||||
|
- Existing OperateHubShell scope and return affordances
|
||||||
|
- Existing Filament action surfaces, tables, empty states, and tenant/workspace scope helpers
|
||||||
|
- Existing browser-smoke and action-surface regression infrastructure
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- The feature could drift into a generic multi-surface framework if the scope is not kept strictly to monitoring and workbench pages.
|
||||||
|
- Shared-pattern loyalty could cause real hierarchy issues to be excused as intentional infrastructure.
|
||||||
|
- Selection-aware pages could hide useful actions too aggressively if the no-selection state is not balanced against active-work state.
|
||||||
|
- Calm bounded-scope pages could be pulled into unnecessary churn if the no-op classification is not taken seriously.
|
||||||
|
|
||||||
|
## Review Questions
|
||||||
|
|
||||||
|
- Can a reviewer tell within a few seconds which surface state is active: scope only, selected object, or related drilldown?
|
||||||
|
- Do scope labels now read as context instead of as peer calls to action?
|
||||||
|
- Are navigation actions clearly calmer than actual work actions on remediated pages?
|
||||||
|
- Are already calm monitoring pages preserved instead of standardized for their own sake?
|
||||||
|
- Is Tenant Diagnostics clearly documented as an exception rather than a silent contradiction?
|
||||||
|
|
||||||
|
## 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 |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Finding Exceptions Queue | `apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php` | Shared scope and return affordances remain quiet; `Clear filters` stays utility; `Close details`, `Open tenant detail`, and `Open finding` move out of the same peer lane as `Approve exception` and `Reject exception` | Existing explicit inspect or select flow remains the only open model | Existing inspect flow remains; no extra visible row actions are added | none | Existing queue empty-state CTA remains | Header becomes `context + utility + related links + selected-object actions` instead of one flat row | n/a | Existing exception approval and rejection audit behavior unchanged | Remediation-required workbench page |
|
||||||
|
| Tenantless Operation Run Viewer | `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` | Scope label, back or origin, show-all, refresh, `Open` group, and resumable action are reordered into distinct layers rather than equal peers | Page itself is the canonical detail destination | n/a | none | n/a | Viewer header is split into context, navigation, utility, related, and follow-up layers; no redundant peer links | n/a | Existing resume and follow-up audit behavior unchanged | Remediation-required monitoring detail page |
|
||||||
|
| Operations | `apps/platform/app/Filament/Pages/Monitoring/Operations.php` | Scope label, return, and `Show all tenants` remain quiet and do not act as a mixed primary action strip | `recordUrl()` and clickable row remain the only inspect model | Existing row click remains primary open affordance | none | Existing table empty state unchanged | Header is reduced to context and scope reset only; tabs and filters remain the work controls | n/a | Read-only landing; no new audit behavior | Remediation-required monitoring landing |
|
||||||
|
| Alerts | `apps/platform/app/Filament/Pages/Monitoring/Alerts.php` | Shared scope and origin affordances remain quiet; only minor tightening if origin link still competes visually with overview behavior | Page-level overview only | n/a | none | Existing overview empty state unchanged | No structural rebuild unless audit finds real ambiguity | n/a | Existing downstream alert-management audit behavior unchanged | Minor alignment only |
|
||||||
|
| Audit Log | `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php` | Shared scope and return remain quiet; selected-event inspection continues downstream rather than being elevated into mixed work actions | Explicit `Inspect event` remains the inspect model | `Inspect event` remains the visible row action | none | `Clear filters` remains the only empty-state CTA | Header remains calm; no new action lane unless minor ambiguity is found | n/a | Read-only audit history; no new audit behavior | Minor alignment only |
|
||||||
|
| List Alert Deliveries | `apps/platform/app/Filament/Resources/AlertDeliveryResource/Pages/ListAlertDeliveries.php` | Shared scope and return affordances remain as quiet context; no added peer actions | Existing resource inspect flow remains unchanged | Existing resource row actions unchanged | Existing resource bulk actions unchanged | Existing resource empty state unchanged | No structural rebuild unless shared pattern still reads as mixed CTA | n/a | Existing delivery-history behavior unchanged | Minor alignment only |
|
||||||
|
| Evidence Overview | `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php` | Single `Clear filters` utility remains sufficient and unchanged | Clickable row remains the inspect model | Single row drilldown remains | none | Existing clear-filters empty or header CTA remains | No added layering required | n/a | Read-only report | Compliant / no-op reference |
|
||||||
|
| Baseline Compare Matrix | `apps/platform/app/Filament/Pages/BaselineCompareMatrix.php` | Existing focused compare affordances remain local to the matrix context | Matrix page itself remains the inspect surface | n/a | none | Existing matrix empty state unchanged | No header rebuild required | n/a | Read-only focused analysis | Compliant / no-op reference |
|
||||||
|
| Baseline Compare Landing | `apps/platform/app/Filament/Pages/BaselineCompareLanding.php` | Existing compare-state actions remain tied to current compare truth and are not expanded into a layered header unless review finds ambiguity | Page-level landing remains canonical | n/a | none | Existing landing empty or state CTAs remain | No structural rebuild required | n/a | Existing compare-run behavior unchanged | Compliant / no-op reference |
|
||||||
|
| Review Register | `apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php` | Single `Clear filters` utility remains sufficient; no extra header layering required | Clickable row remains the inspect model | Existing `Export executive pack` row action remains scoped and subordinate | none | Existing clear-filters empty-state CTA remains | No header rebuild required | n/a | Existing review export audit behavior unchanged | Compliant / no-op reference |
|
||||||
|
| Tenant Diagnostics | `apps/platform/app/Filament/Pages/TenantDiagnostics.php` | Capability-gated repair actions remain on the page only when a defect exists; no extra context buttons are added to mimic other pages | Page itself remains the focused diagnostic surface | n/a | none | n/a | Repair actions remain explicit exception-surface actions with confirmation | n/a | Existing repair audit behavior unchanged | Special-type acceptable exception |
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Monitoring Surface Classification**: The explicit catalog assigning each in-scope monitoring or workbench page to remediation required, minor alignment only, compliant or no-op, or special-type acceptable.
|
||||||
|
- **Action Layer Contract**: The bounded rule that separates scope or context, navigation, surface utility, selection or focused-object work, and related or drilldown behavior on monitoring surfaces.
|
||||||
|
- **Selection State**: The operator-visible distinction between no-selection monitoring mode and active-selection workbench mode.
|
||||||
|
- **Scope Signal**: The context element that shows workspace or tenant scope, origin, or remembered context without behaving like a peer call to action.
|
||||||
|
- **Special-Type Diagnostic Exception**: The documented allowance for a focused diagnostic page to expose repair actions when inconsistency exists without weakening the general monitoring hierarchy rule.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-193-001**: 100% of in-scope monitoring and workbench surfaces are explicitly classified in the spec and implementation notes.
|
||||||
|
- **SC-193-002**: 100% of remediation-required surfaces show visibly separate context or scope, navigation, utility, and selected-object or related-action layers in acceptance review.
|
||||||
|
- **SC-193-003**: On remediated queue or workbench surfaces, selected-object actions are absent or clearly subordinate when no valid selection exists and become prominent only when a valid selection exists.
|
||||||
|
- **SC-193-004**: During acceptance walkthroughs, reviewers can correctly identify current scope, whether anything is selected, and the next actionable step on each remediated surface within 5 seconds.
|
||||||
|
- **SC-193-005**: No compliant or no-op reference page receives a structural rebuild unless a documented minor-alignment finding exists.
|
||||||
|
- **SC-193-006**: Regression coverage fails any newly introduced monitoring surface that treats scope as a peer CTA, mixes selection-bound actions with global surface controls in one flat lane, or introduces an undocumented exception.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
This feature is complete when:
|
||||||
|
|
||||||
|
- every in-scope monitoring and workbench surface is classified,
|
||||||
|
- every remediated surface shows explicit action layers instead of a flat mixed header,
|
||||||
|
- scope and context no longer read as peer CTAs on remediated pages,
|
||||||
|
- selection-bound work actions only become prominent in an active work state,
|
||||||
|
- navigation and work actions are clearly separated,
|
||||||
|
- shared monitoring patterns are either confirmed or intentionally tightened,
|
||||||
|
- calm bounded-scope pages are explicitly preserved,
|
||||||
|
- a lightweight regression guard exists for this surface class,
|
||||||
|
- and browser smoke checks confirm the visible hierarchy on remediated and exception surfaces.
|
||||||
|
|
||||||
|
## Recommended Sequencing
|
||||||
|
|
||||||
|
- Spec 194 should follow with governance friction hardening so structure and friction rules remain separate concerns.
|
||||||
245
specs/193-monitoring-action-hierarchy/tasks.md
Normal file
245
specs/193-monitoring-action-hierarchy/tasks.md
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
# Tasks: Monitoring Surface Action Hierarchy and Workbench Semantics
|
||||||
|
|
||||||
|
**Input**: Design documents from `/specs/193-monitoring-action-hierarchy/`
|
||||||
|
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `contracts/monitoring-action-hierarchy.logical.openapi.yaml`, `quickstart.md`
|
||||||
|
|
||||||
|
**Tests**: Required. This feature changes runtime behavior on existing Filament v5 / Livewire v4 monitoring surfaces, so Pest guard, feature, RBAC, and browser smoke coverage must be added or extended.
|
||||||
|
|
||||||
|
**Organization**: Tasks are grouped by user story so each slice stays independently testable. Recommended delivery order is `US1 -> US2 -> US3 -> US4`, with `US1` as the MVP cut after the shared guard foundation is in place.
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Infrastructure)
|
||||||
|
|
||||||
|
**Purpose**: Prepare dedicated test entry points for the monitoring-surface hierarchy slice.
|
||||||
|
|
||||||
|
- [X] T001 Create the Spec 193 guard test scaffold in `apps/platform/tests/Feature/Guards/Spec193MonitoringSurfaceHierarchyGuardTest.php`
|
||||||
|
- [X] T002 [P] Create focused monitoring hierarchy test scaffolds in `apps/platform/tests/Feature/Monitoring/FindingExceptionsQueueHierarchyTest.php` and `apps/platform/tests/Feature/Monitoring/OperationsHeaderHierarchyTest.php`
|
||||||
|
- [X] T003 [P] Create the browser smoke scaffold in `apps/platform/tests/Browser/Spec193MonitoringSurfaceHierarchySmokeTest.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Dedicated Spec 193 test entry points exist and the implementation can proceed without mixing this slice into unrelated suites.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Codify the shared monitoring-surface inventory and validation rules that every user story depends on.
|
||||||
|
|
||||||
|
**⚠️ CRITICAL**: No user story work should start before this phase is complete.
|
||||||
|
|
||||||
|
- [X] T004 [P] Add Spec 193 inventory contract expectations in `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`
|
||||||
|
- [X] T005 [P] Add Spec 193 validation-rule expectations in `apps/platform/tests/Feature/Guards/ActionSurfaceValidatorTest.php`
|
||||||
|
- [X] T006 [P] Add completeness and exception-reason coverage in `apps/platform/tests/Feature/Guards/Spec193MonitoringSurfaceHierarchyGuardTest.php`
|
||||||
|
- [X] T007 Implement the Spec 193 monitoring-surface inventory and retire the blanket Alerts exemption in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceExemptions.php`
|
||||||
|
- [X] T008 Implement Spec 193 classification and exception validation in `apps/platform/app/Support/Ui/ActionSurface/ActionSurfaceValidator.php`
|
||||||
|
|
||||||
|
**Checkpoint**: The repo can enumerate, classify, and fail CI on undocumented monitoring-surface hierarchy regressions before any page-level refactor starts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Review a Queue Without Header Ambiguity (Priority: P1) 🎯 MVP
|
||||||
|
|
||||||
|
**Goal**: Make Finding Exceptions Queue read as a quiet monitoring surface when nothing is selected and as a focused review workbench only when a decision-ready exception is active.
|
||||||
|
|
||||||
|
**Independent Test**: Open the queue with and without a selected exception and confirm that scope, utility, drilldown, and decision actions no longer render as one flat peer strip.
|
||||||
|
|
||||||
|
### Tests for User Story 1
|
||||||
|
|
||||||
|
> **NOTE**: Write these tests first and confirm they fail before implementation.
|
||||||
|
|
||||||
|
- [X] T009 [P] [US1] Add no-selection vs selected-workbench hierarchy coverage in `apps/platform/tests/Feature/Monitoring/FindingExceptionsQueueHierarchyTest.php`
|
||||||
|
- [X] T010 [P] [US1] Extend approval and rejection authorization continuity coverage in `apps/platform/tests/Feature/Rbac/ActionSurfaceRbacSemanticsTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [X] T011 [US1] Refactor layered header actions and selected-state visibility in `apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php`
|
||||||
|
- [X] T012 [US1] Align queue detail, utility, and related-drilldown rendering in `apps/platform/resources/views/filament/pages/monitoring/finding-exceptions-queue.blade.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Finding Exceptions Queue is independently functional with a calm no-selection state and a distinct selected-exception work lane.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Read One Operation Run Without Mixed Context Signals (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Make Operations and the tenantless operation run viewer separate scope, navigation, utility, related links, and follow-up actions so the monitoring flow reads clearly from list to detail.
|
||||||
|
|
||||||
|
**Independent Test**: Open Operations and drill into the tenantless viewer from multiple origin contexts, then verify that return navigation stays quieter than refresh or run follow-up actions and that related links do not compete with the main work lane.
|
||||||
|
|
||||||
|
### Tests for User Story 2
|
||||||
|
|
||||||
|
> **NOTE**: Write these tests first and confirm they fail before implementation.
|
||||||
|
|
||||||
|
- [X] T013 [P] [US2] Extend viewer hierarchy and calm-no-action coverage in `apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php`
|
||||||
|
- [X] T014 [P] [US2] Add operations landing header hierarchy coverage in `apps/platform/tests/Feature/Monitoring/OperationsHeaderHierarchyTest.php`
|
||||||
|
- [X] T015 [P] [US2] Extend canonical navigation and related-link assertions in `apps/platform/tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php` and `apps/platform/tests/Feature/Monitoring/OperationsRelatedNavigationTest.php`
|
||||||
|
- [X] T016 [P] [US2] Extend shared scope-label and return-affordance coverage in `apps/platform/tests/Feature/OpsUx/OperateHubShellTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [X] T017 [US2] Refactor viewer context, navigation, utility, and follow-up action layering in `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
|
||||||
|
- [X] T018 [US2] Align the tenantless operation viewer header rendering in `apps/platform/resources/views/filament/pages/operations/tenantless-operation-run-viewer.blade.php`
|
||||||
|
- [X] T019 [US2] Refactor operations landing scope-reset and header hierarchy in `apps/platform/app/Filament/Pages/Monitoring/Operations.php`
|
||||||
|
- [X] T020 [US2] Align operations landing context-bar and header rendering in `apps/platform/resources/views/filament/pages/monitoring/operations.blade.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Operations and the tenantless run viewer are independently functional and clearly separate context, navigation, utility, and follow-up actions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Preserve Calm Monitoring Pages Without Forced Churn (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Explicitly classify shared-pattern monitoring pages and bounded-scope reference pages so only genuinely ambiguous surfaces change while calm pages remain calm.
|
||||||
|
|
||||||
|
**Independent Test**: Review Alerts, Audit Log, alert deliveries, Evidence Overview, Review Register, Baseline Compare Landing, and Baseline Compare Matrix and confirm they either remain calm or receive only documented minor alignment.
|
||||||
|
|
||||||
|
### Tests for User Story 3
|
||||||
|
|
||||||
|
> **NOTE**: Write these tests first and confirm they fail before implementation.
|
||||||
|
|
||||||
|
- [X] T021 [P] [US3] Add minor-alignment coverage for the Alerts overview in `apps/platform/tests/Feature/Monitoring/AlertsHierarchyTest.php` and alert-delivery calm/deep-link behavior in `apps/platform/tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php` and `apps/platform/tests/Feature/Alerts/AlertDeliveryDeepLinkFiltersTest.php`
|
||||||
|
- [X] T022 [P] [US3] Extend Audit Log minor-alignment coverage in `apps/platform/tests/Feature/Monitoring/AuditLogInspectFlowTest.php` and `apps/platform/tests/Feature/Filament/AuditLogPageTest.php`
|
||||||
|
- [X] T023 [P] [US3] Add calm-reference no-regression coverage for Evidence Overview and Review Register in `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php` and `apps/platform/tests/Feature/TenantReview/TenantReviewRegisterTest.php`
|
||||||
|
- [X] T024 [P] [US3] Add calm-reference no-regression coverage for Baseline Compare Landing and Baseline Compare Matrix in `apps/platform/tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php` and `apps/platform/tests/Feature/Filament/BaselineCompareMatrixPageTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [X] T025 [US3] Add explicit monitoring-surface declaration and origin-context alignment in `apps/platform/app/Filament/Pages/Monitoring/Alerts.php`
|
||||||
|
- [X] T026 [US3] Tighten shared-pattern hierarchy only where needed in `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php` and `apps/platform/app/Filament/Resources/AlertDeliveryResource/Pages/ListAlertDeliveries.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Minor-alignment pages are explicit and bounded, while calm reference pages stay independently testable without cosmetic rebuilds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: User Story 4 - Keep Special Diagnostic Surfaces Explicit (Priority: P3)
|
||||||
|
|
||||||
|
**Goal**: Preserve Tenant Diagnostics as the single documented diagnostic exception, with repair actions visible only when a real defect exists.
|
||||||
|
|
||||||
|
**Independent Test**: Open Tenant Diagnostics with and without repair-needed conditions and confirm that repair actions only appear when justified and remain protected by explicit exception coverage and existing access semantics.
|
||||||
|
|
||||||
|
### Tests for User Story 4
|
||||||
|
|
||||||
|
> **NOTE**: Write these tests first and confirm they fail before implementation.
|
||||||
|
|
||||||
|
- [X] T027 [P] [US4] Extend explicit exception-reason coverage in `apps/platform/tests/Feature/Guards/Spec193MonitoringSurfaceHierarchyGuardTest.php`
|
||||||
|
- [X] T028 [P] [US4] Extend tenant diagnostics repair-state coverage in `apps/platform/tests/Feature/Filament/TenantDiagnosticsRepairsTest.php`
|
||||||
|
- [X] T029 [P] [US4] Extend tenant diagnostics access semantics in `apps/platform/tests/Feature/TenantRBAC/TenantDiagnosticsAccessTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 4
|
||||||
|
|
||||||
|
- [X] T030 [US4] Update repair-action visibility and explicit special-type declaration handling in `apps/platform/app/Filament/Pages/TenantDiagnostics.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Tenant Diagnostics remains independently functional as an explicit exception surface without weakening the general monitoring hierarchy rule.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 7: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Purpose**: Lock the slice down with browser proof, copy review, and focused verification.
|
||||||
|
|
||||||
|
- [X] T031 [P] Add remediated, exception, and calm-reference browser smoke coverage in `apps/platform/tests/Browser/Spec193MonitoringSurfaceHierarchySmokeTest.php`
|
||||||
|
- [X] T032 Review operator-facing labels, modal titles, notifications, and audit prose in `apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php`, `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`, `apps/platform/app/Filament/Pages/Monitoring/Operations.php`, `apps/platform/app/Filament/Pages/Monitoring/Alerts.php`, `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php`, `apps/platform/app/Filament/Resources/AlertDeliveryResource/Pages/ListAlertDeliveries.php`, and `apps/platform/app/Filament/Pages/TenantDiagnostics.php`
|
||||||
|
- [X] T033 [P] Add explicit non-regression assertions for confirmation depth, reason capture, provider-dispatch semantics, and record-page header leakage in `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php` and `apps/platform/tests/Feature/Guards/Spec193MonitoringSurfaceHierarchyGuardTest.php`
|
||||||
|
- [X] T034 [P] Run the focused Sail verification and formatting workflow from `specs/193-monitoring-action-hierarchy/quickstart.md` against the changed guard, feature, browser, and page files
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- **Setup (Phase 1)**: No dependencies; can start immediately.
|
||||||
|
- **Foundational (Phase 2)**: Depends on Setup completion; blocks all user stories.
|
||||||
|
- **User Story 1 (Phase 3)**: Depends on Foundational completion; recommended MVP cut.
|
||||||
|
- **User Story 2 (Phase 4)**: Depends on Foundational completion; can run in parallel with US1 if capacity allows, but is easier to review after US1 establishes the pattern.
|
||||||
|
- **User Story 3 (Phase 5)**: Depends on Foundational completion; can proceed in parallel with US1 or US2 because it focuses on classification preservation and minor alignment.
|
||||||
|
- **User Story 4 (Phase 6)**: Depends on Foundational completion; can proceed in parallel with US3 once the exception contract exists.
|
||||||
|
- **Polish (Phase 7)**: Depends on all desired user stories being complete.
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- **US1**: No dependencies beyond Foundational.
|
||||||
|
- **US2**: No dependencies beyond Foundational, but it reuses the hierarchy and context patterns proven in US1.
|
||||||
|
- **US3**: No dependencies beyond Foundational; it validates that shared-pattern and bounded-scope pages stay intentionally calm.
|
||||||
|
- **US4**: No dependencies beyond Foundational; it specializes the exception path after the inventory and validator rules exist.
|
||||||
|
|
||||||
|
### Within Each User Story
|
||||||
|
|
||||||
|
- Write the story tests first and confirm they fail before implementation.
|
||||||
|
- Update page classes before finalizing any matching Blade view adjustments.
|
||||||
|
- Keep each story independently shippable before moving to the next priority.
|
||||||
|
|
||||||
|
### Parallel Opportunities
|
||||||
|
|
||||||
|
- `T002` and `T003` can run in parallel after `T001`.
|
||||||
|
- `T004`, `T005`, and `T006` can run in parallel before implementing `T007` and `T008`.
|
||||||
|
- Within US1, `T009` and `T010` can run in parallel.
|
||||||
|
- Within US2, `T013`, `T014`, `T015`, and `T016` can run in parallel.
|
||||||
|
- Within US3, `T021`, `T022`, `T023`, and `T024` can run in parallel.
|
||||||
|
- Within US4, `T027`, `T028`, and `T029` can run in parallel.
|
||||||
|
- `T031` and `T033` can run in parallel once all page-level changes are complete.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Example: User Story 1
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parallel test pass for US1
|
||||||
|
T009 Add no-selection vs selected-workbench hierarchy coverage in apps/platform/tests/Feature/Monitoring/FindingExceptionsQueueHierarchyTest.php
|
||||||
|
T010 Extend approval and rejection authorization continuity coverage in apps/platform/tests/Feature/Rbac/ActionSurfaceRbacSemanticsTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel Example: User Story 2
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parallel test pass for US2
|
||||||
|
T013 Extend viewer hierarchy coverage in apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php
|
||||||
|
T014 Add operations landing header hierarchy coverage in apps/platform/tests/Feature/Monitoring/OperationsHeaderHierarchyTest.php
|
||||||
|
T015 Extend canonical navigation and related-link assertions in apps/platform/tests/Feature/Monitoring/OperationsCanonicalUrlsTest.php and apps/platform/tests/Feature/Monitoring/OperationsRelatedNavigationTest.php
|
||||||
|
T016 Extend shared scope-label and return-affordance coverage in apps/platform/tests/Feature/OpsUx/OperateHubShellTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel Example: User Story 3
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parallel test pass for US3
|
||||||
|
T021 Add Alerts overview and alert-delivery minor-alignment coverage
|
||||||
|
T022 Extend Audit Log minor-alignment coverage
|
||||||
|
T023 Add Evidence Overview and Review Register no-regression coverage
|
||||||
|
T024 Add Baseline Compare Landing and Matrix no-regression coverage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel Example: User Story 4
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Parallel test pass for US4
|
||||||
|
T027 Extend explicit exception-reason coverage in apps/platform/tests/Feature/Guards/Spec193MonitoringSurfaceHierarchyGuardTest.php
|
||||||
|
T028 Extend tenant diagnostics repair-state coverage in apps/platform/tests/Feature/Filament/TenantDiagnosticsRepairsTest.php
|
||||||
|
T029 Extend tenant diagnostics access semantics in apps/platform/tests/Feature/TenantRBAC/TenantDiagnosticsAccessTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### MVP First (User Story 1 Only)
|
||||||
|
|
||||||
|
1. Complete Phase 1: Setup.
|
||||||
|
2. Complete Phase 2: Foundational guard and inventory work.
|
||||||
|
3. Complete Phase 3: User Story 1.
|
||||||
|
4. Validate the queue hierarchy through the focused US1 tests.
|
||||||
|
5. Stop and review the remediated workbench pattern before widening the slice.
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Ship US1 to establish the first remediated monitoring workbench pattern.
|
||||||
|
2. Add US2 to carry the same action hierarchy into Operations and the canonical run viewer.
|
||||||
|
3. Add US3 to classify shared-pattern surfaces and preserve calm references without churn.
|
||||||
|
4. Add US4 to formalize the Tenant Diagnostics exception path.
|
||||||
|
5. Finish with explicit FR-193-019 non-regression assertions, browser smoke, and focused Sail verification from Phase 7.
|
||||||
|
|
||||||
|
### Parallel Team Strategy
|
||||||
|
|
||||||
|
1. One contributor completes Setup and Foundational tasks.
|
||||||
|
2. After Foundation is green:
|
||||||
|
- Contributor A takes US1.
|
||||||
|
- Contributor B takes US2.
|
||||||
|
- Contributor C takes US3.
|
||||||
|
- Contributor D takes US4.
|
||||||
|
3. Merge back for Phase 7 browser smoke and focused verification.
|
||||||
Loading…
Reference in New Issue
Block a user