From 36c41f1021c34fa4cca373c5242f33c7bb0c22db Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Wed, 13 May 2026 11:32:56 +0200 Subject: [PATCH] fix: gate environment navigation by route scope --- .../Filament/Pages/BaselineCompareLanding.php | 7 ++ .../Resources/BackupScheduleResource.php | 4 +- .../Filament/Resources/BackupSetResource.php | 4 +- .../Resources/BaselineProfileResource.php | 4 +- .../Resources/BaselineSnapshotResource.php | 4 +- .../Resources/EvidenceSnapshotResource.php | 7 ++ .../Resources/FindingExceptionResource.php | 4 +- .../Filament/Resources/FindingResource.php | 4 +- .../Resources/InventoryItemResource.php | 4 +- .../app/Filament/Resources/PolicyResource.php | 4 +- .../Resources/PolicyVersionResource.php | 4 +- .../Filament/Resources/RestoreRunResource.php | 4 +- .../Filament/Resources/ReviewPackResource.php | 7 ++ .../Resources/StoredReportResource.php | 7 ++ .../Resources/TenantReviewResource.php | 4 +- .../EnsureFilamentTenantSelected.php | 65 +++++++++++++++- .../Support/Navigation/NavigationScope.php | 55 ++++++++++++++ .../BaselineProfileWorkspaceOwnershipTest.php | 9 ++- ...anceArtifactAdminPanelRegistrationTest.php | 4 +- .../PanelNavigationSegregationTest.php | 76 ++++++++++++++++++- .../plan.md | 39 ++++++++-- .../spec.md | 21 +++-- .../tasks.md | 13 ++++ .../terminology-audit.md | 37 +++++++++ 24 files changed, 360 insertions(+), 31 deletions(-) create mode 100644 apps/platform/app/Support/Navigation/NavigationScope.php diff --git a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php index 29411f13..75c80904 100644 --- a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php +++ b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php @@ -18,6 +18,7 @@ use App\Support\Baselines\BaselineCompareEvidenceGapDetails; use App\Support\Baselines\BaselineCompareStats; use App\Support\Navigation\CanonicalNavigationContext; +use App\Support\Navigation\NavigationScope; use App\Support\Baselines\TenantGovernanceAggregate; use App\Support\Baselines\TenantGovernanceAggregateResolver; use App\Support\OperationRunLinks; @@ -105,6 +106,12 @@ class BaselineCompareLanding extends Page protected string $view = 'filament.pages.baseline-compare-landing'; + public static function shouldRegisterNavigation(): bool + { + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); + } + public ?string $state = null; public ?string $message = null; diff --git a/apps/platform/app/Filament/Resources/BackupScheduleResource.php b/apps/platform/app/Filament/Resources/BackupScheduleResource.php index c7517300..e19c39d1 100644 --- a/apps/platform/app/Filament/Resources/BackupScheduleResource.php +++ b/apps/platform/app/Filament/Resources/BackupScheduleResource.php @@ -24,6 +24,7 @@ use App\Support\Badges\TagBadgeDomain; use App\Support\Badges\TagBadgeRenderer; use App\Support\Filament\TablePaginationProfiles; +use App\Support\Navigation\NavigationScope; use App\Support\OperationRunLinks; use App\Support\OperationRunOutcome; use App\Support\OperationRunType; @@ -81,7 +82,8 @@ class BackupScheduleResource extends Resource public static function shouldRegisterNavigation(): bool { - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function canViewAny(): bool diff --git a/apps/platform/app/Filament/Resources/BackupSetResource.php b/apps/platform/app/Filament/Resources/BackupSetResource.php index 9681ee61..eb2d52d5 100644 --- a/apps/platform/app/Filament/Resources/BackupSetResource.php +++ b/apps/platform/app/Filament/Resources/BackupSetResource.php @@ -26,6 +26,7 @@ use App\Support\Badges\BadgeRenderer; use App\Support\Filament\TablePaginationProfiles; use App\Support\Navigation\CrossResourceNavigationMatrix; +use App\Support\Navigation\NavigationScope; use App\Support\Navigation\RelatedContextEntry; use App\Support\Navigation\RelatedNavigationResolver; use App\Support\OperationRunLinks; @@ -75,7 +76,8 @@ class BackupSetResource extends Resource public static function shouldRegisterNavigation(): bool { - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration diff --git a/apps/platform/app/Filament/Resources/BaselineProfileResource.php b/apps/platform/app/Filament/Resources/BaselineProfileResource.php index 6edc6dda..f47c29c8 100644 --- a/apps/platform/app/Filament/Resources/BaselineProfileResource.php +++ b/apps/platform/app/Filament/Resources/BaselineProfileResource.php @@ -30,6 +30,7 @@ use App\Support\Filament\FilterOptionCatalog; use App\Support\Inventory\InventoryPolicyTypeMeta; use App\Support\Navigation\CrossResourceNavigationMatrix; +use App\Support\Navigation\NavigationScope; use App\Support\Navigation\RelatedContextEntry; use App\Support\Navigation\RelatedNavigationResolver; use App\Support\OperationCatalog; @@ -96,7 +97,8 @@ public static function shouldRegisterNavigation(): bool return false; } - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function canViewAny(): bool diff --git a/apps/platform/app/Filament/Resources/BaselineSnapshotResource.php b/apps/platform/app/Filament/Resources/BaselineSnapshotResource.php index cf2b69f4..59a387e2 100644 --- a/apps/platform/app/Filament/Resources/BaselineSnapshotResource.php +++ b/apps/platform/app/Filament/Resources/BaselineSnapshotResource.php @@ -17,6 +17,7 @@ use App\Support\Baselines\BaselineSnapshotLifecycleState; use App\Support\Filament\FilterPresets; use App\Support\Navigation\CrossResourceNavigationMatrix; +use App\Support\Navigation\NavigationScope; use App\Support\Navigation\RelatedContextEntry; use App\Support\Navigation\RelatedNavigationResolver; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; @@ -67,7 +68,8 @@ public static function shouldRegisterNavigation(): bool return false; } - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function canViewAny(): bool diff --git a/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php b/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php index 575e4f56..c3f57edd 100644 --- a/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php +++ b/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php @@ -22,6 +22,7 @@ use App\Support\Evidence\EvidenceCompletenessState; use App\Support\Evidence\EvidenceSnapshotStatus; use App\Support\Navigation\RelatedContextEntry; +use App\Support\Navigation\NavigationScope; use App\Support\OperationCatalog; use App\Support\OperationRunLinks; use App\Support\OperationRunOutcome; @@ -83,6 +84,12 @@ class EvidenceSnapshotResource extends Resource protected static ?int $navigationSort = 55; + public static function shouldRegisterNavigation(): bool + { + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); + } + public static function canViewAny(): bool { $tenant = static::resolveTenantContextForCurrentPanel(); diff --git a/apps/platform/app/Filament/Resources/FindingExceptionResource.php b/apps/platform/app/Filament/Resources/FindingExceptionResource.php index 7ffb0e83..37bc51f8 100644 --- a/apps/platform/app/Filament/Resources/FindingExceptionResource.php +++ b/apps/platform/app/Filament/Resources/FindingExceptionResource.php @@ -23,6 +23,7 @@ use App\Support\Badges\BadgeRenderer; use App\Support\Filament\FilterOptionCatalog; use App\Support\Filament\TablePaginationProfiles; +use App\Support\Navigation\NavigationScope; use App\Support\Navigation\RelatedContextEntry; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; @@ -73,7 +74,8 @@ class FindingExceptionResource extends Resource public static function shouldRegisterNavigation(): bool { - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function canViewAny(): bool diff --git a/apps/platform/app/Filament/Resources/FindingResource.php b/apps/platform/app/Filament/Resources/FindingResource.php index fdd577c9..8d28ba27 100644 --- a/apps/platform/app/Filament/Resources/FindingResource.php +++ b/apps/platform/app/Filament/Resources/FindingResource.php @@ -26,6 +26,7 @@ use App\Support\Filament\TablePaginationProfiles; use App\Support\Navigation\CanonicalNavigationContext; use App\Support\Navigation\CrossResourceNavigationMatrix; +use App\Support\Navigation\NavigationScope; use App\Support\Navigation\RelatedContextEntry; use App\Support\Navigation\RelatedNavigationResolver; use App\Support\OperationRunLinks; @@ -80,7 +81,8 @@ class FindingResource extends Resource public static function shouldRegisterNavigation(): bool { - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function getNavigationLabel(): string diff --git a/apps/platform/app/Filament/Resources/InventoryItemResource.php b/apps/platform/app/Filament/Resources/InventoryItemResource.php index 59253277..94820679 100644 --- a/apps/platform/app/Filament/Resources/InventoryItemResource.php +++ b/apps/platform/app/Filament/Resources/InventoryItemResource.php @@ -18,6 +18,7 @@ use App\Support\Badges\TagBadgeRenderer; use App\Support\Filament\FilterOptionCatalog; use App\Support\Inventory\InventoryPolicyTypeMeta; +use App\Support\Navigation\NavigationScope; use App\Support\OperationRunLinks; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; @@ -60,7 +61,8 @@ class InventoryItemResource extends Resource public static function shouldRegisterNavigation(): bool { - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function getRouteBaseName(?Panel $panel = null): string diff --git a/apps/platform/app/Filament/Resources/PolicyResource.php b/apps/platform/app/Filament/Resources/PolicyResource.php index e0867130..4ef32adc 100644 --- a/apps/platform/app/Filament/Resources/PolicyResource.php +++ b/apps/platform/app/Filament/Resources/PolicyResource.php @@ -26,6 +26,7 @@ use App\Support\Badges\TagBadgeDomain; use App\Support\Badges\TagBadgeRenderer; use App\Support\Filament\TablePaginationProfiles; +use App\Support\Navigation\NavigationScope; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; use App\Support\OpsUx\OpsUxBrowserEvents; @@ -86,7 +87,8 @@ public static function getPluralModelLabel(): string public static function shouldRegisterNavigation(): bool { - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function canViewAny(): bool diff --git a/apps/platform/app/Filament/Resources/PolicyVersionResource.php b/apps/platform/app/Filament/Resources/PolicyVersionResource.php index 902d2049..373a7a82 100644 --- a/apps/platform/app/Filament/Resources/PolicyVersionResource.php +++ b/apps/platform/app/Filament/Resources/PolicyVersionResource.php @@ -33,6 +33,7 @@ use App\Support\Filament\FilterOptionCatalog; use App\Support\Filament\FilterPresets; use App\Support\Navigation\CrossResourceNavigationMatrix; +use App\Support\Navigation\NavigationScope; use App\Support\Navigation\RelatedContextEntry; use App\Support\Navigation\RelatedNavigationResolver; use App\Support\OperationRunLinks; @@ -84,7 +85,8 @@ class PolicyVersionResource extends Resource public static function shouldRegisterNavigation(): bool { - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function canViewAny(): bool diff --git a/apps/platform/app/Filament/Resources/RestoreRunResource.php b/apps/platform/app/Filament/Resources/RestoreRunResource.php index 8447ae33..f657f887 100644 --- a/apps/platform/app/Filament/Resources/RestoreRunResource.php +++ b/apps/platform/app/Filament/Resources/RestoreRunResource.php @@ -39,6 +39,7 @@ use App\Support\Badges\BadgeRenderer; use App\Support\Filament\FilterOptionCatalog; use App\Support\Filament\FilterPresets; +use App\Support\Navigation\NavigationScope; use App\Support\OperationCatalog; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; @@ -97,7 +98,8 @@ class RestoreRunResource extends Resource public static function shouldRegisterNavigation(): bool { - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-arrow-path-rounded-square'; diff --git a/apps/platform/app/Filament/Resources/ReviewPackResource.php b/apps/platform/app/Filament/Resources/ReviewPackResource.php index 5020fae0..f5e9ecee 100644 --- a/apps/platform/app/Filament/Resources/ReviewPackResource.php +++ b/apps/platform/app/Filament/Resources/ReviewPackResource.php @@ -18,6 +18,7 @@ use App\Support\Badges\BadgeCatalog; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; +use App\Support\Navigation\NavigationScope; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; use App\Support\Rbac\UiEnforcement; @@ -67,6 +68,12 @@ class ReviewPackResource extends Resource protected static ?int $navigationSort = 50; + public static function shouldRegisterNavigation(): bool + { + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); + } + public static function canViewAny(): bool { $tenant = static::resolveTenantContextForCurrentPanel(); diff --git a/apps/platform/app/Filament/Resources/StoredReportResource.php b/apps/platform/app/Filament/Resources/StoredReportResource.php index 15182565..0a12966a 100644 --- a/apps/platform/app/Filament/Resources/StoredReportResource.php +++ b/apps/platform/app/Filament/Resources/StoredReportResource.php @@ -16,6 +16,7 @@ use App\Support\Badges\BadgeRenderer; use App\Support\Filament\TablePaginationProfiles; use App\Support\ManagedEnvironmentLinks; +use App\Support\Navigation\NavigationScope; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; @@ -70,6 +71,12 @@ class StoredReportResource extends Resource protected static ?int $navigationSort = 49; + public static function shouldRegisterNavigation(): bool + { + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); + } + public static function canViewAny(): bool { return static::visibleReportTypesForCurrentUser() !== []; diff --git a/apps/platform/app/Filament/Resources/TenantReviewResource.php b/apps/platform/app/Filament/Resources/TenantReviewResource.php index d8e2799e..699edbc8 100644 --- a/apps/platform/app/Filament/Resources/TenantReviewResource.php +++ b/apps/platform/app/Filament/Resources/TenantReviewResource.php @@ -24,6 +24,7 @@ use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; use App\Support\Findings\FindingOutcomeSemantics; +use App\Support\Navigation\NavigationScope; use App\Support\OperationRunLinks; use App\Support\OperationRunType; use App\Support\OpsUx\OperationUxPresenter; @@ -87,7 +88,8 @@ class TenantReviewResource extends Resource public static function shouldRegisterNavigation(): bool { - return parent::shouldRegisterNavigation(); + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); } public static function getSlug(?Panel $panel = null): string diff --git a/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php b/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php index d5a8e6ff..a05431dc 100644 --- a/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php +++ b/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php @@ -3,14 +3,21 @@ namespace App\Support\Middleware; use App\Filament\Pages\WorkspaceOverview; +use App\Filament\Pages\Governance\GovernanceInbox; +use App\Filament\Pages\Reviews\CustomerReviewWorkspace; +use App\Filament\Pages\Settings\WorkspaceSettings; use App\Filament\Resources\AlertDeliveryResource; use App\Filament\Resources\AlertDestinationResource; use App\Filament\Resources\AlertRuleResource; +use App\Filament\Resources\ProviderConnectionResource; use App\Models\ManagedEnvironment; use App\Models\User; +use App\Models\Workspace; use App\Models\WorkspaceMembership; +use App\Services\Auth\WorkspaceCapabilityResolver; use App\Services\Auth\WorkspaceRoleCapabilityMap; use App\Support\Auth\Capabilities; +use App\Support\Navigation\NavigationScope; use App\Support\OperationRunLinks; use App\Support\OperateHub\OperateHubShell; use App\Support\Tenants\TenantPageCategory; @@ -162,7 +169,7 @@ private function configureNavigationForRequest(\Filament\Panel $panel): void return; } - if (filled(Filament::getTenant())) { + if (NavigationScope::isEnvironmentSurface()) { $panel->navigation(true); return; @@ -171,6 +178,36 @@ private function configureNavigationForRequest(\Filament\Panel $panel): void $panel->navigation(function (): NavigationBuilder { return app(NavigationBuilder::class) ->item(WorkspaceOverview::navigationItem()) + ->item( + NavigationItem::make('Governance inbox') + ->url(fn (): string => GovernanceInbox::getUrl(panel: 'admin')) + ->icon('heroicon-o-inbox-stack') + ->group('Governance') + ->sort(5), + ) + ->item( + NavigationItem::make('Customer reviews') + ->url(fn (): string => CustomerReviewWorkspace::getUrl(panel: 'admin')) + ->icon('heroicon-o-document-text') + ->group('Reporting') + ->sort(44), + ) + ->item( + NavigationItem::make('Integrations') + ->url(fn (): string => route('filament.admin.resources.provider-connections.index')) + ->icon('heroicon-o-link') + ->group('Settings') + ->sort(15) + ->visible(fn (): bool => ProviderConnectionResource::canViewAny()), + ) + ->item( + NavigationItem::make('Settings') + ->url(fn (): string => WorkspaceSettings::getUrl(panel: 'admin')) + ->icon('heroicon-o-cog-6-tooth') + ->group('Settings') + ->sort(20) + ->visible(fn (): bool => $this->canViewWorkspaceSettings()), + ) ->item( NavigationItem::make('Manage workspaces') ->url(fn (): string => route('filament.admin.resources.workspaces.index')) @@ -240,6 +277,32 @@ private function configureNavigationForRequest(\Filament\Panel $panel): void }); } + private function canViewWorkspaceSettings(): bool + { + $user = auth()->user(); + + if (! $user instanceof User) { + return false; + } + + $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); + + if (! is_int($workspaceId)) { + return false; + } + + $workspace = Workspace::query()->whereKey($workspaceId)->first(); + + if (! $workspace instanceof Workspace) { + return false; + } + + $resolver = app(WorkspaceCapabilityResolver::class); + + return $resolver->isMember($user, $workspace) + && $resolver->can($user, $workspace, Capabilities::WORKSPACE_SETTINGS_VIEW); + } + private function isWorkspaceScopedPageWithTenant(string $path): bool { return preg_match('#^/admin/workspaces/[^/]+/environments/[^/]+/(required-permissions|diagnostics|access-scopes)$#', $path) === 1; diff --git a/apps/platform/app/Support/Navigation/NavigationScope.php b/apps/platform/app/Support/Navigation/NavigationScope.php new file mode 100644 index 00000000..9bea4cf2 --- /dev/null +++ b/apps/platform/app/Support/Navigation/NavigationScope.php @@ -0,0 +1,55 @@ +path(), '/'); + + if (! self::isLivewireUpdatePath($path)) { + return $path; + } + + $refererPath = parse_url((string) $request->headers->get('referer', ''), PHP_URL_PATH); + + return is_string($refererPath) && $refererPath !== '' + ? '/'.ltrim($refererPath, '/') + : $path; + } + + private static function isLivewireUpdatePath(string $path): bool + { + return preg_match('#^/livewire(?:-[^/]+)?/update$#', $path) === 1; + } +} diff --git a/apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php b/apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php index a88ad50c..a5fdd4c7 100644 --- a/apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php +++ b/apps/platform/tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php @@ -6,12 +6,19 @@ use App\Models\BaselineProfile; use App\Support\Baselines\BaselineProfileStatus; use Filament\Facades\Filament; +use Illuminate\Http\Request; -it('keeps baseline profiles workspace-owned while retired tenant navigation URLs stay unavailable', function (): void { +it('keeps baseline profiles workspace-owned while environment navigation is route scoped', function (): void { Filament::setCurrentPanel('admin'); [$user, $tenant] = createUserWithTenant(role: 'owner'); + app()->instance('request', Request::create('/admin/workspaces/'.$tenant->workspace_id)); + + expect(BaselineProfileResource::shouldRegisterNavigation())->toBeFalse(); + + app()->instance('request', Request::create('/admin/workspaces/'.$tenant->workspace_id.'/environments/'.$tenant->slug)); + expect(BaselineProfileResource::shouldRegisterNavigation())->toBeTrue(); $this->actingAs($user) diff --git a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php index 688447ba..1dcc6154 100644 --- a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php +++ b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php @@ -21,9 +21,11 @@ use App\Models\WorkspaceMembership; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; +use Illuminate\Http\Request; it('registers governance resource navigation on the admin panel', function (): void { Filament::setCurrentPanel('admin'); + app()->instance('request', Request::create('/admin/workspaces/spec-282-workspace/environments/spec-282-production')); expect(FindingResource::shouldRegisterNavigation())->toBeTrue() ->and(FindingExceptionResource::shouldRegisterNavigation())->toBeTrue() @@ -171,4 +173,4 @@ expect($resourceClass::hasPage('view') || $resourceClass::hasPage('edit')) ->toBeTrue($resourceClass.' must keep a truthful global-search destination on the admin panel.'); } -}); \ No newline at end of file +}); diff --git a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php index f1554f42..91003772 100644 --- a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php +++ b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php @@ -14,9 +14,11 @@ use App\Filament\Resources\PolicyResource; use App\Filament\Resources\PolicyVersionResource; use App\Filament\Resources\RestoreRunResource; +use App\Support\ManagedEnvironmentLinks; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Http\Request; uses(RefreshDatabase::class); @@ -30,7 +32,7 @@ EntraGroupResource::class, ]); -dataset('admin visible navigation classes', [ +dataset('environment visible navigation classes', [ InventoryItemResource::class, PolicyResource::class, PolicyVersionResource::class, @@ -40,17 +42,30 @@ FindingResource::class, ]); +function bindNavigationRequestPath(string $path): void +{ + app()->instance('request', Request::create($path)); +} + it('keeps admin-hidden tenant surfaces out of navigation registration', function (string $class): void { Filament::setCurrentPanel('admin'); expect($class::shouldRegisterNavigation())->toBeFalse(); })->with('admin hidden navigation classes'); -it('registers current tenant-owned surfaces on the admin navigation', function (string $class): void { +it('hides environment-owned navigation classes on workspace surfaces', function (string $class): void { Filament::setCurrentPanel('admin'); + bindNavigationRequestPath('/admin/workspaces/workspace-alpha'); + + expect($class::shouldRegisterNavigation())->toBeFalse(); +})->with('environment visible navigation classes'); + +it('registers environment-owned surfaces only on environment surfaces', function (string $class): void { + Filament::setCurrentPanel('admin'); + bindNavigationRequestPath('/admin/workspaces/workspace-alpha/environments/environment-alpha'); expect($class::shouldRegisterNavigation())->toBeTrue(); -})->with('admin visible navigation classes'); +})->with('environment visible navigation classes'); it('keeps retired tenant-panel entry routes unavailable', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); @@ -61,12 +76,25 @@ ]) ->get("/admin/t/{$tenant->external_id}") ->assertNotFound(); + + $this->actingAs($user) + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, + ]) + ->get("/admin/tenants/{$tenant->external_id}") + ->assertNotFound(); }); -it('keeps baseline navigation on the admin panel and off retired tenant routes', function (): void { +it('keeps baseline navigation route scoped and off retired tenant routes', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); Filament::setCurrentPanel('admin'); + bindNavigationRequestPath('/admin/workspaces/workspace-alpha'); + + expect(BaselineProfileResource::shouldRegisterNavigation())->toBeFalse(); + expect(BaselineSnapshotResource::shouldRegisterNavigation())->toBeFalse(); + + bindNavigationRequestPath('/admin/workspaces/workspace-alpha/environments/environment-alpha'); expect(BaselineProfileResource::shouldRegisterNavigation())->toBeTrue(); expect(BaselineSnapshotResource::shouldRegisterNavigation())->toBeTrue(); @@ -89,6 +117,8 @@ it('keeps the workspace panel sidebar free of tenant-sensitive entries even with a remembered tenant', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); + Filament::setTenant($tenant, true); + $response = $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, @@ -99,6 +129,16 @@ ->get(route('admin.workspace.home', ['workspace' => $tenant->workspace_id])) ->assertOk(); + $response->assertSeeText('Overview'); + $response->assertSeeText('Operations'); + $response->assertSeeText('Alerts'); + $response->assertSeeText('Audit Log'); + $response->assertSeeText('Governance inbox'); + $response->assertSeeText('Customer reviews'); + $response->assertSeeText('Manage workspaces'); + $response->assertSeeText('Integrations'); + $response->assertSeeText('Settings'); + $response->assertDontSee('>Items', false); $response->assertDontSee('>Policies', false); $response->assertDontSee('>Policy Versions', false); @@ -106,4 +146,32 @@ $response->assertDontSee('>Backup Sets', false); $response->assertDontSee('>Restore Runs', false); $response->assertDontSee('>Findings', false); + $response->assertDontSee('>Baselines', false); + $response->assertDontSee('>Baseline Snapshots', false); + $response->assertDontSee('>Baseline Compare', false); + $response->assertDontSee('>Evidence', false); + $response->assertDontSee('>Risk exceptions', false); +}); + +it('shows environment-owned sidebar entries on the canonical environment route', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + $response = $this->actingAs($user) + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, + ]) + ->get(ManagedEnvironmentLinks::viewUrl($tenant)) + ->assertOk(); + + $response->assertSeeText('Policies'); + $response->assertSeeText('Policy Versions'); + $response->assertSeeText('Backup Schedules'); + $response->assertSeeText('Backup Sets'); + $response->assertSeeText('Restore Runs'); + $response->assertSeeText('Findings'); + $response->assertSeeText('Baselines'); + $response->assertSeeText('Baseline Snapshots'); + $response->assertSeeText('Baseline Compare'); + $response->assertSeeText('Evidence'); + $response->assertSeeText('Risk exceptions'); }); diff --git a/specs/298-managed-environment-terminology-copy-cleanup/plan.md b/specs/298-managed-environment-terminology-copy-cleanup/plan.md index 4e80a069..a9755ce0 100644 --- a/specs/298-managed-environment-terminology-copy-cleanup/plan.md +++ b/specs/298-managed-environment-terminology-copy-cleanup/plan.md @@ -5,7 +5,7 @@ # Implementation Plan: Managed Environment Terminology & Copy Cleanup ## Summary -Spec 298 is a bounded post-cutover terminology cleanup. The implementation will audit remaining tenant-first copy, update active visible UI/localization/test vocabulary to Workspace and Managed Environment / Environment terminology, clarify forbidden legacy guard literals, update affected browser-smoke selectors, and document allowed provider/internal/historical references. It must not rename the database/model layer or reintroduce legacy tenant routes, panels, helper aliases, or compatibility surfaces. +Spec 298 is a bounded post-cutover terminology and navigation cleanup. The implementation will audit remaining tenant-first copy, update active visible UI/localization/test vocabulary to Workspace and Managed Environment / Environment terminology, clarify forbidden legacy guard literals, update affected browser-smoke selectors, gate environment-owned navigation by the current route/surface, and document allowed provider/internal/historical references. It must not rename the database/model layer or reintroduce legacy tenant routes, panels, helper aliases, or compatibility surfaces. This plan is preparation only. It does not implement application code. @@ -19,8 +19,8 @@ ## Technical Context **Target Platform**: Laravel Sail local runtime and Gitea-compatible CI runners **Project Type**: Laravel web application under `apps/platform` **Performance Goals**: Keep scans and tests focused; no full-suite repair and no browser timeout inflation -**Constraints**: no `/admin/t...` restoration, no `/admin/tenants...` restoration, no `TenantPanelProvider`, no `setTenantPanelContext()` alias, no DB/model rename, no new localization architecture -**Scale/Scope**: Copy, localization values, test wording, guard descriptions, browser selectors, and spec-local audit only +**Constraints**: no `/admin/t...` restoration, no `/admin/tenants...` restoration, no `TenantPanelProvider`, no `setTenantPanelContext()` alias, no DB/model rename, no new localization architecture, no broad UI polish +**Scale/Scope**: Copy, localization values, test wording, guard descriptions, browser selectors, route-scope-first sidebar gating, and spec-local audit only ## Initial Repo Baseline @@ -80,7 +80,7 @@ ## Shared Pattern & System Fit - **Cross-cutting feature marker**: yes. - **Systems touched**: localization arrays, Filament page/resource labels, support copy emitters, Blade views, tests, browser smokes, and guard tests. - **Shared abstractions reused**: existing localization keys where safe, `ManagedEnvironmentLinks`, `setAdminEnvironmentContext()`, existing data-testid/browser patterns, existing guard-test pattern. -- **New abstraction introduced? why?**: none. +- **New abstraction introduced? why?**: one small `NavigationScope` helper is allowed for route-scope-first sidebar gating. It centralizes the existing `TenantPageCategory` decision and avoids per-resource ad hoc checks against stale `Filament::getTenant()` state. - **Why the existing abstraction was sufficient or insufficient**: Canonical route/helper truth already exists. The remaining issue is copy/test terminology, which can be fixed in place. - **Bounded deviation / spread control**: Technical class/model/table names and provider-specific Microsoft/Entra tenant wording remain only with audit documentation. @@ -111,7 +111,7 @@ ## Constitution Check - Read/write separation: no new write workflow. Touched destructive labels such as restore/remove must keep existing confirmation, authorization, and audit behavior. - Single Graph contract path: no Graph calls added. - Deterministic capabilities: no capability resolver change. -- Proportionality / no premature abstraction: no new abstraction; copy updates are in place. +- Proportionality / no premature abstraction: copy updates are in place. The route-scope navigation helper is intentionally small, derived from existing route categories, and carries no persisted state. - No new persisted truth: no migrations, tables, columns, compatibility shims, or dual-read paths. - Workspace isolation: copy/helper/test changes must not weaken workspace membership checks. - Tenant/managed-environment isolation: existing environment entitlement checks remain intact. @@ -288,6 +288,35 @@ ## Phase 5: Final Scans And Proof Pack Run the final scans from Phase 1 again. Every remaining hit must be absent or documented in `terminology-audit.md`. +## Phase 5A: Navigation Segregation Addendum + +The implementation must also close the observed post-cutover navigation leak: + +```bash +cd apps/platform +./vendor/bin/sail artisan route:list | rg "workspaces/.*/environments|admin/tenants|admin/t|operations|provider-connections|required-permissions" +rg "shouldRegisterNavigation|getNavigationGroup|getNavigationLabel|getNavigationSort|navigation" app/Filament app/Providers resources tests --glob '!vendor' --glob '!node_modules' +rg "Filament::getTenant|Filament::setTenant|WorkspaceContext|ManagedEnvironment|current.*tenant|tenant context|environment context|setAdminEnvironmentContext|SESSION_KEY" app tests --glob '!vendor' --glob '!node_modules' +``` + +Implementation direction: + +- Reuse `TenantPageCategory` through a small central helper that answers workspace surface vs environment surface. +- Current route/surface wins over remembered environment, session environment, query hints, or stale `Filament::getTenant()`. +- Keep workspace-owned navigation visible on workspace surfaces. +- Hide environment-owned Resource/Page navigation on workspace surfaces. +- Show environment-owned navigation again on canonical environment routes such as `/admin/workspaces/{workspace}/environments/{environment}` and child routes. +- Keep retired `/admin/t...`, `/admin/tenants...`, and `TenantPanelProvider` absent. + +Focused proof commands: + +```bash +cd apps/platform +./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php +./vendor/bin/sail artisan test --compact tests/Feature/Guards/ManagedEnvironmentCanonicalRouteContractTest.php tests/Feature/Guards/NoLegacyTenantPanelRuntimeTest.php +./vendor/bin/sail artisan test --compact tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php +``` + Run proof commands: ```bash diff --git a/specs/298-managed-environment-terminology-copy-cleanup/spec.md b/specs/298-managed-environment-terminology-copy-cleanup/spec.md index 72513be4..6a97048d 100644 --- a/specs/298-managed-environment-terminology-copy-cleanup/spec.md +++ b/specs/298-managed-environment-terminology-copy-cleanup/spec.md @@ -7,12 +7,12 @@ # Feature Specification: Managed Environment Terminology & Copy Cleanup ## Spec Candidate Check *(mandatory - SPEC-GATE-001)* -- **Problem**: The route/runtime cutover from legacy tenant surfaces is complete enough to make old tenant-first language misleading, but active copy, localization values, tests, guard descriptions, and browser-smoke selectors still expose the retired TenantPanel mental model. -- **Today's failure**: Operators and contributors can still see or assert strings such as `Tenant dashboard`, `Tenant scope`, `Select tenant`, `No active tenants`, `Open tenant detail`, `Remove tenant`, `Restore tenant`, `Tenant memberships`, `tenant blocker`, or guard/helper language around `setTenantPanelContext()` even though the product is workspace-first and managed-environment-first. -- **User-visible improvement**: Current UI surfaces, localization strings, tests, and smoke anchors read as Workspace -> Managed Environment / Environment context, while provider-specific phrases such as Microsoft tenant ID remain allowed only where they describe external Microsoft/Entra truth. -- **Smallest enterprise-capable version**: Run and record a terminology audit, update active user-facing copy and localization values for the targeted phrases, clarify/rename ambiguous test helper and guard wording, update affected browser-smoke selectors or assertions, keep active legacy route scans clean, and document allowed technical/provider/historical exceptions. +- **Problem**: The route/runtime cutover from legacy tenant surfaces is complete enough to make old tenant-first language misleading, but active copy, localization values, tests, guard descriptions, browser-smoke selectors, and some sidebar navigation decisions still expose the retired TenantPanel mental model. +- **Today's failure**: Operators and contributors can still see or assert strings such as `Tenant dashboard`, `Tenant scope`, `Select tenant`, `No active tenants`, `Open tenant detail`, `Remove tenant`, `Restore tenant`, `Tenant memberships`, `tenant blocker`, or guard/helper language around `setTenantPanelContext()` even though the product is workspace-first and managed-environment-first. A stale selected environment can also make Workspace Overview show environment-owned navigation such as Policies, Policy Versions, Backup Sets, Restore Runs, Findings, Baselines, Evidence, or Risk exceptions. +- **User-visible improvement**: Current UI surfaces, localization strings, tests, smoke anchors, and sidebar navigation read as Workspace -> Managed Environment / Environment context, while provider-specific phrases such as Microsoft tenant ID remain allowed only where they describe external Microsoft/Entra truth. +- **Smallest enterprise-capable version**: Run and record a terminology/navigation audit, update active user-facing copy and localization values for the targeted phrases, clarify/rename ambiguous test helper and guard wording, update affected browser-smoke selectors or assertions, gate environment-owned navigation by route surface instead of stale session/Filament tenant state, keep active legacy route scans clean, and document allowed technical/provider/historical exceptions. - **Explicit non-goals**: No database/table/model rename from `Tenant` to `ManagedEnvironment`, no migration rewrite, no new routing architecture, no new localization foundation, no UI redesign, no RBAC remodel, no old route or provider restoration, no broad historical spec rewrite, and no full-suite fix-all. -- **Permanent complexity imported**: One spec-local terminology audit artifact plus targeted tests/guard updates. No new table, enum, status family, provider framework, route framework, or localization architecture is introduced. +- **Permanent complexity imported**: One spec-local terminology audit artifact, one small route-scope navigation helper, plus targeted tests/guard updates. No new table, enum, status family, provider framework, broad route framework, or localization architecture is introduced. - **Why now**: Spec 297 retired active legacy tenant route surfaces and centralized canonical managed-environment links. Leaving visible/test copy on the old vocabulary makes future specs and tests regress toward tenant-first product truth. - **Why not local**: The drift crosses localization catalogs, Blade views, Filament labels/actions, support copy, tests, guard regex literals, and browser smokes. A single local copy fix would leave contradictory terminology in other active surfaces. - **Approval class**: Cleanup @@ -59,9 +59,9 @@ ## Cross-Cutting / Shared Pattern Reuse *(mandatory)* - `apps/platform/tests/Feature/Guards/Spec288NoLegacyRouteAndHelperGuardTest.php` - affected `apps/platform/tests/Feature/**` and `apps/platform/tests/Browser/**` - `specs/298-managed-environment-terminology-copy-cleanup/terminology-audit.md` -- **Existing pattern(s) to extend**: Spec 297 canonical managed-environment route/link contract, existing `ManagedEnvironmentLinks`, existing `setAdminEnvironmentContext()` helper, current localization arrays, existing Spec 288 guard-test style, existing browser-smoke anchors. -- **Shared contract / presenter / builder / renderer to reuse**: Use existing localization keys where key rename is risky; update visible values first. Use `ManagedEnvironmentLinks` and existing `data-testid` patterns for browser-safe anchors when selectors are needed. -- **Why the existing shared path is sufficient or insufficient**: The canonical route/link path already exists. What remains insufficient is visible and test vocabulary that still uses tenant-first product language for current environment surfaces. +- **Existing pattern(s) to extend**: Spec 297 canonical managed-environment route/link contract, existing `ManagedEnvironmentLinks`, existing `setAdminEnvironmentContext()` helper, current localization arrays, existing `TenantPageCategory` route categorization, existing Spec 288 guard-test style, existing browser-smoke anchors. +- **Shared contract / presenter / builder / renderer to reuse**: Use existing localization keys where key rename is risky; update visible values first. Use `ManagedEnvironmentLinks`, `TenantPageCategory`, and existing `data-testid` patterns for browser-safe anchors when selectors are needed. +- **Why the existing shared path is sufficient or insufficient**: The canonical route/link path and route category enum already exist. What remains insufficient is visible/test vocabulary and sidebar navigation that still use tenant-first product language or stale selected-environment state for current workspace surfaces. - **Allowed deviation and why**: Internal model/class names such as `TenantResource`, `TenantDashboard`, `TenantRequiredPermissions`, `tenant_id`, and Microsoft/Entra tenant ID copy may remain where they are internal, provider-specific, historical, or out of scope for DB/model rename. They must be documented in the audit if surfaced by final scans. - **Consistency impact**: Current UI, tests, localization values, smoke anchors, and guard descriptions must converge on Workspace, Managed Environment, Environment, Provider Connection, Operation, Finding, Review, Evidence, and Governance vocabulary. - **Review focus**: Reviewers must verify that no active product UI uses retired TenantPanel or tenant-first language, route/runtime legacy scans remain clean, and guard literals only remain when they explicitly forbid retired behavior. @@ -248,6 +248,9 @@ ## Functional Requirements - **FR-008 Browser smokes updated**: Affected smokes must use stable selectors or current copy, with no blind timeout increases. - **FR-009 Allowed exceptions documented**: Remaining tenant-related hits must be categorized in `terminology-audit.md`. - **FR-010 No active legacy route reintroduced**: Final route/source scans must remain clean for `/admin/t...`, `/admin/tenants...`, old URL generators, tenant panel IDs, and `setTenantPanelContext()` in runtime surfaces. +- **FR-011 Route-scope-first navigation gating**: Sidebar navigation must decide workspace vs environment navigation from the current route/surface first. Stale session, remembered environment, or `Filament::getTenant()` state must not make Workspace Overview show environment-owned navigation. +- **FR-012 Workspace sidebar stays portfolio-oriented**: Workspace-level surfaces must keep repo-real workspace-owned navigation available, including Overview, Operations, Alerts, Audit Log, Governance inbox, Customer reviews, Manage workspaces, Integrations, and Settings where authorized. Stored reports and Review Packs remain environment-owned until a separate workspace aggregate route exists. +- **FR-013 Environment sidebar appears only in environment surface scope**: Environment-owned entries such as Policies, Policy Versions, Backup Schedules, Backup Sets, Restore Runs, Findings, Baselines, Baseline Snapshots, Baseline Compare, Evidence, Risk exceptions, Required permissions, and Diagnostics may appear only when the route is clearly environment-scoped. ## Acceptance Criteria @@ -258,6 +261,8 @@ ## Acceptance Criteria - **AC-005**: Route scan does not show active `/admin/t...` or `/admin/tenants...` product routes. - **AC-006**: Spec 297 route/runtime proof remains intact. - **AC-007**: Canonical browser anchors pass when affected. +- **AC-008**: Workspace Overview hides environment-owned primary navigation even when a previous environment remains selected in session or Filament tenant state. +- **AC-009**: Environment dashboard/detail routes show the intended environment-owned navigation for entitled users. - **AC-008**: Pint dirty and `git diff --check` pass. ## Out Of Scope diff --git a/specs/298-managed-environment-terminology-copy-cleanup/tasks.md b/specs/298-managed-environment-terminology-copy-cleanup/tasks.md index bb56bf69..6cb9948f 100644 --- a/specs/298-managed-environment-terminology-copy-cleanup/tasks.md +++ b/specs/298-managed-environment-terminology-copy-cleanup/tasks.md @@ -127,6 +127,19 @@ ## Phase 8: Close-Out Summary - [x] T060 Record remaining allowed references and reasons from `terminology-audit.md`. - [x] T061 Choose one final decision string: `298 merge-ready; terminology cleanup complete`, `298 merge-ready with documented allowed technical tenant references`, `298 blocked by active legacy tenant copy`, or `298 blocked by runtime legacy regression`. +## Phase 9: Navigation Segregation Addendum + +**Goal**: Prevent stale environment/session context from making Workspace Overview show environment-owned primary navigation. + +- [x] T062 Run the navigation and context baseline scans from `plan.md` Phase 5A and record the current leak path in `terminology-audit.md`. +- [x] T063 Add or reuse one central route-scope helper that distinguishes workspace surfaces from environment surfaces with current route scope taking precedence over session and `Filament::getTenant()` state. +- [x] T064 Update the Admin panel navigation override so workspace surfaces render workspace-owned navigation even when a stale environment is remembered or selected. +- [x] T065 Gate environment-owned Resource/Page navigation so Policies, Policy Versions, Backup Schedules, Backup Sets, Restore Runs, Findings, Baselines, Baseline Snapshots, Baseline Compare, Evidence, Risk exceptions, Stored reports, Review Packs, and Reviews do not register on Workspace Overview. +- [x] T066 Keep repo-real workspace-owned navigation visible on workspace surfaces: Overview, Operations, Alerts, Audit Log, Governance inbox, Customer reviews, Manage workspaces, Integrations, and Settings where authorized. Do not invent workspace aggregate routes for Stored reports or Review Packs under this cleanup spec. +- [x] T067 Update focused navigation guard tests for Workspace Overview with stale Filament tenant, canonical environment route visibility, and retired `/admin/t...` and `/admin/tenants...` route absence. +- [x] T068 Run `cd /Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php`. +- [x] T069 Run affected legacy/canonical guard tests and targeted browser smoke anchors only; do not run a raw full suite. + ## Dependencies & Execution Order - Phase 1 blocks all runtime edits. diff --git a/specs/298-managed-environment-terminology-copy-cleanup/terminology-audit.md b/specs/298-managed-environment-terminology-copy-cleanup/terminology-audit.md index 7f490f34..44585cfb 100644 --- a/specs/298-managed-environment-terminology-copy-cleanup/terminology-audit.md +++ b/specs/298-managed-environment-terminology-copy-cleanup/terminology-audit.md @@ -63,10 +63,47 @@ ## Final Audit | `Tenant`, `tenant_id`, `tenantRouteKey`, `TenantResource`, tenant review model/resource names | Models, resources, relations, fixtures, historical test names | allowed-internal-model | keep | DB/model/resource rename is explicitly out of scope for Spec 298. Runtime routes were not restored. | | Historical specs, archived decision context, and prior spec names | `specs/**`, `.specify/**`, historical tests where applicable | allowed-historical | keep | This cleanup does not rewrite historical records or prior spec names. | +## Navigation Segregation Addendum + +The workspace/environment sidebar leak was confirmed as a route/context-gating issue: the admin middleware previously switched back to full Filament navigation whenever `Filament::getTenant()` was filled, even when the current route was Workspace Overview. Spec 298 now treats the current route/surface as the navigation source of truth. + +| Entry | Workspace nav | Environment nav | Reason | +|---|---:|---:|---| +| Overview | yes | yes | Workspace landing remains the stable return point. | +| Operations | yes | yes | Workspace-scoped operations can optionally filter by environment. | +| Alerts | yes | yes | Workspace monitoring remains portfolio-oriented. | +| Audit Log | yes | yes | Workspace monitoring remains portfolio-oriented. | +| Governance inbox | yes | yes | Existing workspace-level governance triage surface. | +| Customer reviews | yes | yes | Existing workspace-level review workspace. | +| Manage workspaces | yes | yes | Workspace administration entry point. | +| Integrations | yes | yes | Existing provider-connection workspace route. | +| Settings | yes | yes | Existing workspace settings route, capability-gated. | +| Policies / Policy Versions / Inventory Items | no | yes | Tenant-owned inventory routes live under canonical environment routes. | +| Backup Schedules / Backup Sets / Restore Runs | no | yes | Backup/restore records are tenant-owned environment routes. | +| Findings / Risk exceptions / Evidence | no | yes | Governance artifacts are environment-owned records. | +| Baselines / Baseline Snapshots / Baseline Compare | no | yes | Main navigation is hidden on Workspace Overview and restored on environment surfaces; profile URLs remain workspace-owned until broader IA changes. | +| Stored reports / Review Packs / Reviews | no | yes | Current repo routes are environment-owned; no workspace aggregate route was invented under this cleanup spec. | + +Implementation files: + +- `apps/platform/app/Support/Navigation/NavigationScope.php`: central route-scope helper, including Livewire update referer handling. +- `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php`: admin sidebar now uses `NavigationScope::isEnvironmentSurface()` instead of stale `Filament::getTenant()` to decide full environment navigation. +- Environment-owned Filament resources/pages now call `NavigationScope::shouldRegisterEnvironmentNavigation()` from `shouldRegisterNavigation()`. +- `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php`: covers workspace overview with stale Filament tenant, environment route visibility, and retired `/admin/t...` plus `/admin/tenants...` absence. + ## Final Verification Evidence | Command | Result | |---|---| +| `cd apps/platform && ./vendor/bin/sail artisan route:list \| rg "workspaces/.*/environments\|admin/tenants\|admin/t\|operations\|provider-connections\|required-permissions"` | Confirmed canonical workspace/environment routes; no retired route family restored. | +| `cd apps/platform && rg "shouldRegisterNavigation|getNavigationGroup|getNavigationLabel|getNavigationSort|navigation" app/Filament app/Providers resources tests --glob '!vendor' --glob '!node_modules'` | Confirmed navigation registration surfaces and updated route-scope gating. | +| `cd apps/platform && rg "Filament::getTenant|Filament::setTenant|WorkspaceContext|ManagedEnvironment|current.*tenant|tenant context|environment context|setAdminEnvironmentContext|SESSION_KEY" app tests --glob '!vendor' --glob '!node_modules'` | Confirmed context seams; navigation decision no longer relies on stale selected environment alone. | +| `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php` | Passed: 21 tests, 59 assertions after navigation addendum. | +| `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php` | Passed: 4 tests, 48 assertions. | +| `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines/BaselineProfileWorkspaceOwnershipTest.php` | Passed: 2 tests, 9 assertions. | +| `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/Spec288NoLegacyRouteAndHelperGuardTest.php tests/Feature/Guards/ManagedEnvironmentCanonicalRouteContractTest.php tests/Feature/Guards/NoLegacyTenantPanelRuntimeTest.php tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php tests/Feature/Filament/WorkspaceOverviewArrivalContextTest.php` | Passed: 11 tests, 97 assertions. | +| `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/WorkspaceOverviewNavigationTest.php tests/Feature/Filament/WorkspaceOverviewArrivalContextTest.php tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php` | Passed: 6 tests, 52 assertions. | +| `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php` | Passed: 2 tests, 29 assertions. | | `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards` | Passed: 265 tests, 4705 assertions. | | `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Localization` | Passed: 16 tests, 95 assertions after updating stale German auth copy expectation. | | `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Workspaces` | Passed: 96 tests, 276 assertions. |