Spec 316: implement workspace hub clear filter contract (#371)

## Summary
- centralize workspace hub environment filter reset behavior across the affected Filament workspace hubs
- add a shared page concern and resetter service to clear environment-like URL, Livewire, table, deferred, and persisted filter state consistently
- update hub clear actions and clean-entry flows to route back to the canonical clean workspace hub state
- add focused feature and browser coverage for the clear-filter contract
- include Spec 316 artifacts for the workspace hub clear filter contract

## Testing
- not run as part of this commit/push/PR workflow

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #371
This commit is contained in:
ahmido 2026-05-16 14:52:18 +00:00
parent eced9ad50c
commit 9b097f97f9
18 changed files with 2011 additions and 72 deletions

View File

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace App\Filament\Concerns;
use App\Support\Navigation\WorkspaceHubFilterStateResetter;
use App\Support\Navigation\WorkspaceHubRegistry;
use Illuminate\Http\Request;
trait ClearsWorkspaceHubEnvironmentFilterState
{
protected function resetWorkspaceHubEnvironmentFilterStateForCleanEntry(?Request $request = null): void
{
$resetter = $this->workspaceHubFilterStateResetter();
$resetter->neutralizeEnvironmentLikeQueryState($request);
if (
! $resetter->shouldResetForCleanWorkspaceHubEntry($request)
&& WorkspaceHubRegistry::requestHasEnvironmentFilterQuery($request)
) {
return;
}
$this->clearWorkspaceHubEnvironmentFilterState($request);
}
protected function clearWorkspaceHubEnvironmentFilterState(?Request $request = null): void
{
$this->forgetWorkspaceHubEnvironmentFilterSessionState($request);
$this->clearWorkspaceHubEnvironmentTableFilterState();
}
protected function cleanWorkspaceHubUrl(string $url): string
{
return $this->workspaceHubFilterStateResetter()->cleanUrl($url);
}
protected function redirectToCleanWorkspaceHubUrl(string $url, ?Request $request = null): void
{
$this->clearWorkspaceHubEnvironmentFilterState($request);
$this->redirect($this->cleanWorkspaceHubUrl($url), navigate: true);
}
private function forgetWorkspaceHubEnvironmentFilterSessionState(?Request $request = null): void
{
if (! method_exists($this, 'getTableFiltersSessionKey')) {
return;
}
$this->workspaceHubFilterStateResetter()
->forgetPersistedEnvironmentLikeFilters($this->getTableFiltersSessionKey(), $request);
}
private function clearWorkspaceHubEnvironmentTableFilterState(): void
{
$resetter = $this->workspaceHubFilterStateResetter();
$tableFilters = $this->tableFilters ?? null;
if (is_array($tableFilters)) {
$this->tableFilters = $resetter->clearLivewireTableFilterState($tableFilters);
}
$tableDeferredFilters = $this->tableDeferredFilters ?? null;
if (is_array($tableDeferredFilters)) {
$this->tableDeferredFilters = $resetter->clearLivewireTableFilterState($tableDeferredFilters);
}
}
private function workspaceHubFilterStateResetter(): WorkspaceHubFilterStateResetter
{
return app(WorkspaceHubFilterStateResetter::class);
}
}

View File

@ -4,6 +4,7 @@
namespace App\Filament\Pages\Governance;
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\FindingExceptionResource;
use App\Models\FindingException;
use App\Models\ManagedEnvironment;
@ -14,7 +15,6 @@
use App\Support\Auth\Capabilities;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Filament\CanonicalAdminTenantFilterState;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\GovernanceDecisions\GovernanceDecisionRegisterBuilder;
use App\Support\Navigation\CanonicalNavigationContext;
@ -40,6 +40,7 @@
class DecisionRegister extends Page implements HasTable
{
use ClearsWorkspaceHubEnvironmentFilterState;
use InteractsWithTable;
protected static bool $isDiscovered = false;
@ -131,10 +132,10 @@ public static function canAccess(): bool
public function mount(): void
{
app(CanonicalAdminTenantFilterState::class)
->forgetEnvironmentLikeFiltersForCleanWorkspaceHubEntry($this->getTableFiltersSessionKey(), request());
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->mountInteractsWithTable();
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->authorizeWorkspaceMembership();
$this->applyRequestedTenantPrefilter();
$this->registerState = $this->resolveRequestedRegisterState();

View File

@ -4,6 +4,7 @@
namespace App\Filament\Pages\Monitoring;
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
@ -11,7 +12,6 @@
use App\Models\User;
use App\Models\Workspace;
use App\Support\EnvironmentReviewStatus;
use App\Support\Filament\CanonicalAdminTenantFilterState;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
@ -41,6 +41,7 @@
class EvidenceOverview extends Page implements HasTable
{
use ClearsWorkspaceHubEnvironmentFilterState;
use InteractsWithTable;
protected const MONITORING_PAGE_STATE_CONTRACT = [
@ -152,14 +153,12 @@ public static function monitoringPageStateContract(): array
public function mount(): void
{
$this->authorizeWorkspaceAccess();
if (! request()->query->has('environment_id')) {
app(CanonicalAdminTenantFilterState::class)
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
}
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->seedTableStateFromQuery();
$this->rows = $this->rowsForState($this->tableFilters ?? [], $this->tableSearch)->values()->all();
$this->mountInteractsWithTable();
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->rows = $this->rowsForState($this->tableFilters ?? [], $this->tableSearch)->values()->all();
}
public function table(Table $table): Table
@ -252,10 +251,11 @@ public function clearOverviewFilters(): void
$this->tableSearch = '';
$this->rows = $this->rowsForState($this->tableFilters, $this->tableSearch)->values()->all();
session()->put($this->getTableFiltersSessionKey(), $this->tableFilters);
session()->forget($this->getTableFiltersSessionKey());
session()->put($this->getTableSearchSessionKey(), $this->tableSearch);
$this->clearWorkspaceHubEnvironmentFilterState(request());
$this->redirect($this->overviewUrl(), navigate: true);
$this->redirectToCleanWorkspaceHubUrl($this->overviewUrl(), request());
}
/**
@ -271,7 +271,7 @@ public function environmentFilterChip(): ?array
return [
'label' => (string) $tenant->name,
'clear_url' => $this->overviewUrl(),
'clear_url' => $this->cleanWorkspaceHubUrl($this->overviewUrl()),
];
}

View File

@ -4,6 +4,7 @@
namespace App\Filament\Pages\Monitoring;
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\FindingExceptionResource;
use App\Filament\Resources\FindingResource;
use App\Models\FindingException;
@ -51,6 +52,7 @@
class FindingExceptionsQueue extends Page implements HasTable
{
use ClearsWorkspaceHubEnvironmentFilterState;
use InteractsWithTable;
protected const MONITORING_PAGE_STATE_CONTRACT = [
@ -193,12 +195,10 @@ public static function canAccess(): bool
public function mount(): void
{
if (! request()->query->has('environment_id')) {
app(CanonicalAdminTenantFilterState::class)
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
}
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->mountInteractsWithTable();
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->applyRequestedTenantPrefilter();
$requestedExceptionId = is_numeric(request()->query('exception')) ? (int) request()->query('exception') : null;
@ -229,13 +229,7 @@ protected function getHeaderActions(): array
->icon('heroicon-o-x-mark')
->color('gray')
->visible(fn (): bool => $this->hasActiveQueueFilters())
->action(function (): void {
$this->removeTableFilter('managed_environment_id');
$this->removeTableFilter('status');
$this->removeTableFilter('current_validity_state');
$this->selectedFindingExceptionId = null;
$this->resetTable();
});
->action(fn (): mixed => $this->clearQueueFilters());
$actions[] = Action::make('view_tenant_register')
->label('View environment findings')
@ -448,13 +442,7 @@ public function table(Table $table): Table
->label('Clear filters')
->icon('heroicon-o-x-mark')
->color('gray')
->action(function (): void {
$this->removeTableFilter('managed_environment_id');
$this->removeTableFilter('status');
$this->removeTableFilter('current_validity_state');
$this->selectedFindingExceptionId = null;
$this->resetTable();
}),
->action(fn (): mixed => $this->clearQueueFilters()),
]);
}
@ -520,6 +508,22 @@ public function clearSelectedException(): void
$this->selectedFindingExceptionId = null;
}
public function clearQueueFilters(): void
{
$hadEnvironmentFilter = $this->currentTenantFilterId() !== null;
$this->removeTableFilter('managed_environment_id');
$this->removeTableFilter('status');
$this->removeTableFilter('current_validity_state');
$this->selectedFindingExceptionId = null;
$this->clearWorkspaceHubEnvironmentFilterState(request());
$this->resetTable();
if ($hadEnvironmentFilter) {
$this->redirectToCleanWorkspaceHubUrl(static::getUrl(panel: 'admin'), request());
}
}
/**
* @return array{label: string, clear_url: string}|null
*/
@ -533,7 +537,7 @@ public function environmentFilterChip(): ?array
return [
'label' => (string) $tenant->name,
'clear_url' => static::getUrl(panel: 'admin'),
'clear_url' => $this->cleanWorkspaceHubUrl(static::getUrl(panel: 'admin')),
];
}

View File

@ -4,6 +4,7 @@
namespace App\Filament\Pages\Monitoring;
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\OperationRunResource;
use App\Filament\Widgets\Operations\OperationsKpiHeader;
use App\Models\ManagedEnvironment;
@ -42,6 +43,7 @@
class Operations extends Page implements HasForms, HasTable
{
use ClearsWorkspaceHubEnvironmentFilterState;
use InteractsWithForms;
use InteractsWithTable;
@ -200,16 +202,10 @@ public function mount(): void
$this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null;
if (! request()->query->has('environment_id')) {
app(CanonicalAdminTenantFilterState::class)
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
}
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->mountInteractsWithTable();
if (! request()->query->has('environment_id')) {
$this->tableFilters['managed_environment_id']['value'] = null;
$this->tableDeferredFilters['managed_environment_id']['value'] = null;
}
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->applyRequestedDashboardPrefilter();
}
@ -342,9 +338,9 @@ public function environmentFilterChip(): ?array
return [
'label' => (string) $tenant->name,
'clear_url' => route('admin.operations.index', [
'clear_url' => $this->cleanWorkspaceHubUrl(route('admin.operations.index', [
'workspace' => app(WorkspaceContext::class)->currentWorkspace(request()),
]),
])),
];
}

View File

@ -4,6 +4,7 @@
namespace App\Filament\Pages\Reviews;
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\EnvironmentReviewResource;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
@ -18,7 +19,6 @@
use App\Support\Audit\AuditActionId;
use App\Support\Auth\Capabilities;
use App\Support\EnvironmentReviewCompletenessState;
use App\Support\Filament\CanonicalAdminTenantFilterState;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\Findings\FindingOutcomeSemantics;
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
@ -50,6 +50,7 @@
class CustomerReviewWorkspace extends Page implements HasTable
{
use ClearsWorkspaceHubEnvironmentFilterState;
use InteractsWithTable;
public const string DETAIL_CONTEXT_QUERY_KEY = 'customer_workspace';
@ -113,12 +114,10 @@ public static function tenantPrefilterUrl(ManagedEnvironment $tenant): string
public function mount(): void
{
$this->authorizePageAccess();
if (! request()->query->has('environment_id')) {
app(CanonicalAdminTenantFilterState::class)
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
}
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->applyRequestedTenantPrefilter();
$this->mountInteractsWithTable();
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->auditWorkspaceOpen();
}
@ -258,7 +257,7 @@ public function environmentFilterChip(): ?array
return [
'label' => (string) $tenant->name,
'clear_url' => static::getUrl(panel: 'admin'),
'clear_url' => $this->cleanWorkspaceHubUrl(static::getUrl(panel: 'admin')),
];
}
@ -431,7 +430,14 @@ private function hasActiveFilters(): bool
private function clearWorkspaceFilters(): void
{
$hadEnvironmentFilter = $this->currentTenantFilterId() !== null;
$this->removeTableFilters();
$this->clearWorkspaceHubEnvironmentFilterState(request());
if ($hadEnvironmentFilter) {
$this->redirectToCleanWorkspaceHubUrl(static::getUrl(panel: 'admin'), request());
}
}
private function workspaceEmptyStateHeading(): string

View File

@ -4,6 +4,7 @@
namespace App\Filament\Pages\Reviews;
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\EnvironmentReviewResource;
use App\Models\EnvironmentReview;
use App\Models\ManagedEnvironment;
@ -16,7 +17,6 @@
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\EnvironmentReviewCompletenessState;
use App\Support\Filament\CanonicalAdminTenantFilterState;
use App\Support\Filament\FilterPresets;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\Findings\FindingOutcomeSemantics;
@ -45,6 +45,7 @@
class ReviewRegister extends Page implements HasTable
{
use ClearsWorkspaceHubEnvironmentFilterState;
use InteractsWithTable;
protected static bool $isDiscovered = false;
@ -80,13 +81,11 @@ public function mount(): void
{
$this->authorizePageAccess();
if (! request()->query->has('environment_id')) {
app(CanonicalAdminTenantFilterState::class)
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
}
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
$this->applyRequestedTenantPrefilter();
$this->mountInteractsWithTable();
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
}
protected function getHeaderActions(): array
@ -241,7 +240,7 @@ public function environmentFilterChip(): ?array
return [
'label' => (string) $tenant->name,
'clear_url' => static::getUrl(panel: 'admin'),
'clear_url' => $this->cleanWorkspaceHubUrl(static::getUrl(panel: 'admin')),
];
}
@ -338,7 +337,14 @@ private function hasActiveFilters(): bool
private function clearRegisterFilters(): void
{
$hadEnvironmentFilter = $this->currentTenantFilterId() !== null;
$this->removeTableFilters();
$this->clearWorkspaceHubEnvironmentFilterState(request());
if ($hadEnvironmentFilter) {
$this->redirectToCleanWorkspaceHubUrl(static::getUrl(panel: 'admin'), request());
}
}
private function currentTenantFilterId(): ?int

View File

@ -2,12 +2,12 @@
namespace App\Filament\Resources\ProviderConnectionResource\Pages;
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
use App\Filament\Resources\ProviderConnectionResource;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Auth\CapabilityResolver;
use App\Support\Auth\Capabilities;
use App\Support\Filament\CanonicalAdminTenantFilterState;
use Filament\Actions;
use Filament\Resources\Pages\ListRecords;
use Filament\Schemas\Components\EmbeddedTable;
@ -18,16 +18,16 @@
class ListProviderConnections extends ListRecords
{
use ClearsWorkspaceHubEnvironmentFilterState;
protected static string $resource = ProviderConnectionResource::class;
public function mount(): void
{
if (! request()->query->has('environment_id')) {
app(CanonicalAdminTenantFilterState::class)
->forgetEnvironmentLikeFilters($this->getTableFiltersSessionKey(), request());
}
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
parent::mount();
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
}
private function tableHasRecords(): bool
@ -256,7 +256,7 @@ private function environmentFilterChip(): ?array
return [
'label' => (string) $environment->name,
'clear_url' => ProviderConnectionResource::getUrl('index', panel: 'admin'),
'clear_url' => $this->cleanWorkspaceHubUrl(ProviderConnectionResource::getUrl('index', panel: 'admin')),
];
}

View File

@ -5,6 +5,7 @@
namespace App\Support\Filament;
use App\Models\ManagedEnvironment;
use App\Support\Navigation\WorkspaceHubFilterStateResetter;
use App\Support\Navigation\WorkspaceHubRegistry;
use App\Support\OperateHub\OperateHubShell;
use Illuminate\Http\Request;
@ -15,7 +16,10 @@ final class CanonicalAdminTenantFilterState
{
private const STATE_PREFIX = 'filament.admin_tenant_filter_state';
public function __construct(private readonly OperateHubShell $operateHubShell) {}
public function __construct(
private readonly OperateHubShell $operateHubShell,
private readonly WorkspaceHubFilterStateResetter $workspaceHubFilterStateResetter,
) {}
public function currentFilterValue(
string $filtersSessionKey,
@ -119,15 +123,8 @@ public function forgetEnvironmentLikeFilters(
$persistedFilters = [];
}
foreach (WorkspaceHubRegistry::environmentLikeFilterKeys() as $filterName) {
Arr::forget($persistedFilters, $filterName);
}
if ($persistedFilters === []) {
$session->forget($filtersSessionKey);
} else {
$session->put($filtersSessionKey, $persistedFilters);
}
$persistedFilters = $this->workspaceHubFilterStateResetter
->forgetPersistedEnvironmentLikeFilters($filtersSessionKey, $request, $persistedFilters);
$session->forget($this->stateKey($filtersSessionKey));

View File

@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace App\Support\Navigation;
use Illuminate\Http\Request;
use Illuminate\Session\Store;
use Illuminate\Support\Arr;
final class WorkspaceHubFilterStateResetter
{
/**
* @return list<string>
*/
public function environmentLikeFilterKeys(): array
{
return WorkspaceHubRegistry::environmentLikeFilterKeys();
}
public function shouldResetForCleanWorkspaceHubEntry(?Request $request = null): bool
{
return WorkspaceHubRegistry::isCleanWorkspaceHubEntry($request);
}
public function neutralizeEnvironmentLikeQueryState(?Request $request = null): void
{
$request ??= request();
$hasCanonicalEnvironmentFilter = WorkspaceHubRegistry::requestHasEnvironmentFilterQuery($request);
foreach (WorkspaceHubRegistry::forbiddenQueryKeys() as $queryKey) {
if ($queryKey === 'environment_id' && $hasCanonicalEnvironmentFilter) {
continue;
}
$request->query->remove($queryKey);
}
}
/**
* @param array<string, mixed>|null $persistedFilters
* @return array<string, mixed>
*/
public function forgetPersistedEnvironmentLikeFilters(
string $filtersSessionKey,
?Request $request = null,
?array $persistedFilters = null,
): array {
$session = $this->session($request);
$persistedFilters ??= $session->get($filtersSessionKey, []);
if (! is_array($persistedFilters)) {
$persistedFilters = [];
}
$persistedFilters = $this->forgetEnvironmentLikeFilterPaths($persistedFilters);
if ($persistedFilters === []) {
$session->forget($filtersSessionKey);
} else {
$session->put($filtersSessionKey, $persistedFilters);
}
return $persistedFilters;
}
public function forgetPersistedEnvironmentLikeFiltersForCleanWorkspaceHubEntry(
string $filtersSessionKey,
?Request $request = null,
): void {
if (! $this->shouldResetForCleanWorkspaceHubEntry($request)) {
return;
}
$this->forgetPersistedEnvironmentLikeFilters($filtersSessionKey, $request);
}
/**
* @param array<string, mixed>|null $filters
* @return array<string, mixed>
*/
public function clearLivewireTableFilterState(?array $filters): array
{
if (! is_array($filters)) {
return [];
}
foreach ($this->environmentLikeFilterKeys() as $filterName) {
if (! array_key_exists($filterName, $filters)) {
continue;
}
$filterState = $filters[$filterName];
if (is_array($filterState)) {
$filters[$filterName] = ['value' => null];
continue;
}
Arr::forget($filters, $filterName);
}
return $this->forgetNestedEnvironmentLikeFilterState($filters);
}
/**
* @param array<string, mixed> $parameters
* @return array<string, mixed>
*/
public function cleanParameters(array $parameters): array
{
return WorkspaceHubRegistry::cleanParameters($parameters);
}
public function cleanUrl(string $url): string
{
return WorkspaceHubRegistry::cleanUrl($url);
}
/**
* @param array<string, mixed> $state
* @return array<string, mixed>
*/
private function forgetEnvironmentLikeFilterPaths(array $state): array
{
foreach ($this->environmentLikeFilterKeys() as $filterName) {
Arr::forget($state, $filterName);
}
return $this->forgetNestedEnvironmentLikeFilterState($state);
}
/**
* @param array<string, mixed> $state
* @return array<string, mixed>
*/
private function forgetNestedEnvironmentLikeFilterState(array $state): array
{
foreach ($this->environmentLikeFilterKeys() as $filterName) {
Arr::forget($state, "tableFilters.{$filterName}");
Arr::forget($state, "tableDeferredFilters.{$filterName}");
}
foreach (['tableFilters', 'tableDeferredFilters'] as $tableStateKey) {
if (data_get($state, $tableStateKey) === []) {
Arr::forget($state, $tableStateKey);
}
}
return $state;
}
private function session(?Request $request = null): Store
{
return ($request && $request->hasSession()) ? $request->session() : app('session.store');
}
}

View File

@ -0,0 +1,348 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Governance\DecisionRegister;
use App\Filament\Pages\Governance\GovernanceInbox;
use App\Filament\Pages\Monitoring\EvidenceOverview;
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Pages\Reviews\ReviewRegister;
use App\Filament\Resources\ProviderConnectionResource;
use App\Filament\Resources\ProviderConnectionResource\Pages\ListProviderConnections;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\User;
use App\Support\EnvironmentReviewStatus;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\OperationRunLinks;
use App\Support\Workspaces\WorkspaceContext;
use Livewire\Livewire;
pest()->browser()->timeout(60_000);
it('Spec316 smokes filtered workspace hub clear and reload behavior', function (): void {
[$user, $environmentA, $environmentB] = spec316BrowserClearFilterWorkspace();
$workspace = $environmentA->workspace()->firstOrFail();
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
(string) $workspace->getKey() => (int) $environmentA->getKey(),
],
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
(string) $workspace->getKey() => (int) $environmentA->getKey(),
]);
$hubs = [
'operations' => [
'filtered_url' => OperationRunLinks::index($environmentA),
'clean_url' => OperationRunLinks::index(),
'wide_text' => 'Inventory sync',
],
'provider connections' => [
'filtered_url' => ProviderConnectionResource::getUrl('index', [
'environment_id' => (int) $environmentA->getKey(),
], panel: 'admin'),
'clean_url' => ProviderConnectionResource::getUrl('index', panel: 'admin'),
'wide_text' => 'Spec316 Browser Provider B',
],
'finding exceptions queue' => [
'filtered_url' => FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
'environment_id' => (int) $environmentA->getKey(),
]),
'clean_url' => FindingExceptionsQueue::getUrl(panel: 'admin'),
'wide_text' => $environmentB->name,
],
'evidence overview' => [
'filtered_url' => route('admin.evidence.overview', [
'environment_id' => (int) $environmentA->getKey(),
]),
'clean_url' => route('admin.evidence.overview'),
'wide_text' => $environmentB->name,
],
'review register' => [
'filtered_url' => ReviewRegister::getUrl(panel: 'admin', parameters: [
'environment_id' => (int) $environmentA->getKey(),
]),
'clean_url' => ReviewRegister::getUrl(panel: 'admin'),
'wide_text' => $environmentB->name,
],
'customer review workspace' => [
'filtered_url' => CustomerReviewWorkspace::tenantPrefilterUrl($environmentA),
'clean_url' => CustomerReviewWorkspace::getUrl(panel: 'admin'),
'wide_text' => $environmentB->name,
],
'governance inbox' => [
'filtered_url' => GovernanceInbox::getUrl(panel: 'admin', parameters: [
'environment_id' => (int) $environmentA->getKey(),
]),
'clean_url' => GovernanceInbox::getUrl(panel: 'admin'),
'wide_text' => 'Spec316 Browser Governance B',
],
'decision register' => [
'filtered_url' => DecisionRegister::getUrl(panel: 'admin', parameters: [
'environment_id' => (int) $environmentA->getKey(),
]),
'clean_url' => DecisionRegister::getUrl(panel: 'admin'),
'wide_text' => $environmentB->name,
],
];
foreach ($hubs as $hub) {
$cleanPath = json_encode((string) parse_url($hub['clean_url'], PHP_URL_PATH), JSON_THROW_ON_ERROR);
$page = visit($hub['filtered_url'])
->waitForText('Environment filter:')
->assertSee($environmentA->name)
->assertDontSee($hub['wide_text'])
->assertNoJavaScriptErrors();
$page
->click('[data-testid="workspace-hub-environment-filter-clear"]')
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($hub['wide_text'])
->assertScript("window.location.pathname === {$cleanPath}", true)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertScript('! window.location.search.includes("tenant=")', true)
->assertScript('! window.location.search.includes("managed_environment_id=")', true)
->assertScript('! window.location.search.includes("tenant_scope=")', true)
->assertScript('! window.location.search.includes("tableFilters")', true)
->assertNoJavaScriptErrors();
$page->script('window.location.reload();');
$page
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($hub['wide_text'])
->assertScript("window.location.pathname === {$cleanPath}", true)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertNoJavaScriptErrors();
}
});
it('Spec316 smokes browser back and forward alignment for high risk hubs', function (): void {
[$user, $environmentA, $environmentB] = spec316BrowserClearFilterWorkspace();
$workspace = $environmentA->workspace()->firstOrFail();
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$hubs = [
ProviderConnectionResource::getUrl('index', [
'environment_id' => (int) $environmentA->getKey(),
], panel: 'admin'),
FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [
'environment_id' => (int) $environmentA->getKey(),
]),
CustomerReviewWorkspace::tenantPrefilterUrl($environmentA),
route('admin.evidence.overview', [
'environment_id' => (int) $environmentA->getKey(),
]),
];
foreach ($hubs as $filteredUrl) {
$page = visit($filteredUrl)
->waitForText('Environment filter:')
->assertSee($environmentA->name)
->assertDontSee($environmentB->name);
$page
->click('[data-testid="workspace-hub-environment-filter-clear"]')
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($environmentB->name);
$page->script('window.history.back();');
$page
->waitForText('Environment filter:')
->assertSee($environmentA->name)
->assertDontSee($environmentB->name)
->assertScript('window.location.search.includes("environment_id=")', true);
$page->script('window.history.forward();');
$page
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($environmentB->name)
->assertScript('! window.location.search.includes("environment_id=")', true)
->assertNoJavaScriptErrors();
}
});
it('Spec316 smokes persisted environment filters do not survive clean browser entry', function (): void {
[$user, $environmentA, $environmentB] = spec316BrowserClearFilterWorkspace();
$workspace = $environmentA->workspace()->firstOrFail();
$this->actingAs($user)->withSession([
WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$cases = [
[
'component' => ListProviderConnections::class,
'url' => ProviderConnectionResource::getUrl('index', panel: 'admin'),
'filter_name' => 'tenant',
'filter_value' => (string) $environmentA->external_id,
'wide_text' => 'Spec316 Browser Provider B',
],
[
'component' => FindingExceptionsQueue::class,
'url' => FindingExceptionsQueue::getUrl(panel: 'admin'),
'filter_name' => 'managed_environment_id',
'filter_value' => (string) $environmentA->getKey(),
'wide_text' => $environmentB->name,
],
[
'component' => CustomerReviewWorkspace::class,
'url' => CustomerReviewWorkspace::getUrl(panel: 'admin'),
'filter_name' => 'managed_environment_id',
'filter_value' => (string) $environmentA->getKey(),
'wide_text' => $environmentB->name,
],
[
'component' => EvidenceOverview::class,
'url' => route('admin.evidence.overview'),
'filter_name' => 'managed_environment_id',
'filter_value' => (string) $environmentA->getKey(),
'wide_text' => $environmentB->name,
],
];
foreach ($cases as $case) {
$component = Livewire::actingAs($user)->test($case['component']);
$filtersSessionKey = $component->instance()->getTableFiltersSessionKey();
session()->put($filtersSessionKey, [
$case['filter_name'] => ['value' => $case['filter_value']],
]);
visit($case['url'])
->waitForText(__('localization.shell.no_environment_selected'))
->assertDontSee('Environment filter:')
->assertSee($case['wide_text'])
->assertNoJavaScriptErrors();
}
});
/**
* @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment}
*/
function spec316BrowserClearFilterWorkspace(): array
{
$environmentA = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec316 Browser Environment A',
'external_id' => 'spec316-browser-environment-a',
]);
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'manager');
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'name' => 'Spec316 Browser Environment B',
'external_id' => 'spec316-browser-environment-b',
]);
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'manager');
OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
spec316BrowserFindingException($environmentA, $user, 'Spec316 Browser Governance A', 'Spec316 Browser Decision A');
spec316BrowserFindingException($environmentB, $user, 'Spec316 Browser Governance B', 'Spec316 Browser Decision B');
ProviderConnection::factory()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'managed_environment_id' => (int) $environmentA->getKey(),
'display_name' => 'Spec316 Browser Provider A',
]);
ProviderConnection::factory()->create([
'workspace_id' => (int) $environmentB->workspace_id,
'managed_environment_id' => (int) $environmentB->getKey(),
'display_name' => 'Spec316 Browser Provider B',
]);
$snapshotA = spec316BrowserEvidenceSnapshot($environmentA);
$snapshotB = spec316BrowserEvidenceSnapshot($environmentB);
spec316BrowserPublishedReview($environmentA, $user, $snapshotA);
spec316BrowserPublishedReview($environmentB, $user, $snapshotB);
return [$user, $environmentA, $environmentB];
}
function spec316BrowserFindingException(
ManagedEnvironment $environment,
User $actor,
string $requestReason,
string $decisionReason,
): FindingException {
$finding = Finding::factory()->for($environment)->riskAccepted()->create([
'workspace_id' => (int) $environment->workspace_id,
'subject_external_id' => str()->slug($requestReason),
]);
$exception = FindingException::query()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'finding_id' => (int) $finding->getKey(),
'requested_by_user_id' => (int) $actor->getKey(),
'owner_user_id' => (int) $actor->getKey(),
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
'request_reason' => $requestReason,
'requested_at' => now()->subDay(),
'review_due_at' => now()->addDay(),
'evidence_summary' => ['reference_count' => 0],
]);
$decision = $exception->decisions()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'actor_user_id' => (int) $actor->getKey(),
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
'reason' => $decisionReason,
'metadata' => [],
'decided_at' => now()->subDay(),
]);
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
return $exception->fresh(['currentDecision']);
}
function spec316BrowserEvidenceSnapshot(ManagedEnvironment $environment): EvidenceSnapshot
{
return EvidenceSnapshot::query()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'status' => EvidenceSnapshotStatus::Active->value,
'summary' => ['missing_dimensions' => 0, 'stale_dimensions' => 0],
'generated_at' => now(),
]);
}
function spec316BrowserPublishedReview(ManagedEnvironment $environment, User $user, EvidenceSnapshot $snapshot): void
{
$review = composeEnvironmentReviewForTest($environment, $user, $snapshot);
$review->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
}

View File

@ -101,7 +101,13 @@
$component
->callAction('clear_filters')
->assertActionHidden('clear_filters')
->assertRedirect(ReviewRegister::getUrl(panel: 'admin'));
Livewire::withHeaders(['referer' => ReviewRegister::getUrl(panel: 'admin')])
->withQueryParams([])
->actingAs($user)
->test(ReviewRegister::class)
->assertSet('tableFilters.managed_environment_id.value', null)
->assertCanSeeTableRecords([$reviewA, $reviewB]);
expect(app(WorkspaceContext::class)->lastTenantId())->toBe((int) $tenantA->getKey());

View File

@ -81,8 +81,8 @@
'tenant' => (string) $tenantB->external_id,
])
->test(FindingExceptionsQueue::class)
->assertSet('tableFilters.managed_environment_id.value', (string) $tenantB->getKey())
->assertActionVisible('view_tenant_register');
->assertSet('tableFilters.managed_environment_id.value', null)
->assertActionHidden('view_tenant_register');
$filtersComponent = Livewire::test(FindingExceptionsQueue::class);
$queueInstance = $filtersComponent->instance();

View File

@ -0,0 +1,335 @@
<?php
declare(strict_types=1);
use App\Filament\Pages\Governance\DecisionRegister;
use App\Filament\Pages\Governance\GovernanceInbox;
use App\Filament\Pages\Monitoring\EvidenceOverview;
use App\Filament\Pages\Monitoring\FindingExceptionsQueue;
use App\Filament\Pages\Monitoring\Operations;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Pages\Reviews\ReviewRegister;
use App\Filament\Resources\ProviderConnectionResource;
use App\Filament\Resources\ProviderConnectionResource\Pages\ListProviderConnections;
use App\Models\EvidenceSnapshot;
use App\Models\Finding;
use App\Models\FindingException;
use App\Models\FindingExceptionDecision;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\User;
use App\Support\EnvironmentReviewStatus;
use App\Support\Evidence\EvidenceSnapshotStatus;
use App\Support\Navigation\WorkspaceHubFilterStateResetter;
use App\Support\Navigation\WorkspaceHubRegistry;
use App\Support\OperationRunLinks;
use App\Support\Workspaces\WorkspaceContext;
use Livewire\Livewire;
it('Spec316 resetter removes environment-like persisted table filters without deleting unrelated filters', function (): void {
$filtersSessionKey = 'filament.spec316.filters';
session()->put($filtersSessionKey, [
'tenant' => ['value' => 'legacy-tenant'],
'tenant_id' => ['value' => '101'],
'managed_environment_id' => ['value' => '101'],
'environment_id' => ['value' => '101'],
'environment' => ['value' => 'legacy-environment'],
'tenant_scope' => ['value' => 'environment'],
'tableFilters' => [
'tenant' => ['value' => 'legacy-tenant'],
'managed_environment_id' => ['value' => '101'],
'status' => ['value' => 'pending'],
],
'status' => ['value' => 'pending'],
]);
app(WorkspaceHubFilterStateResetter::class)
->forgetPersistedEnvironmentLikeFilters($filtersSessionKey, request());
$persistedFilters = session()->get($filtersSessionKey);
expect($persistedFilters)
->toMatchArray([
'status' => ['value' => 'pending'],
'tableFilters' => [
'status' => ['value' => 'pending'],
],
]);
foreach (WorkspaceHubRegistry::environmentLikeFilterKeys() as $filterName) {
expect(data_get($persistedFilters, "{$filterName}.value"))->toBeNull()
->and(data_get($persistedFilters, "tableFilters.{$filterName}.value"))->toBeNull();
}
});
it('Spec316 clear filter result is reload safe across table backed workspace hubs', function (): void {
[$user, $environmentA, $environmentB, $records] = spec316ClearFilterWorkspace();
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [
(string) $environmentA->workspace_id => (int) $environmentA->getKey(),
]);
$cases = [
'operations' => [
'component' => Operations::class,
'clean_url' => OperationRunLinks::index(),
'filtered_records' => [$records['runA']],
'hidden_records' => [$records['runB']],
'wide_records' => [$records['runA'], $records['runB']],
'session_environment_value' => (string) $environmentA->getKey(),
],
'finding exceptions queue' => [
'component' => FindingExceptionsQueue::class,
'clean_url' => FindingExceptionsQueue::getUrl(panel: 'admin'),
'filtered_records' => [$records['exceptionA']],
'hidden_records' => [$records['exceptionB']],
'wide_records' => [$records['exceptionA'], $records['exceptionB']],
'session_environment_value' => (string) $environmentA->getKey(),
],
'provider connections' => [
'component' => ListProviderConnections::class,
'clean_url' => ProviderConnectionResource::getUrl('index', panel: 'admin'),
'filtered_records' => [$records['connectionA']],
'hidden_records' => [$records['connectionB']],
'wide_records' => [$records['connectionA'], $records['connectionB']],
'session_environment_value' => (string) $environmentA->external_id,
],
'evidence overview' => [
'component' => EvidenceOverview::class,
'clean_url' => route('admin.evidence.overview'),
'filtered_records' => [(string) $records['snapshotA']->getKey()],
'hidden_records' => [(string) $records['snapshotB']->getKey()],
'wide_records' => [(string) $records['snapshotA']->getKey(), (string) $records['snapshotB']->getKey()],
'session_environment_value' => (string) $environmentA->getKey(),
],
'review register' => [
'component' => ReviewRegister::class,
'clean_url' => ReviewRegister::getUrl(panel: 'admin'),
'filtered_records' => [$records['reviewA']->fresh()],
'hidden_records' => [$records['reviewB']->fresh()],
'wide_records' => [$records['reviewA']->fresh(), $records['reviewB']->fresh()],
'session_environment_value' => (string) $environmentA->getKey(),
],
'customer review workspace' => [
'component' => CustomerReviewWorkspace::class,
'clean_url' => CustomerReviewWorkspace::getUrl(panel: 'admin'),
'filtered_records' => [$environmentA->fresh()],
'hidden_records' => [$environmentB->fresh()],
'wide_records' => [$environmentA->fresh(), $environmentB->fresh()],
'session_environment_value' => (string) $environmentA->getKey(),
],
];
foreach ($cases as $case) {
$filteredComponent = Livewire::withQueryParams(['environment_id' => (int) $environmentA->getKey()])
->actingAs($user)
->test($case['component'])
->assertSee('Environment filter:')
->assertSee('Clear filter')
->assertCanSeeTableRecords($case['filtered_records'])
->assertCanNotSeeTableRecords($case['hidden_records']);
$filtersSessionKey = $filteredComponent->instance()->getTableFiltersSessionKey();
spec316PersistLegacyEnvironmentFilterState($filtersSessionKey, $case['session_environment_value']);
$this->get($case['clean_url'])
->assertOk()
->assertDontSee('Environment filter:');
spec316AssertEnvironmentLikeFiltersForgotten($filtersSessionKey);
$this->get($case['clean_url'])
->assertOk()
->assertDontSee('Environment filter:');
Livewire::withQueryParams([])
->actingAs($user)
->test($case['component'])
->assertDontSee('Environment filter:')
->assertCanSeeTableRecords($case['wide_records']);
}
});
it('Spec316 clear filter result is clean for governance and decision workspace hubs', function (): void {
[$user, $environmentA, $environmentB, $records] = spec316ClearFilterWorkspace();
$this->actingAs($user);
setAdminPanelContext();
session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id);
$this->get(GovernanceInbox::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environmentA->getKey()]))
->assertOk()
->assertSee('Environment filter:')
->assertSee($environmentA->name)
->assertDontSee('Spec316 Governance B');
$this->get(GovernanceInbox::getUrl(panel: 'admin'))
->assertOk()
->assertDontSee('Environment filter:')
->assertSee('Spec316 Governance B');
$this->get(DecisionRegister::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environmentA->getKey()]))
->assertOk()
->assertSee('Environment filter:')
->assertSee($environmentA->name)
->assertDontSee('Spec316 Decision B');
$this->get(DecisionRegister::getUrl(panel: 'admin'))
->assertOk()
->assertDontSee('Environment filter:');
Livewire::withQueryParams([])
->actingAs($user)
->test(DecisionRegister::class)
->assertCanSeeTableRecords([$records['exceptionA'], $records['exceptionB']]);
expect($environmentB->workspace_id)->toBe($environmentA->workspace_id);
});
/**
* @return array{0: User, 1: ManagedEnvironment, 2: ManagedEnvironment, 3: array<string, mixed>}
*/
function spec316ClearFilterWorkspace(): array
{
$environmentA = ManagedEnvironment::factory()->active()->create([
'name' => 'Spec316 Environment A',
'external_id' => 'spec316-environment-a',
]);
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'manager');
$environmentB = ManagedEnvironment::factory()->active()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'name' => 'Spec316 Environment B',
'external_id' => 'spec316-environment-b',
]);
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'manager');
$runA = OperationRun::factory()->forTenant($environmentA)->create(['type' => 'policy.sync']);
$runB = OperationRun::factory()->forTenant($environmentB)->create(['type' => 'inventory_sync']);
$exceptionA = spec316ClearFilterFindingException($environmentA, $user, 'Spec316 Governance A', 'Spec316 Decision A');
$exceptionB = spec316ClearFilterFindingException($environmentB, $user, 'Spec316 Governance B', 'Spec316 Decision B');
$connectionA = ProviderConnection::factory()->create([
'workspace_id' => (int) $environmentA->workspace_id,
'managed_environment_id' => (int) $environmentA->getKey(),
'display_name' => 'Spec316 Provider A',
]);
$connectionB = ProviderConnection::factory()->create([
'workspace_id' => (int) $environmentB->workspace_id,
'managed_environment_id' => (int) $environmentB->getKey(),
'display_name' => 'Spec316 Provider B',
]);
$snapshotA = spec316ClearFilterEvidenceSnapshot($environmentA);
$snapshotB = spec316ClearFilterEvidenceSnapshot($environmentB);
$reviewA = composeEnvironmentReviewForTest($environmentA, $user, $snapshotA);
$reviewA->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
$reviewB = composeEnvironmentReviewForTest($environmentB, $user, $snapshotB);
$reviewB->forceFill([
'status' => EnvironmentReviewStatus::Published->value,
'published_at' => now(),
'published_by_user_id' => (int) $user->getKey(),
])->save();
return [$user, $environmentA, $environmentB, compact(
'runA',
'runB',
'exceptionA',
'exceptionB',
'connectionA',
'connectionB',
'snapshotA',
'snapshotB',
'reviewA',
'reviewB',
)];
}
function spec316ClearFilterFindingException(
ManagedEnvironment $environment,
User $actor,
string $requestReason,
string $decisionReason,
): FindingException {
$finding = Finding::factory()->for($environment)->riskAccepted()->create([
'workspace_id' => (int) $environment->workspace_id,
'subject_external_id' => str()->slug($requestReason),
]);
$exception = FindingException::query()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'finding_id' => (int) $finding->getKey(),
'requested_by_user_id' => (int) $actor->getKey(),
'owner_user_id' => (int) $actor->getKey(),
'status' => FindingException::STATUS_PENDING,
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
'request_reason' => $requestReason,
'requested_at' => now()->subDay(),
'review_due_at' => now()->addDay(),
'evidence_summary' => ['reference_count' => 0],
]);
$decision = $exception->decisions()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'actor_user_id' => (int) $actor->getKey(),
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
'reason' => $decisionReason,
'metadata' => [],
'decided_at' => now()->subDay(),
]);
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
return $exception->fresh(['currentDecision']);
}
function spec316ClearFilterEvidenceSnapshot(ManagedEnvironment $environment): EvidenceSnapshot
{
return EvidenceSnapshot::query()->create([
'managed_environment_id' => (int) $environment->getKey(),
'workspace_id' => (int) $environment->workspace_id,
'status' => EvidenceSnapshotStatus::Active->value,
'summary' => ['missing_dimensions' => 0, 'stale_dimensions' => 0],
'generated_at' => now(),
]);
}
function spec316PersistLegacyEnvironmentFilterState(string $filtersSessionKey, string $environmentValue): void
{
session()->put($filtersSessionKey, [
'tenant' => ['value' => $environmentValue],
'tenant_id' => ['value' => $environmentValue],
'managed_environment_id' => ['value' => $environmentValue],
'environment_id' => ['value' => $environmentValue],
'environment' => ['value' => $environmentValue],
'tenant_scope' => ['value' => 'environment'],
'tableFilters' => [
'tenant' => ['value' => $environmentValue],
'managed_environment_id' => ['value' => $environmentValue],
],
]);
}
function spec316AssertEnvironmentLikeFiltersForgotten(string $filtersSessionKey): void
{
$persistedFilters = session()->get($filtersSessionKey, []);
foreach (WorkspaceHubRegistry::environmentLikeFilterKeys() as $filterName) {
expect(data_get($persistedFilters, "{$filterName}.value"))->toBeNull()
->and(data_get($persistedFilters, "tableFilters.{$filterName}.value"))->toBeNull();
}
}

View File

@ -0,0 +1,72 @@
# Requirements Checklist: Workspace Hub Clear Filter Contract
**Spec**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/spec.md)
**Plan**: [plan.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/plan.md)
**Generated**: 2026-05-16
## Candidate Selection Gate
- [x] Explicit user-provided Spec 316 request was selected as the source of truth for this preparation pass.
- [x] Completed-spec guardrail checked that no existing `specs/316-*` artifact was present before generation.
- [x] Specs 313, 314, and 315 were treated as completed historical baseline context, not rewritten.
- [x] Close alternatives were identified as follow-up specs 317 and 318 rather than merged into this spec.
- [x] The selected slice is clear-filter lifecycle hardening only.
## Spec Readiness
- [x] Problem statement is operator-visible and tied to stale clear-filter state.
- [x] Hard-cutover policy is explicit.
- [x] `environment_id` remains the only canonical Environment filter source.
- [x] Legacy keys are explicitly neutralized as filter sources.
- [x] URL, Livewire, Filament table, deferred, session/persisted, chip/header, data, reload, and back/forward layers are named.
- [x] Required hubs are named.
- [x] Optional hub handling is bounded and does not add new filter support.
- [x] Workspace isolation and authorization constraints are explicit.
- [x] Follow-up boundaries for Specs 317 and 318 are explicit.
## Plan Readiness
- [x] Laravel, Filament, Livewire, Pest, and PostgreSQL context is recorded.
- [x] No migration, seeder, package, env var, queue, scheduler, or storage change is planned.
- [x] Shared reset mechanism proportionality is justified.
- [x] Existing repo surfaces are named: `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, `CanonicalAdminTenantFilterState`, chip partial, and required hub pages/resources.
- [x] Provider/platform boundary is classified.
- [x] OperationRun impact is N/A.
- [x] Browser verification scope is defined.
- [x] Deployment impact is assessed.
## Task Readiness
- [x] Tasks are ordered from guardrails and tests through runtime changes and validation.
- [x] Tests are required before or alongside runtime work.
- [x] Critical hubs are named in tasks.
- [x] Optional hubs are classified instead of assumed.
- [x] Legacy alias neutralization has explicit tasks.
- [x] Spec 314 and Spec 315 regressions have explicit tasks.
- [x] Browser reload and back/forward flows have explicit tasks.
- [x] Non-tasks prevent scope creep into Specs 317 and 318.
## Constitution / Guardrail Coverage
- [x] Workspace isolation is covered.
- [x] RBAC and no-access behavior are covered.
- [x] No Graph write/read integration change is introduced.
- [x] No destructive action behavior is introduced.
- [x] No persisted truth is introduced.
- [x] No status/reason family is introduced.
- [x] Filament v5 / Livewire v4 compliance is recorded.
- [x] Native/shared UI primitive preference is recorded.
- [x] Test governance classification and lanes are recorded.
- [x] Proportionality review is completed for the shared reset helper.
## Open Questions
- [x] No blocking requirements questions remain for preparation.
- [x] Runtime implementation must still confirm exact Filament persisted filter/session keys per hub.
- [x] Runtime implementation must document optional pages intentionally excluded.
- [x] Browser back/forward limitations must be documented if tooling is unstable.
## Review Outcome
- [x] Spec artifacts are ready for `/spec-kit-implementation-loop` or equivalent implementation pass.
- [x] No application implementation was performed during preparation.

View File

@ -0,0 +1,342 @@
# Implementation Plan: Workspace Hub Clear Filter Contract
**Branch**: `316-workspace-hub-clear-filter-contract` | **Date**: 2026-05-16 | **Spec**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/spec.md)
**Input**: Feature specification from `/specs/316-workspace-hub-clear-filter-contract/spec.md`
**Preparation status**: Specification artifacts only. No runtime implementation has been performed by this preparation step.
## Summary
Spec 316 completes the active Workspace Hub Environment filter lifecycle:
```text
314: sidebar/global entry -> clean workspace-wide hub
315: Environment CTA -> workspace hub ?environment_id=...
316: Clear filter -> clean workspace-wide hub, reload-safe
```
The implementation must make clear-filter behavior shared, complete, and reliable across URL query state, Livewire/page state, Filament table/deferred filters, persisted/session filters, visible chip state, header/scope wording, rendered data scope, reload, and focused browser history flows.
## Technical Context
**Language/Version**: PHP 8.4.15, Laravel 12.52.0
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Laravel Sail, Laravel Socialite, Laravel MCP
**Storage**: PostgreSQL; no schema changes for this spec
**Testing**: Pest 4.3.1 / PHPUnit 12.5.4; focused browser smoke where applicable
**Validation Lanes**: fast-feedback, confidence for Filament/Livewire state, browser for reload/history smoke
**Target Platform**: Laravel admin application under `apps/platform`, local development through Sail, staging/production through Dokploy
**Project Type**: Web application, Laravel/Filament admin panel
**Performance Goals**: No material performance change. Reset logic should touch bounded session/filter arrays and existing page state only.
**Constraints**: No migrations, seeders, new packages, env vars, queues, scheduler, storage, compatibility aliases, compatibility redirects, or broad legacy cleanup.
**Scale/Scope**: Cross-cutting filter-state hardening across required Spec 315 workspace hubs.
## UI / Surface Guardrail Plan
- **Guardrail scope**: Changed operator-facing scope truth for clear-filter behavior on existing workspace hubs.
- **Native vs custom classification summary**: Native Filament/Livewire pages/resources with the existing shared chip partial. No redesign and no new styling system.
- **Shared-family relevance**: Scope signals, filter summaries, clear links/actions, workspace hub navigation, and table filter state.
- **State layers in scope**: URL query, Livewire public properties, Filament `tableFilters`, `tableDeferredFilters`, persisted/session table filters, page-derived state, chip/header wording, data scope, reload, and focused browser history.
- **Audience modes in scope**: Operator-MSP and support-platform. Customer-read-only applies only to existing Customer Review Workspace behavior.
- **Decision/diagnostic/raw hierarchy plan**: Filter truth stays default-visible. Existing diagnostics and raw/support evidence remain unchanged.
- **Raw/support gating plan**: No raw evidence exposure changes.
- **One-primary-action / duplicate-truth control**: When filtered, the chip is the single page-level truth and `Clear filter` is the exit. After clear, absence of chip plus workspace-wide rows is the truth.
- **Handling modes by drift class or surface**: Hard-stop for legacy aliases recreating Environment-filtered state; review-mandatory for any page-specific clear exception.
- **Repository-signal treatment**: Feature tests and focused browser screenshots/notes are required evidence.
- **Special surface test profiles**: global-context-shell, monitoring-state-page, standard-native-filament.
- **Required tests or manual smoke**: Clear-state contract tests, persisted-state tests, clean-entry equivalence tests, Spec 314/315 regression tests, reload smoke, and high-risk back/forward smoke.
- **Exception path and spread control**: Optional hubs are classified and excluded unless already Environment-filterable via `environment_id`. Any browser limitation is documented in the implementation report.
- **Active feature PR close-out entry**: Guardrail and Smoke Coverage.
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes.
- **Systems touched**: `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, `CanonicalAdminTenantFilterState`, possible `WorkspaceHubFilterStateResetter`, workspace hub Filament pages/resources, shared chip view, table/session filter keys, and Pest/browser tests.
- **Shared abstractions reused**: `WorkspaceHubRegistry` for hub paths, clean URLs, and Environment-like key lists; `WorkspaceHubEnvironmentFilter` for valid `environment_id`; existing chip partial for visible state; existing Filament table state APIs/patterns.
- **New abstraction introduced? why?**: Prefer formalizing one shared reset service/helper only if existing helpers cannot safely own all required Environment-like state layers. The abstraction is justified by repeated current hubs and reload defects.
- **Why the existing abstraction was sufficient or insufficient**: Existing pieces solve entry and immediate visible filter display, but the reset path is not yet complete across table/deferred/session/page state.
- **Bounded deviation / spread control**: Page-specific data refresh can stay local, but Environment-like key removal and persisted filter/session reset must use the shared contract or a documented equivalent.
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: no.
- **Central contract reused**: N/A.
- **Delegated UX behaviors**: N/A.
- **Surface-owned behavior kept local**: Existing Operations hub inspect/detail behavior remains unchanged.
- **Queued DB-notification policy**: N/A.
- **Terminal notification path**: N/A.
- **Exception path**: none.
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: yes.
- **Provider-owned seams**: Existing Provider Connection records and provider external tenant identifiers remain model/provider data only.
- **Platform-core seams**: Workspace hub query contract, Environment filter reset contract, session/table filter key cleanup, and scope wording.
- **Neutral platform terms / contracts preserved**: `Workspace`, `Environment`, `environment_id`, `Environment filter`, `Clear filter`.
- **Retained provider-specific semantics and why**: Provider-specific connection identity remains unchanged and cannot be fallback filter state.
- **Bounded extraction or follow-up path**: Broader old Tenant naming and compatibility seam cleanup is Spec 317.
## Constitution Check
*GATE: Must pass before implementation. Re-check after runtime changes.*
- Inventory-first: no inventory/snapshot truth changes.
- Read/write separation: no Graph writes or destructive operations are added.
- Graph contract path: no Graph calls are introduced.
- Deterministic capabilities: existing page/resource capabilities remain; clear does not grant access.
- RBAC-UX: workspace/page authorization remains authoritative. Non-member access remains 404/safe no-access; member-missing-capability remains existing 403 behavior.
- Workspace isolation: clear keeps the current Workspace and does not infer or switch Environment shell context.
- Tenant isolation: Environment filters are never authorization substitutes; clearing widens only to the user's entitled workspace data.
- Run observability: no new OperationRun lifecycle behavior.
- Test governance (TEST-GOV-001): lane, fixture cost, browser scope, and reviewer handoff are explicit in spec/plan/tasks.
- Proportionality (PROP-001): shared reset behavior is justified by cross-hub current-release stale state.
- No premature abstraction (ABSTR-001): reset helper stays bounded to Environment-like workspace hub filter state.
- Persisted truth (PERSIST-001): no persisted truth is added.
- Behavioral state (STATE-001): no new state/status family is added.
- UI semantics (UI-SEM-001): chip/scope state is direct domain-to-UI mapping.
- Shared pattern first (XCUT-001): extend Spec 314/315 registry, resolver, chip, and existing filter helper behavior before adding local patterns.
- Provider boundary (PROV-001): provider external tenant IDs cannot become platform filter truth.
- V1 explicitness / few layers: direct hard cutover, bounded helper, page-local data refresh where needed.
- Spec discipline / bloat check: follow-up specs 317/318 remain separate.
- Filament-native UI (UI-FIL-001): no ad-hoc styling; existing Filament/Blade primitives only.
- UI/UX scope, truth, and naming: scope signals must distinguish Workspace shell from explicit Environment filter state.
- UI naming: visible clear action uses `Clear filter`; visible filter wording uses `Environment`.
## Test Governance Check
- **Test purpose / classification by changed surface**: Feature/Livewire for page/table/session state, Browser for reload/history rendered state.
- **Affected validation lanes**: fast-feedback, confidence, browser.
- **Why this lane mix is the narrowest sufficient proof**: Route/Livewire tests prove reset mechanics; browser smoke proves reload/history URL/chip/data alignment.
- **Narrowest proving command(s)**:
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubClearFilter`
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubEnvironmentFilter`
- `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHubNavigation`
- focused browser smoke command or manual browser verification documented in close-out
- **Fixture / helper / factory / seed / context cost risks**: Existing workspace/environment/member context and page-specific factories may be needed. Keep helper setup opt-in and avoid broad seeders.
- **Expensive defaults or shared helper growth introduced?**: no. Any shared reset helper is runtime behavior, not a test setup default.
- **Heavy-family additions, promotions, or visibility changes**: Focused browser smoke only. Durable browser infrastructure is deferred to Spec 318.
- **Surface-class relief / special coverage rule**: Native Filament/Livewire tests for page/table state; browser smoke for integrated UI state.
- **Closing validation and reviewer handoff**: Reviewers must verify no hidden Environment state survives clear/reload, no legacy aliases become canonical, no unrelated filters are over-cleared without reason, and browser limitations are documented.
- **Budget / baseline / trend follow-up**: none expected.
- **Review-stop questions**: Does any hub still restore `managed_environment_id`/`tenant` from session after clear? Does clean URL show filtered rows? Does back/forward mismatch URL and chip? Does clear switch Workspace or Environment shell context?
- **Escalation path**: Follow-up Spec 318 only for durable browser no-drift infrastructure; Spec 317 for broad legacy naming/query cleanup.
- **Active feature PR close-out entry**: Guardrail and Smoke Coverage.
- **Why no dedicated follow-up spec is needed**: This is the dedicated clear-filter spec. Only broad cleanup and durable browser infrastructure remain separate.
## Project Structure
### Documentation (this feature)
```text
specs/316-workspace-hub-clear-filter-contract/
|-- spec.md
|-- plan.md
|-- tasks.md
|-- checklists/
| `-- requirements.md
`-- artifacts/
`-- screenshots/ # created during implementation/browser verification if useful
```
No `research.md`, `data-model.md`, `quickstart.md`, or `contracts/` artifact is required for preparation because this feature introduces no data model, external API contract, or new workflow API.
### Source Code (repository root)
Likely runtime files to inspect or update during implementation:
```text
apps/platform/app/Support/Navigation/WorkspaceHubRegistry.php
apps/platform/app/Support/Navigation/WorkspaceHubEnvironmentFilter.php
apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php
apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php # if new helper is required
apps/platform/app/Filament/Pages/Monitoring/Operations.php
apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php
apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php
apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php
apps/platform/app/Filament/Pages/Governance/DecisionRegister.php
apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php
apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php
apps/platform/app/Filament/Resources/ProviderConnectionResource.php
apps/platform/resources/views/filament/partials/workspace-hub-environment-filter-chip.blade.php
apps/platform/tests/Feature/
apps/platform/tests/Browser/
```
Potential classification-only inspection areas:
```text
apps/platform/app/Filament/Pages/Monitoring/AuditLog.php
apps/platform/app/Filament/Resources/Alert*
apps/platform/app/Filament/Resources/StoredReport*
apps/platform/app/Filament/Resources/SupportRequest*
```
**Structure Decision**: Laravel/Filament platform app under `apps/platform`. New runtime source, if needed, stays under existing `app/Support` boundaries. Tests stay in existing Pest feature/browser directories.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| Shared reset service/helper if existing helper cannot own the contract | Clear state spans repeated current hubs and multiple state layers | Page-local clear handlers already caused inconsistent reload/session behavior |
| Possible page integration helper/trait | Repeated resolver/chip/reset wiring may otherwise drift | Eight local implementations would recreate the defect; a broad context framework is still rejected |
## Phase 0: Discovery Completed During Preparation
Relevant repository facts discovered before authoring this plan:
- Current branch before creation was `platform-dev`, clean, at `eced9ad5 Spec 315: implement environment CTA explicit filter contract (#370)`.
- `specs/316-workspace-hub-clear-filter-contract` did not exist before this preparation.
- Related Specs 313, 314, and 315 contain completed/checklist/close-out signals and are historical context only.
- `WorkspaceHubRegistry` already centralizes hub paths, forbidden query keys, Environment-like keys, `cleanUrl()`, and clean workspace hub entry detection.
- `WorkspaceHubEnvironmentFilter` already resolves canonical `environment_id` inside the current Workspace and exposes display/query/clear helpers.
- `workspace-hub-environment-filter-chip.blade.php` already renders `Environment filter:` and `Clear filter`.
- `CanonicalAdminTenantFilterState` already handles some persisted filter cleanup for Environment-like keys on clean workspace hub entry.
- Operations, Finding Exceptions Queue, Evidence Overview, Review Register, Customer Review Workspace, Decision Register, Governance Inbox, and Provider Connections all contain Environment-like filter/page state that may need reset integration.
- Laravel Boost docs confirmed Filament v5 supports `persistFiltersInSession()` and deferred filters, and Livewire v4 URL-bound properties read/write query string state on page load.
## Technical Approach
### 1. Formalize the shared reset mechanism
Preferred implementation:
```text
apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php
```
or a bounded extension/delegation of:
```text
apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php
```
Responsibilities:
- identify the current workspace hub through `WorkspaceHubRegistry`
- clear Environment-like query/session/table/deferred keys
- clear legacy alias keys where present
- remove nested Environment-like Filament table filter entries
- support per-hub session key mapping where needed
- preserve unrelated user filters when safe
- never clear selected Workspace, auth/session, or unrelated table preferences
### 2. Keep the canonical filter source narrow
Only `WorkspaceHubEnvironmentFilter::fromRequest()` may create filtered state for workspace hubs.
Invalid sources:
```text
tenant
tenant_id
managed_environment_id
environment
tenant_scope
tableFilters
remembered Environment
Filament tenant
session last environment
lastTenantId / lastEnvironmentId
provider external tenant id
```
### 3. Standardize clear targets
The final clear URL should be the clean hub URL with no query string by default:
```text
/admin/workspaces/{workspace}/operations
/admin/governance/inbox
/admin/governance/decisions
/admin/finding-exceptions/queue
/admin/provider-connections
/admin/evidence/overview
/admin/reviews
/admin/reviews/workspace
```
If preserving unrelated query parameters is implemented, tests must prove stale Environment-like state cannot survive.
### 4. Integrate required hubs
Each required hub must:
- resolve valid `environment_id` through `WorkspaceHubEnvironmentFilter`
- call shared reset behavior when `environment_id` is absent or clear entry is used
- clear local Livewire properties or table arrays that represent Environment filters
- clear persisted session filter keys for Environment-like entries
- refresh derived rows/counts/header state to workspace-wide
- render chip only when a valid filter is active
- keep shell context workspace-first
### 5. Preserve Spec 314 and Spec 315 behavior
Regression requirements:
- sidebar/global workspace hub entries remain clean and workspace-wide
- Environment-owned CTAs still use `environment_id`
- legacy params remain noncanonical
- cross-workspace `environment_id` remains rejected
- Decision Register clean URL remains valid
## Data Model
No data model changes.
No migrations, seeders, backfills, stored filter migrations, stored URL migrations, compatibility transforms, retention changes, queues, scheduler, or storage changes.
## Security and RBAC
- Clear filter never grants access.
- Clear filter widens only to the current user's authorized workspace-wide data.
- Existing page/resource policies and workspace membership checks remain authoritative.
- Non-member workspace/environment access remains deny-as-not-found.
- Member-without-capability behavior remains existing 403 where applicable.
- Clear filter must not reveal cross-workspace Environment existence.
- No provider external tenant ID fallback may become an authorization or filter boundary.
## Deployment / Operations
- No new environment variables.
- No migrations.
- No queues or scheduler changes.
- No storage persistence or volume changes.
- No package changes.
- No Dokploy-specific deployment changes expected.
- If implementation registers Filament assets unexpectedly, deployment must include `cd apps/platform && php artisan filament:assets`; no registered assets are expected.
## Browser Verification Plan
Use the in-app browser or existing Pest Browser tooling after runtime changes.
Required hubs:
- Operations
- Governance Inbox
- Decision Register
- Finding Exceptions Queue
- Provider Connections
- Evidence
- Reviews
- Customer Reviews
Flows:
1. Filter -> Clear -> Reload for every required hub.
2. Persisted Filter -> Sidebar Entry where a persisted filter can be applied.
3. Browser Back/Forward for Provider Connections, Finding Exceptions Queue, Customer Reviews, and Evidence.
Screenshots may be saved under:
```text
specs/316-workspace-hub-clear-filter-contract/artifacts/screenshots/
```
## Implementation Notes
- Use direct hard cuts.
- Do not add compatibility middleware, alias readers, or redirects.
- Do not broaden optional hubs.
- Do not update tests by broad rebaseline.
- Prefer removing Environment-like persisted state over preserving it.
- Keep any new helper narrow and repo-local.
- Document intentional exclusions and browser limitations in the final implementation report.

View File

@ -0,0 +1,480 @@
# Feature Specification: Workspace Hub Clear Filter Contract
**Feature Branch**: `316-workspace-hub-clear-filter-contract`
**Created**: 2026-05-16
**Status**: Draft
**Input**: User supplied Spec 316 draft for completing the Workspace Hub Environment filter lifecycle after Specs 314 and 315.
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: Workspace hubs can now be entered cleanly through sidebar/global navigation and explicitly filtered through `environment_id`, but clearing that filter can leave stale URL, Livewire, Filament table, deferred, session, or derived page state behind.
- **Today's failure**: A user can click `Clear filter` and see a clean URL or no chip while rows, header wording, persisted filters, or reload state remain scoped to the old Environment.
- **User-visible improvement**: Clearing an Environment filter returns the hub to the same state as clean sidebar/global entry: Workspace hub, no Environment chip, workspace-wide data, reload safe.
- **Smallest enterprise-capable version**: Formalize one shared clear/reset contract for in-scope workspace hubs, wire it into existing page/table state where needed, and add focused feature/browser coverage for reload and back/forward safety.
- **Explicit non-goals**: No new Environment CTA filtering, no broad legacy naming cleanup, no durable browser no-drift infrastructure, no migrations, no seeders, no package/env/queue/scheduler/storage changes, and no compatibility aliases.
- **Permanent complexity imported**: One shared reset service/helper or formalized existing helper path, optional small page integration trait/helper if the existing page pattern cannot stay consistent, focused Pest and browser-smoke coverage. No persisted entity, enum/status family, queue, external API, or new source of truth is introduced.
- **Why now**: Spec 314 made clean workspace hub entry safe and Spec 315 made Environment CTA entry explicit. The remaining lifecycle gap is clear-filter exit; leaving it open keeps user-visible scope truth unreliable.
- **Why not local**: The same stale-state risk exists across Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Reviews, and Customer Reviews. Page-local clear code already caused drift.
- **Approval class**: Core Enterprise.
- **Red flags triggered**: Cross-cutting filter state reset and a possible shared helper abstraction. Defense: the helper is bounded to clearing Environment-like filter state for current workspace hubs and is required by more than two current surfaces.
- **Score**: Value: 2 | Urgency: 2 | Scope: 2 | Complexity: 1 | Product proximity: 2 | Reuse: 2 | **Total: 11/12**
- **Decision**: approve.
## Spec Scope Fields *(mandatory)*
- **Scope**: canonical-view.
- **Primary Routes**: In-scope workspace hub routes for Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence Overview, Review Register, and Customer Review Workspace. Optional Audit Log, Alerts, Reports/Stored Reports, Risk Exceptions, and Support Requests are in scope only if existing runtime already made them Environment-filterable through `environment_id`.
- **Data Ownership**: Workspace hubs remain workspace-owned/canonical-view pages. Existing Environment-owned records remain owned by `workspace_id` plus `managed_environment_id` or their current repo-real relationship. This spec changes filter-state handling only.
- **RBAC**: Existing workspace membership, environment entitlement, page/resource policies, and capability checks remain authoritative. Clear filter must not grant access, switch Workspace, activate Environment shell context, or reveal cross-workspace Environment existence.
For canonical-view specs:
- **Default filter behavior when tenant-context is active**: Clean workspace hub entry has no Environment filter, regardless of remembered Environment, Filament tenant state, session state, old table filters, or legacy query params.
- **Explicit entitlement checks preventing cross-tenant leakage**: A hub is Environment-filtered only when `WorkspaceHubEnvironmentFilter` resolves a valid `environment_id` inside the selected Workspace for the current user. No legacy source may create a valid filter.
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
- **Cross-cutting feature?**: yes.
- **Interaction class(es)**: workspace hub scope signals, visible Environment filter chips, clear-filter links/actions, Filament table filters, page-level derived state, persisted/session filter state, browser reload/back-forward state, and navigation entry contracts.
- **Systems touched**: `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, `CanonicalAdminTenantFilterState`, existing workspace hub Filament pages/resources, shared chip partial, page table state, session filter keys, and related Pest/browser tests.
- **Existing pattern(s) to extend**: Spec 314 clean hub registry and URL cleaning, Spec 315 canonical `environment_id` resolver and visible chip, current `CanonicalAdminTenantFilterState` persisted filter cleanup behavior, and each page's existing table/query state pattern.
- **Shared contract / presenter / builder / renderer to reuse**: Prefer formalizing a shared `WorkspaceHubFilterStateResetter` behavior around existing reset logic. Reuse `WorkspaceHubRegistry::environmentLikeFilterKeys()`, `WorkspaceHubRegistry::cleanUrl()`, `WorkspaceHubEnvironmentFilter`, and `workspace-hub-environment-filter-chip`.
- **Why the existing shared path is sufficient or insufficient**: The repo already has the correct entry-point pieces, but the current reset surface is not yet guaranteed across all relevant layers and pages. Spec 316 makes the reset behavior explicit, complete, and covered.
- **Allowed deviation and why**: Page-specific data refresh/query mechanics may remain local when pages use different data sources, but Environment-like state identification, URL cleanup, chip clear targets, and persisted table/session cleanup must delegate to shared behavior.
- **Consistency impact**: URL state, Livewire state, Filament table/deferred filters, persisted session filters, visible chip, header/scope wording, data scope, reload, and back/forward behavior must agree.
- **Review focus**: Verify no page-local clear implementation preserves stale Environment state, no legacy key becomes canonical, and no unrelated user filters are cleared unnecessarily unless the implementation cannot safely preserve them.
## OperationRun UX Impact *(mandatory)*
- **Touches OperationRun start/completion/link UX?**: no.
- **Shared OperationRun UX contract/layer reused**: N/A.
- **Delegated start/completion UX behaviors**: N/A.
- **Local surface-owned behavior that remains**: Existing Operations hub and OperationRun inspect/detail behavior remain unchanged.
- **Queued DB-notification policy**: N/A.
- **Terminal notification path**: N/A.
- **Exception required?**: none.
## Provider Boundary / Platform Core Check *(mandatory)*
- **Shared provider/platform boundary touched?**: yes.
- **Boundary classification**: platform-core filter contract with provider-adjacent surfaces.
- **Seams affected**: Workspace hub query keys, provider connection Environment filters, legacy Tenant/Environment aliases, persisted table filter state, and scope wording.
- **Neutral platform terms preserved or introduced**: `Workspace`, `Environment`, `Workspace hub`, `Environment filter`, `Clear filter`, `environment_id`.
- **Provider-specific semantics retained and why**: Existing Provider Connection model data and external provider tenant identifiers remain unchanged, but they cannot be filter-state fallbacks for workspace hubs.
- **Why this does not deepen provider coupling accidentally**: The only valid filter source remains platform `environment_id` resolved through `ManagedEnvironment` ownership inside a Workspace.
- **Follow-up path**: Spec 317 removes/quarantines broader legacy Tenant / Environment context names. Spec 318 adds durable browser no-drift infrastructure.
## UI / Surface Guardrail Impact *(mandatory)*
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Shared Environment filter chip clear behavior | yes | Existing shared Blade partial / Filament page region | scope signals, header utility action | URL, page, table/session, visible state | no | Wording stays `Clear filter` |
| Operations | yes | Existing Filament page/table | monitoring-state page | URL, Livewire properties, table filters, deferred filters, persisted filters, data query | no | Reset must equal clean Operations entry |
| Governance Inbox | yes | Existing Filament page/list | governance queue | URL, page property, derived state, visible chip | no | Existing `tenantId` internals may remain only as noncanonical implementation detail |
| Decision Register | yes | Existing Filament page/list | decision register | URL, page property, derived state, visible chip | no | Clean URL must remain authorized/open |
| Finding Exceptions Queue | yes | Existing Filament page/table | exception queue | URL, table filters, deferred filters, persisted filters, selected state | no | Previously high-risk reload restoration surface |
| Provider Connections | yes | Existing Filament resource/table | provider connection registry | URL, query state, table/session filters | no | No provider external tenant fallback |
| Evidence Overview | yes | Existing Filament page/table or in-memory list | evidence viewer | URL, in-memory row filter, table/search filters, persisted state | no | Reset must refresh rows to workspace-wide |
| Review Register | yes | Existing Filament page/table | review register | URL, table/deferred filters, persisted filters | no | Workspace-wide review list after clear |
| Customer Review Workspace | yes | Existing Filament page/table | customer-safe review workspace | URL, table/deferred filters, persisted filters, page state | no | Previously high-risk reload restoration surface |
| Optional hubs | maybe | Existing page/resource | audit/alerts/reports/support | Only where already Environment-filterable | no | Do not add new filter support here |
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|---|---|---|---|---|---|---|---|
| Cleared workspace hub | Secondary Context | Decide from workspace-wide data after removing an Environment filter | Workspace context, no Environment chip, workspace-wide header/scope wording, workspace-wide rows | Existing row/detail diagnostics | The hub remains the decision/workspace surface; this spec restores truthful scope | Completes sidebar -> CTA -> clear lifecycle | Removes hidden stale state after clear/reload |
| Browser back/forward across filtered and cleared states | Secondary Context | Avoid acting on a page whose URL, chip, and data disagree | URL, chip presence/absence, data scope | Existing page details | Not a new workflow, but critical state truth | Keeps browser navigation trustworthy | Prevents repeated manual scope reconstruction |
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|---|---|---|---|---|---|---|---|
| Workspace hub filter state | operator-MSP, support-platform, customer-read-only where existing Customer Review Workspace applies | Either visible `Environment filter: {display name}` with `Clear filter`, or no chip and workspace-wide wording | Existing page/table diagnostics | Existing support/raw surfaces | Clear filter when filtered; inspect rows otherwise | Query/table/session internals | Chip, URL, header, and data scope must say the same thing |
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Operations | List / Table / Bulk | Monitoring/state page | Inspect operation or clear filter | Existing operations inspect pattern | existing | Existing page/table actions | Existing placement; no destructive changes | `/admin/workspaces/{workspace}/operations` | existing | Workspace-wide or Environment chip | Operations / Operation | Whether data is workspace-wide | none |
| Governance Inbox | List / Table / Bulk | Queue | Inspect governance item or clear filter | Existing inbox inspect pattern | existing | Existing page actions | Existing placement; no destructive changes | `/admin/governance/inbox` | existing | Workspace-wide or Environment chip | Governance Inbox | Whether data is workspace-wide | none |
| Decision Register | List / Table / Bulk | Register | Inspect decision or clear filter | Existing decision inspect pattern | existing | Existing page actions | Existing placement; no destructive changes | `/admin/governance/decisions` | existing | Workspace-wide or Environment chip | Decision Register | Whether data is workspace-wide | none |
| Finding Exceptions Queue | List / Table / Bulk | Queue | Inspect exception or clear filter | Existing exception inspect pattern | existing | Existing page/table actions | Existing placement; no destructive changes | `/admin/finding-exceptions/queue` | existing | Workspace-wide or Environment chip | Finding Exceptions | Whether data is workspace-wide | none |
| Provider Connections | List / Table / Bulk | Registry | Inspect provider connection or clear filter | Existing provider connection inspect pattern | existing | Existing resource actions | Existing placement; no destructive changes | `/admin/provider-connections` | existing | Workspace-wide or Environment chip | Provider Connections | Whether data is workspace-wide | none |
| Evidence Overview | List / Table / Bulk | Evidence viewer | Inspect evidence or clear filter | Existing evidence inspect pattern | existing | Existing page actions | Existing placement; no destructive changes | `/admin/evidence/overview` | existing | Workspace-wide or Environment chip | Evidence | Whether data is workspace-wide | none |
| Reviews / Customer Reviews | List / Table / Bulk | Review workspace | Inspect review or clear filter | Existing review inspect pattern | existing | Existing page/table actions | Existing placement; no destructive changes | `/admin/reviews`, `/admin/reviews/workspace` | existing | Workspace-wide or Environment chip | Reviews | Whether data is workspace-wide | none |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| Workspace hub after filter clear | TenantPilot operator | Continue from workspace-wide data after clearing a visible Environment filter | Existing list/table/page | Am I seeing all entitled workspace data again? | Clean URL, no Environment chip, workspace-wide rows/header/scope | Existing page diagnostics | Existing page-specific dimensions only | TenantPilot navigation/filter state only | Inspect rows, apply normal filters | None added or changed |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no.
- **New persisted entity/table/artifact?**: no.
- **New abstraction?**: yes, if existing reset behavior cannot be formalized directly; expected as a narrow `WorkspaceHubFilterStateResetter` service/helper or equivalent shared reset contract.
- **New enum/state/reason family?**: no.
- **New cross-domain UI framework/taxonomy?**: no.
- **Current operator problem**: Clear-filter can leave hidden Environment state active after the visible filter is gone, causing operators to act on the wrong scope.
- **Existing structure is insufficient because**: Spec 315 gives a resolver and chip, and `CanonicalAdminTenantFilterState` has some persisted filter cleanup, but there is not yet one guaranteed cross-hub contract that clears URL, Livewire, Filament table, deferred, persisted/session, and derived page state together.
- **Narrowest correct implementation**: Formalize shared reset behavior for Environment-like keys and call it from in-scope hubs on clean entry/clear. Leave page-specific query builders local.
- **Ownership cost**: One shared reset owner, per-hub integration, feature/browser regression coverage, and reviewer attention to session/table key maps.
- **Alternative intentionally rejected**: Eight independent clear handlers or compatibility aliases would preserve drift. A broad legacy cleanup belongs to Spec 317.
- **Release truth**: Current-release truth. This completes the active 314/315/316 filter lifecycle before broader cleanup.
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope. Canonical replacement is required. Legacy Environment-like keys may be removed or neutralized; they must not be preserved as valid filter sources.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature/Livewire for clear-state contracts and page behavior; Browser for reload and high-risk back/forward rendered-state safety.
- **Validation lane(s)**: fast-feedback for focused Pest tests, confidence for Filament/Livewire state pages/resources, browser for integrated reload/back-forward smoke.
- **Why this classification and these lanes are sufficient**: The risk spans server-resolved query/session state and rendered browser truth. Feature tests prove reset/data contracts; browser smoke proves URL/chip/data alignment after reload and history navigation.
- **New or expanded test families**: Spec 316 workspace hub clear contract tests, persisted table filter reset tests, legacy state reset tests, clean-entry equivalence tests, and focused browser smoke for high-risk hubs.
- **Fixture / helper cost impact**: Existing Workspace, ManagedEnvironment, membership/capability, OperationRun, FindingException, ProviderConnection, EvidenceSnapshot, Review, and Customer Review factories/helpers may be used. Any helper widening must stay opt-in.
- **Heavy-family visibility / justification**: Browser coverage is explicit and limited to critical/higher-risk flows because reload/history state cannot be fully proven by route tests alone.
- **Special surface test profile**: global-context-shell, monitoring-state-page, standard-native-filament.
- **Standard-native relief or required special coverage**: Native Filament tests cover page/table behavior; browser smoke covers integrated rendered state and history.
- **Reviewer handoff**: Reviewers must confirm tests prove URL, chip, Livewire/table/session state, data scope, reload safety, and Spec 314/315 regressions without broad suite rebaseline.
- **Budget / baseline / trend impact**: No material long-term lane shift expected. Any durable browser suite expansion belongs to Spec 318.
- **Escalation needed**: none unless implementation discovers structural no-drift browser infrastructure is required; that becomes Spec 318 work.
- **Active feature PR close-out entry**: Guardrail and Smoke Coverage.
- **Planned validation commands**: Focused Pest filters for Spec 316 feature tests, existing Spec 314/315 regression tests, `git diff --check`, Pint for touched PHP files, and focused browser smoke/manual verification.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Clear returns a filtered workspace hub to workspace-wide state (Priority: P1)
An operator opens a workspace hub with `environment_id`, sees the Environment chip, clicks `Clear filter`, and lands on the clean hub URL with workspace-wide data and no stale chip or scope wording.
**Why this priority**: This is the core contract and the visible operator problem.
**Independent Test**: For each required hub, start filtered by `environment_id`, assert chip and filtered data, clear the filter, and assert clean URL, no legacy params, no chip, workspace-wide header/scope wording, and workspace-wide seeded rows.
**Acceptance Scenarios**:
1. **Given** Operations is filtered by Environment A, **When** the operator clicks `Clear filter`, **Then** the final URL has no query string, the chip disappears, table filters no longer apply Environment A, and operations from other entitled environments are visible.
2. **Given** Governance Inbox is filtered by Environment A, **When** the operator clears the filter, **Then** the page-level Environment property is null and the inbox returns to workspace-wide rows.
3. **Given** Decision Register is filtered by Environment A, **When** the operator clears the filter, **Then** the clean register URL opens for the authorized workspace user and no 403 is caused by missing filter state.
---
### User Story 2 - Clear removes persisted and deferred Environment-like state (Priority: P1)
An operator has stale Filament/session filter state from an Environment filter. Clearing must remove the Environment-like persisted state so reload does not silently restore it.
**Why this priority**: Previously observed defects included chip removal while table/session filters continued to apply.
**Independent Test**: Persist Environment-like table filters for Finding Exceptions Queue, Customer Reviews, Evidence, and Provider Connections; visit filtered state; clear; reload; assert persisted keys are gone and workspace-wide rows are visible.
**Acceptance Scenarios**:
1. **Given** Finding Exceptions Queue has a persisted `managed_environment_id` table filter, **When** the operator clears the visible `environment_id` filter, **Then** the persisted table filter is removed and reload remains workspace-wide.
2. **Given** Customer Review Workspace has stale `tenant` or `managed_environment_id` persisted filter state, **When** the operator clears the filter, **Then** no stale review Environment filter is reapplied after reload.
---
### User Story 3 - Legacy Environment aliases cannot recreate filtered state (Priority: P1)
An operator or stale browser/session state contains legacy keys. Clean hub entry and clear results must neutralize those aliases instead of treating them as canonical filter state.
**Why this priority**: Hard cutover is a product and constitution requirement in this pre-production repo.
**Independent Test**: Seed stale query/session/table keys for `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and nested `tableFilters`; assert clear neutralizes them and reload does not reapply hidden filtering.
**Acceptance Scenarios**:
1. **Given** Provider Connections session state contains `tableFilters.tenant.value`, **When** the operator clears the Environment filter, **Then** the legacy key is removed or ignored and provider rows are workspace-wide.
2. **Given** Evidence Overview URL contains no `environment_id` but session state contains old `tenant_scope`, **When** the page renders through clean hub entry, **Then** no Environment chip appears and rows are not silently Environment-scoped.
---
### User Story 4 - Clear result equals clean sidebar/global entry (Priority: P1)
An operator clearing a filter and an operator entering through sidebar/global navigation should see the same hub state.
**Why this priority**: This connects Specs 314, 315, and 316 into one lifecycle.
**Independent Test**: Capture clean hub entry state, visit filtered URL, clear, and assert the final URL, chip/scope indicators, and data set match clean entry.
**Acceptance Scenarios**:
1. **Given** a user enters Evidence Overview through the sidebar, **When** the same user later opens Evidence with `environment_id` and clears it, **Then** the final state matches the sidebar entry state.
---
### User Story 5 - Reload and browser history keep URL, chip, and data aligned (Priority: P2)
An operator clears a filter, reloads, and uses browser back/forward. The browser must not show a clean URL with filtered data or a filtered URL with missing chip.
**Why this priority**: The stale state defect is most visible after reload/history navigation, but durable no-drift infrastructure remains Spec 318.
**Independent Test**: Browser-smoke Provider Connections, Finding Exceptions Queue, Customer Reviews, Evidence, and Operations for filter -> clear -> reload and high-risk back/forward alignment.
**Acceptance Scenarios**:
1. **Given** Customer Review Workspace was filtered and then cleared, **When** the browser reloads the clean URL, **Then** no Environment chip returns and workspace-wide reviews remain visible.
2. **Given** Provider Connections filtered URL is in browser history, **When** the operator goes back and forward, **Then** each visible state matches the current URL: filtered URL has chip and filtered data; clean URL has no chip and workspace-wide data.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: A workspace hub is Environment-filtered only when `environment_id` is present, valid, workspace-owned, user-accessible, and resolved by `WorkspaceHubEnvironmentFilter`.
- **FR-002**: `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, remembered Environment, Filament tenant state, session last environment, and `tableFilters` MUST NOT create canonical Environment-filtered hub state.
- **FR-003**: Clear filter final URLs MUST remove `environment_id`, `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`.
- **FR-004**: The default final clear target SHOULD be the clean hub URL with no query string unless preserving unrelated query params is proven safe and intentional.
- **FR-005**: A shared reset mechanism MUST clear Environment-like persisted/session filter state for in-scope workspace hubs.
- **FR-006**: Environment-like keys for reset MUST include `environment_id`, `environment`, `tenant`, `tenant_id`, `managed_environment_id`, and `tenant_scope`.
- **FR-007**: Nested Filament table filters representing Environment selection MUST be removed or neutralized, including `tableFilters.environment.value`, `tableFilters.environment_id.value`, `tableFilters.tenant.value`, `tableFilters.tenant_id.value`, `tableFilters.managed_environment_id.value`, and `tableFilters.tenant_scope.value` where present.
- **FR-008**: Clear MUST reset Environment-related Livewire public properties or derived page state for the affected hub.
- **FR-009**: Clear MUST reset Environment-related Filament `tableFilters` state where the hub uses tables.
- **FR-010**: Clear MUST reset Environment-related `tableDeferredFilters` state where the hub uses deferred filters.
- **FR-011**: Clear MUST remove Environment-like persisted table/session filters without clearing unrelated safe user filters such as search/status/date unless the page implementation cannot safely preserve them.
- **FR-012**: Visible Environment filter chips MUST disappear after clear.
- **FR-013**: Header, scope, empty-state, and shell wording MUST return to workspace-wide/all-environments wording after clear where applicable.
- **FR-014**: Data MUST return to workspace-wide scope after clear.
- **FR-015**: Reload after clear MUST NOT restore Environment-filtered state.
- **FR-016**: Sidebar/global revisit after clear MUST remain clean and workspace-wide.
- **FR-017**: Browser back/forward MUST keep URL, chip, shell, and data scope aligned where feasible; any tooling limitation must be documented.
- **FR-018**: Clear MUST keep the selected Workspace active and MUST NOT activate or infer Environment shell context.
- **FR-019**: Clear MUST NOT weaken existing page authorization or use Environment filters as an authorization substitute.
- **FR-020**: Operations MUST clear `environment_id`, Operations-specific Environment/table state, deferred filters, persisted Environment-like table state, chip, and misleading filtered wording.
- **FR-021**: Governance Inbox MUST clear `environment_id`, page Environment property state, query-derived filter state, and chip.
- **FR-022**: Decision Register MUST clear `environment_id`, Environment-derived page state, chip, and any persisted/deferred state if applicable; clean URL remains authorized.
- **FR-023**: Finding Exceptions Queue MUST clear `environment_id`, Filament Environment table filter, deferred/persisted table filter state, old `tenant` persisted state, and chip.
- **FR-024**: Provider Connections MUST clear `environment_id`, provider/environment filter state, hidden query-level Environment filters, persisted table filters if present, and chip.
- **FR-025**: Evidence Overview MUST clear `environment_id`, in-memory Environment row filtering, table/search Environment filters, persisted state, and chip.
- **FR-026**: Review Register MUST clear `environment_id`, review-list Environment filters, persisted state, old `tenant` state if present, and chip.
- **FR-027**: Customer Review Workspace MUST clear `environment_id`, Customer Review Environment filters, page properties, persisted/session table state, old aliases if present, and chip.
- **FR-028**: Optional hubs MUST NOT receive new Environment filter support in this spec. They are included only if Spec 315 already made them Environment-filterable via `environment_id`.
- **FR-029**: Page-local clear implementations MUST be removed or delegated to shared reset behavior where they can drift.
- **FR-030**: No backwards compatibility layer, legacy alias preservation, migration, seeder, package, env var, queue, scheduler, or storage change may be introduced.
### Non-Functional Requirements
- **NFR-001**: The implementation MUST preserve workspace isolation and deny-as-not-found behavior for out-of-scope workspace/environment access.
- **NFR-002**: The implementation MUST target Filament v5.2.1 and Livewire v4.1.4 patterns; no Livewire v3 or Filament v3/v4 APIs are allowed.
- **NFR-003**: The shared reset mechanism MUST stay narrow and must not become a broad context/session framework.
- **NFR-004**: Tests MUST prove reload safety and persisted-state clearing for high-risk hubs.
- **NFR-005**: Browser smoke MUST stay focused; durable no-drift infrastructure is deferred to Spec 318.
## Key Entities *(include if feature involves data)*
- **Workspace**: Primary SaaS and operating context. It remains selected after clear.
- **ManagedEnvironment**: Secondary operational context inside a Workspace. It is valid as a hub filter only through canonical `environment_id`.
- **WorkspaceHubRegistry**: Existing hub registry and URL/query cleanup helper. It supplies Environment-like key lists and clean hub URL behavior.
- **WorkspaceHubEnvironmentFilter**: Existing canonical resolver for valid `environment_id`.
- **WorkspaceHubFilterStateResetter**: Proposed/formalized shared reset behavior for Environment-like URL, table, deferred, persisted, and page state. It is not persisted and is not a new source of truth.
- **CanonicalAdminTenantFilterState**: Existing persisted filter helper that may be reused, narrowed, or delegated to by the new shared reset behavior.
## In Scope
- Clear-filter behavior for required Spec 315 `environment_id` hubs.
- Shared reset mechanism for Environment-like query/table/session/page state.
- Existing visible chip clear target behavior.
- Reset of Livewire public properties and derived state where used for Environment filtering.
- Reset of Filament `tableFilters`, `tableDeferredFilters`, and persisted filter session entries where used for Environment filtering.
- Reload and focused back/forward safety.
- Spec 314 and Spec 315 regressions.
- Focused browser verification and screenshot artifacts where useful.
## Out of Scope
- New Environment CTA filtering.
- Retrofitting optional hubs that were not already Environment-filterable through `environment_id`.
- Broad legacy Tenant/Environment naming cleanup.
- Removing every `tenant` occurrence in the codebase.
- Durable browser regression/no-drift infrastructure beyond focused Spec 316 smoke.
- RBAC redesign.
- Data model, migration, seeder, backfill, package, env var, queue, scheduler, or storage changes.
- Compatibility redirects or old URL preservation.
## Page-Specific Requirements
### Operations
- Clear removes `environment_id`, Operations Environment/table filter state, deferred filters, persisted Environment-like filters, chip, and misleading filtered header/scope state.
- After reload, Operations remains workspace-wide and no hidden Environment filter applies.
### Governance Inbox
- Clear removes `environment_id`, `$tenantId` or equivalent page Environment property, query-derived state, and chip.
- Existing internal `tenantId` naming may remain only where renaming is unnecessary for correctness; Spec 317 owns naming cleanup.
### Decision Register
- Clear removes `environment_id`, Environment-derived page state, visible chip, and persisted/deferred filters if applicable.
- Clean URL remains authorized and open for workspace users.
### Finding Exceptions Queue
- Clear removes `environment_id`, Filament Environment table filter, deferred filter, persisted table filter state, old `tenant` persisted state, and chip.
- Filtered queue -> clear -> reload equals workspace-wide queue with no Environment chip.
### Provider Connections
- Clear removes `environment_id`, provider/environment filter state, hidden query-level Environment filtering, persisted table filters if present, and chip.
- No remembered Environment inference and no provider external tenant fallback may survive clear.
### Evidence Overview
- Clear removes `environment_id`, in-memory row Environment filtering, table/search Environment filter state, persisted filter state, and chip.
- Reload remains workspace-wide.
### Review Register
- Clear removes `environment_id`, review-list Environment filter, persisted filter state, visible chip, and old `tenant` state if present.
- Reload remains workspace-wide.
### Customer Review Workspace
- Clear removes `environment_id`, customer-review Environment filter, page property state, persisted table/session state, visible chip, and old aliases if present.
- Customer Reviews filtered -> clear -> reload equals clean workspace-wide customer review workspace.
## Required Tests
- **Shared Clear State Contract**: `it_workspace_hub_clear_filter_removes_environment_id_and_state_layers` covering Operations, Finding Exceptions Queue, Provider Connections, Evidence, Reviews, Customer Reviews, Governance Inbox, and Decision Register.
- **Persisted Table Filter Reset**: `it_clear_filter_removes_persisted_environment_like_table_filters` covering Finding Exceptions Queue, Customer Reviews, Evidence, and Provider Connections.
- **Legacy Persisted State Reset**: `it_clear_filter_removes_legacy_environment_alias_state` covering legacy keys `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and nested `tableFilters` where repo supports them.
- **Clean Entry Equivalence**: `it_clear_filter_result_matches_clean_workspace_hub_entry` for critical hubs.
- **Browser Reload Safety**: `it_cleared_workspace_hub_environment_filter_does_not_restore_after_reload` for Provider Connections, Finding Exceptions Queue, Customer Reviews, and Operations.
- **Browser Back/Forward Safety**: `it_workspace_hub_clear_filter_does_not_create_mismatched_back_forward_state` for Provider Connections, Finding Exceptions Queue, Customer Reviews, and Evidence where tooling is stable.
- **Spec 314 Regression**: `it_sidebar_workspace_hub_entry_remains_clean_after_clear_filter_contract`.
- **Spec 315 Regression**: `it_environment_cta_filter_contract_still_uses_environment_id_after_clear_filter_contract`.
## Browser Verification Required
Required hubs:
- Operations
- Governance Inbox
- Decision Register
- Finding Exceptions Queue
- Provider Connections
- Evidence
- Reviews
- Customer Reviews
Required flows:
1. **Filter -> Clear -> Reload**: Open with `?environment_id={id}`, assert chip, click `Clear filter`, assert clean URL/no chip/workspace-wide state, reload, assert chip does not return.
2. **Persisted Filter -> Sidebar Entry**: Apply/persist Environment filter where possible, navigate away, enter through sidebar/global, assert clean URL/no chip/no hidden data filter.
3. **Browser Back/Forward**: For Provider Connections, Finding Exceptions Queue, Customer Reviews, and Evidence, verify URL/chip/data scope alignment after back/forward.
Screenshots may be saved under:
```text
specs/316-workspace-hub-clear-filter-contract/artifacts/screenshots/
```
Suggested screenshot names:
```text
operations--filtered.png
operations--after-clear.png
operations--after-clear-reload.png
provider-connections--filtered.png
provider-connections--after-clear.png
provider-connections--after-clear-reload.png
finding-exceptions-queue--filtered.png
finding-exceptions-queue--after-clear.png
finding-exceptions-queue--after-clear-reload.png
customer-reviews--filtered.png
customer-reviews--after-clear.png
customer-reviews--after-clear-reload.png
```
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Clearing a valid `environment_id` filter removes all Environment-like URL/query state from the final URL.
- **SC-002**: Clearing resets Environment-related Livewire properties, Filament table filters, deferred filters, and persisted/session filters for required hubs.
- **SC-003**: Clearing removes the visible Environment chip and returns header/scope/empty-state wording to workspace-wide state.
- **SC-004**: Clearing returns data to workspace-wide scope and reload does not restore Environment-filtered data.
- **SC-005**: Sidebar/global entry remains clean and workspace-wide after clear.
- **SC-006**: Browser back/forward does not create a misleading URL/chip/data mismatch on high-risk hubs or limitations are documented.
- **SC-007**: Spec 314 sidebar/global clean-entry contract and Spec 315 Environment CTA `environment_id` contract still pass.
## Assumptions
- Specs 313, 314, and 315 are completed historical context and must not be rewritten.
- `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, and `CanonicalAdminTenantFilterState` are the starting points for implementation.
- There is no production data or production environment to preserve.
- Existing factories and feature/browser harnesses can support the required focused coverage.
## Risks
- **Session key discovery risk**: Filament persisted table filters may use page/resource-specific session keys. Mitigation: inspect actual `getTableFiltersSessionKey()` usage and test persisted reset per high-risk hub.
- **Over-clearing risk**: A blunt reset could erase unrelated user filters. Mitigation: remove only Environment-like keys by default and document any unavoidable full clean URL behavior.
- **Back/forward tool instability**: Browser history assertions can be flaky. Mitigation: keep required coverage focused and document limitations if tooling blocks deterministic proof.
- **Abstraction drift risk**: A resetter could become a broad context framework. Mitigation: scope it to Environment-like workspace hub filter state only.
## Required Final Implementation Report
When implementation is complete, report:
```text
Spec 316 completed.
Changed behavior:
...
Shared clear mechanism:
...
Hubs covered:
...
Files changed:
...
Tests:
- command:
- result:
Browser verification:
...
Known remaining issues:
...
Remaining follow-ups:
- 317:
- 318:
No migrations were created.
No seeders were changed.
No packages, env vars, queues, scheduler, or storage changes were made.
No backwards compatibility layer was introduced.
No legacy query alias preservation was added.
```
Also include:
- list of clear-state layers covered
- list of hubs verified
- list of legacy state keys removed or neutralized
- pages intentionally excluded
- browser limitations
- unrelated residual test failures, if any
## Follow-Up Specs
- **317 - Legacy Tenant / Environment Context Cleanup**: Remove/quarantine old aliases, stale Tenant naming, remembered Environment as data boundary, `Filament::getTenant()` workspace hub usage, and compatibility seams.
- **318 - Browser Regression Coverage / No-Drift Guard**: Durable automated browser/regression coverage for sidebar/global entry, Environment CTA entry, clear filter, reload, back/forward, visible scope correctness, and hidden filter drift.
## Filament v5 Output Contract
1. **Livewire v4.0+ compliance**: This spec targets the current app stack: Filament v5.2.1 with Livewire v4.1.4. No Livewire v3 APIs or assumptions are allowed.
2. **Provider registration location**: No new Filament panel provider is expected. If implementation discovers provider registration work, Laravel 12 requires panel providers in `apps/platform/bootstrap/providers.php`, not `bootstrap/app.php`.
3. **Global search**: No resource is made globally searchable by this spec. Existing globally searchable resources must still have Edit/View pages; resources without safe View/Edit pages must keep global search disabled.
4. **Destructive actions**: No destructive actions are added or changed. Clear filter is a navigation/filter-state action, not a destructive mutation. If any implementation path touches a destructive action, it must use `->action(...)`, `->requiresConfirmation()`, authorization, and audit logging.
5. **Asset strategy**: No new heavy assets are expected. Reusing the existing chip partial requires no asset deployment change. If Filament assets are registered unexpectedly, deployment must include `cd apps/platform && php artisan filament:assets`.
6. **Testing plan**: Cover Filament pages/resources as Livewire components or feature routes following Pest 4 conventions. Required browser smoke remains focused on reload/history rendered-state safety.

View File

@ -0,0 +1,111 @@
# Tasks: Workspace Hub Clear Filter Contract
**Input**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/spec.md), [plan.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/316-workspace-hub-clear-filter-contract/plan.md)
**Prerequisites**: Specs 313, 314, and 315 are completed historical baseline context. Do not rewrite them.
**Important**: These tasks track the Spec 316 implementation, runtime verification, and close-out evidence.
## Test Governance Checklist
- [x] Lane assignment is named and is the narrowest sufficient proof for URL, Livewire, Filament, persisted/session, reload, and browser-history behavior.
- [x] New or changed tests stay in the smallest honest family, and any browser addition is explicit.
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default; any widening is isolated or documented.
- [x] Planned validation commands cover the change without pulling in unrelated lane cost.
- [x] The declared surface test profile (`global-context-shell`, `monitoring-state-page`, `standard-native-filament`) is explicit.
- [x] Any material budget, baseline, trend, browser limitation, or escalation note is recorded in the active spec or implementation close-out.
## Phase 1: Guardrails and Baseline
- [x] T001 Verify the implementation starts from branch `316-workspace-hub-clear-filter-contract` and the worktree has no unrelated user changes before runtime edits.
- [x] T002 Re-read Specs 313, 314, and 315 to confirm the completed baseline: clean sidebar/global entry, canonical `environment_id` CTA entry, visible chip, and clean clear target.
- [x] T003 Confirm Laravel/Filament/Livewire/Pest versions through Laravel Boost `application_info`.
- [x] T004 Confirm no migration, seeder, package, env var, queue, scheduler, storage, or deployment asset change is required.
- [x] T005 Inventory actual persisted/table/session state keys for Operations, Finding Exceptions Queue, Provider Connections, Evidence, Review Register, Customer Review Workspace, Governance Inbox, and Decision Register.
- [x] T006 Classify optional hubs and document that no new Environment filter support is added for optional pages.
## Phase 2: Tests First - Shared Clear Contract
- [x] T007 Add `it_workspace_hub_clear_filter_removes_environment_id_and_state_layers` covering Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Review Register, and Customer Review Workspace.
- [x] T008 Add assertions that each filtered hub starts with a valid `environment_id`, visible chip, clear action/link, and seeded filtered data where the page has data.
- [x] T009 Add assertions that after clear the URL has no `environment_id`, no legacy params, no chip, workspace-wide wording, and workspace-wide seeded rows.
- [x] T010 Add reload assertions to the shared clear contract where the test harness can revisit or rerender the page.
- [x] T011 Add `it_clear_filter_removes_persisted_environment_like_table_filters` covering Finding Exceptions Queue, Customer Review Workspace, Evidence Overview, and Provider Connections.
- [x] T012 Add `it_clear_filter_removes_legacy_environment_alias_state` for `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and nested `tableFilters` state where repo support exists.
- [x] T013 Add `it_clear_filter_result_matches_clean_workspace_hub_entry` for critical hubs by comparing clean entry and filtered-then-cleared state.
- [x] T014 Add or update `it_cleared_workspace_hub_environment_filter_does_not_restore_after_reload` for Provider Connections, Finding Exceptions Queue, Customer Review Workspace, and Operations.
- [x] T015 Add or update `it_workspace_hub_clear_filter_does_not_create_mismatched_back_forward_state` for Provider Connections, Finding Exceptions Queue, Customer Review Workspace, and Evidence, or document tooling limitations.
## Phase 3: Shared Reset Mechanism
- [x] T016 Create or formalize a shared reset mechanism such as `apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php` or a bounded extension of `CanonicalAdminTenantFilterState`.
- [x] T017 Make the reset mechanism use `WorkspaceHubRegistry` to identify workspace hubs and Environment-like keys.
- [x] T018 Make the reset mechanism remove or neutralize `environment_id`, `environment`, `tenant`, `tenant_id`, `managed_environment_id`, and `tenant_scope`.
- [x] T019 Make the reset mechanism remove nested Environment-like table filter entries such as `environment.value`, `environment_id.value`, `tenant.value`, `tenant_id.value`, `managed_environment_id.value`, and `tenant_scope.value`.
- [x] T020 Make the reset mechanism handle persisted Filament/session filter arrays and forget empty session filter keys.
- [x] T021 Make the reset mechanism preserve unrelated safe filters such as search/status/date when possible.
- [x] T022 Ensure the reset mechanism never clears selected Workspace, auth/session, unrelated table preferences, or unrelated resource state.
- [x] T023 Add focused unit/feature coverage for the reset mechanism with representative nested filter arrays.
## Phase 4: Shared Page Integration
- [x] T024 Reuse or add a small shared page integration helper only if repeated reset/resolver/chip wiring cannot stay consistent locally.
- [x] T025 Ensure clean workspace hub entry with no valid `environment_id` triggers shared Environment-like state reset before data rendering.
- [x] T026 Ensure clear-link navigation ends on the clean hub URL and the entry pipeline clears persisted Environment-like state before rendering.
- [x] T027 Ensure `WorkspaceHubEnvironmentFilter` remains the only valid source of Environment-filtered hub state.
- [x] T028 Ensure legacy query params are removed/ignored and cannot hydrate Environment-related page properties.
- [x] T029 Ensure the shared chip still renders only for valid `environment_id` and uses `Clear filter` as the action label.
## Phase 5: Required Hub Runtime Integration
- [x] T030 Update Operations to delegate clear/reset behavior, clear `environment_id`, `managed_environment_id` table/deferred/persisted state, and refresh workspace-wide data/header state.
- [x] T031 Update Governance Inbox to clear `environment_id`, `$tenantId` or equivalent derived property, query-derived state, and chip state through shared behavior.
- [x] T032 Update Decision Register to clear `environment_id`, Environment-derived page state, chip state, and any persisted/deferred filters while keeping clean URL authorized.
- [x] T033 Update Finding Exceptions Queue to clear `environment_id`, Environment table filter, deferred filters, persisted table/session filters, old `tenant` state, and chip state.
- [x] T034 Update Provider Connections to clear `environment_id`, provider/environment filter state, hidden query-level filters, persisted table filters if present, and provider external tenant fallback behavior.
- [x] T035 Update Evidence Overview to clear `environment_id`, in-memory row Environment filtering, table/search filters, persisted filters, and chip state.
- [x] T036 Update Review Register to clear `environment_id`, review-list Environment filters, persisted filters, old `tenant` state if present, and chip state.
- [x] T037 Update Customer Review Workspace to clear `environment_id`, page properties, table/deferred/session filters, old aliases if present, and chip state.
## Phase 6: Legacy Alias Neutralization
- [x] T038 Search required hubs for `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`, remembered Environment, `Filament::getTenant()`, `lastTenantId`, and `lastEnvironmentId` as workspace hub filter sources.
- [x] T039 Remove or neutralize legacy alias handling where it can reapply workspace hub Environment filtering.
- [x] T040 Preserve unrelated legacy names only when they are outside workspace hub Environment filter behavior, and document them for Spec 317.
- [x] T041 Confirm Provider Connections does not infer filter state from provider external tenant identifiers after clear.
- [x] T042 Confirm clean hub entry never restores Environment filter state from persisted table/session keys.
## Phase 7: Regression Safety
- [x] T043 Re-run or update Spec 314 regression coverage proving sidebar/global workspace hub entry remains clean, has no Environment params, and shows workspace-wide data.
- [x] T044 Re-run or update Spec 315 regression coverage proving Environment-owned CTAs still use `environment_id`, visible chip still renders when filtered, legacy params remain noncanonical, and cross-workspace IDs are rejected.
- [x] T045 Add a Decision Register clean URL regression proving authorized users can open it after clear without 403 caused by missing filter state.
- [x] T046 Confirm clear behavior does not bypass page/resource authorization, does not reveal inaccessible Workspace/Environment existence, and preserves existing 404/403 semantics.
- [x] T047 Confirm no globally searchable resource or destructive action behavior changed; if a resource/action is touched, confirm Edit/View/global-search status remains valid or disabled and destructive actions still use `->action(...)`, `->requiresConfirmation()`, authorization, and audit behavior.
## Phase 8: Browser Verification
- [x] T048 Start the local platform stack using Sail or the repo's platform dev command.
- [x] T049 Run Flow A `Filter -> Clear -> Reload` for Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Review Register, and Customer Review Workspace.
- [x] T050 Run Flow B `Persisted Filter -> Sidebar Entry` for hubs where an Environment filter can be persisted.
- [x] T051 Run Flow C browser back/forward for Provider Connections, Finding Exceptions Queue, Customer Review Workspace, and Evidence.
- [x] T052 Save screenshots where useful under `specs/316-workspace-hub-clear-filter-contract/artifacts/screenshots/`.
- [x] T053 Document browser tooling limitations if back/forward or persisted-filter browser setup cannot be made deterministic.
## Phase 9: Final Validation
- [x] T054 Run focused Pest tests for the Spec 316 clear/reset contract.
- [x] T055 Run existing related Spec 314 and Spec 315 regression tests.
- [x] T056 Run formatting/static checks expected by the touched files, including Pint if PHP files changed.
- [x] T057 Run `git diff --check`.
- [x] T058 Prepare the final implementation report with changed behavior, shared clear mechanism, hubs covered, files changed, tests, browser verification, known issues, and remaining follow-ups.
- [x] T059 Confirm final report lists clear-state layers covered, hubs verified, legacy state keys removed/neutralized, pages intentionally excluded, browser limitations, and unrelated residual test failures if any.
- [x] T060 Confirm final report states no migrations, seeders, packages, env vars, queues, scheduler, storage changes, compatibility layer, or legacy query alias preservation were introduced.
## Explicit Non-Tasks
- [x] NT001 Do not implement new Environment CTA filtering beyond preserving Spec 315 behavior.
- [x] NT002 Do not retrofit optional pages that were not already Environment-filterable through `environment_id`.
- [x] NT003 Do not perform broad legacy Tenant / Environment naming cleanup; leave to Spec 317.
- [x] NT004 Do not build durable browser no-drift infrastructure; leave to Spec 318.
- [x] NT005 Do not add compatibility redirects, dual-param support, alias readers, or adapter layers.
- [x] NT006 Do not create migrations, seeders, packages, env vars, queues, scheduler changes, or storage changes.