From b159dacd3655401a42433284b0845f6ed11530b5 Mon Sep 17 00:00:00 2001 From: ahmido Date: Sat, 16 May 2026 18:25:36 +0000 Subject: [PATCH] feat: clean up legacy tenant environment context (#372) ## Summary - remove legacy tenant-scoped routing and middleware paths in favor of the current environment/workspace context flow - update Filament pages and resources to use the cleaned-up admin surface and environment filter context - add the related spec 317 artifacts and targeted tests for environment filter state and legacy context cleanup ## Testing - not run as part of this commit/push/PR workflow Co-authored-by: Ahmed Darrazi Reviewed-on: https://git.cloudarix.de/ahmido/TenantAtlas/pulls/372 --- .../CleansAdminTenantQueryParameter.php | 27 ++ ...leansAdminTenantResourceQueryParameter.php | 27 ++ ...esAdminEnvironmentFilterQueryParameter.php | 81 ++++ ...p => WorkspaceScopedEnvironmentRoutes.php} | 14 +- .../Filament/Pages/BaselineCompareLanding.php | 10 +- .../app/Filament/Pages/ChooseEnvironment.php | 2 +- .../Pages/Findings/FindingsHygieneReport.php | 14 +- .../Pages/Findings/FindingsIntakeQueue.php | 18 +- .../Pages/Findings/MyFindingsInbox.php | 20 +- .../Pages/Governance/DecisionRegister.php | 2 + .../Pages/Governance/GovernanceInbox.php | 3 + .../Filament/Pages/Monitoring/AuditLog.php | 14 +- .../Monitoring/FindingExceptionsQueue.php | 6 +- .../Filament/Pages/Monitoring/Operations.php | 28 +- .../TenantlessOperationRunViewer.php | 36 +- .../Pages/Reviews/CustomerReviewWorkspace.php | 6 +- .../Filament/Pages/Reviews/ReviewRegister.php | 4 +- .../ManagedEnvironmentOnboardingWizard.php | 23 +- .../Pages/ListAlertDeliveries.php | 4 +- .../Resources/BackupScheduleResource.php | 5 +- .../Pages/ListBackupSchedules.php | 18 +- .../Filament/Resources/BackupSetResource.php | 5 +- .../Pages/ListBackupSets.php | 6 +- .../Resources/BaselineProfileResource.php | 9 +- .../Resources/BaselineSnapshotResource.php | 5 +- .../Filament/Resources/EntraGroupResource.php | 4 +- .../Pages/ListEntraGroups.php | 6 +- .../Resources/EnvironmentReviewResource.php | 32 +- .../Pages/ViewEnvironmentReview.php | 10 +- .../Resources/EvidenceSnapshotResource.php | 12 +- .../Resources/FindingExceptionResource.php | 6 +- .../Filament/Resources/FindingResource.php | 10 +- .../FindingResource/Pages/ListFindings.php | 12 +- .../Resources/InventoryItemResource.php | 8 +- .../Pages/ListInventoryItems.php | 10 +- .../Pages/ViewInventoryItem.php | 2 +- .../Resources/OperationRunResource.php | 8 +- .../app/Filament/Resources/PolicyResource.php | 7 +- .../PolicyResource/Pages/ListPolicies.php | 12 +- .../Resources/PolicyVersionResource.php | 7 +- .../Pages/ListPolicyVersions.php | 6 +- .../Resources/ProviderConnectionResource.php | 21 +- .../Filament/Resources/RestoreRunResource.php | 14 +- .../Pages/ListRestoreRuns.php | 6 +- .../Filament/Resources/ReviewPackResource.php | 16 +- .../Resources/StoredReportResource.php | 6 +- .../ManagedEnvironmentReviewPackCard.php | 8 +- .../Operations/OperationsKpiHeader.php | 8 +- .../ClearEnvironmentContextController.php | 10 +- .../OpenFindingExceptionsQueueController.php | 2 +- .../SelectEnvironmentController.php | 4 +- .../Controllers/SwitchWorkspaceController.php | 2 +- .../InventoryItemDependencyEdgesTable.php | 2 +- apps/platform/app/Models/User.php | 8 +- .../app/Policies/FindingExceptionPolicy.php | 2 +- ...agedEnvironmentOnboardingSessionPolicy.php | 2 +- .../app/Policies/ProviderConnectionPolicy.php | 6 +- .../Providers/Filament/AdminPanelProvider.php | 18 +- .../Tenants/TenantOperabilityService.php | 8 +- .../Workspaces/WorkspaceLifecycleService.php | 2 +- .../WorkspaceHealthSummaryQuery.php | 10 +- .../EnvironmentDashboardSummaryBuilder.php | 26 +- ... CanonicalAdminEnvironmentFilterState.php} | 46 +- .../GovernanceInboxSectionBuilder.php | 21 +- ...p => EnsureEnvironmentContextSelected.php} | 10 +- .../AdminSurfaceScope.php} | 28 +- .../Support/Navigation/NavigationScope.php | 9 +- .../Navigation/RelatedNavigationResolver.php | 9 +- .../Support/OperateHub/OperateHubShell.php | 66 +-- .../OperateHub/ResolvedShellContext.php | 4 +- .../app/Support/OperationRunLinks.php | 2 +- .../SupportDiagnosticBundleBuilder.php | 8 +- .../Support/Tenants/TenantInteractionLane.php | 18 +- .../app/Support/Tenants/TenantLifecycle.php | 2 +- .../Tenants/TenantLifecyclePresentation.php | 2 +- .../Tenants/TenantOperabilityContext.php | 5 +- .../Tenants/TenantOperabilityQuestion.php | 2 +- .../Tenants/TenantOperabilityReasonCode.php | 2 +- .../ArtifactTruthPresenter.php | 14 +- .../TenantOwnedModelFamilies.php | 6 +- .../Support/Workspaces/WorkspaceContext.php | 48 +- apps/platform/bootstrap/app.php | 9 +- .../filament/partials/context-bar.blade.php | 48 +- apps/platform/routes/web.php | 23 +- ...TenantDashboardProductizationSmokeTest.php | 2 +- .../CustomerReviewWorkspaceSmokeTest.php | 4 +- ...enceFreshnessPublicationTrustSmokeTest.php | 4 +- .../Spec190BaselineCompareMatrixSmokeTest.php | 6 +- ...192RecordPageHeaderDisciplineSmokeTest.php | 8 +- .../Spec194GovernanceFrictionSmokeTest.php | 8 +- .../Spec198MonitoringPageStateSmokeTest.php | 4 +- .../Spec277StoredReportsSurfaceSmokeTest.php | 2 +- ...pec281ProviderConnectionScopeSmokeTest.php | 2 +- ...283ProviderCapabilityRegistrySmokeTest.php | 2 +- ...Spec284ArtifactSourceTaxonomySmokeTest.php | 4 +- ...nvironmentNamingConsolidationSmokeTest.php | 2 +- ...301InventoryNavigationCutoverSmokeTest.php | 4 +- ...03AdminDirectoryGroupsCutoverSmokeTest.php | 2 +- ...WorkspaceHubNavigationContextSmokeTest.php | 4 +- ...pec316WorkspaceHubClearFilterSmokeTest.php | 8 +- .../Feature/078/RelatedLinksOnDetailTest.php | 12 +- ...icalOperationViewerContextMismatchTest.php | 10 +- .../AlertDeliveryDeepLinkFiltersTest.php | 6 +- .../Feature/Auth/AdminLocalSmokeLoginTest.php | 4 +- .../Auth/TenantChooserSelectionTest.php | 22 +- ...spaceFirstManagedEnvironmentAccessTest.php | 4 +- ...torExplanationSurfaceAuthorizationTest.php | 4 +- .../BackupScheduleAdminTenantParityTest.php | 2 +- .../BuildsBaselineCompareMatrixFixtures.php | 16 +- ...ntDashboardProductizationReadinessTest.php | 16 +- .../NoLiveGraphOnRenderTest.php | 4 +- .../Drift/DriftFindingDiffUnavailableTest.php | 6 +- .../EnvironmentReviewAuditLogTest.php | 6 +- .../EnvironmentReviewExecutivePackTest.php | 4 +- ...nvironmentReviewExplanationSurfaceTest.php | 6 +- .../EnvironmentReviewRbacTest.php | 6 +- ...EnvironmentReviewRegisterPrefilterTest.php | 4 +- .../EnvironmentReviewRegisterTest.php | 6 +- .../EnvironmentReviewUiContractTest.php | 18 +- .../AdminSharedSurfacePanelParityTest.php | 4 +- ...urfacesRedirectToChooseEnvironmentTest.php | 2 +- .../Filament/AdminTenantSurfaceParityTest.php | 10 +- .../Alerts/AlertDeliveryViewerTest.php | 4 +- .../Filament/Alerts/AlertsKpiHeaderTest.php | 8 +- .../ArtifactSourceTaxonomySurfaceTest.php | 4 +- .../Feature/Filament/AuditLogPageTest.php | 8 +- .../BackupSetAdminTenantParityTest.php | 2 +- ...ineCompareLandingAdminTenantParityTest.php | 2 +- ...onicalAdminEnvironmentFilterStateTest.php} | 44 +- .../DatabaseNotificationsPollingTest.php | 2 +- .../Filament/EntraGroupAdminScopeTest.php | 10 +- .../EntraGroupGlobalSearchScopeTest.php | 4 +- ...anceArtifactAdminPanelRegistrationTest.php | 6 +- ...GovernanceArtifactDeepLinkContractTest.php | 9 +- ...vernanceArtifactEnvironmentContextTest.php | 14 +- ...anceArtifactLegacyTenantPanelGuardTest.php | 8 +- ...InventoryCoverageAdminTenantParityTest.php | 6 +- .../Filament/InventoryItemResourceTest.php | 4 +- ...anagedEnvironmentsLandingLifecycleTest.php | 4 +- .../Filament/OperationRunListFiltersTest.php | 10 +- .../PanelNavigationSegregationTest.php | 8 +- .../Feature/Filament/PolicyListingTest.php | 7 +- .../PolicyResourceAdminSearchParityTest.php | 4 +- .../PolicyResourceAdminTenantParityTest.php | 6 +- .../PolicyVersionAdminSearchParityTest.php | 4 +- .../PolicyVersionAdminTenantParityTest.php | 8 +- ...erencedTenantLifecyclePresentationTest.php | 12 +- .../RestoreRunAdminTenantParityTest.php | 4 +- .../Filament/RestoreWizardGraphSafetyTest.php | 4 +- .../SettingsCatalogPolicySyncTest.php | 4 +- ...gsCatalogRestoreApplySettingsPatchTest.php | 4 +- .../Filament/SettingsCatalogRestoreTest.php | 4 +- .../Filament/TableStatePersistenceTest.php | 7 +- .../TenantOwnedResourceScopeParityTest.php | 6 +- ...aceContextTopbarAndTenantSelectionTest.php | 12 +- ...spaceOnlySurfaceTenantIndependenceTest.php | 8 +- ...rkspaceOverviewDrilldownContinuityTest.php | 2 +- .../Findings/FindingAdminTenantParityTest.php | 4 +- .../Findings/FindingExceptionRegisterTest.php | 2 +- .../FindingOutcomeSummaryReportingTest.php | 2 +- .../Feature/Findings/FindingRbacTest.php | 2 +- .../FindingWorkflowRowActionsTest.php | 4 +- .../FindingWorkflowUiEnforcementTest.php | 2 +- .../FindingsAssignmentHygieneReportTest.php | 4 +- .../Findings/FindingsIntakeQueueTest.php | 6 +- .../Feature/Findings/MyWorkInboxTest.php | 6 +- .../Guards/ActionSurfaceContractTest.php | 34 +- .../Guards/AdminTenantResolverGuardTest.php | 2 +- .../FilamentTableStandardsGuardTest.php | 158 ++++--- ...LegacyTenantPlatformContextCleanupTest.php | 224 ++++++++++ .../OperationRunLinkContractGuardTest.php | 4 +- .../Feature/InventoryItemDependenciesTest.php | 2 +- .../CustomerReviewSurfaceLocalizationTest.php | 6 +- .../Localization/LocalePreferenceFlowTest.php | 4 +- .../ManagedEnvironmentPanelContextTest.php | 2 +- ...xceptionsQueueWorkspaceHubContractTest.php | 2 +- .../Monitoring/HeaderContextBarTest.php | 10 +- ...onRunResolvedReferencePresentationTest.php | 12 +- .../OperationsKpiHeaderTenantContextTest.php | 8 +- .../Monitoring/OperationsTenantScopeTest.php | 4 +- .../OperationsWorkspaceHubContractTest.php | 2 +- .../WorkspaceHubClearFilterContractTest.php | 2 +- ...kspaceHubEnvironmentFilterContractTest.php | 75 +++- .../WorkspaceHubSidebarUrlContractTest.php | 4 +- ...edEnvironmentOnboardingEntitlementTest.php | 27 +- .../TenantlessOperationRunViewerTest.php | 32 +- .../Feature/OpsUx/OperateHubShellTest.php | 62 +-- ...derConnectionsWorkspaceHubContractTest.php | 2 +- .../AdminGlobalSearchContextSafetyTest.php | 18 +- .../AdminTenantOwnedPolicyContextTest.php | 8 +- ...ngExceptionLifecycleAccessBoundaryTest.php | 2 +- ...InventoryItemResourceAuthorizationTest.php | 6 +- .../TenantLifecycleActionVisibilityTest.php | 28 +- .../Feature/RestoreRunWizardExecuteTest.php | 4 +- ...CustomerReviewWorkspaceHubContractTest.php | 2 +- ...CustomerReviewWorkspaceLaunchLinksTest.php | 19 +- ...erReviewWorkspaceNavigationContextTest.php | 2 +- .../CustomerReviewWorkspacePackAccessTest.php | 10 +- .../CustomerReviewWorkspacePageTest.php | 22 +- ...nitoringDoesNotMutateTenantContextTest.php | 8 +- .../Spec085/OperationsIndexHeaderTest.php | 24 +- .../TenantRBAC/TenantSwitcherScopeTest.php | 12 +- .../Workspaces/ChooseEnvironmentPageTest.php | 30 +- ...kspaceRedirectsToChooseEnvironmentTest.php | 2 +- .../GlobalContextShellContractTest.php | 18 +- .../SelectEnvironmentControllerTest.php | 36 +- .../WorkspaceHubContextContractTest.php | 4 +- .../tests/Support/TestLaneManifest.php | 10 +- .../ManagedEnvironmentContextResolverTest.php | 18 +- .../OnboardingDraftResolverTest.php | 10 +- .../WorkspaceHealthSummaryQueryTest.php | 14 +- .../OperateHubShellResolutionTest.php | 12 +- ...spaceContextRememberedEnvironmentTest.php} | 40 +- .../Unit/Tenants/AdminSurfaceScopeTest.php | 32 ++ .../Tenants/TenantOperabilityServiceTest.php | 2 +- .../Unit/Tenants/TenantPageCategoryTest.php | 32 -- docs/HANDOVER.md | 9 +- docs/product/implementation-ledger.md | 2 +- docs/product/spec-candidates.md | 42 +- .../spec317-evidence-overview-filtered.png | Bin 0 -> 63392 bytes ...ec317-final-evidence-overview-filtered.png | Bin 0 -> 63411 bytes .../checklists/requirements.md | 65 +++ .../legacy-inventory.md | 35 ++ .../plan.md | 414 ++++++++++++++++++ .../spec.md | 313 +++++++++++++ .../tasks.md | 148 +++++++ .../tenant-usage-allowlist.md | 76 ++++ 227 files changed, 2639 insertions(+), 1193 deletions(-) create mode 100644 apps/platform/app/Filament/Concerns/CleansAdminTenantQueryParameter.php create mode 100644 apps/platform/app/Filament/Concerns/CleansAdminTenantResourceQueryParameter.php create mode 100644 apps/platform/app/Filament/Concerns/UsesAdminEnvironmentFilterQueryParameter.php rename apps/platform/app/Filament/Concerns/{WorkspaceScopedTenantRoutes.php => WorkspaceScopedEnvironmentRoutes.php} (87%) rename apps/platform/app/Support/Filament/{CanonicalAdminTenantFilterState.php => CanonicalAdminEnvironmentFilterState.php} (70%) rename apps/platform/app/Support/Middleware/{EnsureFilamentTenantSelected.php => EnsureEnvironmentContextSelected.php} (94%) rename apps/platform/app/Support/{Tenants/TenantPageCategory.php => Navigation/AdminSurfaceScope.php} (82%) rename apps/platform/tests/Feature/Filament/{CanonicalAdminTenantFilterStateTest.php => CanonicalAdminEnvironmentFilterStateTest.php} (73%) create mode 100644 apps/platform/tests/Feature/Guards/LegacyTenantPlatformContextCleanupTest.php rename apps/platform/tests/Unit/Support/Workspaces/{WorkspaceContextRememberedTenantTest.php => WorkspaceContextRememberedEnvironmentTest.php} (59%) create mode 100644 apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php delete mode 100644 apps/platform/tests/Unit/Tenants/TenantPageCategoryTest.php create mode 100644 specs/317-legacy-tenant-environment-context-cleanup/artifacts/screenshots/spec317-evidence-overview-filtered.png create mode 100644 specs/317-legacy-tenant-environment-context-cleanup/artifacts/screenshots/spec317-final-evidence-overview-filtered.png create mode 100644 specs/317-legacy-tenant-environment-context-cleanup/checklists/requirements.md create mode 100644 specs/317-legacy-tenant-environment-context-cleanup/legacy-inventory.md create mode 100644 specs/317-legacy-tenant-environment-context-cleanup/plan.md create mode 100644 specs/317-legacy-tenant-environment-context-cleanup/spec.md create mode 100644 specs/317-legacy-tenant-environment-context-cleanup/tasks.md create mode 100644 specs/317-legacy-tenant-environment-context-cleanup/tenant-usage-allowlist.md diff --git a/apps/platform/app/Filament/Concerns/CleansAdminTenantQueryParameter.php b/apps/platform/app/Filament/Concerns/CleansAdminTenantQueryParameter.php new file mode 100644 index 00000000..c580f8db --- /dev/null +++ b/apps/platform/app/Filament/Concerns/CleansAdminTenantQueryParameter.php @@ -0,0 +1,27 @@ + $parameters + */ + public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string + { + $panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin'; + + if ($panelId === 'admin') { + unset($parameters['tenant']); + + return parent::getUrl($parameters, $isAbsolute, $panelId, null); + } + + return parent::getUrl($parameters, $isAbsolute, $panelId, $tenant); + } +} diff --git a/apps/platform/app/Filament/Concerns/CleansAdminTenantResourceQueryParameter.php b/apps/platform/app/Filament/Concerns/CleansAdminTenantResourceQueryParameter.php new file mode 100644 index 00000000..235ec496 --- /dev/null +++ b/apps/platform/app/Filament/Concerns/CleansAdminTenantResourceQueryParameter.php @@ -0,0 +1,27 @@ + $parameters + */ + public static function getUrl(?string $name = null, array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null, bool $shouldGuessMissingParameters = false): string + { + $panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin'; + + if ($panelId === 'admin') { + unset($parameters['tenant']); + + return parent::getUrl($name, $parameters, $isAbsolute, $panelId, null, $shouldGuessMissingParameters); + } + + return parent::getUrl($name, $parameters, $isAbsolute, $panelId, $tenant, $shouldGuessMissingParameters); + } +} diff --git a/apps/platform/app/Filament/Concerns/UsesAdminEnvironmentFilterQueryParameter.php b/apps/platform/app/Filament/Concerns/UsesAdminEnvironmentFilterQueryParameter.php new file mode 100644 index 00000000..c62127a6 --- /dev/null +++ b/apps/platform/app/Filament/Concerns/UsesAdminEnvironmentFilterQueryParameter.php @@ -0,0 +1,81 @@ + $parameters + */ + public static function getUrl(array $parameters = [], bool $isAbsolute = true, ?string $panel = null, ?Model $tenant = null): string + { + $panelId = $panel ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin'; + + if ($panelId !== 'admin') { + return parent::getUrl($parameters, $isAbsolute, $panelId, $tenant); + } + + $environment = $tenant instanceof ManagedEnvironment ? $tenant : null; + $parameterTenant = $parameters['tenant'] ?? null; + + if (! $environment instanceof ManagedEnvironment && $parameterTenant instanceof ManagedEnvironment) { + $environment = $parameterTenant; + } + + unset($parameters['tenant']); + + $url = parent::getUrl($parameters, $isAbsolute, $panelId, null); + + return $environment instanceof ManagedEnvironment + ? static::withEnvironmentId($url, $environment) + : $url; + } + + private static function withEnvironmentId(string $url, ManagedEnvironment $environment): string + { + $parts = parse_url($url); + + if (! is_array($parts)) { + return $url; + } + + $query = []; + parse_str((string) ($parts['query'] ?? ''), $query); + $query['environment_id'] = (int) $environment->getKey(); + unset($query['tenant'], $query['tenant_id'], $query['managed_environment_id'], $query['tenant_scope'], $query['environment']); + + $parts['query'] = http_build_query($query, '', '&', PHP_QUERY_RFC3986); + + $rebuilt = ''; + + if (isset($parts['scheme'])) { + $rebuilt .= $parts['scheme'].'://'; + } + + if (isset($parts['host'])) { + $rebuilt .= $parts['host']; + } + + if (isset($parts['port'])) { + $rebuilt .= ':'.$parts['port']; + } + + $rebuilt .= $parts['path'] ?? ''; + + if (($parts['query'] ?? '') !== '') { + $rebuilt .= '?'.$parts['query']; + } + + if (isset($parts['fragment'])) { + $rebuilt .= '#'.$parts['fragment']; + } + + return $rebuilt; + } +} diff --git a/apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php b/apps/platform/app/Filament/Concerns/WorkspaceScopedEnvironmentRoutes.php similarity index 87% rename from apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php rename to apps/platform/app/Filament/Concerns/WorkspaceScopedEnvironmentRoutes.php index 17fb6d75..24f5695c 100644 --- a/apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php +++ b/apps/platform/app/Filament/Concerns/WorkspaceScopedEnvironmentRoutes.php @@ -10,7 +10,7 @@ use Filament\Panel; use Illuminate\Database\Eloquent\Model; -trait WorkspaceScopedTenantRoutes +trait WorkspaceScopedEnvironmentRoutes { public static function getSlug(?Panel $panel = null): string { @@ -28,7 +28,7 @@ public static function getUrl(?string $name = null, array $parameters = [], bool return parent::getUrl($name, $parameters, $isAbsolute, $panelId, $tenant, $shouldGuessMissingParameters); } - $resolvedTenant = static::resolveWorkspaceScopedTenant($parameters, $tenant); + $resolvedTenant = static::resolveWorkspaceScopedEnvironment($parameters, $tenant); if (! $resolvedTenant instanceof ManagedEnvironment) { return url('/admin'); @@ -49,7 +49,7 @@ public static function getUrl(?string $name = null, array $parameters = [], bool protected static function workspaceScopedSlug(string $slug, ?Panel $panel = null): string { - if (! static::shouldUseWorkspaceScopedTenantRoutes($panel)) { + if (! static::shouldUseWorkspaceScopedEnvironmentRoutes($panel)) { return $slug; } @@ -60,7 +60,7 @@ protected static function workspaceScopedSlug(string $slug, ?Panel $panel = null : $prefix.ltrim($slug, '/'); } - protected static function shouldUseWorkspaceScopedTenantRoutes(?Panel $panel = null): bool + protected static function shouldUseWorkspaceScopedEnvironmentRoutes(?Panel $panel = null): bool { $panelId = $panel?->getId() ?? Filament::getCurrentOrDefaultPanel()?->getId() ?? 'admin'; @@ -70,7 +70,7 @@ protected static function shouldUseWorkspaceScopedTenantRoutes(?Panel $panel = n /** * @param array $parameters */ - protected static function resolveWorkspaceScopedTenant(array $parameters, ?Model $tenant = null): ?ManagedEnvironment + protected static function resolveWorkspaceScopedEnvironment(array $parameters, ?Model $tenant = null): ?ManagedEnvironment { $parameterTenant = $parameters['tenant'] ?? $parameters['environment'] ?? null; @@ -85,7 +85,7 @@ protected static function resolveWorkspaceScopedTenant(array $parameters, ?Model $record = $parameters['record'] ?? null; if ($record instanceof Model) { - $relationshipName = static::workspaceScopedTenantRelationshipName(); + $relationshipName = static::workspaceScopedEnvironmentRelationshipName(); if (method_exists($record, $relationshipName)) { $recordTenant = $record->getRelationValue($relationshipName); @@ -139,7 +139,7 @@ protected static function resolveWorkspaceScopedWorkspace(ManagedEnvironment $te return $tenant->workspace()->first(); } - protected static function workspaceScopedTenantRelationshipName(): string + protected static function workspaceScopedEnvironmentRelationshipName(): string { $relationshipName = property_exists(static::class, 'tenantOwnershipRelationshipName') ? static::$tenantOwnershipRelationshipName diff --git a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php index 75c80904..5b02db98 100644 --- a/apps/platform/app/Filament/Pages/BaselineCompareLanding.php +++ b/apps/platform/app/Filament/Pages/BaselineCompareLanding.php @@ -5,11 +5,12 @@ namespace App\Filament\Pages; use App\Filament\Concerns\ResolvesPanelTenantContext; +use App\Filament\Concerns\UsesAdminEnvironmentFilterQueryParameter; use App\Filament\Resources\BaselineProfileResource; use App\Filament\Resources\FindingResource; use App\Models\BaselineProfile; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Models\User; use App\Services\Auth\CapabilityResolver; use App\Services\Baselines\BaselineCompareService; @@ -17,16 +18,16 @@ use App\Support\Baselines\BaselineCaptureMode; 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\Navigation\CanonicalNavigationContext; +use App\Support\Navigation\NavigationScope; use App\Support\OperationRunLinks; use App\Support\OperationRunType; use App\Support\OpsUx\OperationUxPresenter; use App\Support\OpsUx\OpsUxBrowserEvents; -use App\Support\ReasonTranslation\ReasonPresenter; use App\Support\Rbac\UiEnforcement; +use App\Support\ReasonTranslation\ReasonPresenter; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; @@ -39,6 +40,7 @@ class BaselineCompareLanding extends Page { use ResolvesPanelTenantContext; + use UsesAdminEnvironmentFilterQueryParameter; protected const MONITORING_PAGE_STATE_CONTRACT = [ 'surfaceKey' => 'baseline_compare_landing', diff --git a/apps/platform/app/Filament/Pages/ChooseEnvironment.php b/apps/platform/app/Filament/Pages/ChooseEnvironment.php index 7a5ad6a6..39cc1f33 100644 --- a/apps/platform/app/Filament/Pages/ChooseEnvironment.php +++ b/apps/platform/app/Filament/Pages/ChooseEnvironment.php @@ -126,7 +126,7 @@ public function selectEnvironment(int $tenantId): void $this->persistLastTenant($user, $tenant); - if (! $workspaceContext->rememberTenantContext($tenant, request())) { + if (! $workspaceContext->rememberEnvironmentContext($tenant, request())) { abort(404); } diff --git a/apps/platform/app/Filament/Pages/Findings/FindingsHygieneReport.php b/apps/platform/app/Filament/Pages/Findings/FindingsHygieneReport.php index 5394de20..b658781a 100644 --- a/apps/platform/app/Filament/Pages/Findings/FindingsHygieneReport.php +++ b/apps/platform/app/Filament/Pages/Findings/FindingsHygieneReport.php @@ -12,7 +12,7 @@ use App\Models\Workspace; use App\Services\Auth\WorkspaceCapabilityResolver; use App\Services\Findings\FindingAssignmentHygieneService; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\Filament\TablePaginationProfiles; use App\Support\Navigation\CanonicalNavigationContext; use App\Support\OperateHub\OperateHubShell; @@ -78,7 +78,7 @@ public function mount(): void $this->reasonFilter = $this->resolveRequestedReasonFilter(); $this->authorizePageAccess(); - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), [], request(), @@ -486,14 +486,14 @@ private function filteredTenant(): ?ManagedEnvironment private function activeVisibleTenant(): ?ManagedEnvironment { - $activeTenant = app(OperateHubShell::class)->activeEntitledTenant(request()); + $activeEnvironment = app(OperateHubShell::class)->activeEntitledTenant(request()); - if (! $activeTenant instanceof ManagedEnvironment) { + if (! $activeEnvironment instanceof ManagedEnvironment) { return null; } foreach ($this->visibleTenants() as $tenant) { - if ($tenant->is($activeTenant)) { + if ($tenant->is($activeEnvironment)) { return $tenant; } } @@ -509,9 +509,9 @@ private function tenantPrefilterSource(): string return 'none'; } - $activeTenant = $this->activeVisibleTenant(); + $activeEnvironment = $this->activeVisibleTenant(); - if ($activeTenant instanceof ManagedEnvironment && $activeTenant->is($tenant)) { + if ($activeEnvironment instanceof ManagedEnvironment && $activeEnvironment->is($tenant)) { return 'active_tenant_context'; } diff --git a/apps/platform/app/Filament/Pages/Findings/FindingsIntakeQueue.php b/apps/platform/app/Filament/Pages/Findings/FindingsIntakeQueue.php index e73d310e..f70cf920 100644 --- a/apps/platform/app/Filament/Pages/Findings/FindingsIntakeQueue.php +++ b/apps/platform/app/Filament/Pages/Findings/FindingsIntakeQueue.php @@ -16,7 +16,7 @@ use App\Support\Auth\Capabilities; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\Filament\TablePaginationProfiles; use App\Support\Navigation\CanonicalNavigationContext; use App\Support\OperateHub\OperateHubShell; @@ -92,7 +92,7 @@ public function mount(): void $this->queueView = $this->resolveRequestedQueueView(); $this->authorizePageAccess(); - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), [], request(), @@ -494,12 +494,12 @@ private function filteredQueueQuery( return $query ->orderByRaw( - "case + 'case when due_at is not null and due_at < ? then 0 when status = ? then 1 when status = ? then 2 else 3 - end asc", + end asc', [now(), Finding::STATUS_REOPENED, Finding::STATUS_NEW], ) ->orderByRaw('case when due_at is null then 1 else 0 end asc') @@ -605,14 +605,14 @@ private function filteredTenant(): ?ManagedEnvironment private function activeVisibleTenant(): ?ManagedEnvironment { - $activeTenant = app(OperateHubShell::class)->activeEntitledTenant(request()); + $activeEnvironment = app(OperateHubShell::class)->activeEntitledTenant(request()); - if (! $activeTenant instanceof ManagedEnvironment) { + if (! $activeEnvironment instanceof ManagedEnvironment) { return null; } foreach ($this->visibleTenants() as $tenant) { - if ($tenant->is($activeTenant)) { + if ($tenant->is($activeEnvironment)) { return $tenant; } } @@ -628,9 +628,9 @@ private function tenantPrefilterSource(): string return 'none'; } - $activeTenant = $this->activeVisibleTenant(); + $activeEnvironment = $this->activeVisibleTenant(); - if ($activeTenant instanceof ManagedEnvironment && $activeTenant->is($tenant)) { + if ($activeEnvironment instanceof ManagedEnvironment && $activeEnvironment->is($tenant)) { return 'active_tenant_context'; } diff --git a/apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php b/apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php index bd22ae4d..962b97be 100644 --- a/apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php +++ b/apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php @@ -15,7 +15,7 @@ use App\Support\Auth\Capabilities; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\Filament\TablePaginationProfiles; use App\Support\Navigation\CanonicalNavigationContext; use App\Support\OperateHub\OperateHubShell; @@ -84,7 +84,7 @@ public function mount(): void { $this->authorizePageAccess(); - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), ['overdue', 'reopened', 'high_severity'], request(), @@ -270,9 +270,9 @@ public function emptyState(): array ]; } - $activeTenant = $this->activeVisibleTenant(); + $activeEnvironment = $this->activeVisibleTenant(); - if ($activeTenant instanceof ManagedEnvironment) { + if ($activeEnvironment instanceof ManagedEnvironment) { return [ 'title' => 'No visible assigned findings right now', 'body' => 'Nothing currently assigned to you needs attention in the visible environment scope. You can still open environment findings for broader context.', @@ -280,7 +280,7 @@ public function emptyState(): array 'action_name' => 'open_tenant_findings_empty', 'action_label' => 'Open environment findings', 'action_kind' => 'url', - 'action_url' => FindingResource::getUrl('index', tenant: $activeTenant), + 'action_url' => FindingResource::getUrl('index', tenant: $activeEnvironment), ]; } @@ -561,14 +561,14 @@ private function filteredTenant(): ?ManagedEnvironment private function activeVisibleTenant(): ?ManagedEnvironment { - $activeTenant = app(OperateHubShell::class)->activeEntitledTenant(request()); + $activeEnvironment = app(OperateHubShell::class)->activeEntitledTenant(request()); - if (! $activeTenant instanceof ManagedEnvironment) { + if (! $activeEnvironment instanceof ManagedEnvironment) { return null; } foreach ($this->visibleTenants() as $tenant) { - if ($tenant->is($activeTenant)) { + if ($tenant->is($activeEnvironment)) { return $tenant; } } @@ -584,9 +584,9 @@ private function tenantPrefilterSource(): string return 'none'; } - $activeTenant = $this->activeVisibleTenant(); + $activeEnvironment = $this->activeVisibleTenant(); - if ($activeTenant instanceof ManagedEnvironment && $activeTenant->is($tenant)) { + if ($activeEnvironment instanceof ManagedEnvironment && $activeEnvironment->is($tenant)) { return 'active_tenant_context'; } diff --git a/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php b/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php index 046fec00..46541104 100644 --- a/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php +++ b/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php @@ -4,6 +4,7 @@ namespace App\Filament\Pages\Governance; +use App\Filament\Concerns\CleansAdminTenantQueryParameter; use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState; use App\Filament\Resources\FindingExceptionResource; use App\Models\FindingException; @@ -40,6 +41,7 @@ class DecisionRegister extends Page implements HasTable { + use CleansAdminTenantQueryParameter; use ClearsWorkspaceHubEnvironmentFilterState; use InteractsWithTable; diff --git a/apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php b/apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php index e63411b4..b6132ee0 100644 --- a/apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php +++ b/apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php @@ -4,6 +4,7 @@ namespace App\Filament\Pages\Governance; +use App\Filament\Concerns\CleansAdminTenantQueryParameter; use App\Models\ManagedEnvironment; use App\Models\User; use App\Models\Workspace; @@ -29,6 +30,8 @@ class GovernanceInbox extends Page { + use CleansAdminTenantQueryParameter; + protected static bool $isDiscovered = false; protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-inbox-stack'; diff --git a/apps/platform/app/Filament/Pages/Monitoring/AuditLog.php b/apps/platform/app/Filament/Pages/Monitoring/AuditLog.php index 87195f97..d640aa5f 100644 --- a/apps/platform/app/Filament/Pages/Monitoring/AuditLog.php +++ b/apps/platform/app/Filament/Pages/Monitoring/AuditLog.php @@ -5,8 +5,8 @@ namespace App\Filament\Pages\Monitoring; use App\Models\AuditLog as AuditLogModel; -use App\Models\SupportAccessGrant; use App\Models\ManagedEnvironment; +use App\Models\SupportAccessGrant; use App\Models\User; use App\Models\Workspace; use App\Services\Auth\WorkspaceCapabilityResolver; @@ -14,7 +14,7 @@ use App\Support\Auth\Capabilities; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\Filament\FilterOptionCatalog; use App\Support\Filament\FilterPresets; use App\Support\Filament\TablePaginationProfiles; @@ -164,7 +164,7 @@ public function mount(): void $this->supportAccessOnly = request()->boolean('supportAccess'); $requestedEventId = is_numeric(request()->query('event')) ? (int) request()->query('event') : null; - app(CanonicalAdminTenantFilterState::class)->sync($this->getTableFiltersSessionKey(), request: request()); + app(CanonicalAdminEnvironmentFilterState::class)->sync($this->getTableFiltersSessionKey(), request: request()); $this->mountInteractsWithTable(); @@ -616,14 +616,14 @@ private function tenantFilterOptions(): array private function defaultTenantFilter(): ?string { - $activeTenant = app(OperateHubShell::class)->activeEntitledTenant(request()); + $activeEnvironment = app(OperateHubShell::class)->activeEntitledTenant(request()); - if (! $activeTenant instanceof ManagedEnvironment) { + if (! $activeEnvironment instanceof ManagedEnvironment) { return null; } - return array_key_exists((int) $activeTenant->getKey(), $this->authorizedTenants()) - ? (string) $activeTenant->getKey() + return array_key_exists((int) $activeEnvironment->getKey(), $this->authorizedTenants()) + ? (string) $activeEnvironment->getKey() : null; } diff --git a/apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php b/apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php index 926e0a81..b9e38492 100644 --- a/apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php +++ b/apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php @@ -4,6 +4,7 @@ namespace App\Filament\Pages\Monitoring; +use App\Filament\Concerns\CleansAdminTenantQueryParameter; use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState; use App\Filament\Resources\FindingExceptionResource; use App\Filament\Resources\FindingResource; @@ -17,7 +18,7 @@ use App\Support\Auth\Capabilities; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\Filament\FilterOptionCatalog; use App\Support\Filament\TablePaginationProfiles; use App\Support\Navigation\CanonicalNavigationContext; @@ -52,6 +53,7 @@ class FindingExceptionsQueue extends Page implements HasTable { + use CleansAdminTenantQueryParameter; use ClearsWorkspaceHubEnvironmentFilterState; use InteractsWithTable; @@ -653,7 +655,7 @@ private function filteredTenant(): ?ManagedEnvironment private function currentTenantFilterId(): ?int { - $tenantFilter = app(CanonicalAdminTenantFilterState::class)->currentFilterValue( + $tenantFilter = app(CanonicalAdminEnvironmentFilterState::class)->currentFilterValue( $this->getTableFiltersSessionKey(), $this->tableFilters ?? [], request(), diff --git a/apps/platform/app/Filament/Pages/Monitoring/Operations.php b/apps/platform/app/Filament/Pages/Monitoring/Operations.php index ff9a10fb..402c3cf9 100644 --- a/apps/platform/app/Filament/Pages/Monitoring/Operations.php +++ b/apps/platform/app/Filament/Pages/Monitoring/Operations.php @@ -13,7 +13,7 @@ use App\Models\Workspace; use App\Services\Auth\ManagedEnvironmentAccessScopeResolver; use App\Services\Auth\WorkspaceCapabilityResolver; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\ManagedEnvironmentLinks; use App\Support\Navigation\CanonicalNavigationContext; use App\Support\Navigation\WorkspaceHubEnvironmentFilter; @@ -231,7 +231,7 @@ protected function getHeaderActions(): array ->disabled(), ]; - $activeTenant = $this->currentTenantFilterId() === null + $activeEnvironment = $this->currentTenantFilterId() === null ? $operateHubShell->activeEntitledTenant(request()) : null; @@ -241,22 +241,22 @@ protected function getHeaderActions(): array ->icon('heroicon-o-arrow-left') ->color('gray') ->url($navigationContext->backLinkUrl); - } elseif ($activeTenant instanceof ManagedEnvironment) { + } elseif ($activeEnvironment instanceof ManagedEnvironment) { $actions[] = Action::make('operate_hub_back_to_tenant_operations') - ->label('Back to '.$activeTenant->name) + ->label('Back to '.$activeEnvironment->name) ->icon('heroicon-o-arrow-left') ->color('gray') - ->url(ManagedEnvironmentLinks::viewUrl($activeTenant)); + ->url(ManagedEnvironmentLinks::viewUrl($activeEnvironment)); } - if ($activeTenant instanceof ManagedEnvironment) { + if ($activeEnvironment instanceof ManagedEnvironment) { $actions[] = Action::make('operate_hub_show_all_tenants') ->label(__('localization.shell.show_all_environments')) ->color('gray') ->action(function (): void { Filament::setTenant(null, true); - app(WorkspaceContext::class)->clearLastTenantId(request()); + app(WorkspaceContext::class)->clearLastEnvironmentId(request()); $this->removeTableFilter('managed_environment_id'); @@ -283,7 +283,7 @@ public function landingHierarchySummary(): array $operateHubShell = app(OperateHubShell::class); $navigationContext = $this->navigationContext(); $filteredTenant = $this->filteredTenant(); - $activeTenant = $filteredTenant instanceof ManagedEnvironment + $activeEnvironment = $filteredTenant instanceof ManagedEnvironment ? null : $operateHubShell->activeEntitledTenant(request()); @@ -293,8 +293,8 @@ public function landingHierarchySummary(): array if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) { $returnLabel = $navigationContext->backLinkLabel; $returnBody = 'Return to the originating monitoring surface without competing with the current tab, filters, or row inspection flow.'; - } elseif ($activeTenant instanceof ManagedEnvironment) { - $returnLabel = 'Back to '.$activeTenant->name; + } elseif ($activeEnvironment instanceof ManagedEnvironment) { + $returnLabel = 'Back to '.$activeEnvironment->name; $returnBody = 'Return to the tenant dashboard when you need tenant-specific context outside this workspace monitoring landing.'; } @@ -302,13 +302,13 @@ public function landingHierarchySummary(): array 'scope_label' => $operateHubShell->scopeLabel(request()), 'scope_body' => $filteredTenant instanceof ManagedEnvironment ? 'The landing is workspace-scoped and filtered by an explicit environment filter.' - : ($activeTenant instanceof ManagedEnvironment + : ($activeEnvironment instanceof ManagedEnvironment ? 'The landing is currently narrowed to one environment inside the active workspace.' : 'The landing is currently showing workspace-wide monitoring across all entitled environments.'), 'return_label' => $returnLabel, 'return_body' => $returnBody, - 'scope_reset_label' => $activeTenant instanceof ManagedEnvironment ? __('localization.shell.show_all_environments') : null, - 'scope_reset_body' => $activeTenant instanceof ManagedEnvironment + 'scope_reset_label' => $activeEnvironment instanceof ManagedEnvironment ? __('localization.shell.show_all_environments') : null, + 'scope_reset_body' => $activeEnvironment instanceof ManagedEnvironment ? 'Reset the landing back to workspace-wide monitoring when environment-specific context is no longer needed.' : null, 'inspect_body' => 'Open a run from the table to enter the canonical monitoring detail viewer.', @@ -534,7 +534,7 @@ private function operationsUrl(array $overrides = []): string private function currentTenantFilterId(): ?int { - $tenantFilter = app(CanonicalAdminTenantFilterState::class)->currentFilterValue( + $tenantFilter = app(CanonicalAdminEnvironmentFilterState::class)->currentFilterValue( $this->getTableFiltersSessionKey(), $this->tableFilters ?? [], request(), diff --git a/apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php b/apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php index 6cb81b19..dddaa38f 100644 --- a/apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php +++ b/apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php @@ -5,9 +5,9 @@ namespace App\Filament\Pages\Operations; use App\Filament\Resources\OperationRunResource; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\SupportRequest; -use App\Models\ManagedEnvironment; use App\Models\User; use App\Services\Audit\WorkspaceAuditLogger; use App\Services\Auth\CapabilityResolver; @@ -27,10 +27,10 @@ use App\Support\OpsUx\RunDetailPolling; use App\Support\ProductTelemetry\ProductTelemetryRecorder; use App\Support\ProductTelemetry\ProductUsageEventCatalog; +use App\Support\Rbac\UiEnforcement; use App\Support\ReasonTranslation\ReasonPresenter; use App\Support\RedactionIntegrity; use App\Support\RestoreSafety\RestoreSafetyCopy; -use App\Support\Rbac\UiEnforcement; use App\Support\SupportDiagnostics\SupportDiagnosticBundleBuilder; use App\Support\SupportRequests\ExternalSupportDeskHandoffService; use App\Support\SupportRequests\SupportRequestSubmissionService; @@ -46,15 +46,15 @@ use Filament\Actions\ActionGroup; use Filament\Forms\Components\Placeholder; use Filament\Forms\Components\Select; -use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; use Filament\Notifications\Notification; use Filament\Pages\Page; use Filament\Schemas\Components\EmbeddedSchema; use Filament\Schemas\Components\Utilities\Get; use Filament\Schemas\Schema; -use Illuminate\Contracts\View\View; use Illuminate\Contracts\Support\Htmlable; +use Illuminate\Contracts\View\View; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Str; @@ -108,7 +108,7 @@ protected function getHeaderActions(): array { $operateHubShell = app(OperateHubShell::class); $navigationContext = $this->navigationContext(); - $activeTenant = $operateHubShell->activeEntitledTenant(request()); + $activeEnvironment = $operateHubShell->activeEntitledTenant(request()); $runTenantId = isset($this->run) ? (int) ($this->run->managed_environment_id ?? 0) : 0; $actions = [ @@ -123,11 +123,11 @@ protected function getHeaderActions(): array ->label($navigationContext->backLinkLabel) ->color('gray') ->url($navigationContext->backLinkUrl); - } elseif ($activeTenant instanceof ManagedEnvironment && (int) $activeTenant->getKey() === $runTenantId) { + } elseif ($activeEnvironment instanceof ManagedEnvironment && (int) $activeEnvironment->getKey() === $runTenantId) { $actions[] = Action::make('operate_hub_back_to_tenant_run_detail') - ->label('← Back to '.$activeTenant->name) + ->label('← Back to '.$activeEnvironment->name) ->color('gray') - ->url(ManagedEnvironmentLinks::viewUrl($activeTenant)); + ->url(ManagedEnvironmentLinks::viewUrl($activeEnvironment)); } else { $actions[] = Action::make('operate_hub_back_to_operations') ->label('Back to Operations') @@ -135,7 +135,7 @@ protected function getHeaderActions(): array ->url(fn (): string => OperationRunLinks::index()); } - if ($activeTenant instanceof ManagedEnvironment) { + if ($activeEnvironment instanceof ManagedEnvironment) { $actions[] = Action::make('operate_hub_show_all_operations') ->label('Show all operations') ->color('gray') @@ -201,7 +201,7 @@ public function monitoringDetailSummary(): array { $operateHubShell = app(OperateHubShell::class); $navigationContext = $this->navigationContext(); - $activeTenant = $operateHubShell->activeEntitledTenant(request()); + $activeEnvironment = $operateHubShell->activeEntitledTenant(request()); $runTenantId = isset($this->run) ? (int) ($this->run->managed_environment_id ?? 0) : 0; $navigationLabel = 'Back to Operations'; @@ -210,8 +210,8 @@ public function monitoringDetailSummary(): array if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) { $navigationLabel = $navigationContext->backLinkLabel; $navigationBody = 'Return to the originating surface while keeping refresh and follow-up work separate from navigation.'; - } elseif ($activeTenant instanceof ManagedEnvironment && (int) $activeTenant->getKey() === $runTenantId) { - $navigationLabel = 'Back to '.$activeTenant->name; + } elseif ($activeEnvironment instanceof ManagedEnvironment && (int) $activeEnvironment->getKey() === $runTenantId) { + $navigationLabel = 'Back to '.$activeEnvironment->name; $navigationBody = 'Return to the active tenant dashboard, then widen back to the workspace view only when you need broader monitoring context.'; } @@ -727,15 +727,15 @@ public function canonicalContextBanner(): ?array return null; } - $activeTenant = app(OperateHubShell::class)->activeEntitledTenant(request()); + $activeEnvironment = app(OperateHubShell::class)->activeEntitledTenant(request()); $runTenant = $this->run->tenant; if (! $runTenant instanceof ManagedEnvironment) { return [ 'tone' => 'slate', 'title' => 'Workspace-level operation', - 'body' => $activeTenant instanceof ManagedEnvironment - ? 'This canonical workspace view is not tied to the current environment context ('.$activeTenant->name.').' + 'body' => $activeEnvironment instanceof ManagedEnvironment + ? 'This canonical workspace view is not tied to the current environment context ('.$activeEnvironment->name.').' : 'This canonical workspace view is not tied to any environment.', ]; } @@ -744,9 +744,9 @@ public function canonicalContextBanner(): ?array $tone = 'sky'; $title = null; - if ($activeTenant instanceof ManagedEnvironment && ! $activeTenant->is($runTenant)) { + if ($activeEnvironment instanceof ManagedEnvironment && ! $activeEnvironment->is($runTenant)) { $title = 'Current environment context differs from this operation'; - array_unshift($messages, 'Current environment context: '.$activeTenant->name.'.'); + array_unshift($messages, 'Current environment context: '.$activeEnvironment->name.'.'); $messages[] = 'This canonical workspace view remains valid without switching environment context.'; } @@ -760,7 +760,7 @@ public function canonicalContextBanner(): ?array if ($referencedTenant->contextNote !== null) { $messages[] = $referencedTenant->contextNote; } - } elseif (! $activeTenant instanceof ManagedEnvironment) { + } elseif (! $activeEnvironment instanceof ManagedEnvironment) { $title ??= 'Canonical workspace view'; $messages[] = 'No environment context is currently selected.'; } diff --git a/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php b/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php index df91f4af..db5dedf9 100644 --- a/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php +++ b/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php @@ -4,6 +4,7 @@ namespace App\Filament\Pages\Reviews; +use App\Filament\Concerns\CleansAdminTenantQueryParameter; use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState; use App\Filament\Resources\EnvironmentReviewResource; use App\Models\EnvironmentReview; @@ -50,6 +51,7 @@ class CustomerReviewWorkspace extends Page implements HasTable { + use CleansAdminTenantQueryParameter; use ClearsWorkspaceHubEnvironmentFilterState; use InteractsWithTable; @@ -99,7 +101,7 @@ public function getTitle(): string return __('localization.review.customer_review_workspace'); } - public static function tenantPrefilterUrl(ManagedEnvironment $tenant): string + public static function environmentFilterUrl(ManagedEnvironment $tenant): string { return static::getUrl(panel: 'admin').'?'.http_build_query([ 'environment_id' => (int) $tenant->getKey(), @@ -573,7 +575,7 @@ private function latestReviewUrl(ManagedEnvironment $tenant): ?string static fn (mixed $value): bool => $value !== null && $value !== '', ); - return $this->appendQuery(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant), $query); + return $this->appendQuery(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant), $query); } private function reviewPackDownloadUrl(EnvironmentReview $review, ManagedEnvironment $tenant): ?string diff --git a/apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php b/apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php index 62875559..aba39a3e 100644 --- a/apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php +++ b/apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php @@ -4,6 +4,7 @@ namespace App\Filament\Pages\Reviews; +use App\Filament\Concerns\CleansAdminTenantQueryParameter; use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState; use App\Filament\Resources\EnvironmentReviewResource; use App\Models\EnvironmentReview; @@ -45,6 +46,7 @@ class ReviewRegister extends Page implements HasTable { + use CleansAdminTenantQueryParameter; use ClearsWorkspaceHubEnvironmentFilterState; use InteractsWithTable; @@ -111,7 +113,7 @@ public function table(Table $table): Table ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() - ->recordUrl(fn (EnvironmentReview $record): string => EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $record], $record->tenant)) + ->recordUrl(fn (EnvironmentReview $record): string => EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $record], $record->tenant)) ->columns([ TextColumn::make('tenant.name')->label('Environment')->searchable(), TextColumn::make('status') diff --git a/apps/platform/app/Filament/Pages/Workspaces/ManagedEnvironmentOnboardingWizard.php b/apps/platform/app/Filament/Pages/Workspaces/ManagedEnvironmentOnboardingWizard.php index 680e4c35..7995a2ce 100644 --- a/apps/platform/app/Filament/Pages/Workspaces/ManagedEnvironmentOnboardingWizard.php +++ b/apps/platform/app/Filament/Pages/Workspaces/ManagedEnvironmentOnboardingWizard.php @@ -4,7 +4,6 @@ namespace App\Filament\Pages\Workspaces; -use BackedEnum; use App\Exceptions\Onboarding\OnboardingDraftConflictException; use App\Exceptions\Onboarding\OnboardingDraftImmutableException; use App\Filament\Resources\ManagedEnvironmentResource; @@ -13,24 +12,22 @@ use App\Jobs\ProviderComplianceSnapshotJob; use App\Jobs\ProviderConnectionHealthCheckJob; use App\Jobs\ProviderInventorySyncJob; -use App\Models\OperationRun; -use App\Models\ProviderConnection; use App\Models\ManagedEnvironment; use App\Models\ManagedEnvironmentOnboardingSession; +use App\Models\OperationRun; +use App\Models\ProviderConnection; use App\Models\User; use App\Models\VerificationCheckAcknowledgement; use App\Models\Workspace; use App\Services\Audit\WorkspaceAuditLogger; use App\Services\Auth\ManagedEnvironmentMembershipManager; +use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver; use App\Services\Intune\AuditLogger; use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder; use App\Services\Onboarding\OnboardingDraftMutationService; use App\Services\Onboarding\OnboardingDraftResolver; use App\Services\Onboarding\OnboardingDraftStageResolver; use App\Services\Onboarding\OnboardingLifecycleService; -use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver; -use App\Services\Entitlements\WorkspaceEntitlementResolver; -use App\Services\OperationRunService; use App\Services\Providers\ProviderConnectionMutationService; use App\Services\Providers\ProviderOperationRegistry; use App\Services\Providers\ProviderOperationStartGate; @@ -50,17 +47,16 @@ use App\Support\OperationRunLinks; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; -use App\Support\OpsUx\OperationUxPresenter; -use App\Support\OpsUx\ProviderOperationStartResultPresenter; use App\Support\OpsUx\OpsUxBrowserEvents; +use App\Support\OpsUx\ProviderOperationStartResultPresenter; use App\Support\ProductKnowledge\ContextualHelpResolver; use App\Support\Providers\ProviderConnectionType; use App\Support\Providers\ProviderConsentStatus; use App\Support\Providers\ProviderReasonCodes; +use App\Support\Providers\ProviderVerificationStatus; use App\Support\Providers\TargetScope\ProviderConnectionSurfaceSummary; use App\Support\Providers\TargetScope\ProviderConnectionTargetScopeDescriptor; use App\Support\Providers\TargetScope\ProviderConnectionTargetScopeNormalizer; -use App\Support\Providers\ProviderVerificationStatus; use App\Support\Tenants\TenantInteractionLane; use App\Support\Tenants\TenantLifecyclePresentation; use App\Support\Tenants\TenantOperabilityQuestion; @@ -68,6 +64,7 @@ use App\Support\Verification\VerificationCheckStatus; use App\Support\Verification\VerificationReportOverall; use App\Support\Workspaces\WorkspaceContext; +use BackedEnum; use Filament\Actions\Action; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\Radio; @@ -233,7 +230,7 @@ private function canViewLinkedEnvironment(): bool return app(TenantOperabilityService::class)->outcomeFor( tenant: $tenant, - question: TenantOperabilityQuestion::TenantBoundViewability, + question: TenantOperabilityQuestion::EnvironmentBoundViewability, actor: $user, workspaceId: (int) $this->workspace->getKey(), lane: TenantInteractionLane::AdministrativeManagement, @@ -1108,7 +1105,7 @@ private function readinessSupportingEvidenceSchema(array $payload, string $keyPr $actions[] = Action::make($keyPrefix.'_required_permissions_assist') ->label('View required permissions') ->color('gray') - ->url($requiredPermissionsUrl); + ->url($requiredPermissionsUrl); } if ($actions === []) { @@ -1206,7 +1203,7 @@ private function readinessPermissionDiagnosticsSchema(array $payload, string $ke * draft: array{id: int, environment_name: string, stage_label: string, draft_status_label: string, started_by: string, updated_by: string, last_updated_human: string}, * checkpoint: array{current_checkpoint: string|null, current_checkpoint_label: string|null, last_completed_checkpoint: string|null, lifecycle_state: string, lifecycle_label: string}, * provider_summary: array|null, - * verification: array{status: string, status_label: string, run_id: int|null, run_url: string|null, is_active: bool, has_report: bool, matches_selected_connection: bool|null, overall: string|null}, + * verification: array{status: string, status_label: string, run_id: int|null, run_url: string|null, is_active: bool, has_report: bool, matches_selected_connection: bool|null, overall: string|null}, * verification_assist: array{is_visible: bool, reason: string}, * permissions: array{overall: string|null, counts: array, freshness: array{last_refreshed_at: string|null, is_stale: bool}, capability_groups: array>, primary_capability_group: array|null, missing_permissions: array{application: list, delegated: list}, required_permissions_url: string|null}|null, * freshness: array{connection_recently_updated: bool, verification_mismatch: bool, permission_last_refreshed_at: string|null, permission_data_is_stale: bool, note: string}, @@ -4195,7 +4192,7 @@ public function startBootstrap(array $operationTypes): void draft: $this->onboardingSession, actor: $user, expectedVersion: $this->expectedDraftVersion(), - mutator: function (ManagedEnvironmentOnboardingSession $draft) use ($tenant, $connection, $types, $registry, $user, &$result): void { + mutator: function (ManagedEnvironmentOnboardingSession $draft) use ($tenant, $connection, $types, $user, &$result): void { $nextOperationType = $this->nextBootstrapOperationType($draft, $types, (int) $connection->getKey()); if ($nextOperationType === null) { diff --git a/apps/platform/app/Filament/Resources/AlertDeliveryResource/Pages/ListAlertDeliveries.php b/apps/platform/app/Filament/Resources/AlertDeliveryResource/Pages/ListAlertDeliveries.php index 2c26faec..1e8bd24e 100644 --- a/apps/platform/app/Filament/Resources/AlertDeliveryResource/Pages/ListAlertDeliveries.php +++ b/apps/platform/app/Filament/Resources/AlertDeliveryResource/Pages/ListAlertDeliveries.php @@ -5,7 +5,7 @@ namespace App\Filament\Resources\AlertDeliveryResource\Pages; use App\Filament\Resources\AlertDeliveryResource; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\Navigation\CanonicalNavigationContext; use App\Support\OperateHub\OperateHubShell; use Filament\Actions\Action; @@ -17,7 +17,7 @@ class ListAlertDeliveries extends ListRecords public function mount(): void { - app(CanonicalAdminTenantFilterState::class)->sync($this->getTableFiltersSessionKey(), request: request()); + app(CanonicalAdminEnvironmentFilterState::class)->sync($this->getTableFiltersSessionKey(), request: request()); parent::mount(); } diff --git a/apps/platform/app/Filament/Resources/BackupScheduleResource.php b/apps/platform/app/Filament/Resources/BackupScheduleResource.php index e19c39d1..9acf7e71 100644 --- a/apps/platform/app/Filament/Resources/BackupScheduleResource.php +++ b/apps/platform/app/Filament/Resources/BackupScheduleResource.php @@ -5,7 +5,7 @@ use App\Exceptions\InvalidPolicyTypeException; use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\BackupScheduleResource\Pages; use App\Filament\Resources\BackupScheduleResource\RelationManagers\BackupScheduleOperationRunsRelationManager; use App\Jobs\RunBackupScheduleJob; @@ -43,7 +43,6 @@ use Filament\Actions\BulkAction; use Filament\Actions\BulkActionGroup; use Filament\Actions\CreateAction; -use Filament\Facades\Filament; use Filament\Forms\Components\CheckboxList; use Filament\Forms\Components\Select; use Filament\Forms\Components\TextInput; @@ -70,7 +69,7 @@ class BackupScheduleResource extends Resource { use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = BackupSchedule::class; diff --git a/apps/platform/app/Filament/Resources/BackupScheduleResource/Pages/ListBackupSchedules.php b/apps/platform/app/Filament/Resources/BackupScheduleResource/Pages/ListBackupSchedules.php index 74e451c9..2a8be6e2 100644 --- a/apps/platform/app/Filament/Resources/BackupScheduleResource/Pages/ListBackupSchedules.php +++ b/apps/platform/app/Filament/Resources/BackupScheduleResource/Pages/ListBackupSchedules.php @@ -3,11 +3,9 @@ namespace App\Filament\Resources\BackupScheduleResource\Pages; use App\Filament\Resources\BackupScheduleResource; -use App\Models\ManagedEnvironment; use App\Support\BackupHealth\TenantBackupHealthAssessment; use App\Support\BackupHealth\TenantBackupHealthResolver; -use App\Support\Filament\CanonicalAdminTenantFilterState; -use Filament\Facades\Filament; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use Filament\Resources\Pages\ListRecords; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -34,7 +32,7 @@ public function mountAction(string $name, array $arguments = [], array $context public function mount(): void { - $this->syncCanonicalAdminTenantFilterState(); + $this->syncCanonicalAdminEnvironmentFilterState(); parent::mount(); } @@ -59,13 +57,13 @@ private function tableHasRecords(): bool return $this->getTableRecords()->count() > 0; } - private function syncCanonicalAdminTenantFilterState(): void + private function syncCanonicalAdminEnvironmentFilterState(): void { - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), - tenantSensitiveFilters: [], + environmentSensitiveFilters: [], request: request(), - tenantFilterName: null, + environmentFilterName: null, ); } @@ -75,9 +73,9 @@ public function getSubheading(): ?string return null; } - $tenant = BackupScheduleResource::panelTenantContext(); + $tenant = BackupScheduleResource::panelTenantContext(); - if ($tenant === null) { + if ($tenant === null) { return 'One or more enabled schedules need follow-up.'; } diff --git a/apps/platform/app/Filament/Resources/BackupSetResource.php b/apps/platform/app/Filament/Resources/BackupSetResource.php index eb2d52d5..6f4de2d3 100644 --- a/apps/platform/app/Filament/Resources/BackupSetResource.php +++ b/apps/platform/app/Filament/Resources/BackupSetResource.php @@ -4,7 +4,7 @@ use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\BackupSetResource\Pages; use App\Filament\Resources\BackupSetResource\RelationManagers\BackupItemsRelationManager; use App\Jobs\BulkBackupSetDeleteJob; @@ -45,7 +45,6 @@ use Filament\Actions\ActionGroup; use Filament\Actions\BulkAction; use Filament\Actions\BulkActionGroup; -use Filament\Facades\Filament; use Filament\Forms; use Filament\Infolists; use Filament\Notifications\Notification; @@ -64,7 +63,7 @@ class BackupSetResource extends Resource { use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = BackupSet::class; diff --git a/apps/platform/app/Filament/Resources/BackupSetResource/Pages/ListBackupSets.php b/apps/platform/app/Filament/Resources/BackupSetResource/Pages/ListBackupSets.php index 79406695..6d988721 100644 --- a/apps/platform/app/Filament/Resources/BackupSetResource/Pages/ListBackupSets.php +++ b/apps/platform/app/Filament/Resources/BackupSetResource/Pages/ListBackupSets.php @@ -4,7 +4,7 @@ use App\Filament\Resources\BackupSetResource; use App\Support\BackupHealth\TenantBackupHealthAssessment; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use Filament\Resources\Pages\ListRecords; class ListBackupSets extends ListRecords @@ -13,10 +13,10 @@ class ListBackupSets extends ListRecords public function mount(): void { - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), request: request(), - tenantFilterName: null, + environmentFilterName: null, ); parent::mount(); diff --git a/apps/platform/app/Filament/Resources/BaselineProfileResource.php b/apps/platform/app/Filament/Resources/BaselineProfileResource.php index f47c29c8..8806151b 100644 --- a/apps/platform/app/Filament/Resources/BaselineProfileResource.php +++ b/apps/platform/app/Filament/Resources/BaselineProfileResource.php @@ -4,13 +4,14 @@ namespace App\Filament\Resources; +use App\Filament\Concerns\CleansAdminTenantResourceQueryParameter; use App\Filament\Pages\BaselineCompareMatrix; use App\Filament\Resources\BaselineProfileResource\Pages; use App\Models\BaselineProfile; use App\Models\BaselineSnapshot; use App\Models\BaselineTenantAssignment; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Models\User; use App\Models\Workspace; use App\Services\Audit\WorkspaceAuditLogger; @@ -21,11 +22,11 @@ use App\Support\Badges\BadgeCatalog; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; -use App\Support\Baselines\BaselineScope; use App\Support\Baselines\BaselineCaptureMode; use App\Support\Baselines\BaselineFullContentRolloutGate; use App\Support\Baselines\BaselineProfileStatus; use App\Support\Baselines\BaselineReasonCodes; +use App\Support\Baselines\BaselineScope; use App\Support\Baselines\Compare\CompareStrategyRegistry; use App\Support\Filament\FilterOptionCatalog; use App\Support\Inventory\InventoryPolicyTypeMeta; @@ -61,16 +62,18 @@ use Filament\Schemas\Schema; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; -use InvalidArgumentException; use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Unique; +use InvalidArgumentException; use UnitEnum; class BaselineProfileResource extends Resource { + use CleansAdminTenantResourceQueryParameter; + protected static bool $isDiscovered = false; protected static bool $isScopedToTenant = false; diff --git a/apps/platform/app/Filament/Resources/BaselineSnapshotResource.php b/apps/platform/app/Filament/Resources/BaselineSnapshotResource.php index 59a387e2..e6dc3aad 100644 --- a/apps/platform/app/Filament/Resources/BaselineSnapshotResource.php +++ b/apps/platform/app/Filament/Resources/BaselineSnapshotResource.php @@ -4,6 +4,7 @@ namespace App\Filament\Resources; +use App\Filament\Concerns\CleansAdminTenantResourceQueryParameter; use App\Filament\Resources\BaselineSnapshotResource\Pages; use App\Models\BaselineProfile; use App\Models\BaselineSnapshot; @@ -25,9 +26,9 @@ use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType; -use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome; use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope; use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter; +use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome; use App\Support\Ui\GovernanceArtifactTruth\SurfaceCompressionContext; use App\Support\Workspaces\WorkspaceContext; use BackedEnum; @@ -44,6 +45,8 @@ class BaselineSnapshotResource extends Resource { + use CleansAdminTenantResourceQueryParameter; + protected static bool $isDiscovered = false; protected static bool $isScopedToTenant = false; diff --git a/apps/platform/app/Filament/Resources/EntraGroupResource.php b/apps/platform/app/Filament/Resources/EntraGroupResource.php index f96425a4..a0c8384b 100644 --- a/apps/platform/app/Filament/Resources/EntraGroupResource.php +++ b/apps/platform/app/Filament/Resources/EntraGroupResource.php @@ -5,7 +5,7 @@ use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; use App\Filament\Concerns\ScopesGlobalSearchToTenant; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\EntraGroupResource\Pages; use App\Models\EntraGroup; use App\Models\ManagedEnvironment; @@ -39,7 +39,7 @@ class EntraGroupResource extends Resource use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; use ScopesGlobalSearchToTenant; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static bool $isScopedToTenant = false; diff --git a/apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php b/apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php index d816a248..d911e32a 100644 --- a/apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php +++ b/apps/platform/app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php @@ -7,7 +7,7 @@ use App\Models\User; use App\Services\Directory\EntraGroupSyncService; use App\Support\Auth\Capabilities; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\OperationRunLinks; use App\Support\OpsUx\OpsUxBrowserEvents; use App\Support\OpsUx\ProviderOperationStartResultPresenter; @@ -22,10 +22,10 @@ class ListEntraGroups extends ListRecords public function mount(): void { - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), request: request(), - tenantFilterName: null, + environmentFilterName: null, ); if ( diff --git a/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php b/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php index 601c59f4..b02dc5a2 100644 --- a/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php +++ b/apps/platform/app/Filament/Resources/EnvironmentReviewResource.php @@ -4,47 +4,46 @@ namespace App\Filament\Resources; +use App\Exceptions\Entitlements\WorkspaceEntitlementBlockedException; use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\EnvironmentReviewResource\Pages; -use App\Exceptions\Entitlements\WorkspaceEntitlementBlockedException; -use App\Models\EvidenceSnapshot; -use App\Models\ReviewPack; -use App\Models\ManagedEnvironment; use App\Models\EnvironmentReview; use App\Models\EnvironmentReviewSection; +use App\Models\EvidenceSnapshot; +use App\Models\ManagedEnvironment; +use App\Models\ReviewPack; use App\Models\User; -use App\Services\ReviewPackService; use App\Services\EnvironmentReviews\EnvironmentReviewService; +use App\Services\ReviewPackService; use App\Support\Auth\Capabilities; use App\Support\Auth\UiTooltips as AuthUiTooltips; use App\Support\Badges\BadgeCatalog; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; +use App\Support\EnvironmentReviewCompletenessState; +use App\Support\EnvironmentReviewStatus; use App\Support\Findings\FindingOutcomeSemantics; use App\Support\Navigation\NavigationScope; use App\Support\OperationRunLinks; use App\Support\OperationRunType; use App\Support\OpsUx\OperationUxPresenter; +use App\Support\Rbac\UiEnforcement; use App\Support\ReasonTranslation\ReasonPresenter; use App\Support\ReviewPackStatus; -use App\Support\Rbac\UiEnforcement; -use App\Support\EnvironmentReviewCompletenessState; -use App\Support\EnvironmentReviewStatus; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType; -use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome; use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope; use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter; +use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome; use App\Support\Ui\GovernanceArtifactTruth\SurfaceCompressionContext; use BackedEnum; use Filament\Actions; -use Filament\Facades\Filament; use Filament\Forms\Components\Select; use Filament\Infolists\Components\RepeatableEntry; use Filament\Infolists\Components\TextEntry; @@ -66,7 +65,7 @@ class EnvironmentReviewResource extends Resource { use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static bool $isDiscovered = false; @@ -317,7 +316,7 @@ public static function table(Table $table): Table ->persistFiltersInSession() ->persistSearchInSession() ->persistSortInSession() - ->recordUrl(fn (EnvironmentReview $record): string => static::tenantScopedUrl('view', ['record' => $record], $record->tenant)) + ->recordUrl(fn (EnvironmentReview $record): string => static::environmentScopedUrl('view', ['record' => $record], $record->tenant)) ->columns([ Tables\Columns\TextColumn::make('status') ->badge() @@ -464,7 +463,7 @@ public static function executeCreateReview(array $data): void ->actions([ Actions\Action::make('view_review') ->label(__('localization.review.view_review')) - ->url(static::tenantScopedUrl('view', ['record' => $review], $tenant)), + ->url(static::environmentScopedUrl('view', ['record' => $review], $tenant)), ]) ->send(); @@ -613,11 +612,10 @@ public static function executeExport(EnvironmentReview $review): void /** * @param array $parameters */ - public static function tenantScopedUrl( + public static function environmentScopedUrl( string $page = 'index', array $parameters = [], ?ManagedEnvironment $tenant = null, - ?string $panel = null, ): string { $panelId = 'admin'; @@ -881,7 +879,7 @@ private static function summaryContextLinks(EnvironmentReview $record, bool $cus $links[] = [ 'title' => __('localization.review.customer_workspace'), 'label' => __('localization.review.open_customer_workspace'), - 'url' => CustomerReviewWorkspace::tenantPrefilterUrl($record->tenant), + 'url' => CustomerReviewWorkspace::environmentFilterUrl($record->tenant), 'description' => __('localization.review.customer_workspace_description'), ]; } diff --git a/apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php b/apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php index 82569c68..5bfeac60 100644 --- a/apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php +++ b/apps/platform/app/Filament/Resources/EnvironmentReviewResource/Pages/ViewEnvironmentReview.php @@ -6,19 +6,19 @@ use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\EnvironmentReviewResource; -use App\Models\ReviewPack; -use App\Models\ManagedEnvironment; use App\Models\EnvironmentReview; +use App\Models\ManagedEnvironment; +use App\Models\ReviewPack; use App\Models\User; -use App\Services\ReviewPackService; use App\Services\Audit\WorkspaceAuditLogger; use App\Services\EnvironmentReviews\EnvironmentReviewLifecycleService; use App\Services\EnvironmentReviews\EnvironmentReviewService; +use App\Services\ReviewPackService; use App\Support\Audit\AuditActionId; use App\Support\Auth\Capabilities; +use App\Support\EnvironmentReviewStatus; use App\Support\Rbac\UiEnforcement; use App\Support\ReviewPackStatus; -use App\Support\EnvironmentReviewStatus; use App\Support\Ui\GovernanceActions\GovernanceActionCatalog; use Filament\Actions; use Filament\Forms\Components\Textarea; @@ -302,7 +302,7 @@ private function createNextReviewAction(): Actions\Action return; } - $this->redirect(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $nextReview], $nextReview->tenant)); + $this->redirect(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $nextReview], $nextReview->tenant)); }), ) ->requireCapability(Capabilities::ENVIRONMENT_REVIEW_MANAGE) diff --git a/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php b/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php index c3f57edd..7cd82f90 100644 --- a/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php +++ b/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php @@ -6,10 +6,9 @@ use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\EvidenceSnapshotResource\Pages; -use App\Filament\Resources\ReviewPackResource; use App\Models\EvidenceSnapshot; use App\Models\EvidenceSnapshotItem; use App\Models\ManagedEnvironment; @@ -21,8 +20,8 @@ use App\Support\Badges\BadgeRenderer; use App\Support\Evidence\EvidenceCompletenessState; use App\Support\Evidence\EvidenceSnapshotStatus; -use App\Support\Navigation\RelatedContextEntry; use App\Support\Navigation\NavigationScope; +use App\Support\Navigation\RelatedContextEntry; use App\Support\OperationCatalog; use App\Support\OperationRunLinks; use App\Support\OperationRunOutcome; @@ -35,13 +34,12 @@ use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType; use App\Support\Ui\GovernanceActions\GovernanceActionCatalog; -use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome; use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope; use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter; +use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome; use App\Support\Ui\GovernanceArtifactTruth\SurfaceCompressionContext; use BackedEnum; use Filament\Actions; -use Filament\Facades\Filament; use Filament\Forms\Components\Textarea; use Filament\Infolists\Components\RepeatableEntry; use Filament\Infolists\Components\TextEntry; @@ -66,7 +64,7 @@ class EvidenceSnapshotResource extends Resource { use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = EvidenceSnapshot::class; @@ -311,7 +309,7 @@ public static function relatedContextEntries(EvidenceSnapshot $record): array label: 'Customer workspace', value: $record->tenant->name, secondaryValue: 'Open the customer-safe review workspace prefiltered to this tenant.', - targetUrl: CustomerReviewWorkspace::tenantPrefilterUrl($record->tenant), + targetUrl: CustomerReviewWorkspace::environmentFilterUrl($record->tenant), targetKind: 'canonical_page', priority: 30, actionLabel: 'Open customer workspace', diff --git a/apps/platform/app/Filament/Resources/FindingExceptionResource.php b/apps/platform/app/Filament/Resources/FindingExceptionResource.php index 98e3a271..52da4558 100644 --- a/apps/platform/app/Filament/Resources/FindingExceptionResource.php +++ b/apps/platform/app/Filament/Resources/FindingExceptionResource.php @@ -6,9 +6,8 @@ use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\FindingExceptionResource\Pages; -use App\Filament\Resources\FindingResource; use App\Models\FindingException; use App\Models\FindingExceptionEvidenceReference; use App\Models\ManagedEnvironment; @@ -31,7 +30,6 @@ use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use BackedEnum; use Filament\Actions\Action; -use Filament\Facades\Filament; use Filament\Forms\Components\DateTimePicker; use Filament\Forms\Components\Repeater; use Filament\Forms\Components\Select; @@ -56,7 +54,7 @@ class FindingExceptionResource extends Resource { use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = FindingException::class; diff --git a/apps/platform/app/Filament/Resources/FindingResource.php b/apps/platform/app/Filament/Resources/FindingResource.php index 8d28ba27..c864196b 100644 --- a/apps/platform/app/Filament/Resources/FindingResource.php +++ b/apps/platform/app/Filament/Resources/FindingResource.php @@ -4,13 +4,13 @@ use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\FindingResource\Pages; use App\Filament\Support\NormalizedDiffSurface; use App\Models\Finding; use App\Models\FindingException; -use App\Models\PolicyVersion; use App\Models\ManagedEnvironment; +use App\Models\PolicyVersion; use App\Models\User; use App\Services\Auth\ManagedEnvironmentAccessScopeResolver; use App\Services\Drift\DriftFindingDiffBuilder; @@ -22,8 +22,8 @@ use App\Support\Badges\BadgeRenderer; use App\Support\Filament\FilterOptionCatalog; use App\Support\Filament\FilterPresets; -use App\Support\Findings\FindingOutcomeSemantics; use App\Support\Filament\TablePaginationProfiles; +use App\Support\Findings\FindingOutcomeSemantics; use App\Support\Navigation\CanonicalNavigationContext; use App\Support\Navigation\CrossResourceNavigationMatrix; use App\Support\Navigation\NavigationScope; @@ -69,7 +69,7 @@ class FindingResource extends Resource { use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = Finding::class; @@ -1229,7 +1229,7 @@ public static function table(Table $table): Table } } - $body = "Resolved {$resolvedCount} finding".($resolvedCount === 1 ? '' : 's')." pending verification."; + $body = "Resolved {$resolvedCount} finding".($resolvedCount === 1 ? '' : 's').' pending verification.'; if ($skippedCount > 0) { $body .= " Skipped {$skippedCount}."; } diff --git a/apps/platform/app/Filament/Resources/FindingResource/Pages/ListFindings.php b/apps/platform/app/Filament/Resources/FindingResource/Pages/ListFindings.php index 7841e1f9..cef057eb 100644 --- a/apps/platform/app/Filament/Resources/FindingResource/Pages/ListFindings.php +++ b/apps/platform/app/Filament/Resources/FindingResource/Pages/ListFindings.php @@ -11,7 +11,7 @@ use App\Models\User; use App\Services\Findings\FindingWorkflowService; use App\Support\Auth\Capabilities; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\Rbac\UiEnforcement; use App\Support\Rbac\UiTooltips; use Filament\Actions; @@ -49,7 +49,7 @@ public function mountAction(string $name, array $arguments = [], array $context public function mount(): void { - $this->syncCanonicalAdminTenantFilterState(); + $this->syncCanonicalAdminEnvironmentFilterState(); parent::mount(); $this->applyRequestedDashboardPrefilter(); @@ -264,13 +264,13 @@ protected function buildAllMatchingQuery(): Builder return $query; } - private function syncCanonicalAdminTenantFilterState(): void + private function syncCanonicalAdminEnvironmentFilterState(): void { - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), - tenantSensitiveFilters: ['scope_key', 'run_ids'], + environmentSensitiveFilters: ['scope_key', 'run_ids'], request: request(), - tenantFilterName: null, + environmentFilterName: null, ); } diff --git a/apps/platform/app/Filament/Resources/InventoryItemResource.php b/apps/platform/app/Filament/Resources/InventoryItemResource.php index 94820679..b4ec0a32 100644 --- a/apps/platform/app/Filament/Resources/InventoryItemResource.php +++ b/apps/platform/app/Filament/Resources/InventoryItemResource.php @@ -5,7 +5,7 @@ use App\Filament\Clusters\Inventory\InventoryCluster; use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\InventoryItemResource\Pages; use App\Models\InventoryItem; use App\Models\ManagedEnvironment; @@ -28,8 +28,8 @@ use Closure; use Filament\Facades\Filament; use Filament\Infolists\Components\TextEntry; -use Filament\Panel; use Filament\Infolists\Components\ViewEntry; +use Filament\Panel; use Filament\Resources\Resource; use Filament\Schemas\Components\Section; use Filament\Schemas\Schema; @@ -37,15 +37,15 @@ use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Str; use UnitEnum; class InventoryItemResource extends Resource { use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = InventoryItem::class; diff --git a/apps/platform/app/Filament/Resources/InventoryItemResource/Pages/ListInventoryItems.php b/apps/platform/app/Filament/Resources/InventoryItemResource/Pages/ListInventoryItems.php index 1a6f5c3a..08f6ddd7 100644 --- a/apps/platform/app/Filament/Resources/InventoryItemResource/Pages/ListInventoryItems.php +++ b/apps/platform/app/Filament/Resources/InventoryItemResource/Pages/ListInventoryItems.php @@ -12,7 +12,7 @@ use App\Services\Inventory\InventorySyncService; use App\Services\OperationRunService; use App\Support\Auth\Capabilities; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\Inventory\InventoryPolicyTypeMeta; use App\Support\OperationRunLinks; use App\Support\OperationRunType; @@ -28,8 +28,8 @@ use Filament\Forms\Components\Toggle; use Filament\Notifications\Notification; use Filament\Resources\Pages\ListRecords; -use Filament\Support\Enums\Width; use Filament\Support\Enums\Size; +use Filament\Support\Enums\Width; class ListInventoryItems extends ListRecords { @@ -41,16 +41,16 @@ class ListInventoryItems extends ListRecords public function mount(): void { - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), request: request(), - tenantFilterName: null, + environmentFilterName: null, ); $tenant = static::resolveTenantContextForCurrentPanel(); if ($tenant instanceof ManagedEnvironment) { - app(WorkspaceContext::class)->rememberTenantContext($tenant, request()); + app(WorkspaceContext::class)->rememberEnvironmentContext($tenant, request()); } parent::mount(); diff --git a/apps/platform/app/Filament/Resources/InventoryItemResource/Pages/ViewInventoryItem.php b/apps/platform/app/Filament/Resources/InventoryItemResource/Pages/ViewInventoryItem.php index 68b5b66c..ef5932c8 100644 --- a/apps/platform/app/Filament/Resources/InventoryItemResource/Pages/ViewInventoryItem.php +++ b/apps/platform/app/Filament/Resources/InventoryItemResource/Pages/ViewInventoryItem.php @@ -20,7 +20,7 @@ public function mount(int|string $record): void $tenant = static::resolveTenantContextForCurrentPanel(); if ($tenant instanceof ManagedEnvironment) { - app(WorkspaceContext::class)->rememberTenantContext($tenant, request()); + app(WorkspaceContext::class)->rememberEnvironmentContext($tenant, request()); } parent::mount($record); diff --git a/apps/platform/app/Filament/Resources/OperationRunResource.php b/apps/platform/app/Filament/Resources/OperationRunResource.php index d93a890e..4130421d 100644 --- a/apps/platform/app/Filament/Resources/OperationRunResource.php +++ b/apps/platform/app/Filament/Resources/OperationRunResource.php @@ -191,19 +191,19 @@ public static function table(Table $table): Table ->all(); }) ->default(function (): ?string { - $activeTenant = app(OperateHubShell::class)->activeEntitledTenant(request()); + $activeEnvironment = app(OperateHubShell::class)->activeEntitledTenant(request()); - if (! $activeTenant instanceof ManagedEnvironment) { + if (! $activeEnvironment instanceof ManagedEnvironment) { return null; } $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(); - if ($workspaceId === null || (int) $activeTenant->workspace_id !== (int) $workspaceId) { + if ($workspaceId === null || (int) $activeEnvironment->workspace_id !== (int) $workspaceId) { return null; } - return (string) $activeTenant->getKey(); + return (string) $activeEnvironment->getKey(); }) ->searchable(), Tables\Filters\SelectFilter::make('type') diff --git a/apps/platform/app/Filament/Resources/PolicyResource.php b/apps/platform/app/Filament/Resources/PolicyResource.php index 4ef32adc..88706c8c 100644 --- a/apps/platform/app/Filament/Resources/PolicyResource.php +++ b/apps/platform/app/Filament/Resources/PolicyResource.php @@ -5,7 +5,7 @@ use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; use App\Filament\Concerns\ScopesGlobalSearchToTenant; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\PolicyResource\Pages; use App\Filament\Resources\PolicyResource\RelationManagers\VersionsRelationManager; use App\Filament\Support\NormalizedSettingsSurface; @@ -13,8 +13,8 @@ use App\Jobs\BulkPolicyExportJob; use App\Jobs\BulkPolicyUnignoreJob; use App\Jobs\SyncPoliciesJob; -use App\Models\Policy; use App\Models\ManagedEnvironment; +use App\Models\Policy; use App\Models\User; use App\Services\Auth\CapabilityResolver; use App\Services\Intune\PolicyNormalizer; @@ -41,7 +41,6 @@ use Filament\Actions\ActionGroup; use Filament\Actions\BulkAction; use Filament\Actions\BulkActionGroup; -use Filament\Facades\Filament; use Filament\Forms; use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\ViewEntry; @@ -63,7 +62,7 @@ class PolicyResource extends Resource use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; use ScopesGlobalSearchToTenant; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = Policy::class; diff --git a/apps/platform/app/Filament/Resources/PolicyResource/Pages/ListPolicies.php b/apps/platform/app/Filament/Resources/PolicyResource/Pages/ListPolicies.php index ad9356f7..4afc9052 100644 --- a/apps/platform/app/Filament/Resources/PolicyResource/Pages/ListPolicies.php +++ b/apps/platform/app/Filament/Resources/PolicyResource/Pages/ListPolicies.php @@ -3,7 +3,7 @@ namespace App\Filament\Resources\PolicyResource\Pages; use App\Filament\Resources\PolicyResource; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use Filament\Resources\Pages\ListRecords; class ListPolicies extends ListRecords @@ -12,7 +12,7 @@ class ListPolicies extends ListRecords public function mount(): void { - $this->syncCanonicalAdminTenantFilterState(); + $this->syncCanonicalAdminEnvironmentFilterState(); parent::mount(); } @@ -31,13 +31,13 @@ protected function getTableEmptyStateActions(): array ]; } - private function syncCanonicalAdminTenantFilterState(): void + private function syncCanonicalAdminEnvironmentFilterState(): void { - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), - tenantSensitiveFilters: [], + environmentSensitiveFilters: [], request: request(), - tenantFilterName: null, + environmentFilterName: null, ); } } diff --git a/apps/platform/app/Filament/Resources/PolicyVersionResource.php b/apps/platform/app/Filament/Resources/PolicyVersionResource.php index 373a7a82..954aa4d0 100644 --- a/apps/platform/app/Filament/Resources/PolicyVersionResource.php +++ b/apps/platform/app/Filament/Resources/PolicyVersionResource.php @@ -5,7 +5,7 @@ use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; use App\Filament\Concerns\ScopesGlobalSearchToTenant; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\PolicyVersionResource\Pages; use App\Filament\Support\NormalizedDiffSurface; use App\Filament\Support\NormalizedSettingsSurface; @@ -14,8 +14,8 @@ use App\Jobs\BulkPolicyVersionRestoreJob; use App\Models\BackupItem; use App\Models\BackupSet; -use App\Models\PolicyVersion; use App\Models\ManagedEnvironment; +use App\Models\PolicyVersion; use App\Models\User; use App\Services\Auth\CapabilityResolver; use App\Services\Intune\AuditLogger; @@ -49,7 +49,6 @@ use Filament\Actions; use Filament\Actions\BulkAction; use Filament\Actions\BulkActionGroup; -use Filament\Facades\Filament; use Filament\Forms; use Filament\Infolists; use Filament\Notifications\Notification; @@ -71,7 +70,7 @@ class PolicyVersionResource extends Resource use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; use ScopesGlobalSearchToTenant; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = PolicyVersion::class; diff --git a/apps/platform/app/Filament/Resources/PolicyVersionResource/Pages/ListPolicyVersions.php b/apps/platform/app/Filament/Resources/PolicyVersionResource/Pages/ListPolicyVersions.php index ff84d1cb..b93d0319 100644 --- a/apps/platform/app/Filament/Resources/PolicyVersionResource/Pages/ListPolicyVersions.php +++ b/apps/platform/app/Filament/Resources/PolicyVersionResource/Pages/ListPolicyVersions.php @@ -3,7 +3,7 @@ namespace App\Filament\Resources\PolicyVersionResource\Pages; use App\Filament\Resources\PolicyVersionResource; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use Filament\Resources\Pages\ListRecords; class ListPolicyVersions extends ListRecords @@ -12,10 +12,10 @@ class ListPolicyVersions extends ListRecords public function mount(): void { - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), request: request(), - tenantFilterName: null, + environmentFilterName: null, ); parent::mount(); diff --git a/apps/platform/app/Filament/Resources/ProviderConnectionResource.php b/apps/platform/app/Filament/Resources/ProviderConnectionResource.php index 12d4a9b2..56951f71 100644 --- a/apps/platform/app/Filament/Resources/ProviderConnectionResource.php +++ b/apps/platform/app/Filament/Resources/ProviderConnectionResource.php @@ -203,20 +203,6 @@ public static function resolveRequestedEnvironment(): ?ManagedEnvironment public static function resolveContextTenantExternalId(): ?string { - $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); - $contextTenantId = app(WorkspaceContext::class)->lastTenantId(request()); - - if ($workspaceId !== null && $contextTenantId !== null) { - $tenant = ManagedEnvironment::query() - ->whereKey($contextTenantId) - ->where('workspace_id', (int) $workspaceId) - ->first(); - - if ($tenant instanceof ManagedEnvironment) { - return (string) $tenant->slug; - } - } - $tenant = static::resolveTenantContextForCurrentPanel(); if ($tenant instanceof ManagedEnvironment) { @@ -228,7 +214,7 @@ public static function resolveContextTenantExternalId(): ?string public static function resolveTenantForCreate(): ?ManagedEnvironment { - $tenantExternalId = static::resolveRequestedTenantExternalId() ?? static::resolveContextTenantExternalId(); + $tenantExternalId = static::resolveRequestedTenantExternalId(); if (! is_string($tenantExternalId) || $tenantExternalId === '') { return null; @@ -1676,11 +1662,6 @@ public static function getUrl(?string $name = null, array $parameters = [], bool $tenantExternalId = null; $isIndexUrl = $name === null || $name === 'index'; - if (array_key_exists('tenant', $parameters)) { - $tenantExternalId = static::normalizeTenantExternalId($parameters['tenant']); - unset($parameters['tenant']); - } - if ($tenantExternalId === null && $tenant instanceof ManagedEnvironment) { $tenantExternalId = (string) $tenant->slug; } diff --git a/apps/platform/app/Filament/Resources/RestoreRunResource.php b/apps/platform/app/Filament/Resources/RestoreRunResource.php index f657f887..290bcea3 100644 --- a/apps/platform/app/Filament/Resources/RestoreRunResource.php +++ b/apps/platform/app/Filament/Resources/RestoreRunResource.php @@ -6,7 +6,7 @@ use App\Exceptions\Hardening\ProviderAccessHardeningRequired; use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\RestoreRunResource\Pages; use App\Jobs\BulkRestoreRunDeleteJob; use App\Jobs\BulkRestoreRunForceDeleteJob; @@ -15,9 +15,9 @@ use App\Models\BackupItem; use App\Models\BackupSet; use App\Models\EntraGroup; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\RestoreRun; -use App\Models\ManagedEnvironment; use App\Models\User; use App\Models\Workspace; use App\Rules\SkipOrUuidRule; @@ -32,21 +32,21 @@ use App\Services\Operations\BulkSelectionIdentity; use App\Services\Providers\ProviderOperationStartGate; use App\Services\Providers\ProviderOperationStartResult; +use App\Support\Audit\AuditActionId; use App\Support\Auth\Capabilities; use App\Support\BackupQuality\BackupQualityResolver; -use App\Support\Audit\AuditActionId; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; use App\Support\Filament\FilterOptionCatalog; use App\Support\Filament\FilterPresets; use App\Support\Navigation\NavigationScope; +use App\Support\OperationalControls\OperationalControlBlockedException; +use App\Support\OperationalControls\OperationalControlEvaluator; use App\Support\OperationCatalog; use App\Support\OperationRunLinks; use App\Support\OpsUx\OperationUxPresenter; use App\Support\OpsUx\OpsUxBrowserEvents; use App\Support\OpsUx\ProviderOperationStartResultPresenter; -use App\Support\OperationalControls\OperationalControlBlockedException; -use App\Support\OperationalControls\OperationalControlEvaluator; use App\Support\Rbac\UiEnforcement; use App\Support\RestoreRunIdempotency; use App\Support\RestoreRunStatus; @@ -63,7 +63,6 @@ use Filament\Actions\ActionGroup; use Filament\Actions\BulkAction; use Filament\Actions\BulkActionGroup; -use Filament\Facades\Filament; use Filament\Forms; use Filament\Infolists; use Filament\Notifications\Notification; @@ -80,7 +79,6 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\QueryException; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; @@ -90,7 +88,7 @@ class RestoreRunResource extends Resource { use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = RestoreRun::class; diff --git a/apps/platform/app/Filament/Resources/RestoreRunResource/Pages/ListRestoreRuns.php b/apps/platform/app/Filament/Resources/RestoreRunResource/Pages/ListRestoreRuns.php index c45f8517..42d45f97 100644 --- a/apps/platform/app/Filament/Resources/RestoreRunResource/Pages/ListRestoreRuns.php +++ b/apps/platform/app/Filament/Resources/RestoreRunResource/Pages/ListRestoreRuns.php @@ -3,7 +3,7 @@ namespace App\Filament\Resources\RestoreRunResource\Pages; use App\Filament\Resources\RestoreRunResource; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use Filament\Resources\Pages\ListRecords; use Illuminate\Database\Eloquent\ModelNotFoundException; @@ -30,10 +30,10 @@ public function mountAction(string $name, array $arguments = [], array $context public function mount(): void { - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $this->getTableFiltersSessionKey(), request: request(), - tenantFilterName: null, + environmentFilterName: null, ); parent::mount(); diff --git a/apps/platform/app/Filament/Resources/ReviewPackResource.php b/apps/platform/app/Filament/Resources/ReviewPackResource.php index fe9798d4..e7a38993 100644 --- a/apps/platform/app/Filament/Resources/ReviewPackResource.php +++ b/apps/platform/app/Filament/Resources/ReviewPackResource.php @@ -2,20 +2,19 @@ namespace App\Filament\Resources; -use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; use App\Exceptions\Entitlements\WorkspaceEntitlementBlockedException; use App\Exceptions\ReviewPackEvidenceResolutionException; +use App\Filament\Concerns\ResolvesPanelTenantContext; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\EvidenceSnapshotResource as TenantEvidenceSnapshotResource; use App\Filament\Resources\ReviewPackResource\Pages; -use App\Models\ReviewPack; use App\Models\ManagedEnvironment; +use App\Models\ReviewPack; use App\Models\User; use App\Services\ReviewPackService; use App\Support\Auth\Capabilities; use App\Support\Auth\UiTooltips as AuthUiTooltips; -use App\Support\Badges\BadgeCatalog; use App\Support\Badges\BadgeDomain; use App\Support\Badges\BadgeRenderer; use App\Support\Navigation\NavigationScope; @@ -28,13 +27,12 @@ use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot; use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType; -use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome; use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope; use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter; +use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome; use App\Support\Ui\GovernanceArtifactTruth\SurfaceCompressionContext; use BackedEnum; use Filament\Actions; -use Filament\Facades\Filament; use Filament\Forms\Components\Toggle; use Filament\Infolists\Components\TextEntry; use Filament\Infolists\Components\ViewEntry; @@ -52,7 +50,7 @@ class ReviewPackResource extends Resource { use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; protected static ?string $model = ReviewPack::class; @@ -204,14 +202,14 @@ public static function infolist(Schema $schema): Schema ->label('ManagedEnvironment review') ->formatStateUsing(fn (?int $state): string => $state ? '#'.$state : '—') ->url(fn (ReviewPack $record): ?string => $record->environmentReview && $record->tenant - ? EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $record->environmentReview], $record->tenant) + ? EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $record->environmentReview], $record->tenant) : null) ->placeholder('—'), TextEntry::make('customer_workspace') ->label('Customer workspace') ->state(fn (): string => 'Open workspace') ->url(fn (ReviewPack $record): ?string => $record->tenant instanceof ManagedEnvironment - ? CustomerReviewWorkspace::tenantPrefilterUrl($record->tenant) + ? CustomerReviewWorkspace::environmentFilterUrl($record->tenant) : null) ->placeholder('—'), TextEntry::make('summary.review_status') diff --git a/apps/platform/app/Filament/Resources/StoredReportResource.php b/apps/platform/app/Filament/Resources/StoredReportResource.php index 0a12966a..965ac3cd 100644 --- a/apps/platform/app/Filament/Resources/StoredReportResource.php +++ b/apps/platform/app/Filament/Resources/StoredReportResource.php @@ -6,10 +6,10 @@ use App\Filament\Concerns\InteractsWithTenantOwnedRecords; use App\Filament\Concerns\ResolvesPanelTenantContext; -use App\Filament\Concerns\WorkspaceScopedTenantRoutes; +use App\Filament\Concerns\WorkspaceScopedEnvironmentRoutes; use App\Filament\Resources\StoredReportResource\Pages; -use App\Models\StoredReport; use App\Models\ManagedEnvironment; +use App\Models\StoredReport; use App\Models\User; use App\Support\Auth\Capabilities; use App\Support\Badges\BadgeDomain; @@ -45,7 +45,7 @@ class StoredReportResource extends Resource { use InteractsWithTenantOwnedRecords; use ResolvesPanelTenantContext; - use WorkspaceScopedTenantRoutes; + use WorkspaceScopedEnvironmentRoutes; /** * @var array diff --git a/apps/platform/app/Filament/Widgets/ManagedEnvironment/ManagedEnvironmentReviewPackCard.php b/apps/platform/app/Filament/Widgets/ManagedEnvironment/ManagedEnvironmentReviewPackCard.php index b7705d89..da79f891 100644 --- a/apps/platform/app/Filament/Widgets/ManagedEnvironment/ManagedEnvironmentReviewPackCard.php +++ b/apps/platform/app/Filament/Widgets/ManagedEnvironment/ManagedEnvironmentReviewPackCard.php @@ -6,9 +6,9 @@ use App\Exceptions\Entitlements\WorkspaceEntitlementBlockedException; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\ReviewPack; -use App\Models\ManagedEnvironment; use App\Models\User; use App\Services\ReviewPackService; use App\Support\Auth\Capabilities; @@ -193,7 +193,7 @@ protected function getViewData(): array 'generationBlocked' => $generationBlocked, 'generationBlockReason' => $generationBlockReason, 'generationWarningReason' => $generationWarningReason, - 'customerWorkspaceUrl' => $canView ? CustomerReviewWorkspace::tenantPrefilterUrl($tenant) : null, + 'customerWorkspaceUrl' => $canView ? CustomerReviewWorkspace::environmentFilterUrl($tenant) : null, 'downloadUrl' => null, 'failedReason' => null, 'reviewUrl' => null, @@ -211,7 +211,7 @@ protected function getViewData(): array $reviewUrl = null; if ($latestPack->environmentReview && $canView) { - $reviewUrl = \App\Filament\Resources\EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $latestPack->environmentReview], $tenant); + $reviewUrl = \App\Filament\Resources\EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $latestPack->environmentReview], $tenant); } $failedReason = null; @@ -245,7 +245,7 @@ protected function getViewData(): array 'generationBlocked' => $generationBlocked, 'generationBlockReason' => $generationBlockReason, 'generationWarningReason' => $generationWarningReason, - 'customerWorkspaceUrl' => $canView ? CustomerReviewWorkspace::tenantPrefilterUrl($tenant) : null, + 'customerWorkspaceUrl' => $canView ? CustomerReviewWorkspace::environmentFilterUrl($tenant) : null, 'downloadUrl' => $downloadUrl, 'failedReason' => $failedReason, 'failedReasonDetail' => $failedReasonDetail, diff --git a/apps/platform/app/Filament/Widgets/Operations/OperationsKpiHeader.php b/apps/platform/app/Filament/Widgets/Operations/OperationsKpiHeader.php index c5cfc5b3..807379df 100644 --- a/apps/platform/app/Filament/Widgets/Operations/OperationsKpiHeader.php +++ b/apps/platform/app/Filament/Widgets/Operations/OperationsKpiHeader.php @@ -4,8 +4,8 @@ namespace App\Filament\Widgets\Operations; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Models\User; use App\Services\Auth\ManagedEnvironmentAccessScopeResolver; use App\Support\OperateHub\OperateHubShell; @@ -27,7 +27,7 @@ class OperationsKpiHeader extends StatsOverviewWidget protected function getPollingInterval(): ?string { - $tenant = $this->activeTenant(); + $tenant = $this->activeEnvironment(); if ($tenant instanceof ManagedEnvironment) { return ActiveRuns::existForTenant($tenant) ? '10s' : null; @@ -119,7 +119,7 @@ protected function getStats(): array ]; } - private function activeTenant(): ?ManagedEnvironment + private function activeEnvironment(): ?ManagedEnvironment { $tenant = app(OperateHubShell::class)->activeEntitledTenant(request()); @@ -128,7 +128,7 @@ private function activeTenant(): ?ManagedEnvironment private function scopedOperationRunQuery(): ?Builder { - $tenant = $this->activeTenant(); + $tenant = $this->activeEnvironment(); if ($tenant instanceof ManagedEnvironment) { return OperationRun::query() diff --git a/apps/platform/app/Http/Controllers/ClearEnvironmentContextController.php b/apps/platform/app/Http/Controllers/ClearEnvironmentContextController.php index 1175ddb7..42ab4bbf 100644 --- a/apps/platform/app/Http/Controllers/ClearEnvironmentContextController.php +++ b/apps/platform/app/Http/Controllers/ClearEnvironmentContextController.php @@ -4,8 +4,8 @@ namespace App\Http\Controllers; +use App\Support\Navigation\AdminSurfaceScope; use App\Support\OperationRunLinks; -use App\Support\Tenants\TenantPageCategory; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; use Illuminate\Http\RedirectResponse; @@ -19,7 +19,7 @@ public function __invoke(Request $request): RedirectResponse $workspaceContext = app(WorkspaceContext::class); - $workspaceContext->clearRememberedTenantContext($request); + $workspaceContext->clearRememberedEnvironmentContext($request); $previousUrl = url()->previous(); @@ -30,11 +30,11 @@ public function __invoke(Request $request): RedirectResponse return redirect()->to(OperationRunLinks::index()); } - if ($this->isTenantScopedEvidencePath($previousPath)) { + if ($this->isEnvironmentScopedEvidencePath($previousPath)) { return redirect()->route('admin.evidence.overview'); } - if (TenantPageCategory::fromPath($previousPath) === TenantPageCategory::TenantBound) { + if (AdminSurfaceScope::fromPath($previousPath) === AdminSurfaceScope::EnvironmentBound) { $workspace = $workspaceContext->currentWorkspace($request); if ($workspace !== null) { @@ -51,7 +51,7 @@ public function __invoke(Request $request): RedirectResponse return redirect()->to((string) $previousUrl); } - private function isTenantScopedEvidencePath(string $previousPath): bool + private function isEnvironmentScopedEvidencePath(string $previousPath): bool { if ($previousPath === '/admin/evidence') { return true; diff --git a/apps/platform/app/Http/Controllers/OpenFindingExceptionsQueueController.php b/apps/platform/app/Http/Controllers/OpenFindingExceptionsQueueController.php index 5f3d9b9b..3ce69e9e 100644 --- a/apps/platform/app/Http/Controllers/OpenFindingExceptionsQueueController.php +++ b/apps/platform/app/Http/Controllers/OpenFindingExceptionsQueueController.php @@ -49,7 +49,7 @@ public function __invoke(Request $request, ManagedEnvironment $environment): Red $workspaceContext->setCurrentWorkspace($workspace, $user, $request); - if (! $workspaceContext->rememberTenantContext($environment, $request)) { + if (! $workspaceContext->rememberEnvironmentContext($environment, $request)) { abort(404); } diff --git a/apps/platform/app/Http/Controllers/SelectEnvironmentController.php b/apps/platform/app/Http/Controllers/SelectEnvironmentController.php index be83360a..6b68b30b 100644 --- a/apps/platform/app/Http/Controllers/SelectEnvironmentController.php +++ b/apps/platform/app/Http/Controllers/SelectEnvironmentController.php @@ -8,9 +8,9 @@ use App\Models\User; use App\Models\UserTenantPreference; use App\Services\Tenants\TenantOperabilityService; +use App\Support\ManagedEnvironmentLinks; use App\Support\Tenants\TenantInteractionLane; use App\Support\Tenants\TenantOperabilityQuestion; -use App\Support\ManagedEnvironmentLinks; use App\Support\Workspaces\WorkspaceContext; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -63,7 +63,7 @@ public function __invoke(Request $request): RedirectResponse $this->persistLastTenant($user, $tenant); - if (! app(WorkspaceContext::class)->rememberTenantContext($tenant, $request)) { + if (! app(WorkspaceContext::class)->rememberEnvironmentContext($tenant, $request)) { abort(404); } diff --git a/apps/platform/app/Http/Controllers/SwitchWorkspaceController.php b/apps/platform/app/Http/Controllers/SwitchWorkspaceController.php index f13df14f..004097f7 100644 --- a/apps/platform/app/Http/Controllers/SwitchWorkspaceController.php +++ b/apps/platform/app/Http/Controllers/SwitchWorkspaceController.php @@ -48,7 +48,7 @@ public function __invoke(Request $request): RedirectResponse $prevWorkspaceId = $context->currentWorkspaceId($request); $context->setCurrentWorkspace($workspace, $user, $request); - $context->rememberedTenant($request); + $context->rememberedEnvironment($request); Filament::setTenant(null, true); /** @var WorkspaceAuditLogger $auditLogger */ diff --git a/apps/platform/app/Livewire/InventoryItemDependencyEdgesTable.php b/apps/platform/app/Livewire/InventoryItemDependencyEdgesTable.php index 8d1455ff..54792656 100644 --- a/apps/platform/app/Livewire/InventoryItemDependencyEdgesTable.php +++ b/apps/platform/app/Livewire/InventoryItemDependencyEdgesTable.php @@ -250,7 +250,7 @@ private function resolveCurrentTenant(): ManagedEnvironment $tenant = Filament::getTenant(); if (! $tenant instanceof ManagedEnvironment) { - $tenant = app(WorkspaceContext::class)->rememberedTenant(request()); + $tenant = app(WorkspaceContext::class)->rememberedEnvironment(request()); } if (! $tenant instanceof ManagedEnvironment) { diff --git a/apps/platform/app/Models/User.php b/apps/platform/app/Models/User.php index 38c82610..a37d47dc 100644 --- a/apps/platform/app/Models/User.php +++ b/apps/platform/app/Models/User.php @@ -11,8 +11,8 @@ use Filament\Models\Contracts\HasDefaultTenant; use Filament\Models\Contracts\HasTenants; use Filament\Panel; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -194,10 +194,10 @@ public function getDefaultTenant(Panel $panel): ?Model $operability = app(TenantOperabilityService::class); - $rememberedTenant = $workspaceContext->rememberedTenant(request()); + $rememberedEnvironment = $workspaceContext->rememberedEnvironment(request()); - if ($rememberedTenant instanceof ManagedEnvironment && $this->canAccessTenant($rememberedTenant)) { - return $rememberedTenant; + if ($rememberedEnvironment instanceof ManagedEnvironment && $this->canAccessTenant($rememberedEnvironment)) { + return $rememberedEnvironment; } $tenantId = null; diff --git a/apps/platform/app/Policies/FindingExceptionPolicy.php b/apps/platform/app/Policies/FindingExceptionPolicy.php index 9ab87474..b8fbc2b0 100644 --- a/apps/platform/app/Policies/FindingExceptionPolicy.php +++ b/apps/platform/app/Policies/FindingExceptionPolicy.php @@ -120,7 +120,7 @@ private function resolvedTenant(): ?ManagedEnvironment return null; } - $tenantId = app(WorkspaceContext::class)->lastTenantId(request()); + $tenantId = app(WorkspaceContext::class)->lastEnvironmentId(request()); if (! is_int($tenantId)) { return null; diff --git a/apps/platform/app/Policies/ManagedEnvironmentOnboardingSessionPolicy.php b/apps/platform/app/Policies/ManagedEnvironmentOnboardingSessionPolicy.php index a630756b..dd97861b 100644 --- a/apps/platform/app/Policies/ManagedEnvironmentOnboardingSessionPolicy.php +++ b/apps/platform/app/Policies/ManagedEnvironmentOnboardingSessionPolicy.php @@ -109,7 +109,7 @@ private function authorizeForDraft( $viewability = app(TenantOperabilityService::class)->outcomeFor( tenant: $tenant, - question: TenantOperabilityQuestion::TenantBoundViewability, + question: TenantOperabilityQuestion::EnvironmentBoundViewability, actor: $user, workspaceId: (int) $workspace->getKey(), lane: TenantInteractionLane::AdministrativeManagement, diff --git a/apps/platform/app/Policies/ProviderConnectionPolicy.php b/apps/platform/app/Policies/ProviderConnectionPolicy.php index 65d2c647..d06a4c8b 100644 --- a/apps/platform/app/Policies/ProviderConnectionPolicy.php +++ b/apps/platform/app/Policies/ProviderConnectionPolicy.php @@ -240,11 +240,11 @@ private function resolveCreateTenant(Workspace $workspace): ?ManagedEnvironment $requestedEnvironmentId = request()->query('environment_id'); if (! is_numeric($requestedEnvironmentId)) { - $lastTenantId = app(WorkspaceContext::class)->lastTenantId(request()); + $lastEnvironmentId = app(WorkspaceContext::class)->lastEnvironmentId(request()); - if (is_int($lastTenantId)) { + if (is_int($lastEnvironmentId)) { return ManagedEnvironment::query() - ->whereKey($lastTenantId) + ->whereKey($lastEnvironmentId) ->where('workspace_id', (int) $workspace->getKey()) ->first(); } diff --git a/apps/platform/app/Providers/Filament/AdminPanelProvider.php b/apps/platform/app/Providers/Filament/AdminPanelProvider.php index 8d4afee4..7b8b4fcd 100644 --- a/apps/platform/app/Providers/Filament/AdminPanelProvider.php +++ b/apps/platform/app/Providers/Filament/AdminPanelProvider.php @@ -2,35 +2,35 @@ namespace App\Providers\Filament; +use App\Filament\Clusters\Inventory\InventoryCluster; use App\Filament\Pages\Auth\Login; use App\Filament\Pages\BaselineCompareLanding; use App\Filament\Pages\ChooseEnvironment; use App\Filament\Pages\ChooseWorkspace; use App\Filament\Pages\CrossEnvironmentComparePage; +use App\Filament\Pages\EnvironmentRequiredPermissions; use App\Filament\Pages\Findings\FindingsHygieneReport; use App\Filament\Pages\Findings\FindingsIntakeQueue; +use App\Filament\Pages\Findings\MyFindingsInbox; use App\Filament\Pages\Governance\DecisionRegister; use App\Filament\Pages\Governance\GovernanceInbox; -use App\Filament\Pages\Findings\MyFindingsInbox; use App\Filament\Pages\InventoryCoverage; use App\Filament\Pages\Monitoring\FindingExceptionsQueue; use App\Filament\Pages\NoAccess; -use App\Filament\Pages\Reviews\ReviewRegister; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; +use App\Filament\Pages\Reviews\ReviewRegister; use App\Filament\Pages\Settings\WorkspaceSettings; -use App\Filament\Pages\EnvironmentRequiredPermissions; use App\Filament\Pages\WorkspaceOverview; -use App\Filament\Clusters\Inventory\InventoryCluster; use App\Filament\Resources\AlertDeliveryResource; use App\Filament\Resources\AlertDestinationResource; use App\Filament\Resources\AlertRuleResource; use App\Filament\Resources\BaselineProfileResource; use App\Filament\Resources\BaselineSnapshotResource; use App\Filament\Resources\EntraGroupResource; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\InventoryItemResource; use App\Filament\Resources\PolicyResource; use App\Filament\Resources\ProviderConnectionResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\Workspaces\WorkspaceResource; use App\Models\User; use App\Models\Workspace; @@ -39,14 +39,14 @@ use App\Services\Auth\WorkspaceRoleCapabilityMap; use App\Support\Auth\Capabilities; use App\Support\Filament\PanelThemeAsset; +use App\Support\Navigation\AdminSurfaceScope; use App\Support\Navigation\NavigationScope; use App\Support\Navigation\WorkspaceHubRegistry; use App\Support\OperationRunLinks; -use App\Support\Tenants\TenantPageCategory; use App\Support\Workspaces\WorkspaceContext; +use Filament\FontProviders\LocalFontProvider; use Filament\Http\Middleware\Authenticate; use Filament\Http\Middleware\AuthenticateSession; -use Filament\FontProviders\LocalFontProvider; use Filament\Http\Middleware\DisableBladeIconComponents; use Filament\Http\Middleware\DispatchServingFilamentEvent; use Filament\Navigation\NavigationItem; @@ -189,7 +189,7 @@ public function panel(Panel $panel): Panel ) ->renderHook( PanelsRenderHook::PAGE_START, - fn (): string => TenantPageCategory::fromRequest(request()) === TenantPageCategory::OnboardingWorkflow + fn (): string => AdminSurfaceScope::fromRequest(request()) === AdminSurfaceScope::OnboardingWorkflow || request()->routeIs('admin.workspace.managed-environments.index', 'filament.admin.pages.choose-environment') ? '' : ((bool) config('tenantpilot.bulk_operations.progress_widget_enabled', true) @@ -242,7 +242,7 @@ public function panel(Panel $panel): Panel SubstituteBindings::class, 'ensure-correct-guard:web', 'ensure-workspace-selected', - 'ensure-filament-tenant-selected', + 'ensure-environment-context-selected', DisableBladeIconComponents::class, DispatchServingFilamentEvent::class, ]) diff --git a/apps/platform/app/Services/Tenants/TenantOperabilityService.php b/apps/platform/app/Services/Tenants/TenantOperabilityService.php index 560a9557..f72b5c91 100644 --- a/apps/platform/app/Services/Tenants/TenantOperabilityService.php +++ b/apps/platform/app/Services/Tenants/TenantOperabilityService.php @@ -106,7 +106,7 @@ public function evaluate(TenantOperabilityContext $context, TenantOperabilityQue lifecycle: $lifecycle, lane: $context->lane, reasonCode: TenantOperabilityReasonCode::MissingCapability, - discoverable: $question === TenantOperabilityQuestion::AdministrativeDiscoverability || $question === TenantOperabilityQuestion::TenantBoundViewability, + discoverable: $question === TenantOperabilityQuestion::AdministrativeDiscoverability || $question === TenantOperabilityQuestion::EnvironmentBoundViewability, requiredCapability: $context->requiredCapability, metadata: $this->metadata($context), ); @@ -169,7 +169,7 @@ public function evaluate(TenantOperabilityContext $context, TenantOperabilityQue return match ($question) { TenantOperabilityQuestion::SelectorEligibility => $this->selectorEligibilityOutcome($context, $lifecycle), TenantOperabilityQuestion::RememberedContextValidity => $this->rememberedContextOutcome($context, $lifecycle), - TenantOperabilityQuestion::TenantBoundViewability => $this->tenantBoundViewabilityOutcome($context, $lifecycle), + TenantOperabilityQuestion::EnvironmentBoundViewability => $this->tenantBoundViewabilityOutcome($context, $lifecycle), TenantOperabilityQuestion::CanonicalLinkedRecordViewability => $this->canonicalViewabilityOutcome($context, $lifecycle), TenantOperabilityQuestion::ArchiveEligibility => $this->archiveEligibilityOutcome($context, $lifecycle), TenantOperabilityQuestion::RestoreEligibility => $this->restoreEligibilityOutcome($context, $lifecycle), @@ -375,11 +375,11 @@ private function rememberedContextOutcome(TenantOperabilityContext $context, Ten private function tenantBoundViewabilityOutcome(TenantOperabilityContext $context, TenantLifecycle $lifecycle): TenantOperabilityOutcome { if ($context->lane !== TenantInteractionLane::AdministrativeManagement) { - return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::TenantBoundViewability, $lifecycle); + return $this->wrongLaneOutcome($context, TenantOperabilityQuestion::EnvironmentBoundViewability, $lifecycle); } return TenantOperabilityOutcome::allow( - question: TenantOperabilityQuestion::TenantBoundViewability, + question: TenantOperabilityQuestion::EnvironmentBoundViewability, lifecycle: $lifecycle, lane: $context->lane, discoverable: true, diff --git a/apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php b/apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php index d57b562b..00c1c78d 100644 --- a/apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php +++ b/apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php @@ -181,7 +181,7 @@ public function removeTenantFromWorkspace(ManagedEnvironment $tenant, User $acto 'is_current' => false, ])->save(); - app(WorkspaceContext::class)->clearRememberedTenantContext(); + app(WorkspaceContext::class)->clearRememberedEnvironmentContext(); $this->auditLogger->logTenantLifecycleAction( tenant: $tenant, diff --git a/apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php b/apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php index a4de79bf..76b86002 100644 --- a/apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php +++ b/apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php @@ -6,18 +6,18 @@ use App\Models\Finding; use App\Models\FindingException; +use App\Models\ManagedEnvironment; +use App\Models\ManagedEnvironmentOnboardingSession; use App\Models\OperationRun; use App\Models\PlatformUser; use App\Models\ProductUsageEvent; use App\Models\ProviderConnection; use App\Models\ReviewPack; -use App\Models\ManagedEnvironment; -use App\Models\ManagedEnvironmentOnboardingSession; use App\Models\Workspace; +use App\Support\Auth\PlatformCapabilities; use App\Support\Onboarding\OnboardingLifecycleState; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; -use App\Support\Auth\PlatformCapabilities; use App\Support\ProductTelemetry\ProductUsageEventCatalog; use App\Support\Providers\ProviderConsentStatus; use App\Support\Providers\ProviderVerificationStatus; @@ -68,7 +68,7 @@ public function summaries(SystemConsoleWindow|string|null $window = null, ?Carbo ->map(static fn (mixed $workspaceId): int => (int) $workspaceId) ->all(); - $activeTenants = ManagedEnvironment::query() + $activeEnvironments = ManagedEnvironment::query() ->whereIn('workspace_id', $workspaceIds) ->whereNull('deleted_at') ->where('lifecycle_status', '!=', ManagedEnvironment::STATUS_ARCHIVED) @@ -76,7 +76,7 @@ public function summaries(SystemConsoleWindow|string|null $window = null, ?Carbo ->orderBy('id') ->get(['id', 'workspace_id', 'slug', 'name', 'lifecycle_status']); - $tenantsByWorkspace = $activeTenants->groupBy(static fn (ManagedEnvironment $tenant): int => (int) $tenant->workspace_id); + $tenantsByWorkspace = $activeEnvironments->groupBy(static fn (ManagedEnvironment $tenant): int => (int) $tenant->workspace_id); $latestOnboardingSessions = ManagedEnvironmentOnboardingSession::query() ->whereIn('workspace_id', $workspaceIds) diff --git a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php index 555aceb7..343e538e 100644 --- a/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php +++ b/apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php @@ -8,36 +8,35 @@ use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\BackupScheduleResource; use App\Filament\Resources\BackupSetResource; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\FindingExceptionResource; use App\Filament\Resources\FindingResource; use App\Filament\Resources\ReviewPackResource; -use App\Filament\Resources\EnvironmentReviewResource; +use App\Models\EnvironmentReview; use App\Models\EvidenceSnapshot; use App\Models\Finding; use App\Models\FindingException; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\ProviderConnection; use App\Models\ReviewPack; -use App\Models\ManagedEnvironment; -use App\Models\EnvironmentReview; use App\Models\User; use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder; use App\Support\Auth\Capabilities; -use App\Support\Badges\BadgeDomain; -use App\Support\Badges\BadgeRenderer; use App\Support\BackupHealth\BackupHealthActionTarget; use App\Support\BackupHealth\TenantBackupHealthAssessment; use App\Support\BackupHealth\TenantBackupHealthResolver; +use App\Support\Badges\BadgeDomain; +use App\Support\Badges\BadgeRenderer; use App\Support\Baselines\TenantGovernanceAggregate; use App\Support\Baselines\TenantGovernanceAggregateResolver; use App\Support\Links\RequiredPermissionsLinks; use App\Support\OperationCatalog; -use App\Support\OperationRunOutcome; use App\Support\OperationRunLinks; +use App\Support\OperationRunOutcome; use App\Support\OpsUx\ActiveRuns; use App\Support\OpsUx\OperationUxPresenter; -use App\Support\RestoreSafety\RestoreResultAttention; use App\Support\RestoreSafety\RestoreSafetyResolver; use App\Support\Tenants\TenantRecoveryTriagePresentation; use App\Support\Verification\VerificationReportOverall; @@ -1084,8 +1083,7 @@ private function metricCard( string $icon, array $action, ?array $chart = null, - ): array - { + ): array { return array_merge([ 'key' => $key, 'label' => $label, @@ -1165,8 +1163,8 @@ private function environmentReviewAction(ManagedEnvironment $tenant, ?User $user if ($canOpen) { $url = $review instanceof EnvironmentReview - ? EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant) - : EnvironmentReviewResource::tenantScopedUrl('index', tenant: $tenant); + ? EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant) + : EnvironmentReviewResource::environmentScopedUrl('index', tenant: $tenant); } return $this->actionPayload( @@ -1185,7 +1183,7 @@ private function continueReviewAction(ManagedEnvironment $tenant, ?User $user, E return $this->actionPayload( label: $this->overviewText('action_continue_review'), - url: $canContinue ? EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant) : null, + url: $canContinue ? EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant) : null, helperText: $canContinue ? null : $this->overviewText('helper_continue_review_requires_manage'), ); } @@ -1234,7 +1232,7 @@ private function customerWorkspaceAction(ManagedEnvironment $tenant, ?User $user if ($canOpenWorkspace) { $url = $reviewPack instanceof ReviewPack && $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant) ? ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $tenant) - : CustomerReviewWorkspace::tenantPrefilterUrl($tenant); + : CustomerReviewWorkspace::environmentFilterUrl($tenant); } return $this->actionPayload( @@ -1986,4 +1984,4 @@ private function overviewText(string $key, array $replace = []): string { return (string) __('localization.dashboard.overview.'.$key, $replace); } -} \ No newline at end of file +} diff --git a/apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php b/apps/platform/app/Support/Filament/CanonicalAdminEnvironmentFilterState.php similarity index 70% rename from apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php rename to apps/platform/app/Support/Filament/CanonicalAdminEnvironmentFilterState.php index 513b03d2..87626d7d 100644 --- a/apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php +++ b/apps/platform/app/Support/Filament/CanonicalAdminEnvironmentFilterState.php @@ -12,9 +12,9 @@ use Illuminate\Session\Store; use Illuminate\Support\Arr; -final class CanonicalAdminTenantFilterState +final class CanonicalAdminEnvironmentFilterState { - private const STATE_PREFIX = 'filament.admin_tenant_filter_state'; + private const STATE_PREFIX = 'filament.admin_environment_filter_state'; public function __construct( private readonly OperateHubShell $operateHubShell, @@ -25,20 +25,20 @@ public function currentFilterValue( string $filtersSessionKey, ?array $tableFilters = null, ?Request $request = null, - ?string $tenantFilterName = 'managed_environment_id', + ?string $environmentFilterName = 'managed_environment_id', ): ?string { - if ($tenantFilterName === null) { + if ($environmentFilterName === null) { return null; } - $tableFilterValue = data_get($tableFilters ?? [], "{$tenantFilterName}.value"); + $tableFilterValue = data_get($tableFilters ?? [], "{$environmentFilterName}.value"); if (is_scalar($tableFilterValue) && (string) $tableFilterValue !== '') { return (string) $tableFilterValue; } $persistedFilters = $this->session($request)->get($filtersSessionKey, []); - $persistedValue = data_get(is_array($persistedFilters) ? $persistedFilters : [], "{$tenantFilterName}.value"); + $persistedValue = data_get(is_array($persistedFilters) ? $persistedFilters : [], "{$environmentFilterName}.value"); if (! is_scalar($persistedValue) || (string) $persistedValue === '') { return null; @@ -48,14 +48,14 @@ public function currentFilterValue( } /** - * @param array $tenantSensitiveFilters + * @param array $environmentSensitiveFilters */ public function sync( string $filtersSessionKey, - array $tenantSensitiveFilters = [], + array $environmentSensitiveFilters = [], ?Request $request = null, - ?string $tenantFilterName = 'managed_environment_id', - string $tenantAttribute = 'id', + ?string $environmentFilterName = 'managed_environment_id', + string $environmentAttribute = 'id', ): void { $session = $this->session($request); @@ -73,27 +73,27 @@ public function sync( ); } - $activeTenant = $this->operateHubShell->activeEntitledTenant($request); - $resolvedTenantId = $activeTenant instanceof ManagedEnvironment ? (string) $activeTenant->getKey() : null; + $activeEnvironment = $this->operateHubShell->activeEntitledTenant($request); + $resolvedEnvironmentId = $activeEnvironment instanceof ManagedEnvironment ? (string) $activeEnvironment->getKey() : null; $stateKey = $this->stateKey($filtersSessionKey); - $previousTenantId = $session->get($stateKey); + $previousEnvironmentId = $session->get($stateKey); - if ($previousTenantId !== $resolvedTenantId) { - foreach ($tenantSensitiveFilters as $filterName) { + if ($previousEnvironmentId !== $resolvedEnvironmentId) { + foreach ($environmentSensitiveFilters as $filterName) { Arr::forget($persistedFilters, $filterName); } } - if ($tenantFilterName !== null) { - $tenantFilterValue = match ($tenantAttribute) { - 'external_id' => $activeTenant?->external_id, - default => $resolvedTenantId, + if ($environmentFilterName !== null) { + $environmentFilterValue = match ($environmentAttribute) { + 'external_id' => $activeEnvironment?->external_id, + default => $resolvedEnvironmentId, }; - if ($tenantFilterValue !== null) { - data_set($persistedFilters, "{$tenantFilterName}.value", $tenantFilterValue); + if ($environmentFilterValue !== null) { + data_set($persistedFilters, "{$environmentFilterName}.value", $environmentFilterValue); } else { - Arr::forget($persistedFilters, $tenantFilterName); + Arr::forget($persistedFilters, $environmentFilterName); } } @@ -103,7 +103,7 @@ public function sync( $session->put($filtersSessionKey, $persistedFilters); } - $session->put($stateKey, $resolvedTenantId); + $session->put($stateKey, $resolvedEnvironmentId); } /** diff --git a/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php b/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php index 4b8867e7..f413c818 100644 --- a/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php +++ b/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php @@ -9,24 +9,23 @@ use App\Filament\Pages\Monitoring\FindingExceptionsQueue; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\AlertDeliveryResource; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\FindingExceptionResource; use App\Filament\Resources\FindingResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\AlertDelivery; use App\Models\Finding; use App\Models\FindingException; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; use App\Models\ManagedEnvironmentTriageReview; +use App\Models\OperationRun; use App\Models\User; use App\Models\Workspace; use App\Services\EnvironmentReviews\EnvironmentReviewRegisterService; -use App\Support\Auth\Capabilities; use App\Support\BackupHealth\TenantBackupHealthResolver; use App\Support\Navigation\CanonicalNavigationContext; use App\Support\OperationRunLinks; -use App\Support\PortfolioTriage\PortfolioArrivalContextToken; use App\Support\PortfolioTriage\ManagedEnvironmentTriageReviewStateResolver; +use App\Support\PortfolioTriage\PortfolioArrivalContextToken; use App\Support\RestoreSafety\RestoreSafetyResolver; use Illuminate\Support\Str; @@ -550,7 +549,7 @@ private function reviewFollowUpSection( 'summary' => $this->reviewSummary($followUpCount, $changedCount), 'dominant_action_label' => 'Open customer review workspace', 'dominant_action_url' => $selectedTenant instanceof ManagedEnvironment - ? $this->appendQuery(CustomerReviewWorkspace::tenantPrefilterUrl($selectedTenant), $navigationContext?->toQuery() ?? []) + ? $this->appendQuery(CustomerReviewWorkspace::environmentFilterUrl($selectedTenant), $navigationContext?->toQuery() ?? []) : $this->appendQuery(CustomerReviewWorkspace::getUrl(panel: 'admin'), $navigationContext?->toQuery() ?? []), 'entries' => array_slice($rawEntries, 0, self::PREVIEW_LIMIT), 'empty_state' => $selectedTenant instanceof ManagedEnvironment @@ -609,12 +608,12 @@ private function orderedIntakeFindingsQuery(\Illuminate\Database\Eloquent\Builde { return $query ->orderByRaw( - "case + 'case when due_at is not null and due_at < ? then 0 when status = ? then 1 when status = ? then 2 else 3 - end asc", + end asc', [now(), Finding::STATUS_REOPENED, Finding::STATUS_NEW], ) ->orderByRaw('case when due_at is null then 1 else 0 end asc') @@ -724,13 +723,13 @@ private function orderedFindingExceptionsQuery(\Illuminate\Database\Eloquent\Bui { return $query ->orderByRaw( - "case + 'case when status = ? then 0 when current_validity_state = ? then 1 when current_validity_state = ? then 2 when current_validity_state = ? then 3 else 4 - end asc", + end asc', [ FindingException::STATUS_PENDING, FindingException::VALIDITY_EXPIRED, @@ -909,8 +908,8 @@ private function reviewEntry( : null, ])); $destinationUrl = $latestPublishedReview !== null - ? EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $latestPublishedReview], $tenant) - : CustomerReviewWorkspace::tenantPrefilterUrl($tenant); + ? EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $latestPublishedReview], $tenant) + : CustomerReviewWorkspace::environmentFilterUrl($tenant); return [ 'family_key' => 'review_follow_up', diff --git a/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php b/apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php similarity index 94% rename from apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php rename to apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php index f2b37ea4..cfb2f05b 100644 --- a/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php +++ b/apps/platform/app/Support/Middleware/EnsureEnvironmentContextSelected.php @@ -4,11 +4,11 @@ use App\Models\ManagedEnvironment; use App\Models\User; +use App\Support\Navigation\AdminSurfaceScope; use App\Support\Navigation\NavigationScope; use App\Support\Navigation\WorkspaceHubRegistry; use App\Support\Navigation\WorkspaceSidebarNavigation; use App\Support\OperateHub\OperateHubShell; -use App\Support\Tenants\TenantPageCategory; use App\Support\Workspaces\WorkspaceContext; use Closure; use Filament\Facades\Filament; @@ -16,7 +16,7 @@ use Illuminate\Http\Request; use Symfony\Component\HttpFoundation\Response; -class EnsureFilamentTenantSelected +class EnsureEnvironmentContextSelected { /** * @param Closure(Request): Response $next @@ -100,7 +100,7 @@ public function handle(Request $request, Closure $next): Response return redirect()->route('filament.admin.pages.choose-environment'); } - if ($resolvedContext->pageCategory === TenantPageCategory::TenantBound && ! $resolvedContext->hasTenant()) { + if ($resolvedContext->pageCategory === AdminSurfaceScope::EnvironmentBound && ! $resolvedContext->hasTenant()) { abort(404); } @@ -116,7 +116,7 @@ public function handle(Request $request, Closure $next): Response $resolvedContext->hasTenant() && ( ! $this->isWorkspaceScopedPageWithTenant($path) - && $resolvedContext->pageCategory === TenantPageCategory::TenantBound + && $resolvedContext->pageCategory === AdminSurfaceScope::EnvironmentBound ) ) { Filament::setTenant($resolvedContext->tenant, true); @@ -175,7 +175,7 @@ private function isLivewireUpdatePath(string $path): bool private function isCanonicalWorkspaceRecordViewerPath(string $path): bool { - return TenantPageCategory::fromPath($path) === TenantPageCategory::CanonicalWorkspaceRecordViewer; + return AdminSurfaceScope::fromPath($path) === AdminSurfaceScope::CanonicalWorkspaceRecordViewer; } private function requestHasExplicitTenantHint(Request $request): bool diff --git a/apps/platform/app/Support/Tenants/TenantPageCategory.php b/apps/platform/app/Support/Navigation/AdminSurfaceScope.php similarity index 82% rename from apps/platform/app/Support/Tenants/TenantPageCategory.php rename to apps/platform/app/Support/Navigation/AdminSurfaceScope.php index b6264304..cdb599a8 100644 --- a/apps/platform/app/Support/Tenants/TenantPageCategory.php +++ b/apps/platform/app/Support/Navigation/AdminSurfaceScope.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace App\Support\Tenants; +namespace App\Support\Navigation; -use App\Support\Navigation\WorkspaceHubRegistry; +use App\Support\Tenants\TenantInteractionLane; use Illuminate\Http\Request; -enum TenantPageCategory: string +enum AdminSurfaceScope: string { case WorkspaceWideSurface = 'workspace_wide_surface'; case WorkspaceScoped = 'workspace_scoped'; case WorkspaceChooserException = 'workspace_chooser_exception'; - case TenantBound = 'tenant_bound'; - case TenantScopedEvidence = 'tenant_scoped_evidence'; + case EnvironmentBound = 'environment_bound'; + case EnvironmentScopedEvidence = 'environment_scoped_evidence'; case OnboardingWorkflow = 'onboarding_workflow'; case CanonicalWorkspaceRecordViewer = 'canonical_workspace_record_viewer'; @@ -46,7 +46,7 @@ public static function fromPath(string $path): self str_starts_with($normalizedPath, '/admin/evidence/') && ! str_starts_with($normalizedPath, '/admin/evidence/overview') ) { - return self::TenantScopedEvidence; + return self::EnvironmentScopedEvidence; } if (preg_match('#^/admin/onboarding(?:/[^/]+)?$#', $normalizedPath) === 1) { @@ -54,13 +54,13 @@ public static function fromPath(string $path): self } if (preg_match('#^/admin/workspaces/[^/]+/environments/[^/]+(?:/|$)#', $normalizedPath) === 1) { - return self::TenantBound; + return self::EnvironmentBound; } return self::WorkspaceScoped; } - public function allowsQueryTenantHints(): bool + public function allowsQueryEnvironmentHints(): bool { return match ($this) { self::WorkspaceWideSurface, self::WorkspaceScoped, self::OnboardingWorkflow => true, @@ -68,7 +68,7 @@ public function allowsQueryTenantHints(): bool }; } - public function allowsRememberedTenantRestore(): bool + public function allowsRememberedEnvironmentRestore(): bool { return match ($this) { self::WorkspaceScoped, self::OnboardingWorkflow, self::CanonicalWorkspaceRecordViewer => true, @@ -76,7 +76,7 @@ public function allowsRememberedTenantRestore(): bool }; } - public function allowsTenantlessState(): bool + public function allowsEnvironmentlessState(): bool { return match ($this) { self::WorkspaceWideSurface, @@ -88,7 +88,7 @@ public function allowsTenantlessState(): bool }; } - public function forcesTenantlessShellContext(): bool + public function forcesEnvironmentlessShellContext(): bool { return match ($this) { self::WorkspaceWideSurface, @@ -98,17 +98,17 @@ public function forcesTenantlessShellContext(): bool }; } - public function requiresExplicitTenant(): bool + public function requiresExplicitEnvironment(): bool { return match ($this) { - self::TenantBound, self::TenantScopedEvidence => true, + self::EnvironmentBound, self::EnvironmentScopedEvidence => true, default => false, }; } public function lane(): TenantInteractionLane { - return TenantInteractionLane::fromPageCategory($this); + return TenantInteractionLane::fromSurfaceScope($this); } private static function isWorkspaceWideSurfacePath(string $normalizedPath): bool diff --git a/apps/platform/app/Support/Navigation/NavigationScope.php b/apps/platform/app/Support/Navigation/NavigationScope.php index 9bea4cf2..b9f3e3bb 100644 --- a/apps/platform/app/Support/Navigation/NavigationScope.php +++ b/apps/platform/app/Support/Navigation/NavigationScope.php @@ -4,7 +4,6 @@ namespace App\Support\Navigation; -use App\Support\Tenants\TenantPageCategory; use Illuminate\Http\Request; final class NavigationScope @@ -17,8 +16,8 @@ public static function isWorkspaceSurface(?Request $request = null): bool public static function isEnvironmentSurface(?Request $request = null): bool { return in_array(self::pageCategory($request), [ - TenantPageCategory::TenantBound, - TenantPageCategory::TenantScopedEvidence, + AdminSurfaceScope::EnvironmentBound, + AdminSurfaceScope::EnvironmentScopedEvidence, ], true); } @@ -27,9 +26,9 @@ public static function shouldRegisterEnvironmentNavigation(?Request $request = n return self::isEnvironmentSurface($request); } - public static function pageCategory(?Request $request = null): TenantPageCategory + public static function pageCategory(?Request $request = null): AdminSurfaceScope { - return TenantPageCategory::fromPath(self::effectivePath($request)); + return AdminSurfaceScope::fromPath(self::effectivePath($request)); } private static function effectivePath(?Request $request = null): string diff --git a/apps/platform/app/Support/Navigation/RelatedNavigationResolver.php b/apps/platform/app/Support/Navigation/RelatedNavigationResolver.php index 31d5abb3..bbe18755 100644 --- a/apps/platform/app/Support/Navigation/RelatedNavigationResolver.php +++ b/apps/platform/app/Support/Navigation/RelatedNavigationResolver.php @@ -20,11 +20,11 @@ use App\Models\BaselineSnapshotItem; use App\Models\Finding; use App\Models\FindingException; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\Policy; use App\Models\PolicyVersion; use App\Models\RestoreRun; -use App\Models\ManagedEnvironment; use App\Models\User; use App\Models\Workspace; use App\Services\Auth\CapabilityResolver; @@ -39,7 +39,6 @@ use App\Support\Ui\DerivedState\DerivedStateFamily; use App\Support\Ui\DerivedState\DerivedStateKey; use App\Support\Ui\DerivedState\RequestScopedDerivedStateStore; -use Filament\Facades\Filament; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; @@ -367,7 +366,7 @@ private function memoizedEntries( context: [ 'source_type' => $sourceType, 'surface' => $surface, - 'active_tenant_id' => $this->activeTenantId(), + 'active_tenant_id' => $this->activeEnvironmentId(), 'route_name' => request()?->route()?->getName(), 'user_id' => auth()->id(), ], @@ -631,7 +630,7 @@ private function snapshotPolicyVersionEntry(NavigationMatrixRule $rule, Baseline return $this->policyVersionEntry( rule: $rule, policyVersionId: is_numeric($policyVersionId) ? (int) $policyVersionId : null, - tenantId: $this->activeTenantId(), + tenantId: $this->activeEnvironmentId(), ); } @@ -1094,7 +1093,7 @@ private function contextForOperationRun(OperationRun $run): CanonicalNavigationC ); } - private function activeTenantId(): ?int + private function activeEnvironmentId(): ?int { $tenant = app(OperateHubShell::class)->activeEntitledTenant(request()); diff --git a/apps/platform/app/Support/OperateHub/OperateHubShell.php b/apps/platform/app/Support/OperateHub/OperateHubShell.php index 4c06d175..680a9d6f 100644 --- a/apps/platform/app/Support/OperateHub/OperateHubShell.php +++ b/apps/platform/app/Support/OperateHub/OperateHubShell.php @@ -10,9 +10,9 @@ use App\Services\Auth\CapabilityResolver; use App\Services\Tenants\TenantOperabilityService; use App\Support\ManagedEnvironmentLinks; +use App\Support\Navigation\AdminSurfaceScope; use App\Support\Navigation\WorkspaceHubRegistry; use App\Support\Tenants\TenantOperabilityQuestion; -use App\Support\Tenants\TenantPageCategory; use App\Support\Workspaces\WorkspaceContext; use Filament\Actions\Action; use Filament\Facades\Filament; @@ -30,10 +30,10 @@ public function __construct( public function scopeLabel(?Request $request = null): string { - $activeTenant = $this->activeEntitledTenant($request); + $activeEnvironment = $this->activeEntitledTenant($request); - if ($activeTenant instanceof ManagedEnvironment) { - return __('localization.shell.environment_scope').': '.$activeTenant->name; + if ($activeEnvironment instanceof ManagedEnvironment) { + return __('localization.shell.environment_scope').': '.$activeEnvironment->name; } return __('localization.shell.all_environments'); @@ -44,12 +44,12 @@ public function scopeLabel(?Request $request = null): string */ public function returnAffordance(?Request $request = null): ?array { - $activeTenant = $this->activeEntitledTenant($request); + $activeEnvironment = $this->activeEntitledTenant($request); - if ($activeTenant instanceof ManagedEnvironment) { + if ($activeEnvironment instanceof ManagedEnvironment) { return [ - 'label' => 'Back to '.$activeTenant->name, - 'url' => ManagedEnvironmentLinks::viewUrl($activeTenant), + 'label' => 'Back to '.$activeEnvironment->name, + 'url' => ManagedEnvironmentLinks::viewUrl($activeEnvironment), ]; } @@ -135,7 +135,7 @@ private function buildResolvedContext(?Request $request = null): ResolvedShellCo pageCategory: $pageCategory, state: 'missing_workspace', displayMode: 'recovery', - recoveryAction: $pageCategory === TenantPageCategory::WorkspaceChooserException ? 'none' : 'redirect_choose_workspace', + recoveryAction: $pageCategory === AdminSurfaceScope::WorkspaceChooserException ? 'none' : 'redirect_choose_workspace', recoveryDestination: '/admin/choose-workspace', recoveryReason: 'missing_workspace', ); @@ -157,7 +157,7 @@ private function buildResolvedContext(?Request $request = null): ResolvedShellCo $recoveryReason = $routeTenant['reason']; - if ($pageCategory === TenantPageCategory::TenantBound && $recoveryReason !== null) { + if ($pageCategory === AdminSurfaceScope::EnvironmentBound && $recoveryReason !== null) { return new ResolvedShellContext( workspace: $workspace, tenant: null, @@ -170,7 +170,7 @@ private function buildResolvedContext(?Request $request = null): ResolvedShellCo ); } - if ($pageCategory->forcesTenantlessShellContext()) { + if ($pageCategory->forcesEnvironmentlessShellContext()) { return new ResolvedShellContext( workspace: $workspace, tenant: null, @@ -225,13 +225,13 @@ private function buildResolvedContext(?Request $request = null): ResolvedShellCo ); } - if ($pageCategory->allowsRememberedTenantRestore()) { - $rememberedTenant = $this->workspaceContext->rememberedTenant($request); + if ($pageCategory->allowsRememberedEnvironmentRestore()) { + $rememberedEnvironment = $this->workspaceContext->rememberedEnvironment($request); - if ($rememberedTenant instanceof ManagedEnvironment) { + if ($rememberedEnvironment instanceof ManagedEnvironment) { return new ResolvedShellContext( workspace: $workspace, - tenant: $rememberedTenant, + tenant: $rememberedEnvironment, pageCategory: $pageCategory, state: 'tenant_scoped', displayMode: 'tenant_scoped', @@ -241,7 +241,7 @@ private function buildResolvedContext(?Request $request = null): ResolvedShellCo } } - if ($pageCategory->requiresExplicitTenant()) { + if ($pageCategory->requiresExplicitEnvironment()) { return new ResolvedShellContext( workspace: $workspace, tenant: null, @@ -249,10 +249,10 @@ private function buildResolvedContext(?Request $request = null): ResolvedShellCo state: 'missing_tenant', displayMode: 'recovery', workspaceSource: $workspaceSource, - recoveryAction: $pageCategory === TenantPageCategory::TenantScopedEvidence + recoveryAction: $pageCategory === AdminSurfaceScope::EnvironmentScopedEvidence ? 'redirect_evidence_overview' : 'abort_not_found', - recoveryDestination: $pageCategory === TenantPageCategory::TenantScopedEvidence + recoveryDestination: $pageCategory === AdminSurfaceScope::EnvironmentScopedEvidence ? '/admin/evidence/overview' : null, recoveryReason: $recoveryReason ?? 'missing_tenant', @@ -272,7 +272,7 @@ private function buildResolvedContext(?Request $request = null): ResolvedShellCo private function resolveValidatedFilamentTenant( ?Request $request = null, - ?TenantPageCategory $pageCategory = null, + ?AdminSurfaceScope $pageCategory = null, ?Workspace $workspace = null, ): ?ManagedEnvironment { $tenant = Filament::getTenant(); @@ -297,7 +297,7 @@ private function resolveValidatedRouteTenant( ?ManagedEnvironment $tenant, Workspace $workspace, ?Request $request = null, - ?TenantPageCategory $pageCategory = null, + ?AdminSurfaceScope $pageCategory = null, ): array { $pageCategory ??= $this->pageCategory($request); @@ -316,22 +316,22 @@ private function resolveValidatedRouteTenant( private function resolveWorkspaceForPageCategory( ?ManagedEnvironment $tenant, - TenantPageCategory $pageCategory, + AdminSurfaceScope $pageCategory, ?Request $request = null, ): ?Workspace { return match ($pageCategory) { - TenantPageCategory::TenantScopedEvidence => $this->workspaceContext->currentWorkspace($request), - TenantPageCategory::TenantBound => $this->workspaceContext->currentWorkspaceOrTenantWorkspace($tenant, $request), - default => $this->workspaceContext->currentWorkspaceOrTenantWorkspace($tenant, $request), + AdminSurfaceScope::EnvironmentScopedEvidence => $this->workspaceContext->currentWorkspace($request), + AdminSurfaceScope::EnvironmentBound => $this->workspaceContext->currentWorkspaceOrEnvironmentWorkspace($tenant, $request), + default => $this->workspaceContext->currentWorkspaceOrEnvironmentWorkspace($tenant, $request), }; } private function resolveValidatedQueryHintTenant( ?Request $request, Workspace $workspace, - TenantPageCategory $pageCategory, + AdminSurfaceScope $pageCategory, ): array { - if (! $pageCategory->allowsQueryTenantHints()) { + if (! $pageCategory->allowsQueryEnvironmentHints()) { return ['tenant' => null, 'reason' => null]; } @@ -350,7 +350,7 @@ private function resolveValidatedQueryHintTenant( return ['tenant' => $queryTenant, 'reason' => null]; } - private function resolveRouteTenantCandidate(?Request $request = null, ?TenantPageCategory $pageCategory = null): ?ManagedEnvironment + private function resolveRouteTenantCandidate(?Request $request = null, ?AdminSurfaceScope $pageCategory = null): ?ManagedEnvironment { $route = $request?->route(); $pageCategory ??= $this->pageCategory($request); @@ -445,7 +445,7 @@ private function tenantValidationReason( ManagedEnvironment $tenant, Workspace $workspace, ?Request $request = null, - ?TenantPageCategory $pageCategory = null, + ?AdminSurfaceScope $pageCategory = null, ): ?string { $pageCategory ??= $this->pageCategory($request); @@ -463,8 +463,8 @@ private function tenantValidationReason( return 'not_member'; } - $question = $pageCategory === TenantPageCategory::TenantBound - ? TenantOperabilityQuestion::TenantBoundViewability + $question = $pageCategory === AdminSurfaceScope::EnvironmentBound + ? TenantOperabilityQuestion::EnvironmentBoundViewability : TenantOperabilityQuestion::AdministrativeDiscoverability; $allowed = $this->tenantOperabilityService->outcomeFor( @@ -480,13 +480,13 @@ private function tenantValidationReason( return null; } - return $pageCategory === TenantPageCategory::TenantBound + return $pageCategory === AdminSurfaceScope::EnvironmentBound ? 'inaccessible' : 'not_operable'; } - private function pageCategory(?Request $request = null): TenantPageCategory + private function pageCategory(?Request $request = null): AdminSurfaceScope { - return TenantPageCategory::fromRequest($request); + return AdminSurfaceScope::fromRequest($request); } } diff --git a/apps/platform/app/Support/OperateHub/ResolvedShellContext.php b/apps/platform/app/Support/OperateHub/ResolvedShellContext.php index 6727e1c0..a1f23af0 100644 --- a/apps/platform/app/Support/OperateHub/ResolvedShellContext.php +++ b/apps/platform/app/Support/OperateHub/ResolvedShellContext.php @@ -6,14 +6,14 @@ use App\Models\ManagedEnvironment; use App\Models\Workspace; -use App\Support\Tenants\TenantPageCategory; +use App\Support\Navigation\AdminSurfaceScope; final readonly class ResolvedShellContext { public function __construct( public ?Workspace $workspace, public ?ManagedEnvironment $tenant, - public TenantPageCategory $pageCategory, + public AdminSurfaceScope $pageCategory, public string $state, public string $displayMode, public string $workspaceSource = 'none', diff --git a/apps/platform/app/Support/OperationRunLinks.php b/apps/platform/app/Support/OperationRunLinks.php index 601bc365..642dc84d 100644 --- a/apps/platform/app/Support/OperationRunLinks.php +++ b/apps/platform/app/Support/OperationRunLinks.php @@ -237,7 +237,7 @@ public static function related(OperationRun $run, ?ManagedEnvironment $tenant): ->first(); if ($review instanceof EnvironmentReview) { - $links['ManagedEnvironment Review'] = EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant); + $links['ManagedEnvironment Review'] = EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant); } } diff --git a/apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php b/apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php index 52b13762..bca3d06b 100644 --- a/apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php +++ b/apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php @@ -4,18 +4,18 @@ namespace App\Support\SupportDiagnostics; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\FindingResource; use App\Filament\Resources\ProviderConnectionResource; use App\Filament\Resources\ReviewPackResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\AuditLog; +use App\Models\EnvironmentReview; use App\Models\Finding; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\ProviderConnection; use App\Models\ReviewPack; use App\Models\StoredReport; -use App\Models\ManagedEnvironment; -use App\Models\EnvironmentReview; use App\Models\User; use App\Models\Workspace; use App\Support\Ai\AiDataClassification; @@ -741,7 +741,7 @@ private function environmentReviewSection(?EnvironmentReview $review, ?ManagedEn label: 'ManagedEnvironment review #'.$review->getKey(), actionLabel: 'Open environment review', url: $tenant instanceof ManagedEnvironment - ? EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant) + ? EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant) : null, freshnessAt: $review->generated_at, ), diff --git a/apps/platform/app/Support/Tenants/TenantInteractionLane.php b/apps/platform/app/Support/Tenants/TenantInteractionLane.php index f5312e55..7103d79a 100644 --- a/apps/platform/app/Support/Tenants/TenantInteractionLane.php +++ b/apps/platform/app/Support/Tenants/TenantInteractionLane.php @@ -4,6 +4,8 @@ namespace App\Support\Tenants; +use App\Support\Navigation\AdminSurfaceScope; + enum TenantInteractionLane: string { case StandardActiveOperating = 'standard_active_operating'; @@ -11,16 +13,16 @@ enum TenantInteractionLane: string case AdministrativeManagement = 'administrative_management'; case CanonicalWorkspaceRecord = 'canonical_workspace_record'; - public static function fromPageCategory(TenantPageCategory $pageCategory): self + public static function fromSurfaceScope(AdminSurfaceScope $pageCategory): self { return match ($pageCategory) { - TenantPageCategory::OnboardingWorkflow => self::OnboardingWorkflow, - TenantPageCategory::TenantBound, - TenantPageCategory::TenantScopedEvidence => self::AdministrativeManagement, - TenantPageCategory::CanonicalWorkspaceRecordViewer => self::CanonicalWorkspaceRecord, - TenantPageCategory::WorkspaceWideSurface, - TenantPageCategory::WorkspaceScoped, - TenantPageCategory::WorkspaceChooserException => self::StandardActiveOperating, + AdminSurfaceScope::OnboardingWorkflow => self::OnboardingWorkflow, + AdminSurfaceScope::EnvironmentBound, + AdminSurfaceScope::EnvironmentScopedEvidence => self::AdministrativeManagement, + AdminSurfaceScope::CanonicalWorkspaceRecordViewer => self::CanonicalWorkspaceRecord, + AdminSurfaceScope::WorkspaceWideSurface, + AdminSurfaceScope::WorkspaceScoped, + AdminSurfaceScope::WorkspaceChooserException => self::StandardActiveOperating, }; } } diff --git a/apps/platform/app/Support/Tenants/TenantLifecycle.php b/apps/platform/app/Support/Tenants/TenantLifecycle.php index 5bff1f99..25d9ea2c 100644 --- a/apps/platform/app/Support/Tenants/TenantLifecycle.php +++ b/apps/platform/app/Support/Tenants/TenantLifecycle.php @@ -105,7 +105,7 @@ public function supportsQuestion(TenantOperabilityQuestion $question, TenantInte return match ($question) { TenantOperabilityQuestion::SelectorEligibility, TenantOperabilityQuestion::RememberedContextValidity => $lane === TenantInteractionLane::StandardActiveOperating && $this->isSelectableInStandardLane(), - TenantOperabilityQuestion::TenantBoundViewability => $lane === TenantInteractionLane::AdministrativeManagement, + TenantOperabilityQuestion::EnvironmentBoundViewability => $lane === TenantInteractionLane::AdministrativeManagement, TenantOperabilityQuestion::CanonicalLinkedRecordViewability => $lane === TenantInteractionLane::CanonicalWorkspaceRecord, TenantOperabilityQuestion::ArchiveEligibility => $lane === TenantInteractionLane::AdministrativeManagement && $this->canArchive(), TenantOperabilityQuestion::RestoreEligibility => $lane === TenantInteractionLane::AdministrativeManagement && $this->canRestore(), diff --git a/apps/platform/app/Support/Tenants/TenantLifecyclePresentation.php b/apps/platform/app/Support/Tenants/TenantLifecyclePresentation.php index b9beaa7d..1b7b068e 100644 --- a/apps/platform/app/Support/Tenants/TenantLifecyclePresentation.php +++ b/apps/platform/app/Support/Tenants/TenantLifecyclePresentation.php @@ -100,7 +100,7 @@ public static function invalid(?string $normalizedValue = null): self badgeIcon: 'heroicon-m-exclamation-triangle', badgeIconColor: 'danger', shortDescription: 'Lifecycle data is invalid and requires review.', - longDescription: 'The stored tenant lifecycle value is not canonical. Review the source data before treating this tenant as draft, onboarding, active, or archived.', + longDescription: 'The stored environment lifecycle value is not canonical. Review the source data before treating this tenant as draft, onboarding, active, or archived.', isInvalidFallback: true, lifecycle: null, ); diff --git a/apps/platform/app/Support/Tenants/TenantOperabilityContext.php b/apps/platform/app/Support/Tenants/TenantOperabilityContext.php index 7316e299..efae8b8e 100644 --- a/apps/platform/app/Support/Tenants/TenantOperabilityContext.php +++ b/apps/platform/app/Support/Tenants/TenantOperabilityContext.php @@ -7,6 +7,7 @@ use App\Models\ManagedEnvironment; use App\Models\ManagedEnvironmentOnboardingSession; use App\Models\User; +use App\Support\Navigation\AdminSurfaceScope; final readonly class TenantOperabilityContext { @@ -15,7 +16,7 @@ public function __construct( public ?User $actor, public ?int $workspaceId, public TenantInteractionLane $lane, - public ?TenantPageCategory $pageCategory = null, + public ?AdminSurfaceScope $pageCategory = null, public ?string $linkedRecordType = null, public ?int $linkedRecordId = null, public ?ManagedEnvironmentOnboardingSession $onboardingDraft = null, @@ -28,7 +29,7 @@ public static function forTenant( ?User $actor = null, ?int $workspaceId = null, TenantInteractionLane $lane = TenantInteractionLane::AdministrativeManagement, - ?TenantPageCategory $pageCategory = null, + ?AdminSurfaceScope $pageCategory = null, ?ManagedEnvironmentOnboardingSession $onboardingDraft = null, ?string $requiredCapability = null, ?ManagedEnvironment $selectedTenant = null, diff --git a/apps/platform/app/Support/Tenants/TenantOperabilityQuestion.php b/apps/platform/app/Support/Tenants/TenantOperabilityQuestion.php index 454ef14a..678b8635 100644 --- a/apps/platform/app/Support/Tenants/TenantOperabilityQuestion.php +++ b/apps/platform/app/Support/Tenants/TenantOperabilityQuestion.php @@ -8,7 +8,7 @@ enum TenantOperabilityQuestion: string { case SelectorEligibility = 'selector_eligibility'; case RememberedContextValidity = 'remembered_context_validity'; - case TenantBoundViewability = 'tenant_bound_viewability'; + case EnvironmentBoundViewability = 'environment_bound_viewability'; case CanonicalLinkedRecordViewability = 'canonical_linked_record_viewability'; case ArchiveEligibility = 'archive_eligibility'; case RestoreEligibility = 'restore_eligibility'; diff --git a/apps/platform/app/Support/Tenants/TenantOperabilityReasonCode.php b/apps/platform/app/Support/Tenants/TenantOperabilityReasonCode.php index 8208de27..c930996d 100644 --- a/apps/platform/app/Support/Tenants/TenantOperabilityReasonCode.php +++ b/apps/platform/app/Support/Tenants/TenantOperabilityReasonCode.php @@ -55,7 +55,7 @@ public function shortExplanation(): string self::TenantAlreadyArchived => 'The tenant is already archived, so there is nothing else to do for this action.', self::OnboardingNotResumable => 'This onboarding session can no longer be resumed from the current lifecycle state.', self::CanonicalViewFollowupOnly => 'This canonical workspace view is informational only and cannot complete tenant follow-up directly.', - self::RememberedContextStale => 'The remembered tenant context is no longer valid for the current tenant selector state.', + self::RememberedContextStale => 'The remembered environment context is no longer valid for the current tenant selector state.', self::WorkspaceClosed => 'This workspace is closed and cannot be used for active tenant context or new tenant operations until it is reopened.', self::TenantRemovedFromWorkspace => 'This tenant was removed from the workspace and cannot be selected or used for new tenant operations until it is restored.', }; diff --git a/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php b/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php index 80eb16d5..4e222687 100644 --- a/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php +++ b/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php @@ -5,20 +5,22 @@ namespace App\Support\Ui\GovernanceArtifactTruth; use App\Filament\Resources\BaselineSnapshotResource; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\ReviewPackResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\BaselineSnapshot; +use App\Models\EnvironmentReview; use App\Models\EvidenceSnapshot; use App\Models\FindingExceptionDecision; use App\Models\OperationRun; use App\Models\ReviewPack; use App\Models\StoredReport; -use App\Models\EnvironmentReview; use App\Services\Baselines\BaselineSnapshotTruthResolver; use App\Services\Baselines\SnapshotRendering\FidelityState; use App\Support\Badges\BadgeCatalog; use App\Support\Badges\BadgeDomain; +use App\Support\EnvironmentReviewCompletenessState; +use App\Support\EnvironmentReviewStatus; use App\Support\Evidence\EvidenceCompletenessState; use App\Support\OperationCatalog; use App\Support\OperationRunLinks; @@ -28,8 +30,6 @@ use App\Support\ReasonTranslation\ReasonPresenter; use App\Support\ReasonTranslation\ReasonResolutionEnvelope; use App\Support\ReviewPackStatus; -use App\Support\EnvironmentReviewCompletenessState; -use App\Support\EnvironmentReviewStatus; use App\Support\Ui\DerivedState\DerivedStateFamily; use App\Support\Ui\DerivedState\DerivedStateKey; use App\Support\Ui\DerivedState\RequestScopedDerivedStateStore; @@ -600,7 +600,7 @@ private function buildEnvironmentReviewEnvelope(EnvironmentReview $review): Arti if ($publishBlockers !== [] && $review->tenant !== null) { $nextActionUrl = $this->panelSafeTenantArtifactUrl( - fn (): string => EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $review->tenant) + fn (): string => EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $review->tenant) ); } elseif (($freshnessState === 'stale' || $contentState === 'partial') && $review->tenant !== null && $review->evidenceSnapshot !== null) { $nextActionUrl = $this->panelSafeTenantArtifactUrl( @@ -642,7 +642,7 @@ private function buildEnvironmentReviewEnvelope(EnvironmentReview $review): Arti relatedRunId: $review->operation_run_id !== null ? (int) $review->operation_run_id : null, relatedArtifactUrl: $review->tenant !== null ? $this->panelSafeTenantArtifactUrl( - fn (): string => EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $review->tenant) + fn (): string => EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $review->tenant) ) : null, includePublicationDimension: true, @@ -785,7 +785,7 @@ private function buildReviewPackEnvelope(ReviewPack $pack): ArtifactTruthEnvelop if ($sourceReview instanceof EnvironmentReview && $pack->tenant !== null) { $nextActionUrl = $this->panelSafeTenantArtifactUrl( - fn (): string => EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $sourceReview], $pack->tenant) + fn (): string => EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $sourceReview], $pack->tenant) ); } elseif (($freshnessState === 'stale' || $contentState === 'partial') && $pack->tenant !== null && $pack->evidenceSnapshot !== null) { $nextActionUrl = $this->panelSafeTenantArtifactUrl( diff --git a/apps/platform/app/Support/WorkspaceIsolation/TenantOwnedModelFamilies.php b/apps/platform/app/Support/WorkspaceIsolation/TenantOwnedModelFamilies.php index 27dc20c5..2f62650e 100644 --- a/apps/platform/app/Support/WorkspaceIsolation/TenantOwnedModelFamilies.php +++ b/apps/platform/app/Support/WorkspaceIsolation/TenantOwnedModelFamilies.php @@ -7,6 +7,7 @@ use App\Filament\Resources\BackupScheduleResource; use App\Filament\Resources\BackupSetResource; use App\Filament\Resources\EntraGroupResource; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\FindingExceptionResource; use App\Filament\Resources\FindingResource; @@ -15,10 +16,10 @@ use App\Filament\Resources\PolicyVersionResource; use App\Filament\Resources\RestoreRunResource; use App\Filament\Resources\StoredReportResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\BackupSchedule; use App\Models\BackupSet; use App\Models\EntraGroup; +use App\Models\EnvironmentReview; use App\Models\EvidenceSnapshot; use App\Models\Finding; use App\Models\FindingException; @@ -27,7 +28,6 @@ use App\Models\PolicyVersion; use App\Models\RestoreRun; use App\Models\StoredReport; -use App\Models\EnvironmentReview; final class TenantOwnedModelFamilies { @@ -251,7 +251,7 @@ public static function scopeExceptions(): array 'why_excepted' => 'Workspace-admin tenant-default surface referencing tenant-owned data without being part of the mandatory first-slice canon.', 'still_required_checks' => [ 'workspace membership', - 'remembered tenant entitlement', + 'remembered environment entitlement', 'capability gating on the destination action', ], ], diff --git a/apps/platform/app/Support/Workspaces/WorkspaceContext.php b/apps/platform/app/Support/Workspaces/WorkspaceContext.php index 34cd8f2f..515e76eb 100644 --- a/apps/platform/app/Support/Workspaces/WorkspaceContext.php +++ b/apps/platform/app/Support/Workspaces/WorkspaceContext.php @@ -18,7 +18,7 @@ final class WorkspaceContext public const INTENDED_URL_SESSION_KEY = 'workspace_intended_url'; - public const LAST_TENANT_IDS_SESSION_KEY = 'workspace_last_tenant_ids'; + public const LAST_ENVIRONMENT_IDS_SESSION_KEY = 'workspace_last_environment_ids'; public function __construct( private WorkspaceResolver $resolver, @@ -55,7 +55,7 @@ public function currentWorkspace(?Request $request = null): ?Workspace return $workspace; } - public function currentWorkspaceOrTenantWorkspace(?ManagedEnvironment $tenant = null, ?Request $request = null): ?Workspace + public function currentWorkspaceOrEnvironmentWorkspace(?ManagedEnvironment $tenant = null, ?Request $request = null): ?Workspace { $workspace = $this->currentWorkspace($request); @@ -96,19 +96,19 @@ public function setCurrentWorkspace(Workspace $workspace, ?User $user = null, ?R } } - public function rememberLastTenantId(int $workspaceId, int $tenantId, ?Request $request = null): void + public function rememberLastEnvironmentId(int $workspaceId, int $tenantId, ?Request $request = null): void { $session = ($request && $request->hasSession()) ? $request->session() : session(); - $map = $session->get(self::LAST_TENANT_IDS_SESSION_KEY, []); + $map = $session->get(self::LAST_ENVIRONMENT_IDS_SESSION_KEY, []); $map = is_array($map) ? $map : []; $map[(string) $workspaceId] = $tenantId; - $session->put(self::LAST_TENANT_IDS_SESSION_KEY, $map); + $session->put(self::LAST_ENVIRONMENT_IDS_SESSION_KEY, $map); } - public function rememberTenantContext(ManagedEnvironment $tenant, ?Request $request = null): bool + public function rememberEnvironmentContext(ManagedEnvironment $tenant, ?Request $request = null): bool { $workspaceId = $this->currentWorkspaceId($request); @@ -125,23 +125,23 @@ public function rememberTenantContext(ManagedEnvironment $tenant, ?Request $requ ); if (! $outcome->allowed) { - $this->clearLastTenantId($request); + $this->clearLastEnvironmentId($request); return false; } if (! $this->userCanAccessTenant($tenant, $request)) { - $this->clearRememberedTenantContext($request); + $this->clearRememberedEnvironmentContext($request); return false; } - $this->rememberLastTenantId($workspaceId, (int) $tenant->getKey(), $request); + $this->rememberLastEnvironmentId($workspaceId, (int) $tenant->getKey(), $request); return true; } - public function lastTenantId(?Request $request = null): ?int + public function lastEnvironmentId(?Request $request = null): ?int { $workspaceId = $this->currentWorkspaceId($request); @@ -151,7 +151,7 @@ public function lastTenantId(?Request $request = null): ?int $session = ($request && $request->hasSession()) ? $request->session() : session(); - $map = $session->get(self::LAST_TENANT_IDS_SESSION_KEY, []); + $map = $session->get(self::LAST_ENVIRONMENT_IDS_SESSION_KEY, []); $map = is_array($map) ? $map : []; $id = $map[(string) $workspaceId] ?? null; @@ -159,7 +159,7 @@ public function lastTenantId(?Request $request = null): ?int return is_int($id) ? $id : (is_numeric($id) ? (int) $id : null); } - public function clearLastTenantId(?Request $request = null): void + public function clearLastEnvironmentId(?Request $request = null): void { $workspaceId = $this->currentWorkspaceId($request); @@ -169,20 +169,20 @@ public function clearLastTenantId(?Request $request = null): void $session = ($request && $request->hasSession()) ? $request->session() : session(); - $map = $session->get(self::LAST_TENANT_IDS_SESSION_KEY, []); + $map = $session->get(self::LAST_ENVIRONMENT_IDS_SESSION_KEY, []); $map = is_array($map) ? $map : []; unset($map[(string) $workspaceId]); - $session->put(self::LAST_TENANT_IDS_SESSION_KEY, $map); + $session->put(self::LAST_ENVIRONMENT_IDS_SESSION_KEY, $map); } - public function clearRememberedTenantContext(?Request $request = null): void + public function clearRememberedEnvironmentContext(?Request $request = null): void { - $this->clearLastTenantId($request); + $this->clearLastEnvironmentId($request); } - public function rememberedTenant(?Request $request = null): ?ManagedEnvironment + public function rememberedEnvironment(?Request $request = null): ?ManagedEnvironment { $workspaceId = $this->currentWorkspaceId($request); @@ -190,31 +190,31 @@ public function rememberedTenant(?Request $request = null): ?ManagedEnvironment return null; } - $rememberedTenantId = $this->lastTenantId($request); + $rememberedEnvironmentId = $this->lastEnvironmentId($request); - if ($rememberedTenantId === null) { + if ($rememberedEnvironmentId === null) { return null; } $tenant = ManagedEnvironment::query() ->withTrashed() - ->whereKey($rememberedTenantId) + ->whereKey($rememberedEnvironmentId) ->first(); if (! $tenant instanceof ManagedEnvironment) { - $this->clearRememberedTenantContext($request); + $this->clearRememberedEnvironmentContext($request); return null; } if ((int) $tenant->workspace_id !== $workspaceId) { - $this->clearRememberedTenantContext($request); + $this->clearRememberedEnvironmentContext($request); return null; } if (! $this->userCanAccessTenant($tenant, $request)) { - $this->clearRememberedTenantContext($request); + $this->clearRememberedEnvironmentContext($request); return null; } @@ -228,7 +228,7 @@ public function rememberedTenant(?Request $request = null): ?ManagedEnvironment ); if (! $outcome->allowed) { - $this->clearRememberedTenantContext($request); + $this->clearRememberedEnvironmentContext($request); return null; } diff --git a/apps/platform/bootstrap/app.php b/apps/platform/bootstrap/app.php index 7ee22e95..123470ce 100644 --- a/apps/platform/bootstrap/app.php +++ b/apps/platform/bootstrap/app.php @@ -1,12 +1,11 @@ withRouting( @@ -35,7 +34,7 @@ 'ensure-platform-capability' => \App\Http\Middleware\EnsurePlatformCapability::class, 'ensure-workspace-member' => \App\Http\Middleware\EnsureWorkspaceMember::class, 'ensure-workspace-selected' => \App\Http\Middleware\EnsureWorkspaceSelected::class, - 'ensure-filament-tenant-selected' => \App\Support\Middleware\EnsureFilamentTenantSelected::class, + 'ensure-environment-context-selected' => \App\Support\Middleware\EnsureEnvironmentContextSelected::class, ]); $middleware->prependToPriorityList( diff --git a/apps/platform/resources/views/filament/partials/context-bar.blade.php b/apps/platform/resources/views/filament/partials/context-bar.blade.php index 1336e80f..764c6e2a 100644 --- a/apps/platform/resources/views/filament/partials/context-bar.blade.php +++ b/apps/platform/resources/views/filament/partials/context-bar.blade.php @@ -15,32 +15,32 @@ $user = auth()->user(); - $tenants = collect(); + $environments = collect(); if ($user instanceof User && $workspace) { - $tenants = collect($user->getTenants(Filament::getCurrentOrDefaultPanel())) - ->filter(fn ($tenant): bool => $tenant instanceof ManagedEnvironment && (int) $tenant->workspace_id === (int) $workspace->getKey()) + $environments = collect($user->getTenants(Filament::getCurrentOrDefaultPanel())) + ->filter(fn ($environment): bool => $environment instanceof ManagedEnvironment && (int) $environment->workspace_id === (int) $workspace->getKey()) ->values(); } - $currentTenant = $resolvedContext->tenant; - $currentTenantId = $currentTenant instanceof ManagedEnvironment ? (int) $currentTenant->getKey() : null; - $currentTenantName = $currentTenant instanceof ManagedEnvironment ? $currentTenant->getFilamentName() : null; + $currentEnvironment = $resolvedContext->tenant; + $currentEnvironmentId = $currentEnvironment instanceof ManagedEnvironment ? (int) $currentEnvironment->getKey() : null; + $currentEnvironmentName = $currentEnvironment instanceof ManagedEnvironment ? $currentEnvironment->getFilamentName() : null; - $lastTenantId = $workspaceContext->lastTenantId(request()); - $canClearEnvironmentContext = $currentTenant instanceof ManagedEnvironment || $lastTenantId !== null; + $lastEnvironmentId = $workspaceContext->lastEnvironmentId(request()); + $canClearEnvironmentContext = $currentEnvironment instanceof ManagedEnvironment || $lastEnvironmentId !== null; @endphp @php - $tenantLabel = $currentTenantName ?? __('localization.shell.no_environment_selected'); + $environmentLabel = $currentEnvironmentName ?? __('localization.shell.no_environment_selected'); $workspaceLabel = $workspace?->name ?? __('localization.shell.choose_workspace'); - $hasActiveTenant = $currentTenantName !== null; + $hasActiveEnvironment = $currentEnvironmentName !== null; $managedEnvironmentsUrl = $workspace ? route('admin.workspace.managed-environments.index', ['workspace' => $workspace]) : route('admin.onboarding'); $workspaceUrl = $workspace ? route('admin.home') : ChooseWorkspace::getUrl(panel: 'admin'); - $tenantTriggerLabel = $workspace ? $tenantLabel : __('localization.shell.choose_workspace'); + $environmentTriggerLabel = $workspace ? $environmentLabel : __('localization.shell.choose_workspace'); $localePlane = 'admin'; @endphp @@ -59,7 +59,7 @@ class="inline-flex items-center gap-1.5 rounded-l-lg px-2.5 py-1.5 font-medium t @endif - {{-- Dropdown trigger: tenant label + chevron --}} + {{-- Dropdown trigger: environment label + chevron --}} @endforeach diff --git a/apps/platform/routes/web.php b/apps/platform/routes/web.php index 5079e2f6..63fd17be 100644 --- a/apps/platform/routes/web.php +++ b/apps/platform/routes/web.php @@ -5,24 +5,23 @@ use App\Http\Controllers\Auth\EntraController; use App\Http\Controllers\ClearEnvironmentContextController; use App\Http\Controllers\LocalizationController; +use App\Http\Controllers\ManagedEnvironmentOnboardingController; use App\Http\Controllers\OpenFindingExceptionsQueueController; use App\Http\Controllers\RbacDelegatedAuthController; use App\Http\Controllers\ReviewPackDownloadController; use App\Http\Controllers\SelectEnvironmentController; use App\Http\Controllers\SwitchWorkspaceController; -use App\Http\Controllers\ManagedEnvironmentOnboardingController; use App\Http\Middleware\SuppressDebugbarForSmokeRequests; -use App\Models\ProviderConnection; use App\Models\ManagedEnvironment; use App\Models\ManagedEnvironmentOnboardingSession; use App\Models\User; use App\Models\Workspace; use App\Services\Onboarding\OnboardingDraftResolver; +use App\Services\Tenants\TenantOperabilityService; use App\Support\Auth\WorkspaceRole; use App\Support\ManagedEnvironmentLinks; -use App\Services\Tenants\TenantOperabilityService; +use App\Support\Navigation\AdminSurfaceScope; use App\Support\Tenants\TenantOperabilityQuestion; -use App\Support\Tenants\TenantPageCategory; use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceResolver; use Filament\Http\Middleware\Authenticate as FilamentAuthenticate; @@ -268,7 +267,7 @@ $workspaceContext->setCurrentWorkspace($workspace, $user, $request); if ($tenant instanceof ManagedEnvironment) { - $workspaceContext->rememberTenantContext($tenant, $request); + $workspaceContext->rememberEnvironmentContext($tenant, $request); } else { $workspaceContext->clearRememberedTenantContext($request); } @@ -377,10 +376,10 @@ $allowed = app(TenantOperabilityService::class)->outcomeFor( tenant: $tenant, - question: TenantOperabilityQuestion::TenantBoundViewability, + question: TenantOperabilityQuestion::EnvironmentBoundViewability, actor: $user, workspaceId: $workspaceId, - lane: TenantPageCategory::TenantBound->lane(), + lane: AdminSurfaceScope::EnvironmentBound->lane(), )->allowed; abort_unless($allowed, 404); @@ -394,7 +393,7 @@ DispatchServingFilamentEvent::class, FilamentAuthenticate::class, 'ensure-workspace-member', - 'ensure-filament-tenant-selected', + 'ensure-environment-context-selected', ]) ->prefix('/admin/workspaces/{workspace}') ->group(function (): void { @@ -470,7 +469,7 @@ DispatchServingFilamentEvent::class, FilamentAuthenticate::class, 'ensure-workspace-selected', - 'ensure-filament-tenant-selected', + 'ensure-environment-context-selected', ]) ->get('/admin/audit-log', \App\Filament\Pages\Monitoring\AuditLog::class) ->name('admin.monitoring.audit-log'); @@ -507,7 +506,7 @@ DispatchServingFilamentEvent::class, FilamentAuthenticate::class, 'ensure-workspace-member', - 'ensure-filament-tenant-selected', + 'ensure-environment-context-selected', ]) ->get('/admin/workspaces/{workspace}/environments/{environment:slug}', \App\Filament\Pages\EnvironmentDashboard::class) ->name('admin.workspace.environments.show'); @@ -520,7 +519,7 @@ DispatchServingFilamentEvent::class, FilamentAuthenticate::class, 'ensure-workspace-member', - 'ensure-filament-tenant-selected', + 'ensure-environment-context-selected', ]) ->get('/admin/workspaces/{workspace}/environments/{environment:slug}/diagnostics', \App\Filament\Pages\EnvironmentDiagnostics::class) ->name('admin.workspace.environments.diagnostics'); @@ -533,7 +532,7 @@ DispatchServingFilamentEvent::class, FilamentAuthenticate::class, 'ensure-workspace-member', - 'ensure-filament-tenant-selected', + 'ensure-environment-context-selected', ]) ->get('/admin/workspaces/{workspace}/environments/{environment:slug}/access-scopes', \App\Filament\Resources\ManagedEnvironmentResource\Pages\ManageEnvironmentAccessScopes::class) ->name('admin.workspace.environments.access-scopes'); diff --git a/apps/platform/tests/Browser/Dashboard/TenantDashboardProductizationSmokeTest.php b/apps/platform/tests/Browser/Dashboard/TenantDashboardProductizationSmokeTest.php index 232bcd4f..2add5c70 100644 --- a/apps/platform/tests/Browser/Dashboard/TenantDashboardProductizationSmokeTest.php +++ b/apps/platform/tests/Browser/Dashboard/TenantDashboardProductizationSmokeTest.php @@ -59,7 +59,7 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]); diff --git a/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php b/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php index 3320e264..23d549ff 100644 --- a/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php +++ b/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php @@ -72,12 +72,12 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantPublished->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantPublished->workspace_id => (int) $tenantPublished->getKey(), ], ]); - visit(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $publishedReview], $tenantPublished)) + visit(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $publishedReview], $tenantPublished)) ->waitForText('Verwandter Kontext') ->assertSee('Kunden-Workspace öffnen') ->assertNoJavaScriptErrors() diff --git a/apps/platform/tests/Browser/Spec174EvidenceFreshnessPublicationTrustSmokeTest.php b/apps/platform/tests/Browser/Spec174EvidenceFreshnessPublicationTrustSmokeTest.php index f86e7e32..6bda2eab 100644 --- a/apps/platform/tests/Browser/Spec174EvidenceFreshnessPublicationTrustSmokeTest.php +++ b/apps/platform/tests/Browser/Spec174EvidenceFreshnessPublicationTrustSmokeTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\ReviewPackResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\ManagedEnvironment; use App\Support\EnvironmentReviewCompletenessState; use App\Support\EnvironmentReviewStatus; @@ -90,7 +90,7 @@ ->assertSee('Stale') ->assertSee('Refresh the stale evidence before relying on this snapshot'); - visit(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $partialReview], $partialTenant)) + visit(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $partialReview], $partialTenant)) ->waitForText('Outcome summary') ->assertNoJavaScriptErrors() ->assertSee('Internal only') diff --git a/apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php b/apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php index 08aa96e7..4dc3be8d 100644 --- a/apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php +++ b/apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php @@ -38,7 +38,7 @@ $this->actingAs($fixture['user'])->withSession([ WorkspaceContext::SESSION_KEY => (int) $fixture['workspace']->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $fixture['workspace']->getKey() => (int) $fixture['visibleTenant']->getKey(), ], ]); @@ -97,7 +97,7 @@ $this->actingAs($viewer)->withSession([ WorkspaceContext::SESSION_KEY => (int) $fixture['workspace']->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $fixture['workspace']->getKey() => (int) $fixture['visibleTenant']->getKey(), ], ]); @@ -134,7 +134,7 @@ $this->actingAs($fixture['user'])->withSession([ WorkspaceContext::SESSION_KEY => (int) $fixture['workspace']->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $fixture['workspace']->getKey() => (int) $fixture['visibleTenant']->getKey(), ], ]); diff --git a/apps/platform/tests/Browser/Spec192RecordPageHeaderDisciplineSmokeTest.php b/apps/platform/tests/Browser/Spec192RecordPageHeaderDisciplineSmokeTest.php index a98689fa..7239c08d 100644 --- a/apps/platform/tests/Browser/Spec192RecordPageHeaderDisciplineSmokeTest.php +++ b/apps/platform/tests/Browser/Spec192RecordPageHeaderDisciplineSmokeTest.php @@ -6,12 +6,12 @@ use App\Filament\Resources\BackupSetResource; use App\Filament\Resources\BaselineProfileResource; use App\Filament\Resources\BaselineSnapshotResource; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\FindingExceptionResource; +use App\Filament\Resources\ManagedEnvironmentResource; use App\Filament\Resources\PolicyVersionResource; use App\Filament\Resources\ReviewPackResource; -use App\Filament\Resources\ManagedEnvironmentResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\Workspaces\WorkspaceResource; use App\Models\AlertDestination; use App\Models\BackupSet; @@ -20,11 +20,11 @@ use App\Models\BaselineSnapshotItem; use App\Models\EvidenceSnapshot; use App\Models\Finding; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\Policy; use App\Models\PolicyVersion; use App\Models\ReviewPack; -use App\Models\ManagedEnvironment; use App\Models\User; use App\Services\Findings\FindingExceptionService; use App\Support\Evidence\EvidenceCompletenessState; @@ -157,7 +157,7 @@ function spec192ApprovedFindingException(ManagedEnvironment $tenant, User $reque ->assertSee('Open finding') ->assertSee('Renew exception'); - visit(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + visit(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->waitForText('Related context') ->assertNoJavaScriptErrors() ->assertScript("document.querySelectorAll('[data-supporting-group-kind]').length === 0", true) diff --git a/apps/platform/tests/Browser/Spec194GovernanceFrictionSmokeTest.php b/apps/platform/tests/Browser/Spec194GovernanceFrictionSmokeTest.php index 92d134f2..4328f2b5 100644 --- a/apps/platform/tests/Browser/Spec194GovernanceFrictionSmokeTest.php +++ b/apps/platform/tests/Browser/Spec194GovernanceFrictionSmokeTest.php @@ -3,10 +3,10 @@ declare(strict_types=1); use App\Filament\Pages\Monitoring\FindingExceptionsQueue; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\FindingExceptionResource; use App\Filament\Resources\ManagedEnvironmentResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\EvidenceSnapshot; use App\Models\Finding; use App\Models\FindingException; @@ -17,13 +17,13 @@ use App\Models\User; use App\Services\Findings\FindingExceptionService; use App\Support\Auth\PlatformCapabilities; +use App\Support\EnvironmentReviewCompletenessState; +use App\Support\EnvironmentReviewStatus; use App\Support\Evidence\EvidenceCompletenessState; use App\Support\Evidence\EvidenceSnapshotStatus; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; use App\Support\System\SystemOperationRunLinks; -use App\Support\EnvironmentReviewCompletenessState; -use App\Support\EnvironmentReviewStatus; use Illuminate\Foundation\Testing\RefreshDatabase; pest()->browser()->timeout(20_000); @@ -194,7 +194,7 @@ function spec194RelativeRedirect(string $redirect): string ->assertSee('Renew exception') ->assertSee('Revoke exception'); - visit(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + visit(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->waitForText('Related context') ->assertNoJavaScriptErrors() ->assertNoConsoleLogs() diff --git a/apps/platform/tests/Browser/Spec198MonitoringPageStateSmokeTest.php b/apps/platform/tests/Browser/Spec198MonitoringPageStateSmokeTest.php index 79922a25..b7f99c3f 100644 --- a/apps/platform/tests/Browser/Spec198MonitoringPageStateSmokeTest.php +++ b/apps/platform/tests/Browser/Spec198MonitoringPageStateSmokeTest.php @@ -88,7 +88,7 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]); @@ -154,7 +154,7 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]); diff --git a/apps/platform/tests/Browser/Spec277StoredReportsSurfaceSmokeTest.php b/apps/platform/tests/Browser/Spec277StoredReportsSurfaceSmokeTest.php index e706d335..3ae9a007 100644 --- a/apps/platform/tests/Browser/Spec277StoredReportsSurfaceSmokeTest.php +++ b/apps/platform/tests/Browser/Spec277StoredReportsSurfaceSmokeTest.php @@ -36,7 +36,7 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]); diff --git a/apps/platform/tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php b/apps/platform/tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php index 82b85374..973673b1 100644 --- a/apps/platform/tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php +++ b/apps/platform/tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php @@ -51,7 +51,7 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]); diff --git a/apps/platform/tests/Browser/Spec283ProviderCapabilityRegistrySmokeTest.php b/apps/platform/tests/Browser/Spec283ProviderCapabilityRegistrySmokeTest.php index d62064cb..682ee515 100644 --- a/apps/platform/tests/Browser/Spec283ProviderCapabilityRegistrySmokeTest.php +++ b/apps/platform/tests/Browser/Spec283ProviderCapabilityRegistrySmokeTest.php @@ -15,7 +15,7 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]); diff --git a/apps/platform/tests/Browser/Spec284ArtifactSourceTaxonomySmokeTest.php b/apps/platform/tests/Browser/Spec284ArtifactSourceTaxonomySmokeTest.php index 6966bc8b..80c774cf 100644 --- a/apps/platform/tests/Browser/Spec284ArtifactSourceTaxonomySmokeTest.php +++ b/apps/platform/tests/Browser/Spec284ArtifactSourceTaxonomySmokeTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\FindingResource; use App\Filament\Resources\InventoryItemResource; use App\Filament\Resources\StoredReportResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\Finding; use App\Models\InventoryItem; use App\Models\StoredReport; @@ -41,7 +41,7 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]); diff --git a/apps/platform/tests/Browser/Spec300ManagedEnvironmentNamingConsolidationSmokeTest.php b/apps/platform/tests/Browser/Spec300ManagedEnvironmentNamingConsolidationSmokeTest.php index 2abf6dc3..ce96cbde 100644 --- a/apps/platform/tests/Browser/Spec300ManagedEnvironmentNamingConsolidationSmokeTest.php +++ b/apps/platform/tests/Browser/Spec300ManagedEnvironmentNamingConsolidationSmokeTest.php @@ -50,7 +50,7 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $workspace->getKey() => (int) $environment->getKey(), ], ]); diff --git a/apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php b/apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php index 7bb0e629..39f5b30e 100644 --- a/apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php +++ b/apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php @@ -20,13 +20,13 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Browser/Spec303AdminDirectoryGroupsCutoverSmokeTest.php b/apps/platform/tests/Browser/Spec303AdminDirectoryGroupsCutoverSmokeTest.php index 58720221..e39068ad 100644 --- a/apps/platform/tests/Browser/Spec303AdminDirectoryGroupsCutoverSmokeTest.php +++ b/apps/platform/tests/Browser/Spec303AdminDirectoryGroupsCutoverSmokeTest.php @@ -48,7 +48,7 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $environment->workspace_id => (int) $environment->getKey(), ], ]); diff --git a/apps/platform/tests/Browser/Spec314WorkspaceHubNavigationContextSmokeTest.php b/apps/platform/tests/Browser/Spec314WorkspaceHubNavigationContextSmokeTest.php index 08b40bf0..356aa6b8 100644 --- a/apps/platform/tests/Browser/Spec314WorkspaceHubNavigationContextSmokeTest.php +++ b/apps/platform/tests/Browser/Spec314WorkspaceHubNavigationContextSmokeTest.php @@ -24,13 +24,13 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $workspace->getKey() => (int) $environment->getKey(), ], ]); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspace->getKey() => (int) $environment->getKey(), ]); diff --git a/apps/platform/tests/Browser/Spec316WorkspaceHubClearFilterSmokeTest.php b/apps/platform/tests/Browser/Spec316WorkspaceHubClearFilterSmokeTest.php index c0093f67..e4e2e040 100644 --- a/apps/platform/tests/Browser/Spec316WorkspaceHubClearFilterSmokeTest.php +++ b/apps/platform/tests/Browser/Spec316WorkspaceHubClearFilterSmokeTest.php @@ -32,13 +32,13 @@ $this->actingAs($user)->withSession([ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_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, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspace->getKey() => (int) $environmentA->getKey(), ]); @@ -77,7 +77,7 @@ 'wide_text' => $environmentB->name, ], 'customer review workspace' => [ - 'filtered_url' => CustomerReviewWorkspace::tenantPrefilterUrl($environmentA), + 'filtered_url' => CustomerReviewWorkspace::environmentFilterUrl($environmentA), 'clean_url' => CustomerReviewWorkspace::getUrl(panel: 'admin'), 'wide_text' => $environmentB->name, ], @@ -148,7 +148,7 @@ FindingExceptionsQueue::getUrl(panel: 'admin', parameters: [ 'environment_id' => (int) $environmentA->getKey(), ]), - CustomerReviewWorkspace::tenantPrefilterUrl($environmentA), + CustomerReviewWorkspace::environmentFilterUrl($environmentA), route('admin.evidence.overview', [ 'environment_id' => (int) $environmentA->getKey(), ]), diff --git a/apps/platform/tests/Feature/078/RelatedLinksOnDetailTest.php b/apps/platform/tests/Feature/078/RelatedLinksOnDetailTest.php index 6c79cc74..7beeaa13 100644 --- a/apps/platform/tests/Feature/078/RelatedLinksOnDetailTest.php +++ b/apps/platform/tests/Feature/078/RelatedLinksOnDetailTest.php @@ -4,9 +4,9 @@ use App\Filament\Resources\InventoryItemResource; use App\Filament\Resources\RestoreRunResource; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\RestoreRun; -use App\Models\ManagedEnvironment; use App\Support\OperationRunLinks; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; @@ -96,13 +96,13 @@ public function test_does_not_show_legacy_admin_details_cta_and_keeps_canonical_ public function test_hides_tenant_scoped_follow_up_links_when_the_run_tenant_is_not_selector_eligible(): void { - $activeTenant = ManagedEnvironment::factory()->active()->create([ + $activeEnvironment = ManagedEnvironment::factory()->active()->create([ 'name' => 'Viewer Active ManagedEnvironment', ]); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Viewer Onboarding ManagedEnvironment', ]); @@ -115,13 +115,13 @@ public function test_hides_tenant_scoped_follow_up_links_when_the_run_tenant_is_ ); $run = OperationRun::factory()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'managed_environment_id' => (int) $onboardingTenant->getKey(), 'type' => 'inventory_sync', ]); $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) ->get(\App\Support\OperationRunLinks::tenantlessView($run)) ->assertOk() ->assertSee('Open') diff --git a/apps/platform/tests/Feature/144/CanonicalOperationViewerContextMismatchTest.php b/apps/platform/tests/Feature/144/CanonicalOperationViewerContextMismatchTest.php index 9dc66e8f..5c32e53a 100644 --- a/apps/platform/tests/Feature/144/CanonicalOperationViewerContextMismatchTest.php +++ b/apps/platform/tests/Feature/144/CanonicalOperationViewerContextMismatchTest.php @@ -107,14 +107,14 @@ public function test_keeps_onboarding_tenant_runs_viewable_with_lifecycle_aware_ public function test_keeps_archived_tenant_runs_viewable_with_lifecycle_aware_context(): void { - $activeTenant = ManagedEnvironment::factory()->create([ + $activeEnvironment = ManagedEnvironment::factory()->create([ 'name' => 'Active ManagedEnvironment', ]); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $archivedTenant = ManagedEnvironment::factory()->create([ 'name' => 'Archived ManagedEnvironment', - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'status' => ManagedEnvironment::STATUS_ACTIVE, ]); @@ -122,7 +122,7 @@ public function test_keeps_archived_tenant_runs_viewable_with_lifecycle_aware_co $archivedTenant->delete(); $run = OperationRun::factory()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'managed_environment_id' => (int) $archivedTenant->getKey(), 'type' => 'inventory_sync', 'status' => OperationRunStatus::Completed->value, @@ -132,7 +132,7 @@ public function test_keeps_archived_tenant_runs_viewable_with_lifecycle_aware_co setAdminPanelContext(); $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) ->get(\App\Support\OperationRunLinks::tenantlessView($run)) ->assertOk() ->assertSee('Operation environment is not available in the current environment selector') diff --git a/apps/platform/tests/Feature/Alerts/AlertDeliveryDeepLinkFiltersTest.php b/apps/platform/tests/Feature/Alerts/AlertDeliveryDeepLinkFiltersTest.php index ede88673..18ca8f7a 100644 --- a/apps/platform/tests/Feature/Alerts/AlertDeliveryDeepLinkFiltersTest.php +++ b/apps/platform/tests/Feature/Alerts/AlertDeliveryDeepLinkFiltersTest.php @@ -200,7 +200,7 @@ function alertDeliveryFilterIndicatorLabels($component): array ->assertCanNotSeeTableRecords([$failedDelivery]); }); -it('keeps persisted alert delivery filters tenantless when remembered tenant context changes', function (): void { +it('keeps persisted alert delivery filters tenantless when remembered environment context changes', function (): void { $tenantA = ManagedEnvironment::factory()->create(); [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner'); @@ -241,7 +241,7 @@ function alertDeliveryFilterIndicatorLabels($component): array Filament::setTenant(null, true); session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantA->getKey(), ]); @@ -254,7 +254,7 @@ function alertDeliveryFilterIndicatorLabels($component): array expect(data_get(session()->get($component->instance()->getTableFiltersSessionKey()), 'status.value')) ->toBe(AlertDelivery::STATUS_SENT); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/Auth/AdminLocalSmokeLoginTest.php b/apps/platform/tests/Feature/Auth/AdminLocalSmokeLoginTest.php index fd68f501..5d203d4b 100644 --- a/apps/platform/tests/Feature/Auth/AdminLocalSmokeLoginTest.php +++ b/apps/platform/tests/Feature/Auth/AdminLocalSmokeLoginTest.php @@ -32,7 +32,7 @@ expect(session(App\Support\Workspaces\WorkspaceContext::SESSION_KEY))->toBe((int) $tenant->workspace_id) ->and(session(SuppressDebugbarForSmokeRequests::SESSION_KEY)) ->toBe(SuppressDebugbarForSmokeRequests::COOKIE_VALUE) - ->and(data_get(session(App\Support\Workspaces\WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY), (string) $tenant->workspace_id)) + ->and(data_get(session(App\Support\Workspaces\WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY), (string) $tenant->workspace_id)) ->toBe((int) $tenant->getKey()); $this->get(EnvironmentDashboard::getUrl(tenant: $tenant))->assertSuccessful(); @@ -90,4 +90,4 @@ }); expect($normalMiddlewareState)->toBeTrue(); -}); \ No newline at end of file +}); diff --git a/apps/platform/tests/Feature/Auth/TenantChooserSelectionTest.php b/apps/platform/tests/Feature/Auth/TenantChooserSelectionTest.php index 9e7fade5..d9127db5 100644 --- a/apps/platform/tests/Feature/Auth/TenantChooserSelectionTest.php +++ b/apps/platform/tests/Feature/Auth/TenantChooserSelectionTest.php @@ -13,25 +13,25 @@ uses(RefreshDatabase::class); it('shows only active tenants in the standard chooser and persists the last-used tenant', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Active ManagedEnvironment']); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + $activeEnvironment = ManagedEnvironment::factory()->active()->create(['name' => 'Active ManagedEnvironment']); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $this->actingAs($user); $otherActiveTenant = ManagedEnvironment::factory()->active()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Other Active ManagedEnvironment', ]); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Onboarding ManagedEnvironment', ]); $draftTenant = ManagedEnvironment::factory()->draft()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Draft ManagedEnvironment', ]); $archivedTenant = ManagedEnvironment::factory()->archived()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Archived ManagedEnvironment', ]); @@ -40,7 +40,7 @@ createUserWithTenant(tenant: $draftTenant, user: $user, role: 'owner', ensureDefaultMicrosoftProviderConnection: false); createUserWithTenant(tenant: $archivedTenant, user: $user, role: 'owner', ensureDefaultMicrosoftProviderConnection: false); - session()->put(WorkspaceContext::SESSION_KEY, (int) $activeTenant->workspace_id); + session()->put(WorkspaceContext::SESSION_KEY, (int) $activeEnvironment->workspace_id); $this->get('/admin/choose-environment') ->assertSuccessful() @@ -51,20 +51,20 @@ ->assertDontSee('Archived ManagedEnvironment'); Livewire::test(ChooseEnvironment::class) - ->call('selectEnvironment', (int) $activeTenant->getKey()) - ->assertRedirect(EnvironmentDashboard::getUrl(tenant: $activeTenant)); + ->call('selectEnvironment', (int) $activeEnvironment->getKey()) + ->assertRedirect(EnvironmentDashboard::getUrl(tenant: $activeEnvironment)); $user->refresh(); if (Schema::hasColumn('users', 'last_tenant_id')) { - expect($user->last_tenant_id)->toBe($activeTenant->getKey()); + expect($user->last_tenant_id)->toBe($activeEnvironment->getKey()); return; } if (Schema::hasTable('user_managed_environment_preferences')) { $preference = $user->tenantPreferences() - ->where('managed_environment_id', $activeTenant->getKey()) + ->where('managed_environment_id', $activeEnvironment->getKey()) ->first(); expect($preference)->not->toBeNull(); diff --git a/apps/platform/tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php b/apps/platform/tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php index 0de929be..b55971c0 100644 --- a/apps/platform/tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php +++ b/apps/platform/tests/Feature/Auth/WorkspaceFirstManagedEnvironmentAccessTest.php @@ -72,7 +72,7 @@ app(CapabilityResolver::class)->clearCache(); app(ManagedEnvironmentAccessScopeResolver::class)->clearCache(); - app(WorkspaceContext::class)->rememberLastTenantId((int) $workspace->getKey(), (int) $deniedTenant->getKey()); + app(WorkspaceContext::class)->rememberLastEnvironmentId((int) $workspace->getKey(), (int) $deniedTenant->getKey()); /** @var \Filament\Panel $panel */ $panel = app(PanelRegistry::class)->get('admin'); @@ -83,7 +83,7 @@ expect($defaultTenant?->getKey())->toBe($allowedTenant->getKey()) ->and($allowedDecision->workspaceRole)->toBe('manager') ->and($allowedDecision->capabilityAllowed)->toBeTrue() - ->and(app(WorkspaceContext::class)->lastTenantId())->toBeNull() + ->and(app(WorkspaceContext::class)->lastEnvironmentId())->toBeNull() ->and($tenants)->toHaveCount(1) ->and($tenants->first()?->getKey())->toBe($allowedTenant->getKey()); }); diff --git a/apps/platform/tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php b/apps/platform/tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php index b6e8e250..1a2d11bd 100644 --- a/apps/platform/tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php +++ b/apps/platform/tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php @@ -9,8 +9,8 @@ use App\Filament\Resources\EnvironmentReviewResource; use App\Models\BaselineProfile; use App\Models\BaselineSnapshot; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Models\User; use App\Models\Workspace; use App\Models\WorkspaceMembership; @@ -87,7 +87,7 @@ $review = composeEnvironmentReviewForTest($targetTenant, $reviewOwner); $this->actingAs($member) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $targetTenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $targetTenant)) ->assertNotFound(); }); diff --git a/apps/platform/tests/Feature/BackupScheduling/BackupScheduleAdminTenantParityTest.php b/apps/platform/tests/Feature/BackupScheduling/BackupScheduleAdminTenantParityTest.php index e62ad983..11cf9b08 100644 --- a/apps/platform/tests/Feature/BackupScheduling/BackupScheduleAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/BackupScheduling/BackupScheduleAdminTenantParityTest.php @@ -50,7 +50,7 @@ $session = [ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]; diff --git a/apps/platform/tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php b/apps/platform/tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php index 38fc9aa7..58898961 100644 --- a/apps/platform/tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php +++ b/apps/platform/tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php @@ -9,8 +9,8 @@ use App\Models\BaselineSnapshotItem; use App\Models\BaselineTenantAssignment; use App\Models\Finding; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Models\User; use App\Models\Workspace; use App\Models\WorkspaceMembership; @@ -240,7 +240,7 @@ protected function baselineCompareMatrixGap(string $policyType, string $subjectK ], $overrides)); } - protected function setAdminWorkspaceContext(User $user, Workspace|int $workspace, ?ManagedEnvironment $rememberedTenant = null): array + protected function setAdminWorkspaceContext(User $user, Workspace|int $workspace, ?ManagedEnvironment $rememberedEnvironment = null): array { $workspaceId = $workspace instanceof Workspace ? (int) $workspace->getKey() : (int) $workspace; @@ -248,18 +248,18 @@ protected function setAdminWorkspaceContext(User $user, Workspace|int $workspace WorkspaceContext::SESSION_KEY => $workspaceId, ]; - if ($rememberedTenant instanceof ManagedEnvironment) { - $session[WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY] = [ - (string) $workspaceId => (int) $rememberedTenant->getKey(), + if ($rememberedEnvironment instanceof ManagedEnvironment) { + $session[WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY] = [ + (string) $workspaceId => (int) $rememberedEnvironment->getKey(), ]; } $this->actingAs($user)->withSession($session); session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - if ($rememberedTenant instanceof ManagedEnvironment) { - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ - (string) $workspaceId => (int) $rememberedTenant->getKey(), + if ($rememberedEnvironment instanceof ManagedEnvironment) { + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ + (string) $workspaceId => (int) $rememberedEnvironment->getKey(), ]); } diff --git a/apps/platform/tests/Feature/Dashboard/TenantDashboardProductizationReadinessTest.php b/apps/platform/tests/Feature/Dashboard/TenantDashboardProductizationReadinessTest.php index f7e32f8e..b1fcc6d1 100644 --- a/apps/platform/tests/Feature/Dashboard/TenantDashboardProductizationReadinessTest.php +++ b/apps/platform/tests/Feature/Dashboard/TenantDashboardProductizationReadinessTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Pages\EnvironmentDashboard; +use App\Filament\Pages\Reviews\CustomerReviewWorkspace; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\ReviewPackResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Widgets\Dashboard\RecoveryReadiness; use App\Models\BackupItem; use App\Models\BackupSet; @@ -14,8 +14,8 @@ use App\Models\ProviderConnection; use App\Models\ReviewPack; use App\Services\Intune\ManagedEnvironmentRequiredPermissionsViewModelBuilder; -use App\Support\Links\RequiredPermissionsLinks; use App\Support\EnvironmentDashboard\EnvironmentDashboardSummaryBuilder; +use App\Support\Links\RequiredPermissionsLinks; use Filament\Facades\Filament; use Livewire\Livewire; @@ -83,7 +83,7 @@ function mockEnvironmentDashboardReadinessPermissions(array $overview = []): voi ->not->toBeNull() ->and($outputCard['status'])->toBe('Evidence available') ->and($outputCard['actionLabel'])->toBe('View export artifacts') - ->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant)); + ->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::environmentFilterUrl($tenant)); }); it('links ready customer-safe output directly to the latest review pack', function (): void { @@ -177,13 +177,13 @@ function mockEnvironmentDashboardReadinessPermissions(array $overview = []): voi expect($currentReview) ->not->toBeNull() - ->and($currentReview['actionUrl'])->toBe(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + ->and($currentReview['actionUrl'])->toBe(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->and($evidenceCoverage) ->not->toBeNull() ->and($evidenceCoverage['actionUrl'])->toBe(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin', tenant: $tenant)) ->and($outputCard) ->not->toBeNull() - ->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant)) + ->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::environmentFilterUrl($tenant)) ->and($providerHealth) ->not->toBeNull() ->and($providerHealth['actionUrl'])->toBe(RequiredPermissionsLinks::requiredPermissions($tenant)) @@ -315,7 +315,7 @@ function mockEnvironmentDashboardReadinessPermissions(array $overview = []): voi ->not->toBeNull() ->and($currentReview['status'])->toBe('No active review') ->and($currentReview['body'])->toBe('There is currently no review in progress for this environment.') - ->and($currentReview['actionUrl'])->toBe(EnvironmentReviewResource::tenantScopedUrl('index', tenant: $tenant)) + ->and($currentReview['actionUrl'])->toBe(EnvironmentReviewResource::environmentScopedUrl('index', tenant: $tenant)) ->and($providerHealth) ->not->toBeNull() ->and($providerHealth['status'])->toBe('Provider status unavailable') @@ -326,7 +326,7 @@ function mockEnvironmentDashboardReadinessPermissions(array $overview = []): voi ->and($outputCard['status'])->toBe('No customer-safe output available') ->and($outputCard['body'])->toBe('Generate a review pack once review and evidence are ready for handoff.') ->and($outputCard['actionLabel'])->toBe('View export artifacts') - ->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant)) + ->and($outputCard['actionUrl'])->toBe(CustomerReviewWorkspace::environmentFilterUrl($tenant)) ->and($evidenceCoverage) ->not->toBeNull() ->and($evidenceCoverage['value'])->toBe('Unavailable') diff --git a/apps/platform/tests/Feature/DirectoryGroups/NoLiveGraphOnRenderTest.php b/apps/platform/tests/Feature/DirectoryGroups/NoLiveGraphOnRenderTest.php index ce93d5a8..7e30b037 100644 --- a/apps/platform/tests/Feature/DirectoryGroups/NoLiveGraphOnRenderTest.php +++ b/apps/platform/tests/Feature/DirectoryGroups/NoLiveGraphOnRenderTest.php @@ -1,9 +1,9 @@ withSession([ WorkspaceContext::SESSION_KEY => (int) $this->tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $this->tenant->workspace_id => (int) $this->tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Drift/DriftFindingDiffUnavailableTest.php b/apps/platform/tests/Feature/Drift/DriftFindingDiffUnavailableTest.php index bb3a0530..032c0b18 100644 --- a/apps/platform/tests/Feature/Drift/DriftFindingDiffUnavailableTest.php +++ b/apps/platform/tests/Feature/Drift/DriftFindingDiffUnavailableTest.php @@ -42,7 +42,7 @@ $response = $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) @@ -114,7 +114,7 @@ $response = $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) @@ -187,7 +187,7 @@ $response = $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewAuditLogTest.php b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewAuditLogTest.php index a86dd890..6272c59f 100644 --- a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewAuditLogTest.php +++ b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewAuditLogTest.php @@ -2,14 +2,14 @@ declare(strict_types=1); -use App\Jobs\GenerateReviewPackJob; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\EnvironmentReviewResource; +use App\Jobs\GenerateReviewPackJob; use App\Models\AuditLog; use App\Models\EvidenceSnapshot; -use App\Services\ReviewPackService; use App\Services\EnvironmentReviews\EnvironmentReviewLifecycleService; use App\Services\EnvironmentReviews\EnvironmentReviewService; +use App\Services\ReviewPackService; use App\Support\Audit\AuditActionId; use Illuminate\Support\Facades\Storage; @@ -117,7 +117,7 @@ setAdminEnvironmentContext($tenant); $this->actingAs($user) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([ + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([ CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, 'tenant_filter_id' => (string) $tenant->getKey(), diff --git a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php index fc10eee1..5b487a52 100644 --- a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php +++ b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExecutivePackTest.php @@ -58,7 +58,7 @@ function spec308SeedExpiredDecisionFinding(ManagedEnvironment $tenant, User $req $review = composeEnvironmentReviewForTest($tenant, $user); $this->actingAs($user) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->assertOk() ->assertSee('Executive posture') ->assertSee('Executive summary') @@ -171,7 +171,7 @@ function spec308SeedExpiredDecisionFinding(ManagedEnvironment $tenant, User $req ); $this->actingAs($user) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant).'?customer_workspace=1&source_surface=customer_review_workspace') + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant).'?customer_workspace=1&source_surface=customer_review_workspace') ->assertOk() ->assertSee('Governance decisions requiring awareness') ->assertSee('Privileged role exception') diff --git a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExplanationSurfaceTest.php b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExplanationSurfaceTest.php index 96f7deba..af40dbb6 100644 --- a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExplanationSurfaceTest.php +++ b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewExplanationSurfaceTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use App\Filament\Pages\Reviews\ReviewRegister; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; +use App\Filament\Pages\Reviews\ReviewRegister; use App\Filament\Resources\EnvironmentReviewResource; use App\Models\ManagedEnvironment; use App\Support\OperationRunLinks; @@ -46,7 +46,7 @@ setAdminEnvironmentContext($tenant); $this->actingAs($user) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->assertOk() ->assertSee($detailOutcome?->primaryReason ?? '') ->assertSee($explanation?->nextActionText ?? '') @@ -82,7 +82,7 @@ setAdminEnvironmentContext($tenant); $this->actingAs($user) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([ + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([ CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, 'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE, 'tenant_filter_id' => (string) $tenant->getKey(), diff --git a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRbacTest.php b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRbacTest.php index a549c908..4911c533 100644 --- a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRbacTest.php +++ b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRbacTest.php @@ -18,11 +18,11 @@ $review = composeEnvironmentReviewForTest($targetTenant, $reviewOwner); $this->actingAs($member) - ->get(EnvironmentReviewResource::tenantScopedUrl('index', tenant: $targetTenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('index', tenant: $targetTenant)) ->assertNotFound(); $this->actingAs($member) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $targetTenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $targetTenant)) ->assertNotFound(); }); @@ -33,7 +33,7 @@ $review = composeEnvironmentReviewForTest($tenant, $owner); $this->actingAs($readonly) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->assertOk(); setAdminEnvironmentContext($tenant); diff --git a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRegisterPrefilterTest.php b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRegisterPrefilterTest.php index c1243bad..173fe1e9 100644 --- a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRegisterPrefilterTest.php +++ b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRegisterPrefilterTest.php @@ -7,7 +7,7 @@ use App\Support\Workspaces\WorkspaceContext; use Livewire\Livewire; -it('keeps the canonical review register unfiltered when remembered tenant context is available', function (): void { +it('keeps the canonical review register unfiltered when remembered environment context is available', function (): void { $tenantA = ManagedEnvironment::factory()->create(['name' => 'Alpha ManagedEnvironment']); [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner'); @@ -34,7 +34,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRegisterTest.php b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRegisterTest.php index eafd7c36..6c9350f7 100644 --- a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRegisterTest.php +++ b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewRegisterTest.php @@ -85,7 +85,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -97,7 +97,7 @@ ->assertCanSeeTableRecords([$reviewA]) ->assertCanNotSeeTableRecords([$reviewB]); - expect(app(WorkspaceContext::class)->lastTenantId())->toBe((int) $tenantA->getKey()); + expect(app(WorkspaceContext::class)->lastEnvironmentId())->toBe((int) $tenantA->getKey()); $component ->callAction('clear_filters') @@ -110,7 +110,7 @@ ->assertSet('tableFilters.managed_environment_id.value', null) ->assertCanSeeTableRecords([$reviewA, $reviewB]); - expect(app(WorkspaceContext::class)->lastTenantId())->toBe((int) $tenantA->getKey()); + expect(app(WorkspaceContext::class)->lastEnvironmentId())->toBe((int) $tenantA->getKey()); }); it('keeps stale and partial review rows aligned with environment review detail trust', function (): void { diff --git a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewUiContractTest.php b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewUiContractTest.php index fec81573..aa61a7a4 100644 --- a/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewUiContractTest.php +++ b/apps/platform/tests/Feature/EnvironmentReview/EnvironmentReviewUiContractTest.php @@ -2,17 +2,17 @@ declare(strict_types=1); -use App\Filament\Pages\Reviews\ReviewRegister; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; +use App\Filament\Pages\Reviews\ReviewRegister; use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EnvironmentReviewResource\Pages\ListEnvironmentReviews; use App\Filament\Resources\EnvironmentReviewResource\Pages\ViewEnvironmentReview; -use App\Models\ReviewPack; -use App\Models\ManagedEnvironment; use App\Models\EnvironmentReview; +use App\Models\ManagedEnvironment; +use App\Models\ReviewPack; use App\Models\User; -use App\Support\EnvironmentReviewStatus; use App\Services\EnvironmentReviews\EnvironmentReviewLifecycleService; +use App\Support\EnvironmentReviewStatus; use App\Support\Ui\GovernanceActions\GovernanceActionCatalog; use App\Support\Workspaces\WorkspaceContext; use Filament\Actions\Action; @@ -89,7 +89,7 @@ function environmentReviewContractHeaderActions(Testable $component): array expect($rowActionNames)->toEqualCanonicalizing(['export_executive_pack']) ->and($table->getBulkActions())->toBeEmpty() - ->and($table->getRecordUrl($review))->toBe(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)); + ->and($table->getRecordUrl($review))->toBe(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)); }); it('requires confirmation for destructive environment-review actions and preserves disabled management visibility for readonly users', function (): void { @@ -145,7 +145,7 @@ function environmentReviewContractHeaderActions(Testable $component): array setAdminEnvironmentContext($tenant); $this->actingAs($owner) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->assertOk() ->assertSee('Related context') ->assertSee('Evidence snapshot'); @@ -198,7 +198,7 @@ function environmentReviewContractHeaderActions(Testable $component): array setAdminEnvironmentContext($tenant); $this->actingAs($user) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([ + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([ CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, ])) ->assertOk() @@ -253,7 +253,7 @@ function environmentReviewContractHeaderActions(Testable $component): array ]); $this->actingAs($owner) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->assertOk() ->assertSee('Outcome summary') ->assertDontSee('Artifact truth') @@ -274,7 +274,7 @@ function environmentReviewContractHeaderActions(Testable $component): array $review = $this->makeInternalOnlyArtifactTruthReview($tenant, $owner); $this->actingAs($owner) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->assertOk() ->assertSee('Internal only') ->assertSee('Publication readiness') diff --git a/apps/platform/tests/Feature/Filament/AdminSharedSurfacePanelParityTest.php b/apps/platform/tests/Feature/Filament/AdminSharedSurfacePanelParityTest.php index a9547f15..3940c682 100644 --- a/apps/platform/tests/Feature/Filament/AdminSharedSurfacePanelParityTest.php +++ b/apps/platform/tests/Feature/Filament/AdminSharedSurfacePanelParityTest.php @@ -28,7 +28,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -41,7 +41,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantB->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantB->workspace_id => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseEnvironmentTest.php b/apps/platform/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseEnvironmentTest.php index e7be25fb..b11d4c6d 100644 --- a/apps/platform/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseEnvironmentTest.php +++ b/apps/platform/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseEnvironmentTest.php @@ -96,7 +96,7 @@ ->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Filament/AdminTenantSurfaceParityTest.php b/apps/platform/tests/Feature/Filament/AdminTenantSurfaceParityTest.php index 07801fc1..c362463b 100644 --- a/apps/platform/tests/Feature/Filament/AdminTenantSurfaceParityTest.php +++ b/apps/platform/tests/Feature/Filament/AdminTenantSurfaceParityTest.php @@ -6,9 +6,9 @@ use App\Filament\Resources\PolicyResource; use App\Filament\Resources\PolicyVersionResource; use App\Models\BackupSet; +use App\Models\ManagedEnvironment; use App\Models\Policy; use App\Models\PolicyVersion; -use App\Models\ManagedEnvironment; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -31,13 +31,13 @@ $session = [ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]; session()->put(WorkspaceContext::SESSION_KEY, $session[WorkspaceContext::SESSION_KEY]); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, $session[WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY]); + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, $session[WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY]); expect(PolicyResource::getEloquentQuery()->whereKey($allowedPolicy->getKey())->exists())->toBeTrue(); expect(BackupSetResource::getEloquentQuery()->whereKey($blockedBackupSet->getKey())->exists())->toBeFalse(); @@ -62,13 +62,13 @@ $session = [ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]; session()->put(WorkspaceContext::SESSION_KEY, $session[WorkspaceContext::SESSION_KEY]); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, $session[WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY]); + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, $session[WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY]); expect(PolicyVersionResource::getEloquentQuery()->whereKey($allowedVersion->getKey())->exists())->toBeTrue(); expect(PolicyVersionResource::getEloquentQuery()->whereKey($blockedVersion->getKey())->exists())->toBeFalse(); diff --git a/apps/platform/tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php b/apps/platform/tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php index 95cd40ab..d67bde0c 100644 --- a/apps/platform/tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php +++ b/apps/platform/tests/Feature/Filament/Alerts/AlertDeliveryViewerTest.php @@ -265,7 +265,7 @@ function getAlertDeliveryHeaderAction(Testable $component, string $name): ?Actio ->assertSet('tableFilters.managed_environment_id.value', null); }); -it('keeps alert deliveries workspace-wide when only remembered tenant context exists', function (): void { +it('keeps alert deliveries workspace-wide when only remembered environment context exists', function (): void { $tenantA = ManagedEnvironment::factory()->create(); [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner'); @@ -302,7 +302,7 @@ function getAlertDeliveryHeaderAction(Testable $component, string $name): ?Actio ]); Filament::setTenant(null, true); - app(WorkspaceContext::class)->rememberLastTenantId($workspaceId, (int) $tenantA->getKey()); + app(WorkspaceContext::class)->rememberLastEnvironmentId($workspaceId, (int) $tenantA->getKey()); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => $workspaceId]); diff --git a/apps/platform/tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php b/apps/platform/tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php index ebd81a01..70495047 100644 --- a/apps/platform/tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php +++ b/apps/platform/tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php @@ -24,7 +24,7 @@ function alertsKpiValues($component): array ->all(); } -it('shows workspace-wide KPI deliveries when context is set via lastTenantId fallback only', function (): void { +it('shows workspace-wide KPI deliveries when context is set via lastEnvironmentId fallback only', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $workspaceId = (int) $tenant->workspace_id; @@ -59,7 +59,7 @@ function alertsKpiValues($component): array Filament::setTenant(null, true); session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenant->getKey(), ]); @@ -159,7 +159,7 @@ function alertsKpiValues($component): array ]); })->group('ops-ux'); -it('keeps KPI deliveries workspace-wide when Filament and remembered tenant context conflict', function (): void { +it('keeps KPI deliveries workspace-wide when Filament and remembered environment context conflict', function (): void { [$user, $tenantA] = createUserWithTenant(role: 'owner'); $workspaceId = (int) $tenantA->workspace_id; @@ -194,7 +194,7 @@ function alertsKpiValues($component): array Filament::setTenant($tenantB, true); session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/Artifacts/ArtifactSourceTaxonomySurfaceTest.php b/apps/platform/tests/Feature/Filament/Artifacts/ArtifactSourceTaxonomySurfaceTest.php index 06b11d9d..2b625432 100644 --- a/apps/platform/tests/Feature/Filament/Artifacts/ArtifactSourceTaxonomySurfaceTest.php +++ b/apps/platform/tests/Feature/Filament/Artifacts/ArtifactSourceTaxonomySurfaceTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\FindingResource; use App\Filament\Resources\InventoryItemResource; use App\Filament\Resources\StoredReportResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\Finding; use App\Models\InventoryItem; use App\Models\ManagedEnvironment; @@ -93,7 +93,7 @@ $this->actingAs($owner) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Filament/AuditLogPageTest.php b/apps/platform/tests/Feature/Filament/AuditLogPageTest.php index d12b1a76..35803cc8 100644 --- a/apps/platform/tests/Feature/Filament/AuditLogPageTest.php +++ b/apps/platform/tests/Feature/Filament/AuditLogPageTest.php @@ -207,7 +207,7 @@ function auditLogPageTestRecord(?ManagedEnvironment $tenant, array $attributes = ->assertCanSeeTableRecords([$tenantARecord, $tenantBRecord]); }); -it('keeps the audit log unfiltered when only a remembered tenant context exists', function (): void { +it('keeps the audit log unfiltered when only a remembered environment context exists', function (): void { $tenantA = ManagedEnvironment::factory()->create([ 'name' => 'Phoenicon', 'environment' => 'dev', @@ -238,7 +238,7 @@ function auditLogPageTestRecord(?ManagedEnvironment $tenant, array $attributes = Filament::setTenant(null, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -280,7 +280,7 @@ function auditLogPageTestRecord(?ManagedEnvironment $tenant, array $attributes = $workspaceId = (int) $tenantA->workspace_id; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantA->getKey(), ]); @@ -293,7 +293,7 @@ function auditLogPageTestRecord(?ManagedEnvironment $tenant, array $attributes = expect(data_get(session()->get($component->instance()->getTableFiltersSessionKey()), 'managed_environment_id.value')) ->toBeNull(); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/BackupSetAdminTenantParityTest.php b/apps/platform/tests/Feature/Filament/BackupSetAdminTenantParityTest.php index 0b25c98e..c8ee9bc8 100644 --- a/apps/platform/tests/Feature/Filament/BackupSetAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/Filament/BackupSetAdminTenantParityTest.php @@ -27,7 +27,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php index dbc2083e..030d88d8 100644 --- a/apps/platform/tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/Filament/BaselineCompareLandingAdminTenantParityTest.php @@ -106,7 +106,7 @@ function seedBaselineCompareLandingGapRun(\App\Models\ManagedEnvironment $tenant Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/CanonicalAdminTenantFilterStateTest.php b/apps/platform/tests/Feature/Filament/CanonicalAdminEnvironmentFilterStateTest.php similarity index 73% rename from apps/platform/tests/Feature/Filament/CanonicalAdminTenantFilterStateTest.php rename to apps/platform/tests/Feature/Filament/CanonicalAdminEnvironmentFilterStateTest.php index 0ffbe55d..bead21c5 100644 --- a/apps/platform/tests/Feature/Filament/CanonicalAdminTenantFilterStateTest.php +++ b/apps/platform/tests/Feature/Filament/CanonicalAdminEnvironmentFilterStateTest.php @@ -3,7 +3,7 @@ declare(strict_types=1); use App\Models\ManagedEnvironment; -use App\Support\Filament\CanonicalAdminTenantFilterState; +use App\Support\Filament\CanonicalAdminEnvironmentFilterState; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -11,7 +11,7 @@ uses(RefreshDatabase::class); -function canonicalAdminTenantRequest(): Request +function canonicalAdminEnvironmentRequest(): Request { $request = Request::create('/admin/non-hub-test'); $request->setLaravelSession(app('session.store')); @@ -36,7 +36,7 @@ function canonicalAdminTenantRequest(): Request $filtersSessionKey = 'filament.test.filters'; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantA->getKey(), ]); session()->put($filtersSessionKey, [ @@ -44,21 +44,21 @@ function canonicalAdminTenantRequest(): Request 'status' => ['value' => 'new'], ]); - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $filtersSessionKey, ['run_ids'], - canonicalAdminTenantRequest(), + canonicalAdminEnvironmentRequest(), null, ); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantB->getKey(), ]); - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( $filtersSessionKey, ['run_ids'], - canonicalAdminTenantRequest(), + canonicalAdminEnvironmentRequest(), null, ); @@ -76,15 +76,15 @@ function canonicalAdminTenantRequest(): Request Filament::setTenant(null, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( 'filament.provider.filters', - request: canonicalAdminTenantRequest(), - tenantFilterName: 'tenant', - tenantAttribute: 'external_id', + request: canonicalAdminEnvironmentRequest(), + environmentFilterName: 'tenant', + environmentAttribute: 'external_id', ); expect(session()->get('filament.provider.filters')) @@ -100,18 +100,18 @@ function canonicalAdminTenantRequest(): Request Filament::setTenant(null, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); session()->put('filament.provider.filters', [ 'status' => ['value' => 'active'], ]); - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( 'filament.provider.filters', - request: canonicalAdminTenantRequest(), - tenantFilterName: 'tenant', - tenantAttribute: 'external_id', + request: canonicalAdminEnvironmentRequest(), + environmentFilterName: 'tenant', + environmentAttribute: 'external_id', ); expect(session()->get('filament.provider.filters')) @@ -128,14 +128,14 @@ function canonicalAdminTenantRequest(): Request Filament::setTenant(null, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); - app(CanonicalAdminTenantFilterState::class)->sync( + app(CanonicalAdminEnvironmentFilterState::class)->sync( 'filament.findings.filters', - request: canonicalAdminTenantRequest(), - tenantFilterName: null, + request: canonicalAdminEnvironmentRequest(), + environmentFilterName: null, ); expect(session()->has('filament.findings.filters'))->toBeFalse(); diff --git a/apps/platform/tests/Feature/Filament/DatabaseNotificationsPollingTest.php b/apps/platform/tests/Feature/Filament/DatabaseNotificationsPollingTest.php index 995fc011..a10dbff9 100644 --- a/apps/platform/tests/Feature/Filament/DatabaseNotificationsPollingTest.php +++ b/apps/platform/tests/Feature/Filament/DatabaseNotificationsPollingTest.php @@ -20,7 +20,7 @@ $response = $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Filament/EntraGroupAdminScopeTest.php b/apps/platform/tests/Feature/Filament/EntraGroupAdminScopeTest.php index d168a75a..cadbb5fc 100644 --- a/apps/platform/tests/Feature/Filament/EntraGroupAdminScopeTest.php +++ b/apps/platform/tests/Feature/Filament/EntraGroupAdminScopeTest.php @@ -60,7 +60,7 @@ $this->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ])->get($url) @@ -87,7 +87,7 @@ $session = [ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]; @@ -135,7 +135,7 @@ $this->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ], ])->get($mismatchedWorkspaceUrl) @@ -166,7 +166,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -175,7 +175,7 @@ ->assertCanSeeTableRecords([$groupA]) ->assertCanNotSeeTableRecords([$groupB]); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php b/apps/platform/tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php index 12280b4e..eab0fb0b 100644 --- a/apps/platform/tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php +++ b/apps/platform/tests/Feature/Filament/EntraGroupGlobalSearchScopeTest.php @@ -34,7 +34,7 @@ function entraGroupSearchTitles($results): array ->toHaveCount(0); }); -it('scopes admin global-search results to the remembered tenant context', function (): void { +it('scopes admin global-search results to the remembered environment context', function (): void { $tenantA = ManagedEnvironment::factory()->create(); [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner'); @@ -65,7 +65,7 @@ function entraGroupSearchTitles($results): array Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php index c4b783f0..802cd401 100644 --- a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php +++ b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php @@ -4,16 +4,16 @@ use App\Filament\Resources\BackupScheduleResource; use App\Filament\Resources\BackupSetResource; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\FindingExceptionResource; use App\Filament\Resources\FindingResource; use App\Filament\Resources\InventoryItemResource; use App\Filament\Resources\PolicyResource; use App\Filament\Resources\PolicyVersionResource; -use App\Filament\Resources\ReviewPackResource; use App\Filament\Resources\RestoreRunResource; +use App\Filament\Resources\ReviewPackResource; use App\Filament\Resources\StoredReportResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\ManagedEnvironment; use App\Models\StoredReport; use App\Models\User; @@ -140,7 +140,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactDeepLinkContractTest.php b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactDeepLinkContractTest.php index 43f7cd4d..de8585d7 100644 --- a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactDeepLinkContractTest.php +++ b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactDeepLinkContractTest.php @@ -3,12 +3,12 @@ declare(strict_types=1); use App\Filament\Resources\BackupSetResource; +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\FindingExceptionResource; use App\Filament\Resources\FindingResource; use App\Filament\Resources\RestoreRunResource; use App\Filament\Resources\ReviewPackResource; -use App\Filament\Resources\EnvironmentReviewResource; use App\Models\AuditLog; use App\Models\BackupSet; use App\Models\Finding; @@ -18,7 +18,6 @@ use App\Models\OperationRun; use App\Models\RestoreRun; use App\Models\ReviewPack; -use App\Models\User; use App\Support\Navigation\RelatedNavigationResolver; use App\Support\OperationRunLinks; use App\Support\OperationRunOutcome; @@ -32,7 +31,7 @@ function setGovernanceArtifactAdminContext(ManagedEnvironment $tenant): void setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); } @@ -186,7 +185,7 @@ function governanceArtifactAuditRecord(ManagedEnvironment $tenant, string $resou expect($reviewLinks)->toMatchArray([ OperationRunLinks::collectionLabel() => OperationRunLinks::index($tenant), - 'ManagedEnvironment Review' => EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant), + 'ManagedEnvironment Review' => EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant), ]); expect($packLinks)->toMatchArray([ @@ -283,4 +282,4 @@ function governanceArtifactAuditRecord(ManagedEnvironment $tenant, string $resou ->toBeString() ->not->toContain('/admin/t/'); } -}); \ No newline at end of file +}); diff --git a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactEnvironmentContextTest.php b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactEnvironmentContextTest.php index ce4df406..5f7033bc 100644 --- a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactEnvironmentContextTest.php +++ b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactEnvironmentContextTest.php @@ -2,15 +2,15 @@ declare(strict_types=1); +use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\ReviewPackResource; use App\Filament\Resources\StoredReportResource; -use App\Filament\Resources\EnvironmentReviewResource; +use App\Models\EnvironmentReview; use App\Models\EvidenceSnapshot; use App\Models\ManagedEnvironment; use App\Models\ReviewPack; use App\Models\StoredReport; -use App\Models\EnvironmentReview; use App\Support\Workspaces\WorkspaceContext; it('resolves review pack access from the remembered admin environment context', function (): void { @@ -37,7 +37,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); @@ -55,7 +55,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); @@ -83,7 +83,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); @@ -108,7 +108,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); @@ -152,7 +152,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactLegacyTenantPanelGuardTest.php b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactLegacyTenantPanelGuardTest.php index b7f84981..48c35051 100644 --- a/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactLegacyTenantPanelGuardTest.php +++ b/apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactLegacyTenantPanelGuardTest.php @@ -51,10 +51,6 @@ function governanceArtifactLegacyTenantForbiddenPatterns(): array 'pattern' => '/\\/admin\\/t\\//', 'reason' => 'Touched governance artifact surfaces must not hardcode legacy /admin/t route language.', ], - [ - 'pattern' => "/EnvironmentReviewResource::tenantScopedUrl\\([^\\n]*,\\s*'tenant'\\)/", - 'reason' => 'Touched review drillthrough call-sites must not carry a stale tenant-panel hint.', - ], [ 'pattern' => '/\\bManagedEnvironment::current\\s*\\(/', 'reason' => 'Touched governance artifact surfaces must not rely on tenant-panel-only current-environment fallbacks.', @@ -96,7 +92,7 @@ function governanceArtifactLegacyTenantForbiddenPatterns(): array expect($violations)->toBeEmpty(); })->group('surface-guard'); -it('keeps environment review scoped urls on workspace-first admin routes even when a legacy tenant hint is supplied', function (): void { +it('keeps environment review scoped urls on workspace-first admin routes', function (): void { $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner', setUiContext: false); @@ -107,7 +103,7 @@ function governanceArtifactLegacyTenantForbiddenPatterns(): array setAdminPanelContext(); $path = parse_url( - EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant, 'tenant'), + EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant), PHP_URL_PATH, ); diff --git a/apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php b/apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php index 024782dc..18936c51 100644 --- a/apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); use App\Filament\Pages\InventoryCoverage; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Support\Badges\BadgeCatalog; use App\Support\Badges\BadgeDomain; use App\Support\Inventory\InventoryCoverage as InventoryCoveragePayload; @@ -54,7 +54,7 @@ function seedInventoryCoverageParityRun(ManagedEnvironment $tenant, string $stat Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -97,7 +97,7 @@ function seedInventoryCoverageParityRun(ManagedEnvironment $tenant, string $stat $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Filament/InventoryItemResourceTest.php b/apps/platform/tests/Feature/Filament/InventoryItemResourceTest.php index 492becd4..df8fe297 100644 --- a/apps/platform/tests/Feature/Filament/InventoryItemResourceTest.php +++ b/apps/platform/tests/Feature/Filament/InventoryItemResourceTest.php @@ -3,8 +3,8 @@ use App\Filament\Resources\InventoryItemResource; use App\Filament\Resources\InventoryItemResource\Pages\ListInventoryItems; use App\Models\InventoryItem; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Support\Auth\UiTooltips; use App\Support\Inventory\InventoryCoverage as InventoryCoveragePayload; use App\Support\Workspaces\WorkspaceContext; @@ -58,7 +58,7 @@ ->get(InventoryItemResource::getUrl('index', tenant: $tenant)) ->assertOk(); - expect(session(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY)) + expect(session(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY)) ->toMatchArray([ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/ManagedEnvironmentsLandingLifecycleTest.php b/apps/platform/tests/Feature/Filament/ManagedEnvironmentsLandingLifecycleTest.php index 40e49c43..abeaed54 100644 --- a/apps/platform/tests/Feature/Filament/ManagedEnvironmentsLandingLifecycleTest.php +++ b/apps/platform/tests/Feature/Filament/ManagedEnvironmentsLandingLifecycleTest.php @@ -103,7 +103,7 @@ 'role' => 'owner', ]); - $activeTenant = ManagedEnvironment::factory()->active()->create([ + $activeEnvironment = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $workspace->getKey(), 'name' => 'Landing Active ManagedEnvironment', ]); @@ -121,7 +121,7 @@ ]); $user->tenants()->syncWithoutDetaching([ - $activeTenant->getKey() => ['role' => 'owner'], + $activeEnvironment->getKey() => ['role' => 'owner'], $draftTenant->getKey() => ['role' => 'owner'], $onboardingTenant->getKey() => ['role' => 'owner'], $archivedTenant->getKey() => ['role' => 'owner'], diff --git a/apps/platform/tests/Feature/Filament/OperationRunListFiltersTest.php b/apps/platform/tests/Feature/Filament/OperationRunListFiltersTest.php index 977411c5..8493ac21 100644 --- a/apps/platform/tests/Feature/Filament/OperationRunListFiltersTest.php +++ b/apps/platform/tests/Feature/Filament/OperationRunListFiltersTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); use App\Filament\Pages\Monitoring\Operations; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; use App\Support\Workspaces\WorkspaceContext; @@ -122,7 +122,7 @@ function operationRunFilterIndicatorLabels($component): array ->assertCanSeeTableRecords([$recent, $old]); }); -it('keeps operation type filter options workspace-scoped even when a remembered tenant is active', function (): void { +it('keeps operation type filter options workspace-scoped even when a remembered environment is active', function (): void { $tenantA = ManagedEnvironment::factory()->create(); [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner'); @@ -148,7 +148,7 @@ function operationRunFilterIndicatorLabels($component): array setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -259,7 +259,7 @@ function operationRunFilterIndicatorLabels($component): array $workspaceId = (int) $tenantA->workspace_id; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantA->getKey(), ]); @@ -270,7 +270,7 @@ function operationRunFilterIndicatorLabels($component): array expect(data_get(session()->get($component->instance()->getTableFiltersSessionKey()), 'type.value'))->toBe('policy.sync'); expect(data_get(session()->get($component->instance()->getTableFiltersSessionKey()), 'initiator_name.value'))->toBe('Alpha'); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php index 5d3d4524..e2268528 100644 --- a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php +++ b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php @@ -147,7 +147,7 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $workspace->getKey() => (int) $tenant->getKey(), ], ]) @@ -196,7 +196,7 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $workspace->getKey() => (int) $tenant->getKey(), ], ]) @@ -299,7 +299,7 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a ->assertNotFound(); }); -it('keeps the workspace panel sidebar free of tenant-sensitive entries even with a remembered tenant', function (): void { +it('keeps the workspace panel sidebar free of tenant-sensitive entries even with a remembered environment', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); Filament::setTenant($tenant, true); @@ -307,7 +307,7 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a $response = $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Filament/PolicyListingTest.php b/apps/platform/tests/Feature/Filament/PolicyListingTest.php index e6f7c393..a17bdd38 100644 --- a/apps/platform/tests/Feature/Filament/PolicyListingTest.php +++ b/apps/platform/tests/Feature/Filament/PolicyListingTest.php @@ -1,12 +1,11 @@ actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php b/apps/platform/tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php index 4da8efdf..fb52f644 100644 --- a/apps/platform/tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php +++ b/apps/platform/tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); use App\Filament\Resources\PolicyResource; -use App\Models\Policy; use App\Models\ManagedEnvironment; +use App\Models\Policy; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; @@ -29,7 +29,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/PolicyResourceAdminTenantParityTest.php b/apps/platform/tests/Feature/Filament/PolicyResourceAdminTenantParityTest.php index 02d15adb..4b521ac0 100644 --- a/apps/platform/tests/Feature/Filament/PolicyResourceAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/Filament/PolicyResourceAdminTenantParityTest.php @@ -3,9 +3,9 @@ declare(strict_types=1); use App\Filament\Resources\PolicyResource\Pages\ListPolicies; +use App\Models\ManagedEnvironment; use App\Models\Policy; use App\Models\PolicyVersion; -use App\Models\ManagedEnvironment; use App\Support\Workspaces\WorkspaceContext; use Carbon\CarbonImmutable; use Filament\Facades\Filament; @@ -29,7 +29,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -84,7 +84,7 @@ $session = [ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]; diff --git a/apps/platform/tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php b/apps/platform/tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php index e94eeb82..c9bb890d 100644 --- a/apps/platform/tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php +++ b/apps/platform/tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php @@ -3,9 +3,9 @@ declare(strict_types=1); use App\Filament\Resources\PolicyVersionResource; +use App\Models\ManagedEnvironment; use App\Models\Policy; use App\Models\PolicyVersion; -use App\Models\ManagedEnvironment; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; @@ -32,7 +32,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/PolicyVersionAdminTenantParityTest.php b/apps/platform/tests/Feature/Filament/PolicyVersionAdminTenantParityTest.php index a00325eb..c0e6c8ad 100644 --- a/apps/platform/tests/Feature/Filament/PolicyVersionAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/Filament/PolicyVersionAdminTenantParityTest.php @@ -4,9 +4,9 @@ use App\Filament\Resources\PolicyVersionResource; use App\Filament\Resources\PolicyVersionResource\Pages\ListPolicyVersions; +use App\Models\ManagedEnvironment; use App\Models\Policy; use App\Models\PolicyVersion; -use App\Models\ManagedEnvironment; use App\Support\Workspaces\WorkspaceContext; use Carbon\CarbonImmutable; use Filament\Facades\Filament; @@ -33,7 +33,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -61,7 +61,7 @@ $session = [ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]; @@ -133,7 +133,7 @@ $session = [ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]; diff --git a/apps/platform/tests/Feature/Filament/ReferencedTenantLifecyclePresentationTest.php b/apps/platform/tests/Feature/Filament/ReferencedTenantLifecyclePresentationTest.php index 5091c642..811c5071 100644 --- a/apps/platform/tests/Feature/Filament/ReferencedTenantLifecyclePresentationTest.php +++ b/apps/platform/tests/Feature/Filament/ReferencedTenantLifecyclePresentationTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; use App\Support\Workspaces\WorkspaceContext; @@ -40,13 +40,13 @@ }); it('renders archived referenced tenant lifecycle consistently in the viewer banner and summary card', function (): void { - $activeTenant = ManagedEnvironment::factory()->create([ + $activeEnvironment = ManagedEnvironment::factory()->create([ 'name' => 'Active ManagedEnvironment', ]); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $archivedTenant = ManagedEnvironment::factory()->active()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Archived ManagedEnvironment', ]); @@ -54,7 +54,7 @@ $archivedTenant->delete(); $run = OperationRun::factory()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'managed_environment_id' => (int) $archivedTenant->getKey(), 'type' => 'inventory_sync', 'status' => OperationRunStatus::Completed->value, @@ -64,7 +64,7 @@ setAdminPanelContext(); $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) ->get(\App\Support\OperationRunLinks::tenantlessView($run)) ->assertOk() ->assertSee('Operation environment is not available in the current environment selector') diff --git a/apps/platform/tests/Feature/Filament/RestoreRunAdminTenantParityTest.php b/apps/platform/tests/Feature/Filament/RestoreRunAdminTenantParityTest.php index 822b3245..c89c3e4c 100644 --- a/apps/platform/tests/Feature/Filament/RestoreRunAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/Filament/RestoreRunAdminTenantParityTest.php @@ -4,8 +4,8 @@ use App\Filament\Resources\RestoreRunResource; use App\Models\BackupSet; -use App\Models\RestoreRun; use App\Models\ManagedEnvironment; +use App\Models\RestoreRun; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -29,7 +29,7 @@ $this->followingRedirects()->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ])->get(RestoreRunResource::getUrl('index', panel: 'admin', tenant: $tenantA)) diff --git a/apps/platform/tests/Feature/Filament/RestoreWizardGraphSafetyTest.php b/apps/platform/tests/Feature/Filament/RestoreWizardGraphSafetyTest.php index ef2a3324..9b0b60af 100644 --- a/apps/platform/tests/Feature/Filament/RestoreWizardGraphSafetyTest.php +++ b/apps/platform/tests/Feature/Filament/RestoreWizardGraphSafetyTest.php @@ -29,7 +29,7 @@ function makeAssignment(string $odataType, string $groupId, ?string $displayName $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) @@ -66,7 +66,7 @@ function makeAssignment(string $odataType, string $groupId, ?string $displayName $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Filament/SettingsCatalogPolicySyncTest.php b/apps/platform/tests/Feature/Filament/SettingsCatalogPolicySyncTest.php index 6ea57096..ca3e1006 100644 --- a/apps/platform/tests/Feature/Filament/SettingsCatalogPolicySyncTest.php +++ b/apps/platform/tests/Feature/Filament/SettingsCatalogPolicySyncTest.php @@ -1,10 +1,10 @@ actingAs($user) ->withSession([ \App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - \App\Support\Workspaces\WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + \App\Support\Workspaces\WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Filament/SettingsCatalogRestoreApplySettingsPatchTest.php b/apps/platform/tests/Feature/Filament/SettingsCatalogRestoreApplySettingsPatchTest.php index 23f911ef..51dfab2b 100644 --- a/apps/platform/tests/Feature/Filament/SettingsCatalogRestoreApplySettingsPatchTest.php +++ b/apps/platform/tests/Feature/Filament/SettingsCatalogRestoreApplySettingsPatchTest.php @@ -2,8 +2,8 @@ use App\Models\BackupItem; use App\Models\BackupSet; -use App\Models\Policy; use App\Models\ManagedEnvironment; +use App\Models\Policy; use App\Services\Graph\GraphClientInterface; use App\Services\Graph\GraphResponse; use App\Services\Intune\RestoreService; @@ -191,7 +191,7 @@ public function request(string $method, string $path, array $options = []): Grap $response = $this->withSession([ \App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - \App\Support\Workspaces\WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + \App\Support\Workspaces\WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ])->get(\App\Filament\Resources\RestoreRunResource::getUrl('view', ['record' => $run], panel: 'admin', tenant: $tenant)); diff --git a/apps/platform/tests/Feature/Filament/SettingsCatalogRestoreTest.php b/apps/platform/tests/Feature/Filament/SettingsCatalogRestoreTest.php index bce098ff..717c2289 100644 --- a/apps/platform/tests/Feature/Filament/SettingsCatalogRestoreTest.php +++ b/apps/platform/tests/Feature/Filament/SettingsCatalogRestoreTest.php @@ -2,10 +2,10 @@ use App\Models\BackupItem; use App\Models\BackupSet; +use App\Models\ManagedEnvironment; use App\Models\Policy; use App\Models\ProviderConnection; use App\Models\ProviderCredential; -use App\Models\ManagedEnvironment; use App\Models\User; use App\Services\Graph\GraphClientInterface; use App\Services\Graph\GraphResponse; @@ -228,7 +228,7 @@ public function request(string $method, string $path, array $options = []): Grap $response = $this ->withSession([ \App\Support\Workspaces\WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - \App\Support\Workspaces\WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + \App\Support\Workspaces\WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Filament/TableStatePersistenceTest.php b/apps/platform/tests/Feature/Filament/TableStatePersistenceTest.php index 4ca6f352..78dacc1c 100644 --- a/apps/platform/tests/Feature/Filament/TableStatePersistenceTest.php +++ b/apps/platform/tests/Feature/Filament/TableStatePersistenceTest.php @@ -12,17 +12,16 @@ use App\Filament\Resources\EntraGroupResource\Pages\ListEntraGroups; use App\Filament\Resources\FindingResource\Pages\ListFindings; use App\Filament\Resources\InventoryItemResource\Pages\ListInventoryItems; +use App\Filament\Resources\ManagedEnvironmentResource\Pages\ListManagedEnvironments; use App\Filament\Resources\PolicyResource\Pages\ListPolicies; use App\Filament\Resources\PolicyVersionResource\Pages\ListPolicyVersions; use App\Filament\Resources\ProviderConnectionResource\Pages\ListProviderConnections; use App\Filament\Resources\RestoreRunResource\Pages\ListRestoreRuns; -use App\Filament\Resources\ManagedEnvironmentResource\Pages\ListManagedEnvironments; use App\Models\AuditLog; use App\Models\Finding; use App\Models\FindingException; use App\Models\ManagedEnvironment; use App\Support\Workspaces\WorkspaceContext; -use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; use Livewire\Livewire; @@ -422,14 +421,14 @@ function spec125AssertPersistedTableState( $workspaceId = (int) $tenantA->workspace_id; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantA->getKey(), ]); Livewire::actingAs($user)->test(ListProviderConnections::class) ->assertSet('tableFilters.tenant.value', (string) $tenantA->external_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/TenantOwnedResourceScopeParityTest.php b/apps/platform/tests/Feature/Filament/TenantOwnedResourceScopeParityTest.php index 4f725bc3..f0fa4add 100644 --- a/apps/platform/tests/Feature/Filament/TenantOwnedResourceScopeParityTest.php +++ b/apps/platform/tests/Feature/Filament/TenantOwnedResourceScopeParityTest.php @@ -23,10 +23,10 @@ use App\Models\EntraGroup; use App\Models\Finding; use App\Models\InventoryItem; +use App\Models\ManagedEnvironment; use App\Models\Policy; use App\Models\PolicyVersion; use App\Models\RestoreRun; -use App\Models\ManagedEnvironment; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -38,7 +38,7 @@ function tenantOwnedAdminSession(ManagedEnvironment $tenant): array { return [ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]; @@ -203,7 +203,7 @@ static function (ManagedEnvironment $tenant, string $label): BackupSchedule { Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php b/apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php index c52d04ae..855dee96 100644 --- a/apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php +++ b/apps/platform/tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php @@ -67,15 +67,15 @@ }); test('workspace-wide operations keep shell scope tenantless when a valid tenant query filter is present', function () { - $rememberedTenant = ManagedEnvironment::factory()->create([ + $rememberedEnvironment = ManagedEnvironment::factory()->create([ 'workspace_id' => null, 'status' => 'active', 'name' => 'Remembered Topbar ManagedEnvironment', ]); - [$user, $rememberedTenant] = createUserWithTenant(tenant: $rememberedTenant, role: 'owner'); + [$user, $rememberedEnvironment] = createUserWithTenant(tenant: $rememberedEnvironment, role: 'owner'); $hintedTenant = ManagedEnvironment::factory()->create([ - 'workspace_id' => (int) $rememberedTenant->workspace_id, + 'workspace_id' => (int) $rememberedEnvironment->workspace_id, 'status' => 'active', 'name' => 'Hinted Topbar ManagedEnvironment', ]); @@ -84,13 +84,13 @@ Filament::setTenant(null, true); - $workspaceId = (int) $rememberedTenant->workspace_id; + $workspaceId = (int) $rememberedEnvironment->workspace_id; $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ - (string) $workspaceId => (int) $rememberedTenant->getKey(), + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $workspaceId => (int) $rememberedEnvironment->getKey(), ], ]) ->get(route('admin.operations.index', ['workspace' => $workspaceId, 'environment_id' => (int) $hintedTenant->getKey()])) diff --git a/apps/platform/tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php b/apps/platform/tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php index d4a90569..5c07ed26 100644 --- a/apps/platform/tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php +++ b/apps/platform/tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php @@ -50,7 +50,7 @@ ->assertSee($tenant->name); }); -it('keeps workspace-only admin surfaces independent from remembered tenant changes', function (): void { +it('keeps workspace-only admin surfaces independent from remembered environment changes', function (): void { $tenantA = ManagedEnvironment::factory()->create([ 'name' => 'Phoenicon', 'environment' => 'dev', @@ -80,11 +80,11 @@ $workspaceId = (int) $tenantA->workspace_id; - foreach ([$tenantA, $tenantB] as $rememberedTenant) { + foreach ([$tenantA, $tenantB] as $rememberedEnvironment) { $session = [ WorkspaceContext::SESSION_KEY => $workspaceId, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ - (string) $workspaceId => (int) $rememberedTenant->getKey(), + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $workspaceId => (int) $rememberedEnvironment->getKey(), ], ]; diff --git a/apps/platform/tests/Feature/Filament/WorkspaceOverviewDrilldownContinuityTest.php b/apps/platform/tests/Feature/Filament/WorkspaceOverviewDrilldownContinuityTest.php index 56b082e8..365de15e 100644 --- a/apps/platform/tests/Feature/Filament/WorkspaceOverviewDrilldownContinuityTest.php +++ b/apps/platform/tests/Feature/Filament/WorkspaceOverviewDrilldownContinuityTest.php @@ -139,7 +139,7 @@ $this->actingAs($user); $evidenceUrl = EvidenceSnapshotResource::getUrl('index', panel: 'admin', tenant: $tenant); - $reviewUrl = EnvironmentReviewResource::tenantScopedUrl('index', [], $tenant); + $reviewUrl = EnvironmentReviewResource::environmentScopedUrl('index', [], $tenant); $items = [ [ diff --git a/apps/platform/tests/Feature/Findings/FindingAdminTenantParityTest.php b/apps/platform/tests/Feature/Findings/FindingAdminTenantParityTest.php index 8ff9b10a..d59623f0 100644 --- a/apps/platform/tests/Feature/Findings/FindingAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/Findings/FindingAdminTenantParityTest.php @@ -34,7 +34,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -58,7 +58,7 @@ $session = [ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]; diff --git a/apps/platform/tests/Feature/Findings/FindingExceptionRegisterTest.php b/apps/platform/tests/Feature/Findings/FindingExceptionRegisterTest.php index 355d740d..7acc0a0a 100644 --- a/apps/platform/tests/Feature/Findings/FindingExceptionRegisterTest.php +++ b/apps/platform/tests/Feature/Findings/FindingExceptionRegisterTest.php @@ -144,7 +144,7 @@ ); expect(session(WorkspaceContext::SESSION_KEY))->toBe((int) $tenant->workspace_id) - ->and(session(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) + ->and(session(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) ->toHaveKey((string) $tenant->workspace_id, (int) $tenant->getKey()); }); diff --git a/apps/platform/tests/Feature/Findings/FindingOutcomeSummaryReportingTest.php b/apps/platform/tests/Feature/Findings/FindingOutcomeSummaryReportingTest.php index bbee6a90..9a4ecef7 100644 --- a/apps/platform/tests/Feature/Findings/FindingOutcomeSummaryReportingTest.php +++ b/apps/platform/tests/Feature/Findings/FindingOutcomeSummaryReportingTest.php @@ -118,7 +118,7 @@ function materializeFindingOutcomeSnapshot(\App\Models\ManagedEnvironment $tenan setAdminEnvironmentContext($tenant); $this->actingAs($user) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->assertOk() ->assertSee('Terminal outcomes:') ->assertSee('resolved pending verification') diff --git a/apps/platform/tests/Feature/Findings/FindingRbacTest.php b/apps/platform/tests/Feature/Findings/FindingRbacTest.php index 8060a6b8..4ceaea6c 100644 --- a/apps/platform/tests/Feature/Findings/FindingRbacTest.php +++ b/apps/platform/tests/Feature/Findings/FindingRbacTest.php @@ -105,7 +105,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Feature/Findings/FindingWorkflowRowActionsTest.php b/apps/platform/tests/Feature/Findings/FindingWorkflowRowActionsTest.php index cb3b1bf1..39a282c5 100644 --- a/apps/platform/tests/Feature/Findings/FindingWorkflowRowActionsTest.php +++ b/apps/platform/tests/Feature/Findings/FindingWorkflowRowActionsTest.php @@ -210,7 +210,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -244,7 +244,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Findings/FindingWorkflowUiEnforcementTest.php b/apps/platform/tests/Feature/Findings/FindingWorkflowUiEnforcementTest.php index ba44e6f1..5b2d065f 100644 --- a/apps/platform/tests/Feature/Findings/FindingWorkflowUiEnforcementTest.php +++ b/apps/platform/tests/Feature/Findings/FindingWorkflowUiEnforcementTest.php @@ -78,7 +78,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Findings/FindingsAssignmentHygieneReportTest.php b/apps/platform/tests/Feature/Findings/FindingsAssignmentHygieneReportTest.php index e5eb20ff..3fd8fa24 100644 --- a/apps/platform/tests/Feature/Findings/FindingsAssignmentHygieneReportTest.php +++ b/apps/platform/tests/Feature/Findings/FindingsAssignmentHygieneReportTest.php @@ -11,8 +11,8 @@ use App\Models\Workspace; use App\Models\WorkspaceMembership; use App\Services\Auth\CapabilityResolver; -use App\Support\Auth\Capabilities; use App\Support\Audit\AuditActionId; +use App\Support\Auth\Capabilities; use App\Support\Workspaces\WorkspaceContext; use Carbon\CarbonImmutable; use Livewire\Livewire; @@ -374,7 +374,7 @@ function recordFindingsHygieneAudit(Finding $finding, string $action, CarbonImmu 'subject_display_name' => 'ManagedEnvironment A Issue', ]); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php b/apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php index 91b7c470..9d58a6fb 100644 --- a/apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php +++ b/apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php @@ -159,7 +159,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []): 'status' => Finding::STATUS_TRIAGED, ]); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ]); @@ -215,7 +215,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []): 'status' => Finding::STATUS_TRIAGED, ]); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ]); @@ -340,7 +340,7 @@ function makeIntakeFinding(ManagedEnvironment $tenant, array $attributes = []): ->assertTableEmptyStateActionsExistInOrder(['clear_tenant_filter_empty']); Finding::query()->delete(); - session()->forget(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY); + session()->forget(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY); Filament::setTenant(null, true); findingsIntakePage($user) diff --git a/apps/platform/tests/Feature/Findings/MyWorkInboxTest.php b/apps/platform/tests/Feature/Findings/MyWorkInboxTest.php index 7b95ea7f..4f5b497e 100644 --- a/apps/platform/tests/Feature/Findings/MyWorkInboxTest.php +++ b/apps/platform/tests/Feature/Findings/MyWorkInboxTest.php @@ -169,7 +169,7 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee, 'status' => Finding::STATUS_TRIAGED, ]); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ]); @@ -298,7 +298,7 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee, it('renders the calm zero-work branch and points back to environment selection when no active environment context exists', function (): void { [$user, $tenant] = myWorkInboxActingUser(); - session()->forget(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY); + session()->forget(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY); Filament::setTenant(null, true); myWorkInboxPage($user) @@ -309,7 +309,7 @@ function makeAssignedFindingForInbox(ManagedEnvironment $tenant, User $assignee, it('uses the active visible environment for the calm empty-state drillback when environment context exists', function (): void { [$user, $tenant] = myWorkInboxActingUser(); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php b/apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php index a13ab4c8..00dd27a3 100644 --- a/apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php +++ b/apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php @@ -2,9 +2,12 @@ declare(strict_types=1); -use App\Filament\Pages\InventoryCoverage; use App\Filament\Pages\BaselineCompareLanding; use App\Filament\Pages\BaselineCompareMatrix; +use App\Filament\Pages\EnvironmentDashboard; +use App\Filament\Pages\EnvironmentDiagnostics; +use App\Filament\Pages\EnvironmentRequiredPermissions; +use App\Filament\Pages\InventoryCoverage; use App\Filament\Pages\Monitoring\Alerts; use App\Filament\Pages\Monitoring\AuditLog as AuditLogPage; use App\Filament\Pages\Monitoring\EvidenceOverview; @@ -12,10 +15,7 @@ use App\Filament\Pages\Monitoring\Operations; use App\Filament\Pages\NoAccess; use App\Filament\Pages\Operations\TenantlessOperationRunViewer; -use App\Filament\Pages\EnvironmentDashboard; use App\Filament\Pages\Reviews\ReviewRegister; -use App\Filament\Pages\EnvironmentDiagnostics; -use App\Filament\Pages\EnvironmentRequiredPermissions; use App\Filament\Pages\Workspaces\ManagedEnvironmentOnboardingWizard; use App\Filament\Resources\AlertDeliveryResource; use App\Filament\Resources\AlertDeliveryResource\Pages\ListAlertDeliveries; @@ -34,6 +34,8 @@ use App\Filament\Resources\BaselineProfileResource\Pages\ListBaselineProfiles; use App\Filament\Resources\BaselineSnapshotResource; use App\Filament\Resources\EntraGroupResource; +use App\Filament\Resources\EnvironmentReviewResource; +use App\Filament\Resources\EnvironmentReviewResource\Pages\ListEnvironmentReviews; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\EvidenceSnapshotResource\Pages\ListEvidenceSnapshots; use App\Filament\Resources\FindingExceptionResource; @@ -42,6 +44,11 @@ use App\Filament\Resources\FindingResource\Pages\ListFindings; use App\Filament\Resources\InventoryItemResource; use App\Filament\Resources\InventoryItemResource\Pages\ListInventoryItems; +use App\Filament\Resources\ManagedEnvironmentResource; +use App\Filament\Resources\ManagedEnvironmentResource\Pages\ListManagedEnvironments; +use App\Filament\Resources\ManagedEnvironmentResource\Pages\ManageEnvironmentAccessScopes; +use App\Filament\Resources\ManagedEnvironmentResource\Pages\ViewManagedEnvironment; +use App\Filament\Resources\ManagedEnvironmentResource\RelationManagers\ManagedEnvironmentMembershipsRelationManager; use App\Filament\Resources\OperationRunResource; use App\Filament\Resources\PolicyResource; use App\Filament\Resources\PolicyResource\Pages\ListPolicies; @@ -49,19 +56,12 @@ use App\Filament\Resources\PolicyResource\RelationManagers\VersionsRelationManager; use App\Filament\Resources\PolicyVersionResource; use App\Filament\Resources\ProviderConnectionResource; -use App\Filament\Resources\ProviderConnectionResource\Pages\ViewProviderConnection; use App\Filament\Resources\ProviderConnectionResource\Pages\ListProviderConnections; +use App\Filament\Resources\ProviderConnectionResource\Pages\ViewProviderConnection; use App\Filament\Resources\RestoreRunResource; use App\Filament\Resources\RestoreRunResource\Pages\ListRestoreRuns; use App\Filament\Resources\ReviewPackResource; use App\Filament\Resources\ReviewPackResource\Pages\ListReviewPacks; -use App\Filament\Resources\ManagedEnvironmentResource; -use App\Filament\Resources\ManagedEnvironmentResource\Pages\ManageEnvironmentAccessScopes; -use App\Filament\Resources\ManagedEnvironmentResource\Pages\ListManagedEnvironments; -use App\Filament\Resources\ManagedEnvironmentResource\Pages\ViewManagedEnvironment; -use App\Filament\Resources\ManagedEnvironmentResource\RelationManagers\ManagedEnvironmentMembershipsRelationManager; -use App\Filament\Resources\EnvironmentReviewResource; -use App\Filament\Resources\EnvironmentReviewResource\Pages\ListEnvironmentReviews; use App\Filament\Resources\Workspaces\Pages\ListWorkspaces; use App\Filament\Resources\Workspaces\Pages\ViewWorkspace; use App\Filament\Resources\Workspaces\RelationManagers\WorkspaceMembershipsRelationManager; @@ -87,6 +87,8 @@ use App\Models\Finding; use App\Models\FindingException; use App\Models\InventoryItem; +use App\Models\ManagedEnvironment; +use App\Models\ManagedEnvironmentMembership; use App\Models\OperationRun; use App\Models\PlatformUser; use App\Models\Policy; @@ -94,16 +96,14 @@ use App\Models\ProviderConnection; use App\Models\RestoreRun; use App\Models\ReviewPack; -use App\Models\ManagedEnvironment; -use App\Models\ManagedEnvironmentMembership; use App\Models\User; use App\Models\Workspace; use App\Models\WorkspaceMembership; use App\Support\Auth\PlatformCapabilities; use App\Support\Evidence\EvidenceCompletenessState; use App\Support\Evidence\EvidenceSnapshotStatus; -use App\Support\Navigation\RelatedActionLabelCatalog; use App\Support\ManagedEnvironmentLinks; +use App\Support\Navigation\RelatedActionLabelCatalog; use App\Support\OperationRunLinks; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; @@ -1330,7 +1330,7 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser expect($rowActionNames)->toEqualCanonicalizing(['export_executive_pack']) ->and($table->getBulkActions())->toBeEmpty() - ->and($table->getRecordUrl($review))->toBe(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)); + ->and($table->getRecordUrl($review))->toBe(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)); }); it('uses clickable rows while keeping download direct and grouping expire under More on the review packs list', function (): void { @@ -1608,7 +1608,7 @@ function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser ->and(ReviewRegister::actionSurfaceDeclaration()->slot(ActionSurfaceSlot::InspectAffordance)?->details)->toBe(ActionSurfaceInspectAffordance::ClickableRow->value) ->and($reviewActionNames)->not->toContain('view_review') ->and($reviewActionNames)->toContain('export_executive_pack') - ->and($reviewTable->getRecordUrl($review))->toBe(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $review->tenant, 'tenant')) + ->and($reviewTable->getRecordUrl($review))->toBe(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $review->tenant)) ->and(EvidenceOverview::actionSurfaceDeclaration()->surfaceType)->toBe(ActionSurfaceType::ReadOnlyRegistryReport) ->and(EvidenceOverview::actionSurfaceDeclaration()->slot(ActionSurfaceSlot::InspectAffordance)?->details)->toBe(ActionSurfaceInspectAffordance::ClickableRow->value) ->and($snapshot)->toBeInstanceOf(EvidenceSnapshot::class) diff --git a/apps/platform/tests/Feature/Guards/AdminTenantResolverGuardTest.php b/apps/platform/tests/Feature/Guards/AdminTenantResolverGuardTest.php index 90f008c4..6a48fc7b 100644 --- a/apps/platform/tests/Feature/Guards/AdminTenantResolverGuardTest.php +++ b/apps/platform/tests/Feature/Guards/AdminTenantResolverGuardTest.php @@ -42,7 +42,7 @@ function adminTenantResolverExceptionFiles(): array return [ 'app/Filament/Pages/ChooseEnvironment.php', 'app/Http/Controllers/SelectEnvironmentController.php', - 'app/Support/Middleware/EnsureFilamentTenantSelected.php', + 'app/Support/Middleware/EnsureEnvironmentContextSelected.php', 'app/Filament/Concerns/ResolvesPanelTenantContext.php', ]; } diff --git a/apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php b/apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php index 73b8a3d1..f9359cb2 100644 --- a/apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php +++ b/apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php @@ -169,43 +169,39 @@ it('syncs canonical admin tenant filter state on the persisted admin list surfaces', function (): void { $patternByPath = [ 'app/Filament/Resources/BackupScheduleResource/Pages/ListBackupSchedules.php' => [ - 'CanonicalAdminTenantFilterState::class', + 'CanonicalAdminEnvironmentFilterState::class', '->sync(', ], 'app/Filament/Resources/BackupSetResource/Pages/ListBackupSets.php' => [ - 'CanonicalAdminTenantFilterState::class', + 'CanonicalAdminEnvironmentFilterState::class', '->sync(', ], 'app/Filament/Resources/FindingResource/Pages/ListFindings.php' => [ - 'CanonicalAdminTenantFilterState::class', + 'CanonicalAdminEnvironmentFilterState::class', '->sync(', ], 'app/Filament/Resources/InventoryItemResource/Pages/ListInventoryItems.php' => [ - 'CanonicalAdminTenantFilterState::class', + 'CanonicalAdminEnvironmentFilterState::class', '->sync(', ], 'app/Filament/Resources/RestoreRunResource/Pages/ListRestoreRuns.php' => [ - 'CanonicalAdminTenantFilterState::class', + 'CanonicalAdminEnvironmentFilterState::class', '->sync(', ], 'app/Filament/Resources/PolicyResource/Pages/ListPolicies.php' => [ - 'CanonicalAdminTenantFilterState::class', + 'CanonicalAdminEnvironmentFilterState::class', '->sync(', ], 'app/Filament/Resources/PolicyVersionResource/Pages/ListPolicyVersions.php' => [ - 'CanonicalAdminTenantFilterState::class', - '->sync(', - ], - 'app/Filament/Resources/ProviderConnectionResource/Pages/ListProviderConnections.php' => [ - 'CanonicalAdminTenantFilterState::class', + 'CanonicalAdminEnvironmentFilterState::class', '->sync(', ], 'app/Filament/Resources/EntraGroupResource/Pages/ListEntraGroups.php' => [ - 'CanonicalAdminTenantFilterState::class', + 'CanonicalAdminEnvironmentFilterState::class', '->sync(', ], 'app/Filament/Pages/Monitoring/AuditLog.php' => [ - 'CanonicalAdminTenantFilterState::class', + 'CanonicalAdminEnvironmentFilterState::class', '->sync(', ], ]; @@ -348,84 +344,84 @@ expect($missing)->toBeEmpty('Missing pagination profile helper usage: '.implode(', ', $missing)); }); - it('keeps spec 196 surfaces on native table contracts without faux controls or hand-built primary tables', function (): void { - $requiredPatterns = [ - 'app/Filament/Pages/EnvironmentRequiredPermissions.php' => [ - 'implements HasTable', - 'InteractsWithTable', - ], - 'app/Filament/Pages/Monitoring/EvidenceOverview.php' => [ - 'implements HasTable', - 'InteractsWithTable', - ], - 'app/Livewire/InventoryItemDependencyEdgesTable.php' => [ - 'extends TableComponent', - ], - 'resources/views/filament/components/dependency-edges.blade.php' => [ - 'inventory-item-dependency-edges-table', - ], - 'resources/views/filament/pages/environment-required-permissions.blade.php' => [ - '$this->table', - 'data-testid="technical-details"', - ], - 'resources/views/filament/pages/monitoring/evidence-overview.blade.php' => [ - '$this->table', - ], - ]; +it('keeps spec 196 surfaces on native table contracts without faux controls or hand-built primary tables', function (): void { + $requiredPatterns = [ + 'app/Filament/Pages/EnvironmentRequiredPermissions.php' => [ + 'implements HasTable', + 'InteractsWithTable', + ], + 'app/Filament/Pages/Monitoring/EvidenceOverview.php' => [ + 'implements HasTable', + 'InteractsWithTable', + ], + 'app/Livewire/InventoryItemDependencyEdgesTable.php' => [ + 'extends TableComponent', + ], + 'resources/views/filament/components/dependency-edges.blade.php' => [ + 'inventory-item-dependency-edges-table', + ], + 'resources/views/filament/pages/environment-required-permissions.blade.php' => [ + '$this->table', + 'data-testid="technical-details"', + ], + 'resources/views/filament/pages/monitoring/evidence-overview.blade.php' => [ + '$this->table', + ], + ]; - $forbiddenPatterns = [ - 'resources/views/filament/components/dependency-edges.blade.php' => [ - '
[ - 'wire:model.live="status"', - 'wire:model.live="type"', - 'wire:model.live="features"', - 'wire:model.live.debounce.500ms="search"', - '', - ], - 'resources/views/filament/pages/monitoring/evidence-overview.blade.php' => [ - '
', - ], - ]; + $forbiddenPatterns = [ + 'resources/views/filament/components/dependency-edges.blade.php' => [ + ' [ + 'wire:model.live="status"', + 'wire:model.live="type"', + 'wire:model.live="features"', + 'wire:model.live.debounce.500ms="search"', + '
', + ], + 'resources/views/filament/pages/monitoring/evidence-overview.blade.php' => [ + '
', + ], + ]; - $missing = []; - $unexpected = []; + $missing = []; + $unexpected = []; - foreach ($requiredPatterns as $relativePath => $patterns) { - $contents = file_get_contents(base_path($relativePath)); + foreach ($requiredPatterns as $relativePath => $patterns) { + $contents = file_get_contents(base_path($relativePath)); - if (! is_string($contents)) { - $missing[] = $relativePath; + if (! is_string($contents)) { + $missing[] = $relativePath; - continue; - } - - foreach ($patterns as $pattern) { - if (! str_contains($contents, $pattern)) { - $missing[] = "{$relativePath} ({$pattern})"; - } - } + continue; } - foreach ($forbiddenPatterns as $relativePath => $patterns) { - $contents = file_get_contents(base_path($relativePath)); - - if (! is_string($contents)) { - continue; - } - - foreach ($patterns as $pattern) { - if (str_contains($contents, $pattern)) { - $unexpected[] = "{$relativePath} ({$pattern})"; - } + foreach ($patterns as $pattern) { + if (! str_contains($contents, $pattern)) { + $missing[] = "{$relativePath} ({$pattern})"; } } + } - expect($missing)->toBeEmpty('Missing native table contract patterns: '.implode(', ', $missing)) - ->and($unexpected)->toBeEmpty('Unexpected faux-control or hand-built table patterns remain: '.implode(', ', $unexpected)); - }); + foreach ($forbiddenPatterns as $relativePath => $patterns) { + $contents = file_get_contents(base_path($relativePath)); + + if (! is_string($contents)) { + continue; + } + + foreach ($patterns as $pattern) { + if (str_contains($contents, $pattern)) { + $unexpected[] = "{$relativePath} ({$pattern})"; + } + } + } + + expect($missing)->toBeEmpty('Missing native table contract patterns: '.implode(', ', $missing)) + ->and($unexpected)->toBeEmpty('Unexpected faux-control or hand-built table patterns remain: '.implode(', ', $unexpected)); +}); it('keeps tenant-registry recovery triage columns, filters, and query hydration explicit', function (): void { $patternByPath = [ diff --git a/apps/platform/tests/Feature/Guards/LegacyTenantPlatformContextCleanupTest.php b/apps/platform/tests/Feature/Guards/LegacyTenantPlatformContextCleanupTest.php new file mode 100644 index 00000000..2a4981fd --- /dev/null +++ b/apps/platform/tests/Feature/Guards/LegacyTenantPlatformContextCleanupTest.php @@ -0,0 +1,224 @@ + + */ +function spec317Files(array $roots): array +{ + $files = []; + + foreach ($roots as $root) { + if (is_file($root)) { + $files[] = $root; + + continue; + } + + if (! is_dir($root)) { + continue; + } + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), + ); + + foreach ($iterator as $file) { + if (! $file instanceof SplFileInfo || ! $file->isFile()) { + continue; + } + + if ($file->getFilename() === 'LegacyTenantPlatformContextCleanupTest.php') { + continue; + } + + if (! in_array($file->getExtension(), ['php', 'md'], true)) { + continue; + } + + $files[] = $file->getPathname(); + } + } + + sort($files); + + return array_values(array_unique($files)); +} + +/** + * @param list $files + * @param list $patterns + * @return list + */ +function spec317PatternHits(array $files, array $patterns): array +{ + $hits = []; + + foreach ($files as $path) { + $contents = file_get_contents($path); + + if (! is_string($contents)) { + continue; + } + + $lines = preg_split('/\R/', $contents) ?: []; + + foreach ($patterns as $pattern) { + foreach ($lines as $lineNumber => $line) { + if (preg_match($pattern, $line) !== 1) { + continue; + } + + $hits[] = str_replace(repo_path().'/', '', $path).':'.($lineNumber + 1).' -> '.trim($line); + } + } + } + + return $hits; +} + +it('removes retired platform-context helper and class names from active runtime seams', function (): void { + $files = spec317Files([ + base_path('app'), + base_path('bootstrap/app.php'), + base_path('routes/web.php'), + base_path('tests/Feature/Guards'), + base_path('tests/Feature/Navigation'), + base_path('tests/Feature/Reviews'), + ]); + + $hits = spec317PatternHits($files, [ + '/\btenantPrefilterUrl\s*\(/', + '/\bCanonicalAdminTenantFilterState\b/', + '/\bWorkspaceScopedTenantRoutes\b/', + '/\bTenantPageCategory\b/', + '/\bEnsureFilamentTenantSelected\b/', + '/ensure-filament-tenant-selected/', + '/\blastTenantId\s*\(/', + '/\brememberedTenant\s*\(/', + '/\brememberTenantContext\s*\(/', + '/\bLAST_TENANT_IDS_SESSION_KEY\b/', + '/\bTenantBound\b/', + '/\bTenantScopedEvidence\b/', + ]); + + expect($hits)->toBeEmpty("Retired Tenant platform-context names remain:\n".implode("\n", $hits)); +}); + +it('keeps current product-truth docs on workspace and environment terminology', function (): void { + $files = spec317Files([ + repo_path('docs/HANDOVER.md'), + repo_path('docs/product/spec-candidates.md'), + repo_path('docs/product/implementation-ledger.md'), + repo_path('docs/product/roadmap.md'), + repo_path('docs/product/principles.md'), + repo_path('docs/ui'), + repo_path('docs/architecture-guidelines.md'), + repo_path('docs/filament-guidelines.md'), + repo_path('docs/testing-guidelines.md'), + ]); + + $hits = spec317PatternHits($files, [ + '/\btenantPrefilterUrl\b/', + '/\bCanonicalAdminTenantFilterState\b/', + '/\bWorkspaceScopedTenantRoutes\b/', + '/\bTenantPageCategory\b/', + '/\bEnsureFilamentTenantSelected\b/', + '/\blastTenantId\b/', + '/\btenantScopedUrl\b/', + ]); + + expect($hits)->toBeEmpty("Current docs still describe retired Tenant platform context:\n".implode("\n", $hits)); +}); + +it('keeps workspace hubs free of hidden Filament or remembered Environment scope fallbacks', function (): void { + $files = spec317Files([ + base_path('app/Filament/Pages/Monitoring/Operations.php'), + base_path('app/Filament/Pages/Monitoring/FindingExceptionsQueue.php'), + base_path('app/Filament/Pages/Governance/GovernanceInbox.php'), + base_path('app/Filament/Pages/Governance/DecisionRegister.php'), + base_path('app/Filament/Pages/Monitoring/EvidenceOverview.php'), + base_path('app/Filament/Pages/Reviews/ReviewRegister.php'), + base_path('app/Filament/Pages/Reviews/CustomerReviewWorkspace.php'), + base_path('app/Filament/Resources/ProviderConnectionResource.php'), + base_path('app/Filament/Resources/ProviderConnectionResource/Pages/ListProviderConnections.php'), + ]); + + $hits = spec317PatternHits($files, [ + '/\bFilament::getTenant\s*\(/', + '/\blastEnvironmentId\s*\(/', + '/\brememberedEnvironment\s*\(/', + '/\brememberEnvironmentContext\s*\(/', + ]); + + expect($hits)->toBeEmpty("Workspace hubs must not derive scope from Filament tenant or remembered Environment state:\n".implode("\n", $hits)); +}); + +it('keeps helper APIs hard-cut to Environment names and canonical filter keys', function (): void { + expect(method_exists(CustomerReviewWorkspace::class, 'tenantPrefilterUrl'))->toBeFalse() + ->and(method_exists(CustomerReviewWorkspace::class, 'environmentFilterUrl'))->toBeTrue(); + + $reviewUrlHelper = new ReflectionMethod(EnvironmentReviewResource::class, 'environmentScopedUrl'); + + $providerConnectionResource = (string) file_get_contents(base_path('app/Filament/Resources/ProviderConnectionResource.php')); + + expect(method_exists(EnvironmentReviewResource::class, 'tenantScopedUrl'))->toBeFalse() + ->and($reviewUrlHelper->getNumberOfParameters())->toBe(3) + ->and($providerConnectionResource) + ->not->toContain("array_key_exists('tenant', \$parameters)") + ->not->toContain('resolveRequestedTenantExternalId() ?? static::resolveContextTenantExternalId()'); +}); + +it('keeps active environment dashboard links free of retired tenant query aliases', function (): void { + [$user, $environment] = createUserWithTenant(role: 'owner', workspaceRole: 'owner'); + + $workspace = $environment->workspace()->firstOrFail(); + + $this->actingAs($user); + Filament::setTenant($environment, true); + + $response = $this + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $workspace->getKey() => (int) $environment->getKey(), + ], + ]) + ->get(ManagedEnvironmentLinks::viewUrl($environment)); + + $response->assertOk(); + + $content = html_entity_decode((string) $response->getContent(), ENT_QUOTES | ENT_HTML5); + + expect($content) + ->not->toContain('/admin/t') + ->not->toContain('?tenant=') + ->not->toContain('&tenant=') + ->not->toContain('tenant_id=') + ->not->toContain('managed_environment_id=') + ->not->toContain('tenant_scope='); +}); + +it('does not register active legacy tenant panel routes or providers', function (): void { + $legacyRouteUris = collect(Route::getRoutes()) + ->map(fn ($route): string => ltrim((string) $route->uri(), '/')) + ->filter(fn (string $uri): bool => preg_match('#^admin/t(?:/|$)#', $uri) === 1) + ->values(); + + $registeredProviders = require base_path('bootstrap/providers.php'); + $tenantPanelProviders = collect($registeredProviders) + ->filter(fn (string $provider): bool => str_contains($provider, 'TenantPanelProvider')) + ->values(); + + expect($legacyRouteUris)->toBeEmpty() + ->and($tenantPanelProviders)->toBeEmpty() + ->and(file_exists(app_path('Providers/Filament/TenantPanelProvider.php')))->toBeFalse(); +}); diff --git a/apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php b/apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php index 6b7c38b8..0b2c32a9 100644 --- a/apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php +++ b/apps/platform/tests/Feature/Guards/OperationRunLinkContractGuardTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use Tests\Support\OpsUx\SourceFileScanner; use App\Support\OperationRunLinks; +use Tests\Support\OpsUx\SourceFileScanner; /** * @return array @@ -24,7 +24,7 @@ function operationRunLinkContractIncludePaths(): array 'system_ops_runs' => $root.'/app/Filament/System/Pages/Ops/Runs.php', 'system_ops_view_run' => $root.'/app/Filament/System/Pages/Ops/ViewRun.php', 'admin_panel_provider' => $root.'/app/Providers/Filament/AdminPanelProvider.php', - 'ensure_filament_tenant_selected' => $root.'/app/Support/Middleware/EnsureFilamentTenantSelected.php', + 'ensure_environment_context_selected' => $root.'/app/Support/Middleware/EnsureEnvironmentContextSelected.php', 'clear_environment_context_controller' => $root.'/app/Http/Controllers/ClearEnvironmentContextController.php', 'operation_run_url_delegate' => $root.'/app/Support/OpsUx/OperationRunUrl.php', ]; diff --git a/apps/platform/tests/Feature/InventoryItemDependenciesTest.php b/apps/platform/tests/Feature/InventoryItemDependenciesTest.php index f302e3c5..05bb4c7e 100644 --- a/apps/platform/tests/Feature/InventoryItemDependenciesTest.php +++ b/apps/platform/tests/Feature/InventoryItemDependenciesTest.php @@ -12,7 +12,7 @@ function inventoryItemAdminSession(ManagedEnvironment $tenant): array { return [ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]; diff --git a/apps/platform/tests/Feature/Localization/CustomerReviewSurfaceLocalizationTest.php b/apps/platform/tests/Feature/Localization/CustomerReviewSurfaceLocalizationTest.php index 6615a697..e8a08e11 100644 --- a/apps/platform/tests/Feature/Localization/CustomerReviewSurfaceLocalizationTest.php +++ b/apps/platform/tests/Feature/Localization/CustomerReviewSurfaceLocalizationTest.php @@ -5,8 +5,8 @@ use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Resources\EnvironmentReviewResource\Pages\ViewEnvironmentReview; -use App\Models\ReviewPack; use App\Models\ManagedEnvironment; +use App\Models\ReviewPack; use App\Support\EnvironmentReviewStatus; use App\Support\Workspaces\WorkspaceContext; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -86,7 +86,7 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([ + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([ CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1, ])) ->assertOk() @@ -109,4 +109,4 @@ function localizedEnvironmentReviewComponent($user, int $reviewId): Testable return Livewire::withQueryParams([CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1]) ->actingAs($user) ->test(ViewEnvironmentReview::class, ['record' => $reviewId]); -} \ No newline at end of file +} diff --git a/apps/platform/tests/Feature/Localization/LocalePreferenceFlowTest.php b/apps/platform/tests/Feature/Localization/LocalePreferenceFlowTest.php index afd2ed6f..7de4844b 100644 --- a/apps/platform/tests/Feature/Localization/LocalePreferenceFlowTest.php +++ b/apps/platform/tests/Feature/Localization/LocalePreferenceFlowTest.php @@ -3,9 +3,9 @@ declare(strict_types=1); use App\Filament\Pages\Reviews\CustomerReviewWorkspace; +use App\Models\ManagedEnvironment; use App\Services\Localization\LocaleResolver; use App\Services\Settings\SettingsWriter; -use App\Models\ManagedEnvironment; use App\Support\Workspaces\WorkspaceContext; it('allows users to save and clear a personal locale preference over workspace default', function (): void { @@ -92,7 +92,7 @@ $tenant = ManagedEnvironment::factory()->create(); [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly'); - $workspaceUrl = CustomerReviewWorkspace::tenantPrefilterUrl($tenant); + $workspaceUrl = CustomerReviewWorkspace::environmentFilterUrl($tenant); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) diff --git a/apps/platform/tests/Feature/ManagedEnvironment/ManagedEnvironmentPanelContextTest.php b/apps/platform/tests/Feature/ManagedEnvironment/ManagedEnvironmentPanelContextTest.php index 6456f65c..070e5218 100644 --- a/apps/platform/tests/Feature/ManagedEnvironment/ManagedEnvironmentPanelContextTest.php +++ b/apps/platform/tests/Feature/ManagedEnvironment/ManagedEnvironmentPanelContextTest.php @@ -54,7 +54,7 @@ ->call('selectEnvironment', (int) $environment->getKey()) ->assertRedirect(EnvironmentDashboard::getUrl(tenant: $environment)); - expect(session(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY))->toBe([ + expect(session(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY))->toBe([ (string) $environment->workspace_id => (int) $environment->getKey(), ]); }); diff --git a/apps/platform/tests/Feature/Monitoring/FindingExceptionsQueueWorkspaceHubContractTest.php b/apps/platform/tests/Feature/Monitoring/FindingExceptionsQueueWorkspaceHubContractTest.php index 09653c37..935b2334 100644 --- a/apps/platform/tests/Feature/Monitoring/FindingExceptionsQueueWorkspaceHubContractTest.php +++ b/apps/platform/tests/Feature/Monitoring/FindingExceptionsQueueWorkspaceHubContractTest.php @@ -62,7 +62,7 @@ function spec314FindingException(ManagedEnvironment $environment, User $user, st $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $environmentA->workspace_id => (int) $environmentA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php b/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php index b7ffb4f7..4a8424fa 100644 --- a/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php +++ b/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); use App\Filament\Pages\ChooseWorkspace; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Support\ManagedEnvironmentLinks; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; @@ -20,14 +20,14 @@ $response = $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) ->get(route('admin.operations.index', ['workspace' => $tenant->workspace])) ->assertOk() ->assertSee($workspaceName ?? 'Select workspace') - ->assertSee(__('localization.shell.search_environments')) + ->assertSee(__('localization.shell.search_environments')) ->assertSee('Switch workspace') ->assertSee('admin/select-environment') ->assertSee(__('localization.shell.clear_environment_scope')) @@ -67,7 +67,7 @@ ->get('/admin/workspaces') ->assertOk() ->assertSee('Choose a workspace first.') - ->assertDontSee(__('localization.shell.search_environments')); + ->assertDontSee(__('localization.shell.search_environments')); }); it('keeps tenant-scoped pages selector read-only while exposing the clear tenant scope action', function (): void { @@ -106,7 +106,7 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $currentTenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $currentTenant->workspace_id => (int) $currentTenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Monitoring/OperationRunResolvedReferencePresentationTest.php b/apps/platform/tests/Feature/Monitoring/OperationRunResolvedReferencePresentationTest.php index c90c66d1..3272f6f6 100644 --- a/apps/platform/tests/Feature/Monitoring/OperationRunResolvedReferencePresentationTest.php +++ b/apps/platform/tests/Feature/Monitoring/OperationRunResolvedReferencePresentationTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); use App\Models\BackupSet; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Support\OperationRunLinks; use App\Support\Workspaces\WorkspaceContext; @@ -31,28 +31,28 @@ }); it('keeps archived run references viewable with lifecycle-aware framing', function (): void { - $activeTenant = ManagedEnvironment::factory()->create([ + $activeEnvironment = ManagedEnvironment::factory()->create([ 'name' => 'Active ManagedEnvironment', ]); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $this->actingAs($user); $archivedTenant = ManagedEnvironment::factory()->create([ 'name' => 'Archived ManagedEnvironment', - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, ]); createUserWithTenant(tenant: $archivedTenant, user: $user, role: 'owner'); $archivedTenant->delete(); $run = OperationRun::factory()->for($archivedTenant)->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'type' => 'policy.sync', ]); setAdminPanelContext(); - $this->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) + $this->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) ->get(OperationRunLinks::tenantlessView($run)) ->assertOk() ->assertSee('Operation environment is not available in the current environment selector') diff --git a/apps/platform/tests/Feature/Monitoring/OperationsKpiHeaderTenantContextTest.php b/apps/platform/tests/Feature/Monitoring/OperationsKpiHeaderTenantContextTest.php index eca957a2..b27d8fe6 100644 --- a/apps/platform/tests/Feature/Monitoring/OperationsKpiHeaderTenantContextTest.php +++ b/apps/platform/tests/Feature/Monitoring/OperationsKpiHeaderTenantContextTest.php @@ -3,8 +3,8 @@ declare(strict_types=1); use App\Filament\Widgets\Operations\OperationsKpiHeader; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; use App\Support\Workspaces\WorkspaceContext; @@ -27,7 +27,7 @@ function operationsKpiValues($component): array ->all(); } -it('shows workspace-wide operations KPI stats when remembered tenant context exists', function (): void { +it('shows workspace-wide operations KPI stats when remembered environment context exists', function (): void { $tenantA = ManagedEnvironment::factory()->create(); [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner'); @@ -64,7 +64,7 @@ function operationsKpiValues($component): array Filament::setTenant(null, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -109,7 +109,7 @@ function operationsKpiValues($component): array Filament::setTenant($tenantB, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Monitoring/OperationsTenantScopeTest.php b/apps/platform/tests/Feature/Monitoring/OperationsTenantScopeTest.php index 8312a8e6..989ea23a 100644 --- a/apps/platform/tests/Feature/Monitoring/OperationsTenantScopeTest.php +++ b/apps/platform/tests/Feature/Monitoring/OperationsTenantScopeTest.php @@ -59,7 +59,7 @@ ->assertDontSee(__('localization.shell.environment_scope').': '.$tenantA->name); }); -it('does not default Monitoring → Operations list to the remembered tenant', function () { +it('does not default Monitoring → Operations list to the remembered environment', function () { $tenantA = ManagedEnvironment::factory()->create(); $tenantB = ManagedEnvironment::factory()->create(); @@ -87,7 +87,7 @@ setAdminPanelContext(); $workspaceId = (int) $tenantA->workspace_id; - app(WorkspaceContext::class)->rememberLastTenantId($workspaceId, (int) $tenantA->getKey()); + app(WorkspaceContext::class)->rememberLastEnvironmentId($workspaceId, (int) $tenantA->getKey()); $this->withSession([WorkspaceContext::SESSION_KEY => $workspaceId]); session([WorkspaceContext::SESSION_KEY => $workspaceId]); diff --git a/apps/platform/tests/Feature/Monitoring/OperationsWorkspaceHubContractTest.php b/apps/platform/tests/Feature/Monitoring/OperationsWorkspaceHubContractTest.php index 6249d054..c2b7f73b 100644 --- a/apps/platform/tests/Feature/Monitoring/OperationsWorkspaceHubContractTest.php +++ b/apps/platform/tests/Feature/Monitoring/OperationsWorkspaceHubContractTest.php @@ -17,7 +17,7 @@ Filament::setTenant($environment, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspace->getKey() => (int) $environment->getKey(), ]); diff --git a/apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php b/apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php index 313a4955..529c08fc 100644 --- a/apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php +++ b/apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php @@ -70,7 +70,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $environmentA->workspace_id => (int) $environmentA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php b/apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php index 58afa76e..645d53fa 100644 --- a/apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php +++ b/apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php @@ -32,7 +32,7 @@ OperationRunLinks::index($environment), ManagedEnvironmentLinks::operationsUrl($environment), ManagedEnvironmentLinks::providerConnectionsUrl($environment), - CustomerReviewWorkspace::tenantPrefilterUrl($environment), + CustomerReviewWorkspace::environmentFilterUrl($environment), GovernanceInbox::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environment->getKey()]), DecisionRegister::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environment->getKey()]), FindingExceptionsQueue::getUrl(panel: 'admin', parameters: ['environment_id' => (int) $environment->getKey()]), @@ -170,6 +170,79 @@ ->assertCanSeeTableRecords([$records['runA'], $records['runB']]); }); +it('Spec317 critical workspace hubs ignore every retired tenant query alias', function (): void { + [$user, $environmentA, $environmentB, $records] = spec315SeedEnvironmentFilterWorkspace(); + + $this->actingAs($user); + setAdminPanelContext(); + session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id); + + $legacyQueries = [ + 'tenant' => ['tenant' => (string) $environmentA->getKey()], + 'tenant_id' => ['tenant_id' => (int) $environmentA->getKey()], + 'managed_environment_id' => ['managed_environment_id' => (int) $environmentA->getKey()], + 'tenant_scope' => ['tenant_scope' => 'environment'], + 'environment' => ['environment' => (string) $environmentA->getKey()], + 'tableFilters' => [ + 'tableFilters' => [ + 'managed_environment_id' => ['value' => (string) $environmentA->getKey()], + ], + ], + ]; + + $livewireCases = [ + 'operations' => [ + 'component' => Operations::class, + 'wide_records' => [$records['runA'], $records['runB']], + ], + 'finding exceptions queue' => [ + 'component' => FindingExceptionsQueue::class, + 'wide_records' => [$records['exceptionA'], $records['exceptionB']], + ], + 'provider connections' => [ + 'component' => ListProviderConnections::class, + 'wide_records' => [$records['connectionA'], $records['connectionB']], + ], + 'review register' => [ + 'component' => ReviewRegister::class, + 'wide_records' => [$records['reviewA']->fresh(), $records['reviewB']->fresh()], + ], + 'customer review workspace' => [ + 'component' => CustomerReviewWorkspace::class, + 'wide_records' => [$environmentA->fresh(), $environmentB->fresh()], + ], + 'decision register' => [ + 'component' => DecisionRegister::class, + 'wide_records' => [$records['exceptionA'], $records['exceptionB']], + ], + ]; + + foreach ($legacyQueries as $queryName => $query) { + foreach ($livewireCases as $caseName => $case) { + Livewire::withQueryParams($query) + ->actingAs($user) + ->test($case['component']) + ->assertDontSee('Environment filter:') + ->assertCanSeeTableRecords($case['wide_records']); + } + + $this->get(GovernanceInbox::getUrl(panel: 'admin', parameters: $query)) + ->assertOk() + ->assertDontSee('Environment filter:') + ->assertSee('Spec315 Governance B'); + + $this->get(DecisionRegister::getUrl(panel: 'admin', parameters: $query)) + ->assertOk() + ->assertDontSee('Environment filter:'); + + $this->get(route('admin.evidence.overview', $query)) + ->assertOk() + ->assertDontSee('Environment filter:') + ->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $records['snapshotA']], tenant: $environmentA, panel: 'admin'), false) + ->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $records['snapshotB']], tenant: $environmentB, panel: 'admin'), false); + } +}); + it('Spec315 environment filter must belong to the current workspace', function (): void { $environmentA = ManagedEnvironment::factory()->active()->create(); [$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner'); diff --git a/apps/platform/tests/Feature/Navigation/WorkspaceHubSidebarUrlContractTest.php b/apps/platform/tests/Feature/Navigation/WorkspaceHubSidebarUrlContractTest.php index 21a54a12..3647f5b5 100644 --- a/apps/platform/tests/Feature/Navigation/WorkspaceHubSidebarUrlContractTest.php +++ b/apps/platform/tests/Feature/Navigation/WorkspaceHubSidebarUrlContractTest.php @@ -66,7 +66,7 @@ function spec314NavigationItemUrls(array $items): array $this->actingAs($user); Filament::setTenant($environment, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspace->getKey() => (int) $environment->getKey(), ]); @@ -108,7 +108,7 @@ function spec314NavigationItemUrls(array $items): array $this->actingAs($user); Filament::setTenant($environment, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspace->getKey() => (int) $environment->getKey(), ]); diff --git a/apps/platform/tests/Feature/Onboarding/ManagedEnvironmentOnboardingEntitlementTest.php b/apps/platform/tests/Feature/Onboarding/ManagedEnvironmentOnboardingEntitlementTest.php index d280bf18..7205bf2d 100644 --- a/apps/platform/tests/Feature/Onboarding/ManagedEnvironmentOnboardingEntitlementTest.php +++ b/apps/platform/tests/Feature/Onboarding/ManagedEnvironmentOnboardingEntitlementTest.php @@ -4,11 +4,11 @@ use App\Filament\Pages\Workspaces\ManagedEnvironmentOnboardingWizard; use App\Models\AuditLog; +use App\Models\ManagedEnvironment; +use App\Models\ManagedEnvironmentOnboardingSession; use App\Models\OperationRun; use App\Models\PlatformUser; use App\Models\ProviderConnection; -use App\Models\ManagedEnvironment; -use App\Models\ManagedEnvironmentOnboardingSession; use App\Models\User; use App\Models\Workspace; use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver; @@ -25,13 +25,12 @@ * @return array{workspace: Workspace, user: User, tenant: ManagedEnvironment, draft: ManagedEnvironmentOnboardingSession, component: \Livewire\Features\SupportTesting\Testable} */ function readyOnboardingEntitlementContext( - int $activeTenantCount = 0, + int $activeEnvironmentCount = 0, ?int $limitOverride = null, ?string $overrideReason = null, ?string $commercialState = null, ?array $subscription = null, -): array -{ +): array { Queue::fake(); $workspace = Workspace::factory()->create(); @@ -50,8 +49,8 @@ function readyOnboardingEntitlementContext( ensureDefaultMicrosoftProviderConnection: false, ); - if ($activeTenantCount > 0) { - ManagedEnvironment::factory()->count($activeTenantCount)->create([ + if ($activeEnvironmentCount > 0) { + ManagedEnvironment::factory()->count($activeEnvironmentCount)->create([ 'workspace_id' => (int) $workspace->getKey(), 'status' => ManagedEnvironment::STATUS_ACTIVE, ]); @@ -160,7 +159,7 @@ function readyOnboardingEntitlementContext( } it('allows onboarding activation when the workspace is within its managed environment limit', function (): void { - $context = readyOnboardingEntitlementContext(activeTenantCount: 0); + $context = readyOnboardingEntitlementContext(activeEnvironmentCount: 0); $context['component']->call('completeOnboarding'); @@ -175,7 +174,7 @@ function readyOnboardingEntitlementContext( it('blocks onboarding activation with a business-state reason when the workspace is at limit', function (): void { $context = readyOnboardingEntitlementContext( - activeTenantCount: 1, + activeEnvironmentCount: 1, limitOverride: 1, overrideReason: 'Customer currently allows one active tenant', ); @@ -203,7 +202,7 @@ function readyOnboardingEntitlementContext( it('allows onboarding activation when a workspace override raises the limit above current usage', function (): void { $context = readyOnboardingEntitlementContext( - activeTenantCount: 1, + activeEnvironmentCount: 1, limitOverride: 2, overrideReason: 'Temporary support-approved exception', ); @@ -231,7 +230,7 @@ function readyOnboardingEntitlementContext( it('allows onboarding activation while a workspace is in trial', function (): void { $context = readyOnboardingEntitlementContext( - activeTenantCount: 0, + activeEnvironmentCount: 0, commercialState: WorkspaceCommercialLifecycleResolver::STATE_TRIAL, ); @@ -251,7 +250,7 @@ function readyOnboardingEntitlementContext( it('identifies subscription-backed commercial posture on the onboarding completion step', function (): void { $context = readyOnboardingEntitlementContext( - activeTenantCount: 0, + activeEnvironmentCount: 0, subscription: [ 'state' => 'trial', 'billing_reference' => 'sub_trial_001', @@ -273,7 +272,7 @@ function readyOnboardingEntitlementContext( it('blocks onboarding activation with a grace commercial-state reason before tenant mutation', function (): void { $context = readyOnboardingEntitlementContext( - activeTenantCount: 0, + activeEnvironmentCount: 0, commercialState: WorkspaceCommercialLifecycleResolver::STATE_GRACE, ); @@ -295,7 +294,7 @@ function readyOnboardingEntitlementContext( it('blocks onboarding activation with a suspended read-only commercial-state reason before tenant mutation', function (): void { $context = readyOnboardingEntitlementContext( - activeTenantCount: 0, + activeEnvironmentCount: 0, commercialState: WorkspaceCommercialLifecycleResolver::STATE_SUSPENDED_READ_ONLY, ); diff --git a/apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php b/apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php index 4172c1e8..82d556b9 100644 --- a/apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php +++ b/apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php @@ -5,8 +5,8 @@ use App\Filament\Pages\Operations\TenantlessOperationRunViewer; use App\Models\BaselineProfile; use App\Models\BaselineSnapshot; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Models\User; use App\Models\Workspace; use App\Models\WorkspaceMembership; @@ -272,7 +272,7 @@ expect(mb_substr_count($pageText, 'Automatically reconciled'))->toBe(1); }); -it('keeps a canonical run viewer accessible when the remembered tenant differs from the run tenant', function (): void { +it('keeps a canonical run viewer accessible when the remembered environment differs from the run tenant', function (): void { $workspace = Workspace::factory()->create(); $tenantA = ManagedEnvironment::factory()->for($workspace)->create(); $tenantB = ManagedEnvironment::factory()->for($workspace)->create(); @@ -306,7 +306,7 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $workspace->getKey() => (int) $tenantB->getKey(), ], ]) @@ -333,7 +333,7 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $selectedTenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $selectedTenant->workspace_id => (int) $selectedTenant->getKey(), ], ]) @@ -343,19 +343,19 @@ ->assertSee('This canonical workspace view is not tied to the current environment context'); }); -it('keeps a canonical run viewer accessible when remembered tenant context is cleared as stale', function (): void { +it('keeps a canonical run viewer accessible when remembered environment context is cleared as stale', function (): void { $runTenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Viewer Run ManagedEnvironment', ]); [$user, $runTenant] = createUserWithTenant(tenant: $runTenant, role: 'owner'); - $rememberedTenant = ManagedEnvironment::factory()->onboarding()->create([ + $rememberedEnvironment = ManagedEnvironment::factory()->onboarding()->create([ 'workspace_id' => (int) $runTenant->workspace_id, 'name' => 'Viewer Onboarding ManagedEnvironment', ]); createUserWithTenant( - tenant: $rememberedTenant, + tenant: $rememberedEnvironment, user: $user, role: 'owner', workspaceRole: 'owner', @@ -375,8 +375,8 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ - (string) $runTenant->workspace_id => (int) $rememberedTenant->getKey(), + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $runTenant->workspace_id => (int) $rememberedEnvironment->getKey(), ], ]) ->get(OperationRunLinks::tenantlessView((int) $run->getKey())) @@ -387,13 +387,13 @@ }); it('keeps a canonical run viewer accessible when the run tenant is selector-ineligible but the remembered context is valid', function (): void { - $rememberedTenant = ManagedEnvironment::factory()->active()->create([ + $rememberedEnvironment = ManagedEnvironment::factory()->active()->create([ 'name' => 'Viewer Active ManagedEnvironment', ]); - [$user, $rememberedTenant] = createUserWithTenant(tenant: $rememberedTenant, role: 'owner'); + [$user, $rememberedEnvironment] = createUserWithTenant(tenant: $rememberedEnvironment, role: 'owner'); $runTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $rememberedTenant->workspace_id, + 'workspace_id' => (int) $rememberedEnvironment->workspace_id, 'name' => 'Viewer Onboarding ManagedEnvironment', ]); @@ -407,7 +407,7 @@ $run = OperationRun::factory()->create([ 'managed_environment_id' => (int) $runTenant->getKey(), - 'workspace_id' => (int) $rememberedTenant->workspace_id, + 'workspace_id' => (int) $rememberedEnvironment->workspace_id, 'type' => 'inventory_sync', 'status' => OperationRunStatus::Queued->value, 'outcome' => OperationRunOutcome::Pending->value, @@ -417,9 +417,9 @@ $this->actingAs($user) ->withSession([ - WorkspaceContext::SESSION_KEY => (int) $rememberedTenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ - (string) $rememberedTenant->workspace_id => (int) $rememberedTenant->getKey(), + WorkspaceContext::SESSION_KEY => (int) $rememberedEnvironment->workspace_id, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $rememberedEnvironment->workspace_id => (int) $rememberedEnvironment->getKey(), ], ]) ->get(OperationRunLinks::tenantlessView((int) $run->getKey())) diff --git a/apps/platform/tests/Feature/OpsUx/OperateHubShellTest.php b/apps/platform/tests/Feature/OpsUx/OperateHubShellTest.php index 68e0302f..47255de6 100644 --- a/apps/platform/tests/Feature/OpsUx/OperateHubShellTest.php +++ b/apps/platform/tests/Feature/OpsUx/OperateHubShellTest.php @@ -134,7 +134,7 @@ $response = $this->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => $lastTenantMap, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => $lastTenantMap, ])->get(route('admin.operations.view', [ 'workspace' => (int) $run->workspace_id, 'run' => (int) $run->getKey(), @@ -169,7 +169,7 @@ $response = $this->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [(string) $workspaceId => (int) $nonEntitledTenant->getKey()], + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [(string) $workspaceId => (int) $nonEntitledTenant->getKey()], ])->get(route('admin.operations.view', [ 'workspace' => (int) $run->workspace_id, 'run' => (int) $run->getKey(), @@ -269,12 +269,12 @@ $response = $this->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => $lastTenantMap, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => $lastTenantMap, ])->get(route('admin.operations.index', ['workspace' => $workspaceId])); $response->assertOk(); $response->assertSessionHas(WorkspaceContext::SESSION_KEY, $workspaceId); - $response->assertSessionHas(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, $lastTenantMap); + $response->assertSessionHas(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, $lastTenantMap); })->group('ops-ux'); it('prefers the filament tenant over remembered workspace tenant state when both are entitled', function (): void { @@ -291,7 +291,7 @@ Filament::setTenant($tenantB, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -300,7 +300,7 @@ expect($resolved?->is($tenantB))->toBeTrue(); })->group('ops-ux'); -it('keeps canonical run routes tenantless even when filament and remembered tenant state exist', function (): void { +it('keeps canonical run routes tenantless even when filament and remembered environment state exist', function (): void { $runTenant = ManagedEnvironment::factory()->create([ 'workspace_id' => null, ]); @@ -324,7 +324,7 @@ $workspaceId = (int) $runTenant->workspace_id; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $runTenant->getKey(), ]); @@ -373,7 +373,7 @@ $workspaceId = (int) $runTenant->workspace_id; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $runTenant->getKey(), ]); @@ -391,7 +391,7 @@ expect($resolved)->toBeNull(); })->group('ops-ux'); -it('clears stale remembered tenant ids when the remembered tenant is no longer entitled', function (): void { +it('clears stale remembered environment ids when the remembered environment is no longer entitled', function (): void { [$user, $tenant] = createMinimalUserWithTenant(role: 'owner'); $staleTenant = ManagedEnvironment::factory()->create([ @@ -404,27 +404,27 @@ $workspaceId = (int) $tenant->workspace_id; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspaceId => (int) $staleTenant->getKey(), ]); $resolved = app(OperateHubShell::class)->activeEntitledTenant(); expect($resolved)->toBeNull(); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) ->not->toHaveKey((string) $workspaceId); })->group('ops-ux'); it('prefers the routed tenant over remembered workspace tenant state when a tenant route parameter is present', function (): void { - $rememberedTenant = ManagedEnvironment::factory()->create([ + $rememberedEnvironment = ManagedEnvironment::factory()->create([ 'name' => 'YPTW2', 'environment' => 'dev', 'status' => 'active', ]); - [$user, $rememberedTenant] = createMinimalUserWithTenant(tenant: $rememberedTenant, role: 'owner'); + [$user, $rememberedEnvironment] = createMinimalUserWithTenant(tenant: $rememberedEnvironment, role: 'owner'); $routedTenant = ManagedEnvironment::factory()->create([ - 'workspace_id' => (int) $rememberedTenant->workspace_id, + 'workspace_id' => (int) $rememberedEnvironment->workspace_id, 'name' => 'Phoenicon', 'environment' => 'dev', 'status' => 'active', @@ -435,11 +435,11 @@ $this->actingAs($user); Filament::setTenant(null, true); - $workspaceId = (int) $rememberedTenant->workspace_id; + $workspaceId = (int) $rememberedEnvironment->workspace_id; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ - (string) $workspaceId => (int) $rememberedTenant->getKey(), + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ + (string) $workspaceId => (int) $rememberedEnvironment->getKey(), ]); $request = Request::create(EnvironmentRequiredPermissions::getUrl([ @@ -457,15 +457,15 @@ })->group('ops-ux'); it('prefers the routed tenant resource record over active tenant state on admin tenant view routes', function (): void { - $rememberedTenant = ManagedEnvironment::factory()->create([ + $rememberedEnvironment = ManagedEnvironment::factory()->create([ 'name' => 'YPTW2', 'environment' => 'dev', 'status' => 'active', ]); - [$user, $rememberedTenant] = createMinimalUserWithTenant(tenant: $rememberedTenant, role: 'owner'); + [$user, $rememberedEnvironment] = createMinimalUserWithTenant(tenant: $rememberedEnvironment, role: 'owner'); $routedTenant = ManagedEnvironment::factory()->create([ - 'workspace_id' => (int) $rememberedTenant->workspace_id, + 'workspace_id' => (int) $rememberedEnvironment->workspace_id, 'name' => 'Test', 'environment' => 'dev', 'status' => ManagedEnvironment::STATUS_ONBOARDING, @@ -474,13 +474,13 @@ createMinimalUserWithTenant(tenant: $routedTenant, user: $user, role: 'owner'); $this->actingAs($user); - Filament::setTenant($rememberedTenant, true); + Filament::setTenant($rememberedEnvironment, true); - $workspaceId = (int) $rememberedTenant->workspace_id; + $workspaceId = (int) $rememberedEnvironment->workspace_id; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ - (string) $workspaceId => (int) $rememberedTenant->getKey(), + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ + (string) $workspaceId => (int) $rememberedEnvironment->getKey(), ]); $request = Request::create(EnvironmentDashboard::getUrl(tenant: $routedTenant)); @@ -583,7 +583,7 @@ ->assertDontSee('Scope: Workspace'); })->group('ops-ux'); -it('suppresses tenant indicator on alert rules list with lastTenantId fallback', function (): void { +it('suppresses tenant indicator on alert rules list with lastEnvironmentId fallback', function (): void { [$user, $tenant] = createMinimalUserWithTenant(role: 'owner'); $this->actingAs($user); @@ -594,26 +594,26 @@ $this->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => $lastTenantMap, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => $lastTenantMap, ])->get(AlertRuleResource::getUrl(panel: 'admin')) ->assertOk() ->assertDontSee('Filtered by tenant') ->assertDontSee('Scope: ManagedEnvironment'); })->group('ops-ux'); -it('treats selector-ineligible remembered tenants as no selected tenant on canonical viewer routes', function (): void { +it('treats selector-ineligible remembered environments as no selected tenant on canonical viewer routes', function (): void { $runTenant = ManagedEnvironment::factory()->active()->create([ 'name' => 'Canonical Run ManagedEnvironment', ]); [$user, $runTenant] = createMinimalUserWithTenant(tenant: $runTenant, role: 'owner'); - $rememberedTenant = ManagedEnvironment::factory()->onboarding()->create([ + $rememberedEnvironment = ManagedEnvironment::factory()->onboarding()->create([ 'workspace_id' => (int) $runTenant->workspace_id, 'name' => 'Stale Onboarding ManagedEnvironment', ]); createMinimalUserWithTenant( - tenant: $rememberedTenant, + tenant: $rememberedEnvironment, user: $user, role: 'owner', workspaceRole: 'owner', @@ -631,8 +631,8 @@ $response = $this->withSession([ WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ - (string) $runTenant->workspace_id => (int) $rememberedTenant->getKey(), + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $runTenant->workspace_id => (int) $rememberedEnvironment->getKey(), ], ])->get(route('admin.operations.view', [ 'workspace' => (int) $run->workspace_id, diff --git a/apps/platform/tests/Feature/ProviderConnections/ProviderConnectionsWorkspaceHubContractTest.php b/apps/platform/tests/Feature/ProviderConnections/ProviderConnectionsWorkspaceHubContractTest.php index 342a6aa2..e2325ade 100644 --- a/apps/platform/tests/Feature/ProviderConnections/ProviderConnectionsWorkspaceHubContractTest.php +++ b/apps/platform/tests/Feature/ProviderConnections/ProviderConnectionsWorkspaceHubContractTest.php @@ -44,7 +44,7 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $environmentA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $environmentA->workspace_id => (int) $environmentA->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Rbac/AdminGlobalSearchContextSafetyTest.php b/apps/platform/tests/Feature/Rbac/AdminGlobalSearchContextSafetyTest.php index 3bd90489..1cdeeb53 100644 --- a/apps/platform/tests/Feature/Rbac/AdminGlobalSearchContextSafetyTest.php +++ b/apps/platform/tests/Feature/Rbac/AdminGlobalSearchContextSafetyTest.php @@ -3,15 +3,15 @@ declare(strict_types=1); use App\Filament\Resources\EntraGroupResource; +use App\Filament\Resources\ManagedEnvironmentResource; use App\Filament\Resources\OperationRunResource; use App\Filament\Resources\PolicyResource; use App\Filament\Resources\PolicyVersionResource; use App\Filament\Resources\ProviderConnectionResource; -use App\Filament\Resources\ManagedEnvironmentResource; use App\Models\EntraGroup; +use App\Models\ManagedEnvironment; use App\Models\Policy; use App\Models\PolicyVersion; -use App\Models\ManagedEnvironment; use App\Support\WorkspaceIsolation\TenantOwnedModelFamilies; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; @@ -25,11 +25,11 @@ function adminGlobalSearchTitles($results): array } it('keeps managed environment registry results out of global search regardless of remembered context', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Search Safety Active']); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + $activeEnvironment = ManagedEnvironment::factory()->active()->create(['name' => 'Search Safety Active']); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Search Safety Onboarding', ]); @@ -41,9 +41,9 @@ function adminGlobalSearchTitles($results): array Filament::setTenant(null, true); Filament::bootCurrentPanel(); - session()->put(WorkspaceContext::SESSION_KEY, (int) $activeTenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ - (string) $activeTenant->workspace_id => (int) $onboardingTenant->getKey(), + session()->put(WorkspaceContext::SESSION_KEY, (int) $activeEnvironment->workspace_id); + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ + (string) $activeEnvironment->workspace_id => (int) $onboardingTenant->getKey(), ]); expect(adminGlobalSearchTitles(ManagedEnvironmentResource::getGlobalSearchResults('Search Safety'))) @@ -89,7 +89,7 @@ function adminGlobalSearchTitles($results): array Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Rbac/AdminTenantOwnedPolicyContextTest.php b/apps/platform/tests/Feature/Rbac/AdminTenantOwnedPolicyContextTest.php index eb0f7a42..02e633ca 100644 --- a/apps/platform/tests/Feature/Rbac/AdminTenantOwnedPolicyContextTest.php +++ b/apps/platform/tests/Feature/Rbac/AdminTenantOwnedPolicyContextTest.php @@ -29,7 +29,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -83,7 +83,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -114,7 +114,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -155,7 +155,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); diff --git a/apps/platform/tests/Feature/Rbac/FindingExceptionLifecycleAccessBoundaryTest.php b/apps/platform/tests/Feature/Rbac/FindingExceptionLifecycleAccessBoundaryTest.php index 9713cf34..93f25d9d 100644 --- a/apps/platform/tests/Feature/Rbac/FindingExceptionLifecycleAccessBoundaryTest.php +++ b/apps/platform/tests/Feature/Rbac/FindingExceptionLifecycleAccessBoundaryTest.php @@ -90,7 +90,7 @@ $allowedTenant->makeCurrent(); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); - app(WorkspaceContext::class)->rememberLastTenantId((int) $workspace->getKey(), (int) $allowedTenant->getKey()); + app(WorkspaceContext::class)->rememberLastEnvironmentId((int) $workspace->getKey(), (int) $allowedTenant->getKey()); $response = Gate::forUser($user)->inspect('view', $exception); diff --git a/apps/platform/tests/Feature/Rbac/InventoryItemResourceAuthorizationTest.php b/apps/platform/tests/Feature/Rbac/InventoryItemResourceAuthorizationTest.php index ae8d1276..413a6586 100644 --- a/apps/platform/tests/Feature/Rbac/InventoryItemResourceAuthorizationTest.php +++ b/apps/platform/tests/Feature/Rbac/InventoryItemResourceAuthorizationTest.php @@ -5,8 +5,8 @@ use App\Filament\Resources\InventoryItemResource; use App\Filament\Resources\InventoryItemResource\Pages\ListInventoryItems; use App\Models\InventoryItem; -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Models\User; use App\Support\Inventory\InventoryCoverage as InventoryCoveragePayload; use App\Support\Workspaces\WorkspaceContext; @@ -138,7 +138,7 @@ Filament::bootCurrentPanel(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); @@ -149,7 +149,7 @@ ->assertCanSeeTableRecords([$tenantARecord]) ->assertCanNotSeeTableRecords([$tenantBRecord]); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/Rbac/TenantLifecycleActionVisibilityTest.php b/apps/platform/tests/Feature/Rbac/TenantLifecycleActionVisibilityTest.php index a4ce597e..2eb9faa7 100644 --- a/apps/platform/tests/Feature/Rbac/TenantLifecycleActionVisibilityTest.php +++ b/apps/platform/tests/Feature/Rbac/TenantLifecycleActionVisibilityTest.php @@ -146,13 +146,13 @@ }); it('shows capability-denied actions as disabled but keeps lifecycle-denied actions hidden', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create([ + $activeEnvironment = ManagedEnvironment::factory()->active()->create([ 'name' => 'Capability Denied Active ManagedEnvironment', ]); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'manager', ensureDefaultMicrosoftProviderConnection: false); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'manager', ensureDefaultMicrosoftProviderConnection: false); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Lifecycle Denied Onboarding ManagedEnvironment', ]); @@ -175,19 +175,19 @@ ], ]); - session()->put(WorkspaceContext::SESSION_KEY, (int) $activeTenant->workspace_id); + session()->put(WorkspaceContext::SESSION_KEY, (int) $activeEnvironment->workspace_id); Livewire::actingAs($user) ->test(ListManagedEnvironments::class) - ->assertTableActionVisible('archive', $activeTenant) - ->assertTableActionDisabled('archive', $activeTenant) + ->assertTableActionVisible('archive', $activeEnvironment) + ->assertTableActionDisabled('archive', $activeEnvironment) ->assertTableActionHidden('archive', $onboardingTenant) ->assertTableActionVisible('related_onboarding', $onboardingTenant); Filament::setTenant(null, true); Livewire::actingAs($user) - ->test(ViewManagedEnvironment::class, ['record' => $activeTenant->getRouteKey()]) + ->test(ViewManagedEnvironment::class, ['record' => $activeEnvironment->getRouteKey()]) ->assertActionVisible('archive') ->assertActionDisabled('archive'); @@ -242,11 +242,11 @@ }); it('refuses lifecycle-invalid archive and restore mutations without changing tenant state', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create(); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + $activeEnvironment = ManagedEnvironment::factory()->active()->create(); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, ]); createUserWithTenant( @@ -261,13 +261,13 @@ $auditLogger = app(WorkspaceAuditLogger::class); - ManagedEnvironmentResource::restoreTenant($activeTenant, $auditLogger); + ManagedEnvironmentResource::restoreTenant($activeEnvironment, $auditLogger); ManagedEnvironmentResource::archiveTenant($onboardingTenant, $auditLogger, 'Trying to archive an onboarding tenant should be rejected.'); - $activeTenant->refresh(); + $activeEnvironment->refresh(); $onboardingTenant->refresh(); - expect($activeTenant->trashed())->toBeFalse() + expect($activeEnvironment->trashed())->toBeFalse() ->and($onboardingTenant->trashed())->toBeFalse() ->and($onboardingTenant->status)->toBe(ManagedEnvironment::STATUS_ONBOARDING) ->and(AuditLog::query() @@ -276,7 +276,7 @@ AuditActionId::TenantRestored->value, ]) ->whereIn('resource_id', [ - (string) $activeTenant->getKey(), + (string) $activeEnvironment->getKey(), (string) $onboardingTenant->getKey(), ]) ->exists())->toBeFalse(); diff --git a/apps/platform/tests/Feature/RestoreRunWizardExecuteTest.php b/apps/platform/tests/Feature/RestoreRunWizardExecuteTest.php index 6af87f83..5ff284f1 100644 --- a/apps/platform/tests/Feature/RestoreRunWizardExecuteTest.php +++ b/apps/platform/tests/Feature/RestoreRunWizardExecuteTest.php @@ -4,10 +4,10 @@ use App\Jobs\ExecuteRestoreRunJob; use App\Models\BackupItem; use App\Models\BackupSet; +use App\Models\ManagedEnvironment; use App\Models\OperationRun; use App\Models\Policy; use App\Models\RestoreRun; -use App\Models\ManagedEnvironment; use App\Models\User; use App\Support\RestoreRunStatus; use App\Support\Workspaces\WorkspaceContext; @@ -309,7 +309,7 @@ setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceHubContractTest.php b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceHubContractTest.php index 6dd4563f..7395f7c4 100644 --- a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceHubContractTest.php +++ b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceHubContractTest.php @@ -42,7 +42,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $environmentA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $environmentA->workspace_id => (int) $environmentA->getKey(), ]); diff --git a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php index 739359f9..a851ff05 100644 --- a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php +++ b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php @@ -3,19 +3,18 @@ declare(strict_types=1); use App\Filament\Pages\Reviews\CustomerReviewWorkspace; +use App\Filament\Resources\EnvironmentReviewResource; +use App\Filament\Resources\EnvironmentReviewResource\Pages\ViewEnvironmentReview; use App\Filament\Resources\EvidenceSnapshotResource; use App\Filament\Resources\ReviewPackResource; -use App\Filament\Resources\EnvironmentReviewResource\Pages\ViewEnvironmentReview; -use App\Filament\Resources\EnvironmentReviewResource; use App\Filament\Widgets\ManagedEnvironment\ManagedEnvironmentReviewPackCard; use App\Models\AuditLog; use App\Models\EvidenceSnapshot; -use App\Models\ReviewPack; use App\Models\ManagedEnvironment; -use App\Models\EnvironmentReview; +use App\Models\ReviewPack; use App\Support\Audit\AuditActionId; -use App\Support\Evidence\EvidenceSnapshotStatus; use App\Support\EnvironmentReviewStatus; +use App\Support\Evidence\EvidenceSnapshotStatus; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Storage; use Livewire\Livewire; @@ -39,9 +38,9 @@ ])->save(); $this->actingAs($user) - ->get(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant)) + ->get(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review], $tenant)) ->assertOk() - ->assertSee(CustomerReviewWorkspace::tenantPrefilterUrl($tenant), false); + ->assertSee(CustomerReviewWorkspace::environmentFilterUrl($tenant), false); }); it('adds a customer workspace entry to evidence snapshot related context', function (): void { @@ -59,7 +58,7 @@ ->firstWhere('key', 'customer_review_workspace'); expect($entry)->not->toBeNull() - ->and($entry['targetUrl'] ?? null)->toBe(CustomerReviewWorkspace::tenantPrefilterUrl($tenant)); + ->and($entry['targetUrl'] ?? null)->toBe(CustomerReviewWorkspace::environmentFilterUrl($tenant)); }); it('renders a customer workspace link from review pack detail context', function (): void { @@ -89,7 +88,7 @@ $this->actingAs($user) ->get(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'admin')) ->assertOk() - ->assertSee(CustomerReviewWorkspace::tenantPrefilterUrl($tenant), false); + ->assertSee(CustomerReviewWorkspace::environmentFilterUrl($tenant), false); }); it('renders a customer workspace launch button on the environment review pack widget', function (): void { @@ -121,7 +120,7 @@ Livewire::actingAs($user) ->test(ManagedEnvironmentReviewPackCard::class, ['record' => $tenant]) ->assertSee('Customer workspace') - ->assertSee(CustomerReviewWorkspace::tenantPrefilterUrl($tenant), false); + ->assertSee(CustomerReviewWorkspace::environmentFilterUrl($tenant), false); }); it('keeps the linked environment review detail read-only for a readonly-capable actor', function (): void { diff --git a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php index 68841cbe..0225e348 100644 --- a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php +++ b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php @@ -53,7 +53,7 @@ ->assertSet('tableFilters.managed_environment_id.value', (string) $tenant->getKey()) ->assertActionVisible('return_to_governance_inbox') ->assertCanSeeTableRecords([$tenant->fresh()]) - ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false) + ->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review->fresh()], $tenant), false) ->assertSee(CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY.'=1', false) ->assertSee('nav%5Bsource_surface%5D=governance.inbox', false) ->assertSee('nav%5Bfamily_key%5D=review_follow_up', false) diff --git a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php index 3cad8390..cd25d917 100644 --- a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php +++ b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php @@ -4,9 +4,9 @@ use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\EnvironmentReviewResource; +use App\Models\ManagedEnvironment; use App\Models\PlatformUser; use App\Models\ReviewPack; -use App\Models\ManagedEnvironment; use App\Models\User; use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver; use App\Services\Settings\SettingsWriter; @@ -68,7 +68,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t Livewire::actingAs($user) ->test(CustomerReviewWorkspace::class) - ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false) + ->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review->fresh()], $tenant), false) ->assertSee('Review pack') ->assertSee('Available') ->assertSee('Current review pack is ready to download.') @@ -114,7 +114,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t Livewire::actingAs($user) ->test(CustomerReviewWorkspace::class) - ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false) + ->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review->fresh()], $tenant), false) ->assertSee('Download review pack') ->assertSee('Open review') ->assertDontSee('Generate pack') @@ -141,7 +141,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t Livewire::actingAs($user) ->test(CustomerReviewWorkspace::class) - ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false) + ->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review->fresh()], $tenant), false) ->assertSee('Not available yet') ->assertSee('Review Pack has not been generated for this released review yet.') ->assertDontSee('Download review pack') @@ -181,7 +181,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t Livewire::actingAs($user) ->test(CustomerReviewWorkspace::class) - ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false) + ->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $review->fresh()], $tenant), false) ->assertSee('Evidence incomplete') ->assertSee('Review Pack or decision summary may be incomplete.') ->assertDontSee('Download review pack'); diff --git a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php index c8a60eab..5cce3cb2 100644 --- a/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php +++ b/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php @@ -91,7 +91,7 @@ ->test(CustomerReviewWorkspace::class) ->assertCanSeeTableRecords([$tenantA->fresh(), $tenantB->fresh()]) ->assertCanNotSeeTableRecords([$tenantDenied->fresh()]) - ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $latestPublishedReview->fresh()], $tenantA), false) + ->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $latestPublishedReview->fresh()], $tenantA), false) ->assertSee('Review the executive-ready governance package status for each entitled tenant and open the customer-safe detail when follow-up is needed.') ->assertSee('Each row is an index entry: open the review detail to inspect package status, the executive entrypoint, supporting evidence, current risks, and the next customer-safe action.') ->assertSee('This workspace summarizes current review evidence for service delivery. It does not replace a formal audit opinion, certification, or legal attestation.') @@ -106,15 +106,15 @@ ->assertDontSee('No mapped controls') ->assertDontSee('Compliance evidence mapping v1') ->assertDontSee(ComplianceEvidenceMappingV1::VERSION_KEY) - ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $betaPublishedReview->fresh()], $tenantB), false) - ->assertDontSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $olderPublishedReview->fresh()], $tenantA), false) - ->assertDontSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $newerInternalReview->fresh()], $tenantA), false) + ->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $betaPublishedReview->fresh()], $tenantB), false) + ->assertDontSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $olderPublishedReview->fresh()], $tenantA), false) + ->assertDontSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $newerInternalReview->fresh()], $tenantA), false) ->assertDontSee('Publish review') ->assertDontSee('Refresh review') ->assertDontSee('Create next review') ->assertDontSee('Regenerate') ->assertDontSee('Expire snapshot') - ->assertDontSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $deniedPublishedReview->fresh()], $tenantDenied), false); + ->assertDontSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $deniedPublishedReview->fresh()], $tenantDenied), false); }); it('shows the current released review using deterministic published review ordering', function (): void { @@ -159,7 +159,7 @@ $publishedAt->format('M j, Y'), 'No decisions require awareness', ]) - ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $betaReview->fresh()], $tenantB), false); + ->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $betaReview->fresh()], $tenantB), false); }); it('excludes entitled tenants without a published review from customer workspace rows', function (): void { @@ -199,8 +199,8 @@ ->assertCanNotSeeTableRecords([$tenantWithoutPublished->fresh()]) ->assertDontSee('No published review') ->assertDontSee('No published review available yet') - ->assertDontSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $internalOnlyReview->fresh()], $tenantWithoutPublished), false) - ->assertSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $publishedReview->fresh()], $tenantPublished), false); + ->assertDontSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $internalOnlyReview->fresh()], $tenantWithoutPublished), false) + ->assertSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $publishedReview->fresh()], $tenantPublished), false); }); it('uses a filter-aware empty state when the active environment has no released review', function (): void { @@ -263,7 +263,7 @@ ->assertCanNotSeeTableRecords([$tenant->fresh()]) ->assertSee('No released customer reviews match this view') ->assertSee('Publish an environment review before it appears in the customer-safe workspace.') - ->assertDontSee(EnvironmentReviewResource::tenantScopedUrl('view', ['record' => $internalOnlyReview->fresh()], $tenant), false); + ->assertDontSee(EnvironmentReviewResource::environmentScopedUrl('view', ['record' => $internalOnlyReview->fresh()], $tenant), false); }); it('summarizes accepted risks from the released review without exposing internal accountability details', function (): void { @@ -352,7 +352,7 @@ ->assertDontSee('/admin/t', false); }); -it('keeps the customer review workspace unfiltered when remembered tenant context is available', function (): void { +it('keeps the customer review workspace unfiltered when remembered environment context is available', function (): void { $tenantA = ManagedEnvironment::factory()->create(['name' => 'Alpha ManagedEnvironment']); [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'readonly'); @@ -382,7 +382,7 @@ $this->actingAs($user); setAdminPanelContext(); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenantA->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenantA->workspace_id => (int) $tenantB->getKey(), ]); diff --git a/apps/platform/tests/Feature/Spec085/CanonicalMonitoringDoesNotMutateTenantContextTest.php b/apps/platform/tests/Feature/Spec085/CanonicalMonitoringDoesNotMutateTenantContextTest.php index eb647f30..38f8934d 100644 --- a/apps/platform/tests/Feature/Spec085/CanonicalMonitoringDoesNotMutateTenantContextTest.php +++ b/apps/platform/tests/Feature/Spec085/CanonicalMonitoringDoesNotMutateTenantContextTest.php @@ -2,8 +2,8 @@ declare(strict_types=1); -use App\Models\OperationRun; use App\Models\ManagedEnvironment; +use App\Models\OperationRun; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -44,7 +44,7 @@ expect(Filament::getTenant())->toBeNull(); }); -it('does not mutate remembered tenant context when opening a canonical operation run for another tenant', function (): void { +it('does not mutate remembered environment context when opening a canonical operation run for another tenant', function (): void { $tenantA = ManagedEnvironment::factory()->create(); [$user, $tenantA] = createUserWithTenant($tenantA, role: 'owner'); @@ -69,10 +69,10 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => $lastTenantMap, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => $lastTenantMap, ]) ->get(route('admin.operations.view', ['workspace' => $tenantB->workspace, 'run' => (int) $run->getKey()])) ->assertOk(); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY))->toBe($lastTenantMap); + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY))->toBe($lastTenantMap); }); diff --git a/apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php b/apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php index c7a157fe..13d48b23 100644 --- a/apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php +++ b/apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php @@ -56,7 +56,7 @@ [$user, $tenant] = createUserWithTenant($tenant, role: 'owner'); $workspaceId = (int) $tenant->workspace_id; - $lastTenantIds = [ + $lastEnvironmentIds = [ (string) $workspaceId => (int) $tenant->getKey(), ]; @@ -65,14 +65,14 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => $lastTenantIds, + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => $lastEnvironmentIds, ]) ->from('/admin/alerts') ->post('/admin/clear-environment-context') ->assertRedirect('/admin/alerts'); expect(Filament::getTenant())->toBeNull(); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) ->not->toHaveKey((string) $workspaceId); $this->withSession([ @@ -84,12 +84,12 @@ ->assertDontSee(__('localization.shell.environment_scope').': '.$tenant->name); }); -it('clears remembered tenant scope even when the stored tenant is no longer operable', function (): void { - $activeTenant = ManagedEnvironment::factory()->create(); - [$user, $activeTenant] = createUserWithTenant($activeTenant, role: 'owner'); +it('clears remembered environment scope even when the stored environment is no longer operable', function (): void { + $activeEnvironment = ManagedEnvironment::factory()->create(); + [$user, $activeEnvironment] = createUserWithTenant($activeEnvironment, role: 'owner'); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, ]); createUserWithTenant( @@ -100,19 +100,19 @@ ensureDefaultMicrosoftProviderConnection: false, ); - $workspaceId = (int) $activeTenant->workspace_id; + $workspaceId = (int) $activeEnvironment->workspace_id; $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $workspaceId => (int) $onboardingTenant->getKey(), ], ]) - ->from(route('admin.operations.index', ['workspace' => $activeTenant->workspace])) + ->from(route('admin.operations.index', ['workspace' => $activeEnvironment->workspace])) ->post('/admin/clear-environment-context') - ->assertRedirect(route('admin.operations.index', ['workspace' => $activeTenant->workspace])); + ->assertRedirect(route('admin.operations.index', ['workspace' => $activeEnvironment->workspace])); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) ->not->toHaveKey((string) $workspaceId); }); diff --git a/apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php b/apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php index 0809ad75..4b43847d 100644 --- a/apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php +++ b/apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php @@ -124,15 +124,15 @@ }); it('does not render onboarding or archived tenants in the header selector on workspace pages', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Header Active ManagedEnvironment']); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + $activeEnvironment = ManagedEnvironment::factory()->active()->create(['name' => 'Header Active ManagedEnvironment']); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Header Onboarding ManagedEnvironment', ]); $archivedTenant = ManagedEnvironment::factory()->archived()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Header Archived ManagedEnvironment', ]); @@ -142,8 +142,8 @@ Filament::setTenant(null, true); $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) - ->get(route('admin.operations.index', ['workspace' => $activeTenant->workspace])) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) + ->get(route('admin.operations.index', ['workspace' => $activeEnvironment->workspace])) ->assertSuccessful() ->assertSee('Header Active ManagedEnvironment') ->assertDontSee('Header Onboarding ManagedEnvironment') diff --git a/apps/platform/tests/Feature/Workspaces/ChooseEnvironmentPageTest.php b/apps/platform/tests/Feature/Workspaces/ChooseEnvironmentPageTest.php index c02027b1..9cd62e70 100644 --- a/apps/platform/tests/Feature/Workspaces/ChooseEnvironmentPageTest.php +++ b/apps/platform/tests/Feature/Workspaces/ChooseEnvironmentPageTest.php @@ -11,19 +11,19 @@ uses(RefreshDatabase::class); it('shows only active tenants and no-tenant helper copy on the choose-environment page', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Choose Active ManagedEnvironment']); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + $activeEnvironment = ManagedEnvironment::factory()->active()->create(['name' => 'Choose Active ManagedEnvironment']); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $otherActiveTenant = ManagedEnvironment::factory()->active()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Choose Other Active ManagedEnvironment', ]); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Choose Onboarding ManagedEnvironment', ]); $archivedTenant = ManagedEnvironment::factory()->archived()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Choose Archived ManagedEnvironment', ]); @@ -34,7 +34,7 @@ Filament::setTenant(null, true); $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) ->get('/admin/choose-environment') ->assertSuccessful() ->assertSee('Choose Active ManagedEnvironment') @@ -65,15 +65,15 @@ }); it('keeps selector eligibility narrower than managed-tenant administrative discoverability', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Selector Active ManagedEnvironment']); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner', ensureDefaultMicrosoftProviderConnection: false); + $activeEnvironment = ManagedEnvironment::factory()->active()->create(['name' => 'Selector Active ManagedEnvironment']); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner', ensureDefaultMicrosoftProviderConnection: false); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Selector Onboarding ManagedEnvironment', ]); $archivedTenant = ManagedEnvironment::factory()->archived()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, 'name' => 'Selector Archived ManagedEnvironment', ]); @@ -83,7 +83,7 @@ Filament::setTenant(null, true); $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) ->get('/admin/choose-environment') ->assertSuccessful() ->assertSee('Selector Active ManagedEnvironment') @@ -91,8 +91,8 @@ ->assertDontSee('Selector Archived ManagedEnvironment'); $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) - ->get(route('admin.workspace.managed-environments.index', ['workspace' => $activeTenant->workspace])) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) + ->get(route('admin.workspace.managed-environments.index', ['workspace' => $activeEnvironment->workspace])) ->assertSuccessful() ->assertSee('Selector Active ManagedEnvironment') ->assertSee('Selector Onboarding ManagedEnvironment') @@ -107,7 +107,7 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) @@ -130,7 +130,7 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseEnvironmentTest.php b/apps/platform/tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseEnvironmentTest.php index 652859a7..66c1831e 100644 --- a/apps/platform/tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseEnvironmentTest.php +++ b/apps/platform/tests/Feature/Workspaces/ChooseWorkspaceRedirectsToChooseEnvironmentTest.php @@ -130,7 +130,7 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $tenantA->workspace_id => (int) $tenantA->getKey(), ], ]) diff --git a/apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php b/apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php index 8f0dc035..c74a8170 100644 --- a/apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php +++ b/apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); +use App\Filament\Pages\EnvironmentDashboard; use App\Filament\Pages\Governance\DecisionRegister; use App\Filament\Pages\Governance\GovernanceInbox; -use App\Filament\Pages\EnvironmentDashboard; use App\Filament\Pages\Reviews\CustomerReviewWorkspace; use App\Filament\Resources\ProviderConnectionResource; use App\Models\ManagedEnvironment; @@ -28,7 +28,7 @@ ->assertSee('ManagedEnvironment Panel Entry') ->assertSee(__('localization.shell.switch_environment')) ->assertSee(__('localization.shell.clear_environment_scope')) - ->assertDontSee(__('localization.shell.search_environments')) + ->assertDontSee(__('localization.shell.search_environments')) ->assertDontSee('admin/select-environment'); }); @@ -48,30 +48,30 @@ }); it('keeps workspace-wide surfaces tenantless when valid environment query filters are present', function (string $surface, callable $urlFactory): void { - $rememberedTenant = ManagedEnvironment::factory()->active()->create([ + $rememberedEnvironment = ManagedEnvironment::factory()->active()->create([ 'name' => 'Remembered ManagedEnvironment', 'external_id' => 'remembered-managed-environment', ]); - [$user, $rememberedTenant] = createUserWithTenant(tenant: $rememberedTenant, role: 'owner'); + [$user, $rememberedEnvironment] = createUserWithTenant(tenant: $rememberedEnvironment, role: 'owner'); $hintedTenant = ManagedEnvironment::factory()->active()->create([ - 'workspace_id' => (int) $rememberedTenant->workspace_id, + 'workspace_id' => (int) $rememberedEnvironment->workspace_id, 'name' => 'Hinted ManagedEnvironment', 'external_id' => 'hinted-managed-environment', ]); createUserWithTenant(tenant: $hintedTenant, user: $user, role: 'owner'); - Filament::setTenant($rememberedTenant, true); + Filament::setTenant($rememberedEnvironment, true); - $workspace = $rememberedTenant->workspace()->firstOrFail(); + $workspace = $rememberedEnvironment->workspace()->firstOrFail(); $url = $urlFactory($workspace, $hintedTenant); $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ - (string) $workspace->getKey() => (int) $rememberedTenant->getKey(), + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ + (string) $workspace->getKey() => (int) $rememberedEnvironment->getKey(), ], ]) ->followingRedirects() diff --git a/apps/platform/tests/Feature/Workspaces/SelectEnvironmentControllerTest.php b/apps/platform/tests/Feature/Workspaces/SelectEnvironmentControllerTest.php index a37aa6df..7fcef02d 100644 --- a/apps/platform/tests/Feature/Workspaces/SelectEnvironmentControllerTest.php +++ b/apps/platform/tests/Feature/Workspaces/SelectEnvironmentControllerTest.php @@ -13,27 +13,27 @@ uses(RefreshDatabase::class); it('stores remembered context and redirects to the tenant dashboard for active tenants', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create(); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + $activeEnvironment = ManagedEnvironment::factory()->active()->create(); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $response = $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) ->post(route('admin.select-environment'), [ - 'managed_environment_id' => (int) $activeTenant->getKey(), + 'managed_environment_id' => (int) $activeEnvironment->getKey(), ]); - $response->assertRedirect(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $activeTenant)); + $response->assertRedirect(EnvironmentDashboard::getUrl(panel: 'admin', tenant: $activeEnvironment)); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) - ->toHaveKey((string) $activeTenant->workspace_id, (int) $activeTenant->getKey()); + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) + ->toHaveKey((string) $activeEnvironment->workspace_id, (int) $activeEnvironment->getKey()); }); it('returns 404 when selecting an onboarding tenant that is not eligible for the standard lane', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create(); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + $activeEnvironment = ManagedEnvironment::factory()->active()->create(); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ - 'workspace_id' => (int) $activeTenant->workspace_id, + 'workspace_id' => (int) $activeEnvironment->workspace_id, ]); createUserWithTenant( @@ -45,14 +45,14 @@ ); $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) ->post(route('admin.select-environment'), [ 'managed_environment_id' => (int) $onboardingTenant->getKey(), ]) ->assertNotFound(); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) - ->not->toHaveKey((string) $activeTenant->workspace_id); + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) + ->not->toHaveKey((string) $activeEnvironment->workspace_id); }); it('redirects to choose-workspace when no workspace is selected', function (): void { @@ -89,21 +89,21 @@ }); it('returns 404 when selecting a tenant from another workspace', function (): void { - $activeTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Current Workspace ManagedEnvironment']); - [$user, $activeTenant] = createUserWithTenant(tenant: $activeTenant, role: 'owner'); + $activeEnvironment = ManagedEnvironment::factory()->active()->create(['name' => 'Current Workspace ManagedEnvironment']); + [$user, $activeEnvironment] = createUserWithTenant(tenant: $activeEnvironment, role: 'owner'); $foreignTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Foreign Workspace ManagedEnvironment']); createUserWithTenant(tenant: $foreignTenant, user: $user, role: 'owner'); $this->actingAs($user) - ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeTenant->workspace_id]) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $activeEnvironment->workspace_id]) ->post(route('admin.select-environment'), [ 'managed_environment_id' => (int) $foreignTenant->getKey(), ]) ->assertNotFound(); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) - ->not->toHaveKey((string) $activeTenant->workspace_id); + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) + ->not->toHaveKey((string) $activeEnvironment->workspace_id); }); it('returns 404 when selecting a tenant the user cannot access', function (): void { diff --git a/apps/platform/tests/Feature/Workspaces/WorkspaceHubContextContractTest.php b/apps/platform/tests/Feature/Workspaces/WorkspaceHubContextContractTest.php index 857af40d..e5de8268 100644 --- a/apps/platform/tests/Feature/Workspaces/WorkspaceHubContextContractTest.php +++ b/apps/platform/tests/Feature/Workspaces/WorkspaceHubContextContractTest.php @@ -31,7 +31,7 @@ $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $workspace->getKey() => (int) $environment->getKey(), ], ]) @@ -61,7 +61,7 @@ $this->actingAs($user); Filament::setTenant($rememberedEnvironment, true); session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey()); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $workspace->getKey() => (int) $rememberedEnvironment->getKey(), ]); diff --git a/apps/platform/tests/Support/TestLaneManifest.php b/apps/platform/tests/Support/TestLaneManifest.php index e2819a86..215f68bb 100644 --- a/apps/platform/tests/Support/TestLaneManifest.php +++ b/apps/platform/tests/Support/TestLaneManifest.php @@ -154,7 +154,7 @@ public static function classifications(): array 'reviewerSignals' => [ 'reflection-backed discovery', 'resource or global-search parity scans', - 'remembered tenant or registry breadth dominating runtime', + 'remembered environment or registry breadth dominating runtime', ], 'escalationTriggers' => [ 'new resources or pages increase the touched surface', @@ -228,7 +228,7 @@ public static function families(): array 'hotspotFiles' => [ 'tests/Feature/Filament/BackupSetAdminTenantParityTest.php', ], - 'costSignals' => ['single-mount', 'remembered tenant state', 'localized table assertions'], + 'costSignals' => ['single-mount', 'remembered environment state', 'localized table assertions'], 'confidenceRationale' => 'This remains in Confidence because it protects a narrow remembered-tenant table surface without broad discovery cost.', 'validationStatus' => 'guarded', ], @@ -504,7 +504,7 @@ public static function families(): array 'hotspotFiles' => [ 'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php', ], - 'costSignals' => ['cross-resource surface assertions', 'remembered tenant context variance', 'workspace-only admin invariants'], + 'costSignals' => ['cross-resource surface assertions', 'remembered environment context variance', 'workspace-only admin invariants'], 'validationStatus' => 'guarded', ], [ @@ -880,7 +880,7 @@ public static function mixedFileResolutions(): array 'primaryClassificationId' => 'discovery-heavy', 'secondaryClassificationIds' => ['ui-light'], 'resolutionStrategy' => 'broadest-cost-wins', - 'rationale' => 'The file reads like a narrow admin check but remembered tenant search parity and reflection-backed discovery dominate the runtime shape.', + 'rationale' => 'The file reads like a narrow admin check but remembered environment search parity and reflection-backed discovery dominate the runtime shape.', 'followUpRequired' => true, ], [ @@ -2938,7 +2938,7 @@ private static function seededHeavyGovernanceBaselineSnapshot(): array ['label' => 'tests/Feature/Filament/BaselineActionAuthorizationTest.php::it keeps baseline capture and compare actions capability-gated on the profile detail page', 'wallClockSeconds' => 10.555709], ['label' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php::it does not start capture for workspace members missing workspace_baselines.manage', 'wallClockSeconds' => 10.428982], ['label' => 'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php::triage all matching requires typed confirmation when triaging more than 100 findings', 'wallClockSeconds' => 7.80246], - ['label' => 'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php::it keeps workspace-only admin surfaces independent from remembered tenant changes', 'wallClockSeconds' => 7.779388], + ['label' => 'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php::it keeps workspace-only admin surfaces independent from remembered environment changes', 'wallClockSeconds' => 7.779388], ], 'artifactPaths' => self::heavyGovernanceArtifactPaths('baseline'), 'budgetStatus' => 'warning', diff --git a/apps/platform/tests/Unit/ManagedEnvironment/ManagedEnvironmentContextResolverTest.php b/apps/platform/tests/Unit/ManagedEnvironment/ManagedEnvironmentContextResolverTest.php index e8759727..86125bfe 100644 --- a/apps/platform/tests/Unit/ManagedEnvironment/ManagedEnvironmentContextResolverTest.php +++ b/apps/platform/tests/Unit/ManagedEnvironment/ManagedEnvironmentContextResolverTest.php @@ -18,9 +18,9 @@ $context = app(WorkspaceContext::class); - expect($context->rememberTenantContext($environment, request()))->toBeTrue() - ->and($context->rememberedTenant(request())?->is($environment))->toBeTrue() - ->and($context->lastTenantId(request()))->toBe((int) $environment->getKey()); + expect($context->rememberEnvironmentContext($environment, request()))->toBeTrue() + ->and($context->rememberedEnvironment(request())?->is($environment))->toBeTrue() + ->and($context->lastEnvironmentId(request()))->toBe((int) $environment->getKey()); }); it('rejects remembered managed environments from a different workspace', function (): void { @@ -39,8 +39,8 @@ $context = app(WorkspaceContext::class); - expect($context->rememberTenantContext($environment, request()))->toBeFalse() - ->and($context->rememberedTenant(request()))->toBeNull(); + expect($context->rememberEnvironmentContext($environment, request()))->toBeFalse() + ->and($context->rememberedEnvironment(request()))->toBeNull(); }); it('clears a remembered environment when the actor loses managed-environment membership', function (): void { @@ -60,15 +60,15 @@ $this->actingAs($outsider)->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, - WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [ (string) $workspaceId => (int) $environment->getKey(), ], ]); unset($member); - expect(app(WorkspaceContext::class)->rememberedTenant(request()))->toBeNull() - ->and(session(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY))->toBe([]); + expect(app(WorkspaceContext::class)->rememberedEnvironment(request()))->toBeNull() + ->and(session(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY))->toBe([]); }); it('resolves the current workspace from a managed environment only for members with target access', function (): void { @@ -76,7 +76,7 @@ $this->actingAs($user); - expect(app(WorkspaceContext::class)->currentWorkspaceOrTenantWorkspace($environment, request())?->getKey()) + expect(app(WorkspaceContext::class)->currentWorkspaceOrEnvironmentWorkspace($environment, request())?->getKey()) ->toBe($environment->workspace_id); }); diff --git a/apps/platform/tests/Unit/Onboarding/OnboardingDraftResolverTest.php b/apps/platform/tests/Unit/Onboarding/OnboardingDraftResolverTest.php index 10acc7f8..8f8abcfc 100644 --- a/apps/platform/tests/Unit/Onboarding/OnboardingDraftResolverTest.php +++ b/apps/platform/tests/Unit/Onboarding/OnboardingDraftResolverTest.php @@ -162,12 +162,12 @@ it('excludes linked drafts whose tenant lifecycle no longer allows onboarding resume', function (): void { $workspace = Workspace::factory()->create(); $user = User::factory()->create(); - $activeTenant = ManagedEnvironment::factory()->active()->create([ + $activeEnvironment = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $workspace->getKey(), ]); createUserWithTenant( - tenant: $activeTenant, + tenant: $activeEnvironment, user: $user, role: 'owner', workspaceRole: 'owner', @@ -184,12 +184,12 @@ createOnboardingDraft([ 'workspace' => $workspace, - 'tenant' => $activeTenant, + 'tenant' => $activeEnvironment, 'started_by' => $user, 'updated_by' => $user, 'state' => [ - 'entra_tenant_id' => (string) $activeTenant->managed_environment_id, - 'environment_name' => (string) $activeTenant->name, + 'entra_tenant_id' => (string) $activeEnvironment->managed_environment_id, + 'environment_name' => (string) $activeEnvironment->name, ], ]); diff --git a/apps/platform/tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php b/apps/platform/tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php index cdaf8d05..ef52f6c9 100644 --- a/apps/platform/tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php +++ b/apps/platform/tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php @@ -2,21 +2,21 @@ declare(strict_types=1); +use App\Models\Finding; +use App\Models\ManagedEnvironment; +use App\Models\ManagedEnvironmentOnboardingSession; use App\Models\OperationRun; use App\Models\PlatformUser; use App\Models\ProductUsageEvent; use App\Models\ProviderConnection; use App\Models\ReviewPack; -use App\Models\Finding; -use App\Models\ManagedEnvironment; -use App\Models\ManagedEnvironmentOnboardingSession; use App\Models\Workspace; +use App\Support\Auth\PlatformCapabilities; use App\Support\CustomerHealth\CustomerHealthDimensionCatalog; use App\Support\CustomerHealth\WorkspaceHealthSummaryQuery; use App\Support\Onboarding\OnboardingLifecycleState; use App\Support\OperationRunOutcome; use App\Support\OperationRunStatus; -use App\Support\Auth\PlatformCapabilities; use App\Support\ProductTelemetry\ProductUsageEventCatalog; use App\Support\Providers\ProviderConsentStatus; use App\Support\Providers\ProviderVerificationStatus; @@ -241,7 +241,7 @@ ]); $workspace = Workspace::factory()->create(['name' => 'Mixed Workspace']); - $activeTenant = ManagedEnvironment::factory()->for($workspace)->create([ + $activeEnvironment = ManagedEnvironment::factory()->for($workspace)->create([ 'status' => ManagedEnvironment::STATUS_ACTIVE, 'name' => 'Active ManagedEnvironment', ]); @@ -253,7 +253,7 @@ ]); ProviderConnection::factory() - ->for($activeTenant) + ->for($activeEnvironment) ->verifiedHealthy() ->create([ 'workspace_id' => (int) $workspace->getKey(), @@ -411,4 +411,4 @@ function summaryForWorkspace(Workspace $workspace): array ->summaryForWorkspace($workspace, SystemConsoleWindow::LastDay); return $summary; -} \ No newline at end of file +} diff --git a/apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php b/apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php index c2309b68..58112f10 100644 --- a/apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php +++ b/apps/platform/tests/Unit/Support/OperateHub/OperateHubShellResolutionTest.php @@ -13,11 +13,11 @@ uses(RefreshDatabase::class); it('keeps workspace hub shell tenantless when an explicit environment filter is present', function (): void { - $rememberedTenant = ManagedEnvironment::factory()->active()->create(['name' => 'Remembered ManagedEnvironment']); - [$user, $rememberedTenant] = createUserWithTenant(tenant: $rememberedTenant, role: 'owner'); + $rememberedEnvironment = ManagedEnvironment::factory()->active()->create(['name' => 'Remembered ManagedEnvironment']); + [$user, $rememberedEnvironment] = createUserWithTenant(tenant: $rememberedEnvironment, role: 'owner'); $hintedTenant = ManagedEnvironment::factory()->active()->create([ - 'workspace_id' => (int) $rememberedTenant->workspace_id, + 'workspace_id' => (int) $rememberedEnvironment->workspace_id, 'name' => 'Hinted ManagedEnvironment', ]); @@ -26,11 +26,11 @@ $this->actingAs($user); Filament::setTenant(null, true); - $workspaceId = (int) $rememberedTenant->workspace_id; + $workspaceId = (int) $rememberedEnvironment->workspace_id; session()->put(WorkspaceContext::SESSION_KEY, $workspaceId); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ - (string) $workspaceId => (int) $rememberedTenant->getKey(), + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ + (string) $workspaceId => (int) $rememberedEnvironment->getKey(), ]); $request = Request::create(route('admin.operations.index', [ diff --git a/apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php b/apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedEnvironmentTest.php similarity index 59% rename from apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php rename to apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedEnvironmentTest.php index 809c4b7b..f43daab8 100644 --- a/apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedTenantTest.php +++ b/apps/platform/tests/Unit/Support/Workspaces/WorkspaceContextRememberedEnvironmentTest.php @@ -8,38 +8,38 @@ uses(RefreshDatabase::class); -it('returns the remembered tenant when it remains selectable and entitled', function (): void { +it('returns the remembered environment when it remains selectable and entitled', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $tenant->getKey(), ]); - $rememberedTenant = app(WorkspaceContext::class)->rememberedTenant(request()); + $rememberedEnvironment = app(WorkspaceContext::class)->rememberedEnvironment(request()); - expect($rememberedTenant?->is($tenant))->toBeTrue(); + expect($rememberedEnvironment?->is($tenant))->toBeTrue(); }); -it('clears remembered tenant context when the stored tenant belongs to another workspace', function (): void { +it('clears remembered environment context when the stored environment belongs to another workspace', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $otherWorkspaceTenant = ManagedEnvironment::factory()->active()->create(); $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $otherWorkspaceTenant->getKey(), ]); - expect(app(WorkspaceContext::class)->rememberedTenant(request()))->toBeNull(); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) + expect(app(WorkspaceContext::class)->rememberedEnvironment(request()))->toBeNull(); + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) ->not->toHaveKey((string) $tenant->workspace_id); }); -it('clears remembered tenant context when the stored tenant no longer exists', function (): void { +it('clears remembered environment context when the stored environment no longer exists', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $missingTenant = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, @@ -52,16 +52,16 @@ $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => $missingTenantId, ]); - expect(app(WorkspaceContext::class)->rememberedTenant(request()))->toBeNull(); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) + expect(app(WorkspaceContext::class)->rememberedEnvironment(request()))->toBeNull(); + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) ->not->toHaveKey((string) $tenant->workspace_id); }); -it('clears remembered tenant context when the actor is no longer entitled to the tenant', function (): void { +it('clears remembered environment context when the actor is no longer entitled to the environment', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $nonEntitledTenant = ManagedEnvironment::factory()->active()->create([ 'workspace_id' => (int) $tenant->workspace_id, @@ -70,16 +70,16 @@ $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $nonEntitledTenant->getKey(), ]); - expect(app(WorkspaceContext::class)->rememberedTenant(request()))->toBeNull(); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) + expect(app(WorkspaceContext::class)->rememberedEnvironment(request()))->toBeNull(); + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) ->not->toHaveKey((string) $tenant->workspace_id); }); -it('clears remembered tenant context when the tenant becomes selector ineligible', function (): void { +it('clears remembered environment context when the environment becomes selector ineligible', function (): void { [$user, $tenant] = createUserWithTenant(role: 'owner'); $onboardingTenant = ManagedEnvironment::factory()->onboarding()->create([ 'workspace_id' => (int) $tenant->workspace_id, @@ -96,11 +96,11 @@ $this->actingAs($user); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); - session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + session()->put(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [ (string) $tenant->workspace_id => (int) $onboardingTenant->getKey(), ]); - expect(app(WorkspaceContext::class)->rememberedTenant(request()))->toBeNull(); - expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [])) + expect(app(WorkspaceContext::class)->rememberedEnvironment(request()))->toBeNull(); + expect(session()->get(WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY, [])) ->not->toHaveKey((string) $tenant->workspace_id); }); diff --git a/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php b/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php new file mode 100644 index 00000000..888efe00 --- /dev/null +++ b/apps/platform/tests/Unit/Tenants/AdminSurfaceScopeTest.php @@ -0,0 +1,32 @@ +toBe($expected); +})->with([ + 'workspace overview' => ['/admin', AdminSurfaceScope::WorkspaceWideSurface], + 'workspace chooser exception' => ['/admin/choose-workspace', AdminSurfaceScope::WorkspaceChooserException], + 'tenant chooser' => ['/admin/choose-environment', AdminSurfaceScope::WorkspaceScoped], + 'retired tenant resource detail' => ['/admin/tenants/tenant-123', AdminSurfaceScope::WorkspaceScoped], + 'retired tenant panel route' => ['/admin/t/tenant-123', AdminSurfaceScope::WorkspaceScoped], + 'workspace environment detail' => ['/admin/workspaces/acme/environments/tenant-123', AdminSurfaceScope::EnvironmentBound], + 'tenant scoped evidence detail' => ['/admin/evidence/123', AdminSurfaceScope::EnvironmentScopedEvidence], + 'evidence overview' => ['/admin/evidence/overview', AdminSurfaceScope::WorkspaceWideSurface], + 'customer review workspace' => ['/admin/reviews/workspace', AdminSurfaceScope::WorkspaceWideSurface], + 'review register' => ['/admin/reviews', AdminSurfaceScope::WorkspaceWideSurface], + 'governance decisions' => ['/admin/governance/decisions', AdminSurfaceScope::WorkspaceWideSurface], + 'alerts' => ['/admin/alerts', AdminSurfaceScope::WorkspaceWideSurface], + 'provider connections' => ['/admin/provider-connections', AdminSurfaceScope::WorkspaceWideSurface], + 'workspace home overview' => ['/admin/workspaces/acme/overview', AdminSurfaceScope::WorkspaceWideSurface], + 'onboarding index' => ['/admin/onboarding', AdminSurfaceScope::OnboardingWorkflow], + 'onboarding draft' => ['/admin/onboarding/42', AdminSurfaceScope::OnboardingWorkflow], + 'operations index' => ['/admin/workspaces/acme/operations', AdminSurfaceScope::WorkspaceWideSurface], + 'retired operation run detail' => ['/admin/operations/44', AdminSurfaceScope::WorkspaceScoped], + 'operation run detail' => ['/admin/workspaces/acme/operations/44', AdminSurfaceScope::CanonicalWorkspaceRecordViewer], +]); diff --git a/apps/platform/tests/Unit/Tenants/TenantOperabilityServiceTest.php b/apps/platform/tests/Unit/Tenants/TenantOperabilityServiceTest.php index f53a78f7..3e9600aa 100644 --- a/apps/platform/tests/Unit/Tenants/TenantOperabilityServiceTest.php +++ b/apps/platform/tests/Unit/Tenants/TenantOperabilityServiceTest.php @@ -100,7 +100,7 @@ $tenant = $tenantFactory(); $question = $lane === TenantInteractionLane::AdministrativeManagement - ? TenantOperabilityQuestion::TenantBoundViewability + ? TenantOperabilityQuestion::EnvironmentBoundViewability : TenantOperabilityQuestion::CanonicalLinkedRecordViewability; $outcome = app(TenantOperabilityService::class)->outcomeFor( diff --git a/apps/platform/tests/Unit/Tenants/TenantPageCategoryTest.php b/apps/platform/tests/Unit/Tenants/TenantPageCategoryTest.php deleted file mode 100644 index b6dd0241..00000000 --- a/apps/platform/tests/Unit/Tenants/TenantPageCategoryTest.php +++ /dev/null @@ -1,32 +0,0 @@ -toBe($expected); -})->with([ - 'workspace overview' => ['/admin', TenantPageCategory::WorkspaceWideSurface], - 'workspace chooser exception' => ['/admin/choose-workspace', TenantPageCategory::WorkspaceChooserException], - 'tenant chooser' => ['/admin/choose-environment', TenantPageCategory::WorkspaceScoped], - 'retired tenant resource detail' => ['/admin/tenants/tenant-123', TenantPageCategory::WorkspaceScoped], - 'retired tenant panel route' => ['/admin/t/tenant-123', TenantPageCategory::WorkspaceScoped], - 'workspace environment detail' => ['/admin/workspaces/acme/environments/tenant-123', TenantPageCategory::TenantBound], - 'tenant scoped evidence detail' => ['/admin/evidence/123', TenantPageCategory::TenantScopedEvidence], - 'evidence overview' => ['/admin/evidence/overview', TenantPageCategory::WorkspaceWideSurface], - 'customer review workspace' => ['/admin/reviews/workspace', TenantPageCategory::WorkspaceWideSurface], - 'review register' => ['/admin/reviews', TenantPageCategory::WorkspaceWideSurface], - 'governance decisions' => ['/admin/governance/decisions', TenantPageCategory::WorkspaceWideSurface], - 'alerts' => ['/admin/alerts', TenantPageCategory::WorkspaceWideSurface], - 'provider connections' => ['/admin/provider-connections', TenantPageCategory::WorkspaceWideSurface], - 'workspace home overview' => ['/admin/workspaces/acme/overview', TenantPageCategory::WorkspaceWideSurface], - 'onboarding index' => ['/admin/onboarding', TenantPageCategory::OnboardingWorkflow], - 'onboarding draft' => ['/admin/onboarding/42', TenantPageCategory::OnboardingWorkflow], - 'operations index' => ['/admin/workspaces/acme/operations', TenantPageCategory::WorkspaceWideSurface], - 'retired operation run detail' => ['/admin/operations/44', TenantPageCategory::WorkspaceScoped], - 'operation run detail' => ['/admin/workspaces/acme/operations/44', TenantPageCategory::CanonicalWorkspaceRecordViewer], -]); diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index eef6c912..35d463d4 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.md @@ -21,7 +21,7 @@ ## Executive Summary - **Core value prop**: Immutable JSONB policy snapshots, restore wizard with dry-run/preview, inventory-first drift detection against Golden Master baselines, workspace-scoped RBAC, and alert/evidence pipeline. - **28+ Intune policy types** supported (device config, settings catalog, compliance, app protection, conditional access, scripts, enrollment, endpoint security, update rings, etc.) — defined in [config/tenantpilot.php](config/tenantpilot.php). - **Maturity**: Production-capable MVP with ~109 specs, 708 test files (582 Feature + 125 Unit + 1 Deprecation), extensive guard tests, and an architectural constitution document. -- **Three Filament panels**: `/admin` (workspace-tenant context), `/admin/t` (tenant-scoped), `/system` (platform operator console). +- **Active Filament panels**: `/admin` (workspace and managed-environment context), `/system` (platform operator console). The retired tenant panel route family is guarded as absent. - **Workspace-first multi-tenancy**: All data is workspace-isolated. Tenants belong to workspaces. Non-members get 404 (deny-as-not-found). - **Capability-first RBAC**: ~40+ capabilities in a canonical registry ([app/Support/Auth/Capabilities.php](app/Support/Auth/Capabilities.php)), enforced server-side with UI enforcement helpers. - **Operations system**: Unified `OperationRun` model with 25+ run types, idempotent creation, stale-run detection, 3-surface feedback (toast → progress → DB notification). @@ -138,7 +138,7 @@ ### Core Principles 2. **Read/Write Separation** — Analysis is read-only; writes require preview → dry-run → confirmation → audit log. 3. **Single Contract Path to Graph** — All Microsoft Graph calls via `GraphClientInterface` ([app/Services/Graph/GraphClientInterface.php](app/Services/Graph/GraphClientInterface.php)); endpoints modeled in [config/graph_contracts.php](config/graph_contracts.php) (867 lines, 28+ type definitions). 4. **Deterministic Capabilities** — Backup/restore/risk flags derived from config via `CoverageCapabilitiesResolver`. -5. **Workspace Isolation** — Non-member → 404; workspace is primary session context. Enforced via `DenyNonMemberTenantAccess` middleware + `EnsureFilamentTenantSelected`. +5. **Workspace Isolation** — Non-member → 404; workspace is primary session context. Enforced via `DenyNonMemberTenantAccess` middleware + `EnsureEnvironmentContextSelected`. 6. **Tenant Isolation** — Every read/write must be tenant-scoped; `DerivesWorkspaceIdFromTenant` concern auto-fills `workspace_id` from tenant. 7. **UI/UX Constitution v1** — Every operator-facing admin surface is classified before implementation, each list gets exactly one primary inspect/open model, scope signals must be truthful, critical operational truth stays default-visible, and exceptions are catalogued and tested. 8. **Filament-native first / no ad-hoc styling** — Admin and operator UI must use Filament-native components or shared primitives before any local Blade/Tailwind assembly; page-local status styling is not an acceptable substitute. @@ -301,12 +301,11 @@ ### Scheduled Tasks ([routes/console.php](routes/console.php)) ## Security: Tenancy / RBAC / Auth -### Three-panel architecture +### Active panel architecture | Panel | Path | Guard | Purpose | |-------|------|-------|---------| -| Admin | `/admin` | `web` | Workspace + Tenant management (main UI) | -| Tenant | `/admin/t` | `web` | Tenant-scoped views (within admin session) | +| Admin | `/admin` | `web` | Workspace + Managed Environment management (main UI) | | System | `/system` | `platform` | Platform operator console (separate cookies) | ### Auth flows diff --git a/docs/product/implementation-ledger.md b/docs/product/implementation-ledger.md index bcbdc6fb..529626d4 100644 --- a/docs/product/implementation-ledger.md +++ b/docs/product/implementation-ledger.md @@ -256,7 +256,7 @@ ## Open Gaps & Blockers ## Recommended Manual Promotions - `Cross-Domain Progress / Indicator Semantics candidate group` -> anchored by `specs/268-operationrun-activity-feedback/spec.md`, `specs/270-operationrun-progress-contract/spec.md`, `specs/271-counted-progress-rollout/spec.md`, `specs/272-operationrun-phase-composite-progress/spec.md`, `docs/ui/tenantpilot-enterprise-ui-standards.md`, and the current progress-like UI seams called out in `docs/product/spec-candidates.md` -- `Admin Workspace Navigation & Tenant-owned Surface Repair candidate group` -> anchored by `apps/platform/app/Filament/Clusters/Inventory/InventoryCluster.php`, `apps/platform/app/Filament/Pages/InventoryCoverage.php`, `apps/platform/app/Filament/Resources/InventoryItemResource.php`, `apps/platform/app/Filament/Resources/EntraGroupResource.php`, `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php`, `apps/platform/app/Support/OperateHub/OperateHubShell.php`, and the navigation/runtime tests called out in `docs/product/spec-candidates.md`; Specs 301-304 now cover Inventory cutover, route-audit prep, groups cutover, and tenant-panel dead-code retirement, so only `navigation-contract-split` remains as a conditional follow-through if drift persists +- `Admin Workspace Navigation & Environment-owned Surface Repair candidate group` -> anchored by `apps/platform/app/Filament/Clusters/Inventory/InventoryCluster.php`, `apps/platform/app/Filament/Pages/InventoryCoverage.php`, `apps/platform/app/Filament/Resources/InventoryItemResource.php`, `apps/platform/app/Filament/Resources/EntraGroupResource.php`, `apps/platform/app/Filament/Concerns/WorkspaceScopedEnvironmentRoutes.php`, `apps/platform/app/Support/OperateHub/OperateHubShell.php`, and the navigation/runtime tests called out in `docs/product/spec-candidates.md`; Specs 301-304 now cover Inventory cutover, route-audit prep, groups cutover, and tenant-panel dead-code retirement, so only `navigation-contract-split` remains as a conditional follow-through if drift persists - `Workspace-first / ManagedEnvironment Core Cutover` pack -> anchored by `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, `docs/product/implementation-ledger.md`, and the tenant-centric platform seams already visible across review, support, portfolio, and governance surfaces; keep it as a clean development-stage cutover pack rather than a compatibility-layer program - `Customer Review Workspace v1 Completion` -> anchored by `specs/258-customer-review-productization/spec.md`, `specs/308-decision-register-summary-review-pack/spec.md`, `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, and the review/workspace tests - `Localization v1 Customer-facing Surfaces` -> anchored by `specs/252-platform-localization-v1/spec.md`, `specs/258-customer-review-productization/spec.md`, and `specs/260-governance-service-packaging/spec.md` diff --git a/docs/product/spec-candidates.md b/docs/product/spec-candidates.md index 8985ea71..7e611559 100644 --- a/docs/product/spec-candidates.md +++ b/docs/product/spec-candidates.md @@ -217,7 +217,7 @@ #### Customer Review Workspace v1 Completion - empty states for no released review, filtered no results, no decisions requiring awareness, incomplete evidence, and pack missing/generating/expired - focused tests and browser smoke when UI changes - **Non-scope**: - - no shell, sidebar, topbar, `OperateHubShell`, `TenantPageCategory`, or `NavigationScope` work + - no shell, sidebar, topbar, `OperateHubShell`, `AdminSurfaceScope`, or `NavigationScope` work - no RBAC changes, migrations, new tables, or new `OperationRun` types - no Provider Connection scope work - no Product Truth broad cleanup @@ -237,7 +237,7 @@ #### Provider Connection Scope Hardening - **Impact**: High - **Type**: Security / scope / provider boundary - **Depends on**: Workspace / Environment Surface Scope Contract -- **Problem**: Provider Connections are workspace/provider-level, but legacy audit evidence shows they can still be influenced by hidden context such as remembered tenant, `lastTenantId`, Filament tenant, or query filters. This is high-risk because the surface sits near credentials, consent, permissions, and target connection logic. +- **Problem**: Provider Connections are workspace/provider-level, but legacy audit evidence shows they can still be influenced by hidden context such as remembered Environment switcher state, Filament Environment context, or query filters. This is high-risk because the surface sits near credentials, consent, permissions, and target connection logic. - **Why now**: Spec 311 establishes the workspace-wide shell contract, but Provider Connections still need their own authority and filter semantics hardened before credential-adjacent work grows. - **Risk**: High security/scope risk if hidden context can influence credential-level authority or connection operations. - **Goal**: Provider Connections are unambiguously workspace/provider-level. Environment is an explicit filter or record relationship, not hidden shell/session context. Create/edit/verify/delete/rotate/disconnect actions are capability- and scope-safe. @@ -253,7 +253,7 @@ #### Provider Connection Scope Hardening - Provider Connection list, view, create, edit, and credential-adjacent actions - `ProviderConnectionPolicy` - Provider Connection filters and `managed_environment_id` query behavior - - remembered tenant / Filament tenant fallback review + - remembered Environment / Filament Environment fallback review - Owner, Manager, Operator, and Readonly behavior - wrong workspace and wrong environment tests - **Non-scope**: @@ -267,7 +267,7 @@ #### Provider Connection Scope Hardening - **Acceptance criteria**: - Provider Connection list, view, and action scopes are workspace-safe - `managed_environment_id` is an explicit filter, not hidden context - - `lastTenantId` / remembered tenant does not decide credential-level authority + - remembered Environment switcher state does not decide credential-level authority - non-member and out-of-scope access remains denied - Manager/Operator cannot perform credential-level actions unless repo-real policy explicitly permits it - legitimate existing Provider Connection flows remain green @@ -279,18 +279,18 @@ #### Canonical Link / Query Cleanup - **Impact**: High - **Type**: Route hygiene / link canonicalization - **Depends on**: Workspace / Environment Surface Scope Contract; Provider Connection Scope Hardening preferred -- **Problem**: Link and query seams still contain ambiguous or legacy-shaped names such as `tenantPrefilterUrl`, `?tenant=...`, `tenantScopedUrl`, raw `/admin/...` URLs, mixed `tenant` versus `managed_environment_id`, and helper names that encode the old tenant mental model. +- **Problem**: Link and query seams must stay explicit after the Workspace/Environment cleanup: Environment filters use `environment_id`, Workspace-wide links stay workspace-wide, provider Tenant identifiers stay provider-boundary, and raw `/admin/...` URLs should not bypass canonical helpers. - **Why now**: Once scope is route-owned, links and query keys need to stop teaching future specs the old tenant-context model. - **Risk**: Medium/high regression risk if new productization work keeps copying legacy helper names or query semantics. - **Goal**: Links and query parameters are semantically explicit. Workspace-wide links stay workspace-wide; environment-owned details use canonical workspace/environment routes; environment filters use explicit environment filter query names; no `/admin/t` links are generated. - **Repo evidence**: - - `CustomerReviewWorkspace::tenantPrefilterUrl` + - `CustomerReviewWorkspace::environmentFilterUrl` - `OperationRunLinks` - `ManagedEnvironmentLinks` - - `WorkspaceScopedTenantRoutes` - - `EnvironmentReviewResource::tenantScopedUrl` + - `WorkspaceScopedEnvironmentRoutes` + - `EnvironmentReviewResource::environmentScopedUrl` - Evidence, stored report, review pack, notification, and "View operation" links - - `EnsureFilamentTenantSelected` navigation links + - `EnsureEnvironmentContextSelected` navigation links - **Scope**: - link helper inventory - query key standardization for page-level environment filters @@ -307,7 +307,7 @@ #### Canonical Link / Query Cleanup - no full docs rewrite - **Acceptance criteria**: - workspace-wide surfaces use explicit filter query names - - Customer Review, Review Register, Governance, and Operations links no longer rely on legacy tenant query semantics + - Customer Review, Review Register, Governance, and Operations links rely on canonical `environment_id` query semantics only - environment-owned detail links are canonical workspace/environment URLs - `OperationRun` links remain canonical - no `/admin/t` links are generated @@ -361,12 +361,12 @@ #### Environment Resource Context Follow-through - **Impact**: High - **Type**: Route/resource cutover / context hardening - **Depends on**: Workspace / Environment Surface Scope Contract; Canonical Link / Query Cleanup preferred -- **Problem**: Many environment-owned resources are already on canonical workspace/environment routes, but internally still rely on Filament tenant bridge, `getTenant` fallback, `tenantScopedUrl` naming, or legacy compatibility seams. That bridge is partly necessary today, but it is fragile as a hidden data source. +- **Problem**: Many environment-owned resources are already on canonical workspace/environment routes, but internally still rely on Filament tenant bridge, `getTenant` fallback, `environmentScopedUrl` naming, or legacy compatibility seams. That bridge is partly necessary today, but it is fragile as a hidden data source. - **Why now**: The canonical route family exists; the remaining risk is hidden fallback inside high-value resources, which should be reduced only after link/query semantics are cleaner. - **Risk**: Medium/high correctness risk if route params and hidden Filament/session context disagree. - **Goal**: selected high-value environment-owned resources derive primary context from canonical route parameters: workspace, managed environment, and record scope. Filament tenant bridge remains a controlled Filament integration layer, not hidden product truth. - **Repo evidence**: - - `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php` + - `apps/platform/app/Filament/Concerns/WorkspaceScopedEnvironmentRoutes.php` - `Filament::getTenant` usages - `ManagedEnvironment::current` - Policy and PolicyVersion resources @@ -392,7 +392,7 @@ #### Environment Resource Context Follow-through - **Acceptance criteria**: - selected high-value environment resources work from canonical route context - direct URLs with workspace/environment params resolve correctly - - hidden remembered tenant does not override canonical route params + - hidden remembered environment does not override canonical route params - no `/admin/t` compatibility route is introduced - existing resource tests remain green @@ -434,27 +434,27 @@ #### Legacy Compatibility / Dead Code Retirement - no `/admin/t` route returns - cleanup diff stays small and safe -#### Tenant Helper Naming Cleanup +#### Environment Helper Naming Follow-through - **Priority**: P3 - **Complexity**: M - **Impact**: Medium - **Type**: Naming / domain language cleanup - **Depends on**: Canonical Link / Query Cleanup; Product Truth / Docs Drift Cleanup preferred -- **Problem**: Many helpers and tests still use `tenant*` naming even though the target model is workspace/managed-environment first: `tenantScopedUrl`, `tenantPrefilterUrl`, `tenant` parameter names, tenant-bound wording in tests/docs, and helper aliases. +- **Problem**: Some helper and test names still inherit framework/domain `tenant*` vocabulary even when the target model is Workspace/Managed Environment first. The current contract is Workspace-first, Environment-second, with provider Tenant terminology reserved for Microsoft/Entra/provider identity. - **Why now**: This should follow behavior cleanup, because renaming first would create churn without eliminating the underlying route/query ambiguity. - **Risk**: Medium terminology drift risk, but lower runtime risk than the P1/P2 scope and link candidates. -- **Goal**: new public and internal helper names use managed-environment, environment, workspace, environment-scoped, and environment-filter semantics. Old names remain only as justified compatibility aliases. +- **Goal**: new public and internal helper names use managed-environment, environment, workspace, environment-scoped, and environment-filter semantics. No compatibility aliases are added for platform-context helper names. - **Repo evidence**: - - `tenantScopedUrl` usages - - `tenantPrefilterUrl` usages - - `WorkspaceScopedTenantRoutes` + - Environment-owned resource route helpers + - Customer Review Workspace Environment filter links + - Workspace-scoped Environment route trait usage - tests with tenant naming - docs/spec templates - **Scope**: - naming inventory - safe internal renames in bounded helpers - - compatibility aliases only where required + - no compatibility aliases for platform-context names - tests adjusted where names encode product semantics - no behavior change - **Non-scope**: @@ -1194,7 +1194,7 @@ ### Admin Workspace Navigation & Tenant-owned Surface Repair candidate group - `apps/platform/app/Filament/Pages/InventoryCoverage.php` - `apps/platform/app/Filament/Resources/InventoryItemResource.php` - `apps/platform/app/Filament/Resources/EntraGroupResource.php` - - `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php` + - `apps/platform/app/Filament/Concerns/WorkspaceScopedEnvironmentRoutes.php` - `apps/platform/app/Support/OperateHub/OperateHubShell.php` - `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php` - `apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php` diff --git a/specs/317-legacy-tenant-environment-context-cleanup/artifacts/screenshots/spec317-evidence-overview-filtered.png b/specs/317-legacy-tenant-environment-context-cleanup/artifacts/screenshots/spec317-evidence-overview-filtered.png new file mode 100644 index 0000000000000000000000000000000000000000..bdf3532dd4b5b0dabed8888faca8b190f9294a11 GIT binary patch literal 63392 zcmeFZcT|(j_9z_1itR;+DAkuvXi`E48@-xFAOv`60tp~ps_j*TXo8ebLX*%^AP5p5 zG+$A=fI#R)DWQW>6%_e$e&?R`efQjZ*E;8Zcdc)I>-U@dG0!~D>^+m2J$vujd(RvU zAAAP;&&`yu2q*@(P?icltL$ zfr}Rf1uk3=5|tJg5|$LXaN)AzWl0%Xd64`iaiuFta#y6~@0$Xu zgM&jJp12@+BTuU0?IS@M%_ixa?!^AD>lclT?T~+_uxG4J{ZMsGV zB{8#yubKA!zUuzv^5Snh<=Mh);-&gq_IUIPx+f{8y7LHrix;|8dUX_lK7yC$GLr9{t2uf8SM>*?8IP#J7YM zsh#Tgcf^OySMS7~KWs4b zD1)>zDXWKD7i;&Ha3SvGLMmk-tLn_B%MO!Z z`D~_x&%lPI$6v?6{}^K9!;+&(EVX$V7(3??RQJv^;~oC5Am`hu7hg`aeRq$p^~@+S zRrKrQbOV9fG_X$S!)T#tJ2p$woVN7GpXdGOEkclHlogo*yRTF1xQr%hR$g?)t^USB zl5B{dgYX8L+HCOB=8WBXwIrTK7-$($@xu2N+K5 zOb=lwr8u%K>pTI2i4Z^Sy)~rkNl8aD_<3t=DqF6NUVl8YnqSlaw}WYtDsWZF7M<1( zLTX0X*M;p*;Bd5xfOEbkKuju8vY)PvXo*Z-Hh6IC4HJp0vo>bRO?j*kHxyTz=!2T- z(1ksFV|3`u@oE>cU+-SgvmJN8cBe+GvNIMLJ^3dW1k_(PJE{aC5g}eMM4wIh7yFL% zUb>`O9K#B<(cvk`n_+XjE_+&6C>B;hqA?0TIB3B?nCg@mw-k1eWyPJ51VB5_k^SNV z#FNUDZ6bq|4N4yk??pIdh0lZ1@5lFy=8@d?oyGi3_JpIzt$b6qWJRC zHEiyjJzrUA*nXv7b;xQ*Q{29ei=HlvcE4Ku6QyE5FTUi-`9}?#-3)tbpXL{_mKily zr^j~$<|y?Myq8}7@u`wcl1t6ut<3<9fm#J1MfEL|1Z*PW3WYwj;9__0e#WAx?M*3x zf#k+=58r?ajX?rFV21^S@IfGv>+jO@vX32mhLH!&7rM*SF99tF<>r$#uv!!xU1+^S zM}wu~E=Nr=t0JUY)J&UUl1_TsOVaK8gLe548svHXUWnjD#4I`1`{G4Sa+1Temr02) z6t0^n8{$)Ej^g+e(et?Cqb=IS(w{9~-_5uqsuy+BK*fX-Emz_5?p4haVmdnp9qw`K z!=LBVhFyYUq~Oe?Djn!akR<+srWxhglbM{#j+v7=Ad~(4LQmBDR|87Aw!PVQiA9`> zc^fYFsR&k!`n$`wAC~p49wRw8!T9GWsKv!U9%W}l$F;KfNi(0TK{U$a1HhcFah}I2 zuCU+Gv&aITuYHH;M(QlLK(0J{C-V`&SFo;FmJr@G!zVH~kCWIyN1l6SE%LR(UTj1% z(vyMV00FxyUw;2_|0m7N^dAqbrkQjAb`vq~&eA-aW|l;Y=+(_`W4p=NSuJyZeJUHh zy$F(|72?9hl8hxGT|qKx&z6=B-@+I|%C_x)!n$cJyy#FQesv#5m8@TrZ`D`J0xW#)r&hh$!GH1;_BRPp zs0_8qt^nUnG?Zb61b?P*;4YN$YsV#YuFM+S-gu$psdl~5VcLx8J;<=abgOR9$DaI) zpaTAqK$4IsX21FM__5#j54o;yp2Y+5!|TRYD*b7-gEuV?->)@wS6DEbqc7de_@rOs zKv}*7q{WhF&PMMN$5M>;Hpk*`NG;f>X-c^J!Dt_n-daANi?i&=ah#o<=zUw3VqVT6MAYS~_;kMjG;sJbY&EcD3aIz`MAktG;M8h@8IXLYg6#@u0@&CLbr|H$^;A6);&$p?7ub4uqmQ=q}FKkh+xw|b+M1(##=Le_jqi3AIx3& zqBJJ&6kJg3dpBGTE4#q1c38;j(3$Pls4{l32qi~;jjrGI8+m5kw0je*G>xg#AEU)l z7oo=RnY8M&pWn9v!*aZj-#YeZz()CNjXDLd#-_c|g*#{M7kns-gBJ0V&z`DYS~UL5 za*lre7u}YVaC>e9ySn(*$!D-_VZ`MQrO0e457{6zUhYHyHRcrZ6!4Hx^-SN=PVd!0 z?<9^uwSY}L3=FC|ne}D`yV)H4PL32j=%WYb(3UH~dND>-wM)h?izulRYPaZi$wpAt zc{+?J^s~IiDi99m**bAmrO(ecrSpE0g{|C%A5l6KMALOdAZleKGv7?ON?{~u9QAi1 z>I<56Rg<0Iehg?eEvmtw#UNpyex}yU9fk5zxVV+bKy)+!7t-x!auDH{McE>MdexUg zQ2N;`MCbl4$KcUZ4VvpIl>+V3MoTzWkZO&QjToao2-Y$jfqj|Kj&Jp@vJt|OmLg;O z3{NhmX#~;c!Kc}{L9Wu7` z2o|E(5A+CVzWRrp{de-;shg?VL&+0Q_FmBs0FOLAr{B1A0En^P8TvKE4>%_AL#hJ) z(eY)*S0z|hS>m&&k{2`Jm~9Y8d*36-cYt3qm~yw0Y2WGP!5GL2<%AbguXR1b&ado2;?LP9ngH5C)Eh_?509HZGI58gS@F<7zIQiQ-dJyHk^~v_6+NE>DuSr9j~QM3?w{Ch*CuKU zyReQP{-mJ6T*^Z56JU=(>qfK2z>Smgsd<;FX2iC(0sD-^4jKuSFSC)z zps7w9+hz9QVA}7V-TmYU9R_0jY-_&8Cu>Zz16-hlM~wks!^!{`c%B z{Jgk~fkng?Z9Dx3zpixC{Bpn0J(^HdG+DW2Mp+zBG&F)+ zj|oocEw=;`m^W7S6=SvkctRN-DJ;c=O5BTc3{ec?z!Ip4b5{70@A9pE^)|`uUrzEi zKAfHorg*Y~hz&t*StT^X!UCyx#>ghd-7M?IaHu)5DhQ#y5E*1TlT^eP;VPJWyv9E4!n->o9&ef} z6LP*_2~%KeQMm5!$acvqiMom`ev^Ab!C*Y|)!?qd#zRMpr<=J0mf}7|i`SU{(*wVd z6;NZx(4LB9pIfw7jZz5NXT81N3kk6L!@e{EZY$t=GL9o9-R8d=KqB ztw;H!;g<|AS6vuLBq6TgXxW&7xvXcmK&n*!w6hpj1D{1{9<&80#Y7ilk@4AjZY}p- z+``5zoL&2hN)Q1CzB?B5NXPAE5g~7H-owwA&>gD7r`FgPa4b1yxOgJ0H|>(t(;cJA zJKS8i<2nC@tU$$2$~An#Nb34jkdk?IWD%FtIOgmd`vkI zLG2}%k{!UsPW%z3)-!9gzniJqB6%%6Z&WEDqV2aHAHLGSeEO0UQ8Z;mpNa43b#mz_tui2~a2~;MXjBhnI%Jm@j;O`KUIBx-A@Q z@S`|Cy~K__01R~(3(N6o8+}k-Hu!OZE>9`WkgN4m@-p{Oib^uj73t#L2hnPR7$FA$ zccHG7B+xf9)N#Hj2F3m|mt;MIDjO`FC_V~Dp!J}kAoBLwHG8&lm_lWWpq?K;B~lc< zM)B=yXxqVrRjXToA<-+lBcGqOjs11GFo4-NE-Fm3r_Fh)4Jm|zBbb3*^aW)wWiTHV zhc{}H2)em&Sm%Dkxv>KP2Ws3cDALB+pS^nL06;VcyhZyZH{gB!KMmN_%H-IBse)iP ziBM(L1k`%Z`3W*F$pPe+UA{KHzj;A_(Kv`vw7$)`zkTWQF?w~C-V;u$kCN5PR1EJd zEd)^(?4C#!9(&GYEC+=OEc%T6^y9ZKFEpO7pu-1y=4A!+fw)t)(3}>VlDO;;zek@wlnU(7<H zaj+wF+%|iZ=DOI-YKh1j{YyMWGc4C%^L~3#^GE5k?6H!y68e707(${j+PmySI5ruE zVmZsD9{`?z3~OIKwbE#JUier)4gGsws1W-1Bn&CVJe`9Gl32c{&EnS$CvRMJ0cgui zuI%@bM`jU~skjjrzY;R40GF|bvSYql8hX6Rr?K9xngFpNe2RFrnn3LIG=B6&Pc!x8 z{L9j!!jP2ZxZ7cA20A*S`4DZ+xWT3U-vB>1XYaS+Q{TAyBI;btBT%!*cIk>LuVw*% zbC>5$6kLXpanoi`$!1lZ&fT<*gcKA`0UWh}(bPp*I7k#mwkp=T8O7t`pWHqA4wk^P zb&Y59SocWSQ+H)C+pwmX%(`*PqA#J^4$=>!)2QM!5!er=l!I4=jQ~dooCGvZ>4i!cS*H}BwuJs&sqRnHe)v)F} zslrcmo4y&!4G%;?BFd3Tdubp2U;a%N|GVkX=>tGx(GTna02?0%8Zucj{;n8?WT(S> zekKi*qZE^I#}%oDr^=8r3X6^3T=M^r6>(-lLfcxcjgjB7b0X$!3H3< z4XE+?=dK5;dKxRs6Ywjc~Cl=wxVK(Kpp@tTeX_oYLSJ6KDGBp=Nu&EAj1{3aZ1M zmw!bb0OIs`R>T~_9)T*Oy-s?bjg#ANF3-ofrPY$ZGj*$An` zVl3T#{#cYZt(nNbn?8`W`+ey7*Y0zDljxr*y}Q=s?}s@A!Z%Z50WCOaz<6GGz189e zB)k};PvHdpIZDO4(CJ+O%dDZv|P zTPw-guLwTdxAa*_tsULXsxj%mv1m|Zb8;EBUHmmPSKPYB^g#33SrW4nDr&jXU@-81XOgQH$9(vsLX+=Wq z;|C}9^bJyC*#C(^_g=go=yZ2eDHMA8zqQ%^pE0XL5a5-1Tm$gK)R8`;Pf6);Te@70 z5&*OnnULK+;q_o)TU;->w&(E2!)2zF-SOu4SznQz_QQo7s&d8U#Z!PV{gmVTybbhqE?qbn_^IFDFvYjj8OScpO zrVxGjN5jYr_t=a5u-U}6L8bwS-gfd3q0)KvZmQX`bxRVnoFP`ZscC)HS@5Z(1h-uv zolf<>#J?=-p_FQOqd9U0HeQ2;4bmL`(WigTd0a-zrXU9Z$nJChj_AnyvwG_{rI{{G z`M>fvFHWR<-{N{s?oL1Ozs3c;GIWMEvK)#|NjtSlOQPvxVbxIPcjIWs?Sea!WlB0;7Kny?+3> ztrWMrdH^^8Y`7f&zW%kQYSvEnNHi$JFA%UKu1#nI?p$e*Mc(H?`(H)5dJmI+_(j=& zUz~cx?7qF~9(FOo+43y}5*{kg+Pyitq!c@1QMcNmBOQCx9V#~fZkyK;ip!W9%B2dN z5_V*UStnMvI#u31LGkfOlyILUgcU0zPvDIapP#WkDYTcR`HyHxcGlKkWM*M{ds-_jiDXP6HTW+B9JUd?bF4@@wEl1$b6yq zTePwxH%=i2Kiw@3*&*#&EC{WhO(NayV2AMq&<5b$K<&t$?mBDE7cb2rSIwNRUIiuQ z>yH*G`@f^+EGQZMRKY*X+kx5}-2t8lfTp8X0?rmHp@^Ugp>Y3XbEyP07;pAF0>LFi zo-Kg3^w3Wm-v)4tnYYqi)SlVUS(-cQJ-l0OkTx&wd^WA(*R2vqNcncAu}7-;B2_YK z{$W?IqCYlNK_rLkYk zmMaCz^NdljcD`mC<~HLyvlrRg{Fas)vqNL92>Bg*CA% znbx-35PpTcHrlg<8&ff8fwNg>O(QmzU+*}vSiYRTB*xK~<`+;*JG!7%`8#?*q336>W__$;B)n`!Q%m!8T8_pWJ*a%#tUeF z%c3W7IhH#GG}<2DZ!Ny5y^vMIjPJS`>xKqj~@OZ|b^FBDFfe#3Q zwBfRVf@uz!I_JKJ*R4GNAsCwLu?ib0tsJmH5lb>1vu54VWk?W%Px$e+`|_2v+;wmT z`>u}s>fl&*2TA(fQa;vxz-mUxR2iWY{Fmsg$n-gd!BbC@>j*;*?o`I3PsN20vZU-Y zP<%5m@U5VZF&u8(^i|E!_9q#&)8h`RN=lWd%FDYb$a-|(=DaaF!36Q6kZThUKm1IN zaIV}nE31KwEnpUk3-A?m43J(*t3g<)KxalHDk5B}ChLpYD~K}{%x#U0E&*a>X089C zAat`^jdYKBO*i15x&FUZKEx?C_pqybCKR8tMO}dX+HqO^en?G1!j=ETp_c;@Rt0W$ zh~D9{&>dZQaZ&40(gnrSA8VpWxUHzs0b}D}6m^cFcI@ZrGjxo%^4+@9+FX|dfN4Jr zVVKtuFFcbiR%*wHOagZYcAXkro}kTJf~~9OZ#r1>(kzt>Zc2GzvJfH^T6}{2C4iex zSZXu!4yCN1!BIA^7;Oy$!u<&R2A}ig5=G$e1a2YNp5gsl5Z>6GK;FW!vau+U`>!ZTWIBZgbWr}TC{B?Q% zA4I$#b$u=US)XvHbLmxT2J;>KM>fLGNf-7-(!(zyJi!Hwrm<{G0i8lF#4}^wB|%6D zr6Ro&Wd=LL&Gf0O%Rn*ewa!%A#5sOrk#$4)^bv_j1$1i>qed3sbDfGuP$npUIrUy1YEEEX&sD{5(YbGot<^93kCda<@^eK^1Y<3eZK8>Z^wXm1BZ%16} zbpV;ZRc~J+1nog?wNxxu0`HlUv7kB^dJ{mVD5rHHrM*c5$UOP*hOPw4#5cO;1@JL7L+t*^y5#%GnOwg9ocBn=MOuT ztLa_9CdL>r6x!JA1nw^El%RDQ5#m?f(@JY^CvpYRNF;ue^yVozuI+mno!%uv?*A8-EaJctD;<8bVtv~=QtV^=i?jNUATm~2w=HN zVQeYWVrKOZKZUvbmsXb+moWz*W9i?npV6o=XqT9`J+4ZL!_DhG+H5_{n>(G)(<>O$l*KN%S9EU&-9ez{ z=F2Qv_7&c0ms@{pf0I-@kvzP-DjK?ENt|1?m9hz*y*CXEM#tfeI~a%&BsZdM>!})- z9bdo+5l5q*SS!L1O|NbWB&mv&GuyU*7p9ov8X-T{E`$4%IMd@`bGsUt2!&5K{EZPIcB zHSgY=0GZr*suX&{ zW0niV^zO**A@$pSr|Zfcg8W3};RBI6j>?Y*GmPObdioICj1VhW0YRMTQieb^wS!)zV`#8{6+Z z;&7f7FxWzEjK5zY@p>7dcmQ~+tE2g;<5$1m1QzJ_@Joo$n=vUGg>Cy<2y{P0t4yWQ z%s(n7qH95nCx?GI+@#bk~jS*g*nG!j+h@J>13`7AU~}?G&tIK3=U~zZWziIp|(BM8YtAk3{1dPlU=LfC8vUo zyHVwR(02l_-_o)r-N5JVB6>Hr$+B#kch~_yYH0M%`mz1G7^iZ-QyLZiP;;&Cp?dkY z+(nAXUE{3iMI4`bBWrjo_x1tc><>?$Xk5pV&Uq4%Rs;95K~Zw5Xck}A{KYJHTz1ZV zcO9*JzP?DWA6ud!5ZVRi{Fy>A_Uzdoo}x8e=(}T$V-r~4Y6T}SuCWwkcS$-?%0IlW z%S0huA-6a|dP#4Gy)tjJ5fMC6r+f3*-sm2$MAf0 zZ;T4;LXNw}uib%(yvlE&)cl&e+ZO7T5_RuwewezuVw!>zyh4$P;4iPRT3LigDwd*U zuEbG0m)qI(FLtGikewf$GYx^dlYCpIcjH1lKI|8As$ZFpCfR^m-s=16lTv@bxd>R* z;Cb*k2~vt3uQ8$bs(1DUrbm%O1gJ6P>1 zzf8Mxm%3Js;e2?rWZ;^Bqb-_7%YwL3q;ZXmi1~ZB4gl{~$Gm?Q4NjJB4-PAa4)AqB zPA9dEl+S$$h-=2sXgV-Z7LxL()rOx2IGZrwpf^us9rWT=@xs< zjZr>6nBgzzM;TpW4``GvlqC0ktBX)M0Br7L9sszDPC9h*7R8tTdh=r+(s4pk9s13r z`N^u09l4z0%*&p7?g}5($FF!~9(|>uX;J)E3F{&?pmvQ`5={u&Ko~in>C1xX9IAWp z&-&B9Rpz}v*}4A)5dG)3zG9c7hl5}Emd<^7RR^k5tW%1parX8gf#l^E2iQ!%5JE_h z?)^va-zD3XfIuqhWe>Co>`PC2Rb83tePcxGwPwDhh*!55~n$&&6ek2iJxeMSpMqZV0C`(^&b3M_K>uj6y{Leje9 z_3E{}mvsa09A9s(UpoK{Gsxx~-?B~jCqg({Tth^dAt8Co8pwDQJ3cC80Sfo-t-X61 z&zIh!6Qm5#zV~5eHB>P9jdV!YYj!`oZ*nH5LaH!Wod0g9Tv?)OY7u%;VbRvyx!s*s z?HHYdX4vVDT=K8y?q<%%Z&(Wi8x~3iwcwH7NS$X{?l}2LfOw9QqiT@f+Nqe4v;vlC z<=L{yeh2$G1FfZ<56owed-EW`TBd|2Wnk8Kuz~n6OuR$bKMeHEHn4V_*ly0N#t3&( zquYV+fhCH7{7Ksq^9iniU==CRt2^1NAf*Gqmbs{&Ypu1Hqo=V2S?7XQ3x$-Jb!FD1 zIE(Vs8o=B8-?n`f+TrC$Dw<=9N&45P_}@{c|8=bYdo_7FxyD>@2unwP;m~H3ikHv< zU>SY@_-nFrli@v-)r*x{k8Zt z@xRx6@XrV$BHJanb^>t3BARvp2p;B{mXMH;l`?$rulJjKmgt`?;IWpNH&0CWP3KhW zd6;|Ho=#fBds+#iQzfa9_3o06ZTEA6xZ4W*j0SecU`F_fJGl zv~P^&?%jutEA|S>x`6k3{$x`gR}s#}PfM zp<~n=%7ppG*u=j1gWuK)cKz0V<%i04-nqLi1NblS{~EggBAfqMG7*SiAm8Hi4fw3R zp#f!%j!b&^o4ygPWTp!UxnI8W_tk%hy0X@1i@aa=aU6Pa`jPpCj8$W+3N1RC(6MO? zzR#)>GHCTs08zcnFs`vh7JKnfBgFUm_QF-p*YYP@p8WBDn7B~Xn>a>CmJq(4M$DmLjF|xP3Tdj|jlg;o!Qw*bSpy2Wn6mQqrz00^$ zm~?{KWoaO!j1YRoq{MBtO1EfxJ)E9-U9DFlXud3N3iN}k!kR_y1hurN?hPP1*jy#t zx$N3kWyGq!a$cR}C8ROJ)aLbcLg!oO7a)`Ah&mX}=kzGDM zXAay-mXyVB5N%1QU@PirzK_Hi$q4$x<>#+*Kv{40mGwURBLl$Us0)|@I-YTN_e-uPS(f0X8hnXa&3 zn2jq+-t$Hq}E7l2oOT^w9H`FLhqxYq>hp*HE` z?;jFxps#kVg`pR`ltTw?5e#ux#2lc33dQxA1CFozx8k!{!2 z{5k==801NRNmCZzfij!Rn|CBBswvly~Ov`ihSm3tnKS&k1wyI9uduv@MIt;-W=nBgEoT7$&w7A)wH~lq*`SAt@b6FB+cAC zNb+-%CKw-rC5Q;O>U1Ni0TE>*>C5yB7SD*yYywH=lSu{GEqJ)puI$1R#*WU z`1cK=!+Y3gYiB~6rjANS^`UUy==bikMi@*-IXX*d@>0`Gn(F6h$HJxBsepa_nZtdz zY*M~>-e5^(w+*uP(9dEeBwuBidO{*DM$L`)mHT+8iw(0z{Q|To$XU_&x?qE@GAh&n9WR4Q0#CvxIOf5)6JF zbl_(!`-dkgh1q8@Y7u+O{75qV=IO-ptuI0O1EoP6kq@9UpjAw?)~SZ7Q>CY<7&{Xt zdWgARXE_sv5E%kTa&3whm8mAJmX1ebLjBqmF4Yk+S>xVKMX5|Y7WRk{kl|ea@KK#} z5rvV6kJY->??jcGxmV2WPDETV?nGx8vDr;uJM?r^f0#x#*xA$R3ws)xRd$668mz@z zl#a*v9`>{63W?F>IgVDsDd_a)y8i|7yEp#~ALg zU{dhdHC2c$q%W5{gJ#vWiA%Gch;eiVH>iX4*G0#oNlV+nnnDoFFy|Ajx1h-0Gi1}< zhKbFbs}@Q-L1DFOC{2W=Zd(4h$H?J5e#>DAk_2AZpDka@k9tL^!6u{Do5)|SWYkz0 zJAMR4P9zrJk|Y!l^;uk;#X3m#iv$;<$EVZ_+jD)^=w9+5?>iw}T1%XmEXzi5`{C|; z@6K6~i&>kkl7AH&^ldu?vy44rAO2BoHtal_jVeyGAEKjX(W-o6?2i1J`G;|%XP6W+ z3q3`)F!rprbU9Mqv5Rra6DxJkd|NLPm(h*XPma&6@ns`Inc&~Jlwyu71d;zn1beU( zG}=Hg@q9UzAD!^3Dl)h&~42#hu;Yk~4y3FswSgRf$ zbF@CY3AYWLU;KKA}M-Kdww!XZqOr-71y5)akw3*QG^u`(dpLOxFQ^<#3u;7gnTInO`A^ z8`1ou5N9uxqOm*hDhQ~HA|2x?Cdn*ju_KMt}$QlW`*XGLH2$E|B{{PwADzq z>|R%-4v=6f5$vBz4wz+k+xm}G_1uKmx&+q@D)u!i6lAOz`>!tSGtr5wB*k?+4uuH--S`$?4cVAxNfwJVzTLkKl{1Ihl-G) zR^iF?56_D9LG<9~5)AdM?Ep=D!7Iq>84>uvGDuAa3b!A}BR+E(*wIb&kPa>932Zec z7T3riM^-f&Q3_D<65SWeX|%F3{fOB!P|;Vlx_XZTD`Kn2Ai+VSh2Jxt8@m-zvdc&` zm7~TsmTyne!=}w>YZGE83i5Gy4{|cWD_dA-!CAZM%d}Bz-*=`%llsRW zCcQ!0jfI6_YUIZdPQt?J!$S%|w77X6D;7kJ(G&=`5-M!^5l(*vgF7-(pSJ&ilCp0X z;g<;5S@#eYZlyeEO^(L8ldrE8l`VwqHMi{p$rFd+so8sC&BomHYn%uaOd{t!wN5Sy z74--OGG7lMtbs;Ef}Ipq3NwI5#j)Ld`m!|kBYsmAwq`O@E!rU%C1^#4go96dyNmiJ z-h!4TF-MDAb+D;y51XoFro@LAj{jAyjmdk2Pj^zQ@|E%r^3oTK$a4QIzxKxca;cv$ z)JfT@sMM|sp&9VYm_W_rk$Ys{_&|0M9qJ;UFK)`^ei8-_x18Nt^J%j{No*N-Hk)T+&{UFi^ zxOCOmzc8n?vY(z9^ga@#3JKoWk`+%u(nlyuYbOniTc-CsD4tMAtx1WwBoIi$l@7+? z;%XtoJ&=3*m`C4yDAfbCcE)NlF6YSxG#rkAp>fZK^1+sP>?&6Ao?Si?7T%X&umBU2 zalKO}<##ew3^Cnecqb#auw`(nuxDSmTwhNR#aC68RNc#5q6&}GYLN=Xa&hTB;I|n? zBOSchSwSy*MGYH<)^u#rGQaCM6>{&TX7@FHO< zM6Jd+h#V?=8Qht@w+tyqAaRR6RIXEr-=TNAAZ`%BBm`&YSg@T>osAT7Y%$Vmh z7YE*@srYclw0Q1&hgfthX_OFO(_n3QGmddbL;REP_SR-B%$_#m=84r}R_%y?j6hZ7 z9Y_3W42&E9Ph%g7KXToN!sYaEE&zhx+=QK9|KXgrD$`t$z|iLcVz99vz;Z zbv|pYGfl|~e=$*I@WtXtF|8!$?!+ZWr2XQ>c9@llJ38Iy&N)>+zBecpeu+9+t3C4! zB|l$c1=|$9G_RA%PE7wuA%#fDs21C3mld40K07>MdEL27U;wd>cxQg(Il;`0=-FS` zF~n($C-wllwS!fh%F-N<=8&V_$w?7!<9L1kFs;&3B$({3Zf3;|OxGYfg5J6rBo&OK zg<;$)UBDrUUD8Gg7_$+{mZz^RAwty0&h}GnX+N0)m53|Iab;jq_s-P8#W#`cUI)Y%5zyoga2=@}iX6>E0M8hl1{2Sg#1{5rq9kJUMhEPsAn1XPDQy*j znYY0}|4}xy9|r9nwTQ}JmdQ^t1#jFKz}FHN26sDC?55PRzL%ZV#A=#%v4%7Kb7&-9 zxuvYVDzgEibm9jm8#a!BRA!>d#bS(O=9+eiDYuM|B0Lbcy7y8CnX3+|rt^+zN_GrL z0Hw55`Q&42Lf!Ed`MlBk`IXx z2tt42L^{G+H)JXTm^U)A%IK=7rEVaGT49bFUofmLEJP$ewz%mdyD5l!+0f&|gTtR- zUaP8XvPngken@W#sS@0S_8f&EPh_BF^@X+^J)M_hZm)qXNVSn2W|!0@ljLsu_DO0> z)-c=WWx+O9d>M8I z+NnO05@L%bv9TyTHi+HL2Mk~D`ArolY#vsv)UEd%4%MBI)_85s<2+cKiUjGjaZiV9 zS*DT5f89J3bO$*>(2 zSSiZU0{MKL@Oc*-{zWv$@KLr*XD`fQ!S6$AVJ{S3RVo$L>yc!1uf8F$6~tXaJ3PNl z6xLNGaZumooQ@!Rs-ZU~%yy+0Px6b*d!3lnhwvd|A+W@q#mY1+SZPw>G5r5x@4dsC z%DT33#yS>s5F<)ahbo~-2~}+LZb+k}hMLfO)sazzK!TJ6X+c^@As|Qsp;;I}KtLdc zjz|w+qbUr|FEh^fUC+Gl`+V>7J=d>Y*Ejr;b52gq-kWvST4$fN*L^Pt!oI)EL3ux+FNTsqsj zC|pWuuCCQ>orbf3?6@pqwL(5CWvkH=KB_?c0dL=fO@xbyxE#(2F3*6(;8!;bBmk5y zuHv;0@-;5aI^0Z5W^Uh$KU~XBEti5pv(FJ>WaseZ#&~r)d!XAkEiTPeU1OtMKzP<3beHX{ZgrK3FQrhEHx?!j zNn+X@ks*v;nr$o7RUnV9eexvC~>kHi_>K2_RY z+_oT0qVF4$^L15SsW^eugP{c*rNfnaz-G6iN&W=7lC+|dHt89R5VyL4bYCDTe9;c{7JZ%+?KK> z-_S`>;usN?u$^=#L&rh&4rmEZeFUGb)%WzqfHoyc+1%^RVKEKrrC zYLvXL=gko*2Dyke_QwkZ!-?0b9Ma&F?PErBpud?oVr)IHM6~#2h{_DfASG^E$>I-4 zjq|^7Dy?}odu3y%8-YXvIML%JucGnPIMRP zqq{|;n#XCUV*CmEaL6@Can}>Rg-2-d#0^YJ;*H8cKp%iV=Z^V6C{)PWjG>1rpIDd= zQEHlRj7VvV2R3_TyAsOlp&ZK*{+21%G#%DvCj)-M5CLmasN{MVPb!A&Vc&1gJTW|n zCKX$Tl|f@ZhKe*qQIR<9b^6@KD~OmV=Iu?7McZxLVLv$ zPKkFnZz$3h6c$9F76BPdg2TwV`5I8>;Tmi;yDvcU;a2L;8;*?Q05*OzmhJ(A_6n%0 z;HL5`DLr8EUrZtgLV&*sb!L!jAyAiLSK!M<2`f4IR1zLJ#&P-38~4l7+8vsc>b8-x zvz~E=a~XwRr(cOmmboR49Y-b@^ui{9f@exc;(5l*NEs6%DT1hZI;m!IBQv(}hp z4n!^TPa#x1U?QxB={U@6h~QShzjQr>x}98lyH7;BrNySu6B=~2)6s>m$0rIVCX-3dO);An?=yl&U zeoJji|3TP|!&rw*EK=lihhYGS*Wl|s=~7iB>}3k@OO2;Uv`iFn+1VmGH_iKAf*hq-Onb zyGUBX1fh<*X4WBoDJJPjg`zwx_(L^ent(7gRvV>oXEv{nc=u}vDBNW85IiIl!kMEHRs)shl^qkX>g+YvVPWWp&^>p|W-Q%A#={hN$toLi>r52^qNm+BOEPHI zQg894%qDpC9e%yYkr>SfqD)NQPz6TerC;c=cypq`sf(XnM#r>Rc*cVC&HKsuxk=bn zNP+_AC%NHKe7s5gz+||;2D{(zAt6VSYu-_JuEB*$rra}mOKOZ8%~@2{(FT_(P1RYZ zUo7}&xkv?w^9+mx;iI1)=k&t#v0a2wVNh=a3zzOyVLo6Cvr#Mph)NoIU99pN2KgJW zZAxH8fkYm=)kel?*d-}BU*JVyro}*pbJpc^P=7$Ib^-I;RCze`ou~0Js_I>VBZ|$> z>8*vri^KV-PbOD$S3rwdbxQdU1ZlaRuMt*OvOZ}OE!tK^ZYlZF#o;$X@4d~@4E1Z1 zWMpvlQN;W$0+d-MIUc-dE$csp6^C`Es)ifTLvE@#pWPIo+62~;K4=$0q_eHlQ;xr^ zp*@@rLP!IByTLD0aEh-EF1jj3Bt*?3x;P{9mwySXGR8I^sC<(|&R=~Vk`D_e%ELRu znqA?;)frhm&}(G~dQDz%v5h&>&kH6KMG?F4Ai zhGvZeg1>TJ+~~@G{PVLiN2bmpXry^S^|dk>$kk9%!!&K2<%u$I&P;in#Cj1dSWL{p z;-2X2*o!HP5XT6F5Y>f%vom)bj^g8QbMC{oKNC1#JT0{{FqPsFExLC z?K$Wl02r`mw*?mcB2sMGhIW<6Pr?j~p0R*n)`1}^{($(AT>nSTuE-m4u!uRG7+egf z3JwBODC+nS?}3~p7t*l%lUjWLM2>Q1v1_rp_rT05_#}NbD#%66%?{M4W9E=;ID;2< zTEEu^XNe%g9A3|Tn8UE$^xh`4v*JfB8xCRPC65$2u2h>osWJD0QPN*=>1np$5SZV9jpiI?CA2Iz zY8U`{6X)-QRlFP8)CU?`@+$mn?Xr_{$eU#cFcHl6FTXq^b2_)gR3^RhCe<5}Ps8^e zzpU}o$3r?nHK14`9i;=|sYdAYxUpw{UtyyiRoJP!1vEGNbNg-8O=$Ni=kPJuMFKIS zm>QvedL~)aDm32{29eA<{O~+wJ5))4DYZ)dP~x8XYaf2+u|r8iAay{1cq=9pIaC>E zT9%}%=L3OT*A73}E5YDSdXp^k=>CS12WM_tTu5rC;Q?Tc<~XsTy>D(%w2RvuMP*AH z!FCe!{+h1G$!m_T9~@ihL0*@49UN(hIp_#Pd*cp94XS0B$vCmssYG8i1vO7i%#E;6 zQ5w+8ywG3$*tDiQJ8qveknfJzyYZc_bQuvy^`R_Tah9AMZlZ;rSHJx!gRLoJFzMdyCnz4j3E7?Tp7Q2&bKMxP`r$>%N&VNBR2l338MK zMZ&3L4x6cFJ~1I7-WenWCdOQgwzf$H8XR=-EKMY#XD`_95UY@9jun5YRzDn)W^%3>$Pg zV*=P^oqWug#}H2ub7HkbRNfvw{%WF9M~>F?d~bW@-rs~YUJq0+r-w=gdN}1KHL15D zB(oM^ghPh@&U~5*6aY{QxNoImaC%ecwIGS=YUJp-)0@p}T9u)5{zmwlC|X&7R&!Kv z$c^(&gWoHM9%8(hMcx`1dE3ZSISzxbE#i;8mn+3Y0xf4^*5!4*3D(>WyT;LN29vOk z2(5b_cxTyy8aY}=o6O>H>tD=9O@Z~KCHzMQx*9C!`Wt}yT}(y|lv$9W)Zdi8Fw;7C zkaCX?Yd`GOMw!D3NTPUDg7AwdO1~=5JM%_?sdTs<@#T#l&YMlmKN{jhALxKd)*es8(ZL+y zFcoMHSdi59aJ61KbnUw$?ILs~fzchuLGH%{sSF7STd%+1Ek7alQH z@Oc->&*KhQ*dXL&30;Y1)>SMG;`ez)I5P{0%sw-vY|M^Ds?^ouP86jov7va>M8!{c zy!W&C;whX%2wQ6?Zh&8jq;r-mvLNw;U z;&lwoNG4Ly{gf+dwx3(jRR3>p&zQ@S1!SX(cB2$uWFB95H%`xV~ zU`skf8&^lR*9Yc<%F8O_J^F1V6w1*J`Ze+$bJTfamRpG$0$s<2))ZGJ5~7DXOo!!? zD?^=TprK%x<~;!zed%R&&mwTXu}8{~`o0__F*>5<-@vy>i( zO{{>RLoVX+ra(?;`KM(!S zS=jG8!nglZmiJ)-=k~$0t+ZFj2*~u{Z$dv#V!mSu35ooX`&C*ByCjymOg`~U=&+5( z98pIN5DM;U!Mg!HQ*G4RZfqa;EERLBEXPvVxhuYl+W&YH$Uk_05c*5Cj3*M@#ddVy zyb!=C_PUiEu&90PR1qnl;!ElISbJU;S6)@NK+eBCveK>5w4)Fg10m=aN&O&h10Uh` zcZMfcK39f8vy?&-R5N&iQ)V}Yp?09fPfz3# zkw44?iNe4pp--#=J=^#9U0pAz2^67o=`qsLIKi82w3_6jk(U8N1KPI(P^#f4RI@!Y zFt}Cm20{@^kl}B5vI@>3Cku$}U#w4O0DR&e@!&@OxrX_f;h|v<`Ws0)eR#}rNz){# z9-WNGt`OJZX7Jj@V+1_OpCK+X!j2c-?(ku)MGT=Q|Z?$v0 zB~`^5@CR`v3Y1l9t~n#=84=w%B7iDbM8O4E7sp0#Ds?vW&J(=LiOJ_h&c7CG#IK^r z(upQTgqDJ8cuo8?5~cKTM{&;JV7_ko#S4eqGJOJcBs}+qF;7ilnC1go9?P*gqQkV9 zwW+(d4sV+v_rVHfgp2K6-#v0wi){37FZXt+B+dq9!l>2#B*N9@_khpVUT2yxy$p!% zlwOR)XT6>Y{)i2=@)2w0E;GvaoNwX2@C22XTN^cdxH*ZbPvVxY8jB8rm)85{2#6Xj%zVoip!#0$ELrz1x?m)T;cRBCI(s{1ZHZtt*?nF&p&o5TR6 zrPUF-eaD(#6o2vY#}Jt<`uFXk{4egLnl_-qF=jUwQ)OxIPZ+9O_REj@oB`+Fz)=z();Yij`J!c;k# z<}Yclf;{1^HzvL_5d50P%l|mzxT4<=QlFo!-~P23TQB~udu1=hh+_oy&>mZpsh4k7{})jG`Vno*ap3uhY+wW}2AXob4`808UYJ zRuiQvqdf_lV0Fo73vcFh{3(t75NKaO$g3e{1hKiAt;a%-ZI+aVl*G-xP&o2^Jzo>n z=xr!=xrC6Az0t&OR&|iA@&LE@I6;nvM0e3TMGWfw<_^SYs8-!-c>vZ z$lz}EjT{fCFo|0fH^#=OzkGM76Bb2^njXv=!%5lV;?-BGNqFI3==EO*{CrQnrKT`! zZx1X?>D+)Gx-PU&KKt$Cox8stuEyK}q?#8z@BczAL^$%w`HlnyzY5%lVLpvfQ>N&u zUv2N17G37F@$}8C=7-={Z7v(A&8Ww&{)m)y<$ms!2IN#-070%xwM;PY36FOXZj!3z zd!|BVgjKLL{?37FIRMJ#EkL=$Ck!sVij%)1iIj@#i5Z>Q(3c`SbsTv6|^2a%7Pwj9W`$0dX5szsDS{TK9ZdzDwI%4Me{QV`ppC zv#uBWDMy@~ll4a<5~7FM)Y;#J+9wV*X3J@96WbbU(6t|3bU=XcHQx7}oF6DXD6% zt#nv3E)JK6(@e|xybqZdQ*U>EdWsx%458rHld{5@Gj;IG)t&<&@{THUbq=V+qeR~h z_4{4EDdILyZ&Hg&sU!*ud{L+0!c2T{^wgnDNZ9++GcbgnD22+NZ|yH6F*yArersBh zdy>y($Xl=9ks6ReYSDsdbMp!FxbvRRh$P3@-D1wZiw0NF4S}_5=gQN}ar zm%L_mX6?J_WvC=-QnuNqcIxwPcF9 zRzug1El=>vZX~|qwpW)%PquTgJ*dj48z0rjf2>LB;)tqFH;75dZW%#Z|E&qkV!#PS|>hN zP<)t#NAnu@+Sp7OFo@IJ7vDZui!LzMhT=vkPXbR zQx#$(b17!!h*x5rE;4B5g?w9vb5889>nWF$B>ZxO| z)xF=&M%=^0;>M~B^_xB6%ZIU**p>RhvI~*2EctVpw`DSHV$P^m>FK8j6}Wu-Id0!q zO?u?*+QaZ3r%N}lS=l_Dd=K;@b4qM=GL$~1Rw+-*S<>?-d6WDYc;QH`U+G|D?~;{= zjbmTIhc7`henPX&L`v7MeNP2*=`O|rZJ=HeQIY0S-R+6%Z9u*t6}a=I=NIG7AT-+o zIs`lwCN2!NT0^U4ZHSd&vnYuZ@ z^u?OcYp<>UWe&O0U)+uz1DjWxOn)dKwp6;JoFq=}+mC(^I)Sm&UHQ-kdeK733hTkc zPU1p2bc4SAzV0?;e-p;&s() z>mH+{{u9n2W)>mk32z??3hdoEb-d~YwOUMRKFHmEDu}voOxR7BQD+mJSnwDjr?q~_ zLO8al?LwwB)d&5A=tE9P1(HPUv{#rV_18FDh;!w85{b=;vFA@1*7(5HMPM$h>DEL& z>Wv_YYg$&DS@PHX)(4ox!JtL%7ikd&)-Ul&FF)7`Qpn);f5SLP7 z1|cIsyl$|^Ht92iwfG@-mOTqE_BEbQ1b01r1{sW()WTty7~AXFw;TG`&woE=jX_0`ppFuFTT2YNg)O%? z`Qe&U*h#}lNtT%VA6 z^n4+6qVboSeN8MuRN(OGWW%KO$Ydk9B|K>t&l;-6;7@P+d+vYVA3rM|Ty z_W(58d5!*O2m0Syu{w+H&M0#Jvx7fJM!3w&mb+T_t%IaLTT#AKrTX_U|CZ+8y7~W> z3H{J!bKI8Q{$(N?$htlAaF_L~?Q23;;HT}j1Ac`*!2P-X=BWha2Zuj)ASB#DMJXk^ zbgauebT=kQdw2Ix$F#H4at!3GZ-M;L{N=uS_g9`95e_rC{$tD>fOz4tG^@-scfcF3 zI`s(acSVb-NX;U8K>UG(p|$JGmTu5oVvY}0!Y-(o;_0fK7C@54cHQXgvlJ9vNwX-} zBkrusqmGsYY@2WGQxVqc9~itwn;5qXIK8R&Qjn-1pdvV2CHmcCf-`5(iwRPiydp)K zNQYPV(>6cpot!}es1H%{l}h+t(F9Lh(q5dm$ER48nrREMtF?9}HVzEy3W7z!#RAj@ z>HW{(1!^)4t{W$+aJp;!`ltBq6U54sOBo~!e1F%5hN*thfHG>MSg#>>MX=DXyQ=d* zIrL!sPgUWKs3Jii622m({q~$}2K<-L_^yVrx#C73rQ7H$<17SBA$Pl2pt^+R56n&uWz`eO0q zCYZ!?ksxBGO3*NW_Ht?VN?MUQR6Bj0ONWw?Zzo^&N+xg?tT!pCFV}dP&7a zwP9Ksmxo3Ji^&8ZxUowGInB7gHIYC%AMjs3g8p@Oi{0GcesSe_Zt?WUvciV)(b&9+ z_wfUZkxg3{mtJk_uNJ;fTaISBP9j{=ElwoO-lS>vWT*#y9$$pIooo47>xSQ|nJ{Rs&60PtrZuVjz#E!#gp-{nWT6@qzzbifV^=v#_5EGTzt>UqjC=i z?!3kGp*|-~`*{xgN;SlFg`-{WvbaPks{>WWuxpGR`sux8obChn`q<7GQ!7!~f( zlIB>nEU261{WIm%@V$9~%YjUe)8Y)HC#blb(oZ@0m?qnv4MaMFqkj{sUk$EzL(zce zDDz}e;-qg`Zce|8vpJVm0&{f36!Jj;k@HQWlJ$i9o)=7%L)FiSj%)}(=crj-Rk5$O z;l7?Pwi%-|Jh$eAiEl9_L;PTwhh0v$e7SU89zx0mpq@W<_(Y#QR8T&sIB_^42d^>i ziSv=KSJtrR_Gl=Bn?oC8ALCq*y6)yh!&fSz4XZrE6OBCt3~)q}Q}nS0({9;7)JHGo zhZy-N10an{>b?9=bmCusRtX&yHL$h|3700Hal&Qlx~kZ=Fl5Ixs}Rw4ekk=M_Du56 z3nK4Mtu=PP)j7Q7I{Q&p$gJLb;JsSXnC4<+_Ad{E68r}uDGw!&>9y&P_EB>xr z{_@=|v!lLD3a(1)HqkZ{}n$mOGC8ntpsKvZ_ z6x>_4f04f?VIn20atvq>F**|cZYa61qUO-`D~57ft5>wvK6EP|L0sLemR#UK4$@&W z^)5jQdjP-{HY|;YFtah(47?Am26Z<^zVJT&S&5sUmC(}WTQ7US9bH7R4Ese9WN@a@ z&L|}RQgw#V+IITwjp|x|$ntSXxkE80*p~uXiYZ*nA z#p~RznDZ|gxul|B`UY8EK`s|-LRw1E(mF5gn{`ONm zUQU|wxv9|eGV`Cd_rHH)m$;j9?AF+(_qOV#57tIz+xTrkDN~lkPl?n@Vmg^mO}dWy zox7+(1Wohv?3T`YQ1IeGCGo6nZw@Q>&leIp*bs?g; zsMKHdn66U#3PE0gKc-MlC$pOdw~I`pb_>T4qo^fjJrEnCBA916_B8p|Pmz0HAUW#~ zysjk1^GYD#YPp1(=#hDCS6@*{wxBL{m1L;#6gSp?YVevF*v7U4*xdg#TGQbrhe~Ee zN`ACGX>wfS*^8Qu#WzLaasm__{jCffYK*E5YKah2d>&O~pbF_W*gS3}cOm4RyR>;y zLwXPu+i~1vzD-Z&Z&*xx0H-~&N-ZM zs_w6N45ljA$R-g{3`uG-4w%%`d^OSIWC~-7I~zK=`$UcaG-77UWxag*V{_E{8~FHK zf)lsZ_=!e3`HNJO=@;tQ2kT(BxV?w#)uG`W$OQGeL&-uaM(^a>?SgsKbZYoCAqrU~ z%fPQ{(0$2^F~)SP>@5#d888g2I~78^H^tw5_OLbLK9mhHGqbGGcChyhoh&g5zX66E zYBSuMf(>2tDoZ@6GXNFvY@-K~6R96vLf#0{jR~KzWyr!^8k9l(!mux3aa;fl;a+ke ze|q~2vIWGZ8N4;EQLFGQ)A^Cvpek!U z^2|EPLfvTZ^*SrRaL!wZ2yiaT+{zZZ#pint4qL(jHBxn|7tPOT%k z!nd2sO|aoej5^>v1$U^>H)mHnFyVD=^HGpQTX|S7tRL�o;m0Uix~UG|In8w8c84 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 iAoGESF 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 literal 0 HcmV?d00001 diff --git a/specs/317-legacy-tenant-environment-context-cleanup/checklists/requirements.md b/specs/317-legacy-tenant-environment-context-cleanup/checklists/requirements.md new file mode 100644 index 00000000..070d1574 --- /dev/null +++ b/specs/317-legacy-tenant-environment-context-cleanup/checklists/requirements.md @@ -0,0 +1,65 @@ +# Specification Quality Checklist: Legacy Tenant / Environment Context Cleanup + +**Purpose**: Validate preparation completeness and quality before implementation +**Created**: 2026-05-16 +**Feature**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/317-legacy-tenant-environment-context-cleanup/spec.md) + +## Candidate Selection + +- [x] CHK001 The selected candidate was directly supplied by the user as Spec 317. +- [x] CHK002 The selected candidate is not an existing completed spec package. +- [x] CHK003 Related Specs 313, 314, 315, and 316 are treated as completed historical context and not rewritten. +- [x] CHK004 The candidate aligns with explicit follow-up notes in Specs 314, 315, and 316. +- [x] CHK005 The scope is narrowed to legacy Tenant platform-context cleanup and provider-boundary Tenant preservation. +- [x] CHK006 Durable browser no-drift infrastructure is deferred to Spec 318. + +## Content Quality + +- [x] CHK007 `spec.md` exists. +- [x] CHK008 `plan.md` exists. +- [x] CHK009 `tasks.md` exists. +- [x] CHK010 Spec Candidate Check is completed. +- [x] CHK011 Functional requirements are testable and unambiguous. +- [x] CHK012 Acceptance criteria include inventory, allowlist, query cleanup, helper/class cleanup, route guard, UI copy cleanup, docs cleanup, regression tests, and browser smoke. +- [x] CHK013 Provider-boundary Tenant terminology is explicitly preserved. +- [x] CHK014 Hard cutover/no-compatibility posture is explicit. + +## Repository Alignment + +- [x] CHK015 Laravel, Filament, Livewire, Pest, and PostgreSQL context is recorded. +- [x] CHK016 Provider registration location remains `apps/platform/bootstrap/providers.php`. +- [x] CHK017 Filament v5 / Livewire v4 compliance is recorded. +- [x] CHK018 Global search behavior is not intentionally changed and has a verification task if resources are touched. +- [x] CHK019 Destructive actions are not added or changed and have a verification task if touched. +- [x] CHK020 Asset strategy is unchanged and no new `filament:assets` requirement is planned. +- [x] CHK021 Likely affected repo surfaces are named in the plan. +- [x] CHK022 No migration, seeder, package, env var, queue, scheduler, or storage change is planned by default. + +## Artifact Requirements + +- [x] CHK023 `legacy-inventory.md` is required as an implementation artifact and assigned to tasks. +- [x] CHK024 `tenant-usage-allowlist.md` is required as an implementation artifact and assigned to tasks. +- [x] CHK025 Classification values for inventory entries are specified. +- [x] CHK026 Browser screenshot artifact path is specified. +- [x] CHK027 Historical completed specs are protected from preparation rewrite. + +## Test and Validation Readiness + +- [x] CHK028 Static legacy guard is required. +- [x] CHK029 Workspace hub legacy query alias guard is required. +- [x] CHK030 Filament tenant / remembered Tenant fallback guard is required. +- [x] CHK031 Route guard for `/admin/t` and TenantPanelProvider is required. +- [x] CHK032 UI copy guard/browser verification is required. +- [x] CHK033 Spec 314, 315, and 316 regressions are required. +- [x] CHK034 `git diff --check` and formatting/static checks are required. + +## Open Questions + +- [x] CHK035 No blocking preparation questions remain. +- [x] CHK036 Ambiguous implementation occurrences are handled through `needs_product_decision` inventory classification instead of guessing. + +## Review Outcome + +- [x] CHK037 Candidate Selection Gate passes. +- [x] CHK038 Spec Readiness Gate passes for preparation. +- [x] CHK039 No application implementation was performed during preparation. diff --git a/specs/317-legacy-tenant-environment-context-cleanup/legacy-inventory.md b/specs/317-legacy-tenant-environment-context-cleanup/legacy-inventory.md new file mode 100644 index 00000000..7970bc96 --- /dev/null +++ b/specs/317-legacy-tenant-environment-context-cleanup/legacy-inventory.md @@ -0,0 +1,35 @@ +# Spec 317 Legacy Tenant Usage Inventory + +Status: implementation inventory +Updated: 2026-05-16 + +This inventory classifies high-risk legacy Tenant platform-context occurrences before cleanup. It is intentionally focused on current runtime, tests, and current product-truth docs. Provider-boundary Tenant terminology is preserved by `tenant-usage-allowlist.md`. + +| File | Symbol / Method / Route / Label | Current Meaning | Correct Meaning | Classification | Action | Risk | +|---|---|---|---|---|---|---| +| `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` | `tenantPrefilterUrl()` | URL helper for an explicit Managed Environment filter on a workspace hub | Environment filter URL | `rename_to_environment` | Rename to `environmentFilterUrl()` and update callers/tests with no alias | High: old helper name advertises a valid Tenant filter seam | +| `apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php` | `CanonicalAdminTenantFilterState` | Shared sync/reset helper for environment-like Filament table filters | Canonical admin Environment filter state | `rename_to_environment` | Rename to `CanonicalAdminEnvironmentFilterState` and update callers/tests | High: class name suggests Tenant context can seed workspace hub filter state | +| `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php` | `WorkspaceScopedTenantRoutes` | Resource route helper for workspace/environment route generation | Workspace-scoped Environment routes | `rename_to_environment` | Rename to `WorkspaceScopedEnvironmentRoutes` and update resource usage | High: trait name encodes old Tenant route mental model | +| `apps/platform/app/Support/Tenants/TenantPageCategory.php` | `TenantPageCategory` and cases `TenantBound`, `TenantScopedEvidence` | Admin surface classification for workspace, environment, and record-viewer context | Admin surface scope; environment-bound categories | `rename_to_environment` | Rename enum to `AdminSurfaceScope` and environment-bound cases | High: shell scope classification is platform-core, not provider Tenant identity | +| `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php` | `EnsureFilamentTenantSelected` / `ensure-filament-tenant-selected` | Middleware that resolves admin shell/navigation environment context | Ensure environment context selected | `rename_to_environment` | Rename class and middleware alias to `EnsureEnvironmentContextSelected` / `ensure-environment-context-selected` | High: middleware name is an active bootstrapping seam | +| `apps/platform/app/Support/Workspaces/WorkspaceContext.php` | `LAST_TENANT_IDS_SESSION_KEY`, `lastTenantId()`, `rememberTenantContext()`, `rememberedTenant()` | Per-workspace remembered Managed Environment switcher memory | Last Environment memory, switcher convenience only | `rename_to_environment` | Rename API and session key to Environment wording and update call sites/tests | High: remembered state must not be confused with workspace hub filters | +| `apps/platform/app/Support/OperateHub/OperateHubShell.php` | `Filament::getTenant()` usage | Filament tenant can represent an environment on environment-owned routes | Valid only for environment-bound pages, invalid for workspace hub scope | `allowed_provider_boundary` | Keep only where scope class permits environment-bound context; add static guard for workspace hub paths | Medium: valid Filament tenancy API but unsafe if used on workspace hubs | +| `apps/platform/app/Support/OperateHub/OperateHubShell.php` | Query hints `tenant`, `managed_environment_id` | Legacy query hints for non-hub tenant-owned resources | Not valid Workspace hub Environment filters | `remove` | Existing workspace hub short-circuit remains; add Spec 317 guard coverage | Medium: active code already blocks hubs, but regression guard is required | +| `apps/platform/routes/web.php` | `/admin/t` | Legacy Tenant panel route family | No active route | `remove` | Keep absent and add/extend route guard | High: route family must not return | +| `apps/platform/bootstrap/app.php` and panel/routes | `ensure-filament-tenant-selected` | Middleware alias for environment selection | Environment context alias | `rename_to_environment` | Replace alias usage with `ensure-environment-context-selected` | Medium: active runtime alias name misleads future work | +| `apps/platform/resources/views/filament/partials/context-bar.blade.php` | `$lastTenantId` | Last selected Managed Environment memory | Last Environment ID | `rename_to_environment` | Rename local variable and API call | Low: visible copy is already Environment-oriented but variable name is wrong | +| `apps/platform/app/Filament/Resources/ProviderConnectionResource.php` | `lastTenantId()` fallback in provider policy/resource context | Remembered environment convenience for provider-adjacent behavior | Last Environment ID | `rename_to_environment` | Update API name; do not convert provider tenant IDs into environment filters | Medium: provider-adjacent surface must preserve boundary clarity | +| `apps/platform/app/Support/EnvironmentDashboard/EnvironmentDashboardSummaryBuilder.php` | `tenantPrefilterUrl()` caller | Environment Dashboard CTA into Customer Review Workspace | `environment_id` filter URL | `rename_to_environment` | Update to `environmentFilterUrl()` | High: generated CTA must be canonical | +| `apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php` | `tenantPrefilterUrl()` callers | Governance links into Customer Review Workspace | `environment_id` filter URL | `rename_to_environment` | Update to `environmentFilterUrl()` | Medium: link helper must not preserve old API | +| `apps/platform/app/Filament/Widgets/ManagedEnvironment/ManagedEnvironmentReviewPackCard.php` | `tenantPrefilterUrl()` callers | Environment-owned review pack link | `environment_id` filter URL | `rename_to_environment` | Update to `environmentFilterUrl()` | Medium: environment-owned CTA link | +| `apps/platform/app/Filament/Resources/EnvironmentReviewResource.php` | `tenantScopedUrl()` | Canonical URL helper for Managed Environment-owned review pages | Environment-scoped review URL | `rename_to_environment` | Rename to `environmentScopedUrl()` and remove the ignored legacy panel hint parameter | Medium: helper name and ignored panel argument preserved old route mental model | +| `apps/platform/app/Filament/Resources/ProviderConnectionResource.php` | `lastEnvironmentId()` create/context fallback and `parameters['tenant']` URL alias | Hidden remembered Environment state and legacy parameter alias for Provider Connection context | Explicit `environment_id` filter only | `remove` | Remove remembered-state create fallback and remove array `tenant` URL alias support | High: credential-adjacent Workspace hub must not derive scope from hidden switcher memory | +| `apps/platform/app/Filament/Resources/*Resource.php` | `tenant` relationship and `managed_environment_id` DB columns | Tenant-owned model relationships and database columns | Current schema truth | `out_of_scope` | Do not perform cosmetic DB/relationship renames in Spec 317 | High if renamed broadly: migration and model churn outside scope | +| `apps/platform/app/Support/Tenants/*` except surface-scope classifier | Tenant operability services, questions, lanes | Tenant/Managed Environment operability domain vocabulary | Existing domain vocabulary | `out_of_scope` | Only update type references required by renamed surface-scope enum | Medium: broader terminology cleanup requires separate product/domain decision | +| `docs/product/spec-candidates.md` | Current queue references to Tenant platform context, `tenantPrefilterUrl`, `/admin/t` | Current product-truth docs and old follow-up notes | Workspace-first / Environment-second platform context | `rename_to_environment` | Update current product-truth notes for Spec 317 | Medium: stale docs can reintroduce drift | +| Completed specs under `specs/313-*` to `specs/316-*` | Historical Tenant references | Historical implementation evidence | Historical context | `allowed_historical_archived_doc` | Do not rewrite completed specs | Low: history remains valid as evidence | +| `docs/audits/*`, old specs before current truth | Historical audit wording | Historical provider/platform notes | Historical context | `allowed_historical_archived_doc` | Do not rewrite unless referenced as current truth | Low | + +## Quarantine Decisions + +No active runtime class/helper is intentionally quarantined at the start of implementation. If a later rename proves too broad for this spec, add a row with `needs_product_decision` or `out_of_scope` before skipping it. diff --git a/specs/317-legacy-tenant-environment-context-cleanup/plan.md b/specs/317-legacy-tenant-environment-context-cleanup/plan.md new file mode 100644 index 00000000..88ef766a --- /dev/null +++ b/specs/317-legacy-tenant-environment-context-cleanup/plan.md @@ -0,0 +1,414 @@ +# Implementation Plan: Legacy Tenant / Environment Context Cleanup + +**Branch**: `317-legacy-tenant-environment-context-cleanup` | **Date**: 2026-05-16 | **Spec**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/317-legacy-tenant-environment-context-cleanup/spec.md) +**Input**: Feature specification from `/specs/317-legacy-tenant-environment-context-cleanup/spec.md` + +**Preparation status**: Specification artifacts only. No runtime implementation has been performed by this preparation step. + +## Summary + +Spec 317 performs the hard-cut cleanup that follows Specs 314-316: + +```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 +317: remove old Tenant platform-context names, aliases, fallbacks, routes, copy, and docs drift +``` + +The implementation must remove or quarantine legacy Tenant platform-context residue while preserving provider-boundary Tenant terminology for Microsoft/Entra/provider identity. Workspace remains the primary platform context, Environment remains the explicit secondary filter/context, and Provider Tenant remains external identity only. + +## 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; default expectation is no schema changes +**Testing**: Pest 4.3.1 / PHPUnit 12.5.4; focused browser smoke where applicable +**Validation Lanes**: fast-feedback for static/feature guards, confidence for Filament/Livewire behavior, browser for active UI/link verification +**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**: Static guards should scan bounded paths and avoid broad expensive runtime setup. Runtime cleanup should not add query overhead. +**Constraints**: Hard cutover. No compatibility aliases, legacy redirects, dual-param readers, migration shims, new packages, env vars, queues, scheduler, storage, or broad cosmetic DB renames. +**Scale/Scope**: Cross-cutting cleanup across admin navigation/context code, Workspace hubs, Environment pages, provider-boundary copy, tests, current docs/spec candidates, and route/helper semantics. + +## UI / Surface Guardrail Plan + +- **Guardrail scope**: Existing operator-facing Workspace/Environment surfaces and visible scope terminology. +- **Native vs custom classification summary**: Existing Filament/Livewire/Blade surfaces only. No redesign and no new styling system. +- **Shared-family relevance**: Navigation, context/scope signals, filter chips, URL helpers, route helpers, provider identity labels, and test/static guard outputs. +- **State layers in scope**: URL query, route generation, helper names, page properties, table filter state only where it still carries legacy Tenant-as-Environment state, session/remembered context where it can affect Workspace hubs, visible copy, docs current truth. +- **Audience modes in scope**: Operator-MSP and support-platform. Customer-read-only applies to existing Customer Review Workspace surfaces. +- **Decision/diagnostic/raw hierarchy plan**: Scope truth stays default-visible; provider raw IDs remain diagnostics/detail where already designed. +- **Raw/support gating plan**: No new raw/support data exposure. +- **One-primary-action / duplicate-truth control**: Do not create additional context signals. Replace old Tenant wording with the existing Workspace/Environment signal path. +- **Handling modes by drift class or surface**: Hard-stop for legacy query alias support and `/admin/t` routes; review-mandatory for quarantined names; report-only for historical archived specs. +- **Repository-signal treatment**: Static guards and inventory entries are required evidence. +- **Special surface test profiles**: global-context-shell, standard-native-filament, provider-boundary-copy. +- **Required tests or manual smoke**: Static legacy guard, Workspace hub legacy query guard, no Filament tenant fallback guard, route guard, UI copy guard, helper/API rename tests, Spec 314-316 regression tests, and focused browser smoke. +- **Exception path and spread control**: Quarantine is allowed only with inventory reason, risk, owner, and follow-up. Deprecated aliases are not allowed. +- **Active feature PR close-out entry**: Guardrail and Smoke Coverage. + +## Shared Pattern & System Fit + +- **Cross-cutting feature marker**: yes. +- **Systems touched**: `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `WorkspaceSidebarNavigation`, `CanonicalNavigationContext`, `ManagedEnvironmentLinks`, `OperationRunLinks`, `WorkspaceContext`, `OperateHubShell`, `CanonicalAdminTenantFilterState`, `TenantPageCategory`, `WorkspaceScopedTenantRoutes`, `EnsureFilamentTenantSelected`, active Filament pages/resources/views, admin routes, current docs/spec candidates, and guard/browser tests. +- **Shared abstractions reused**: Existing Workspace hub registry/filter/reset contract from Specs 314-316 and existing Filament/Livewire test patterns. +- **New abstraction introduced? why?**: None planned. If renaming an existing helper/class creates a new class name, it must replace the old name and keep the same bounded responsibility rather than add a parallel abstraction. +- **Why the existing abstraction was sufficient or insufficient**: Runtime helpers now encode the right behavior but some names still encode the wrong legacy concept. The fix is replacement/rename/removal, not another compatibility layer. +- **Bounded deviation / spread control**: Provider-boundary Tenant terminology remains allowed through an explicit allowlist. Historical specs remain untouched unless they are current truth. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: no. +- **Central contract reused**: Existing `OperationRunLinks` only if cleanup touches old route/query helper names. +- **Delegated UX behaviors**: Existing tenant/workspace-safe URL resolution remains. +- **Surface-owned behavior kept local**: Existing run start/completion/status/notification 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**: Microsoft/Entra provider tenant IDs, external provider tenant IDs, OAuth tenant authority segments, Graph payload `tenantId`, provider connection identity metadata, and provider-specific copy. +- **Platform-core seams**: Workspace hub query contract, Environment filter contract, Workspace shell/context, route/helper naming, UI copy, docs current truth, test/static guards. +- **Neutral platform terms / contracts preserved**: `Workspace`, `Managed Environment`, `Environment`, `environment_id`, `Workspace hub`, `Environment filter`, `Provider Connection`, `Provider Scope`. +- **Retained provider-specific semantics and why**: Tenant terminology is retained only where it means external provider identity and is necessary for Microsoft/Entra/Graph accuracy. +- **Bounded extraction or follow-up path**: Spec 318 handles durable browser regression coverage. Any broad schema rename or unresolved product terminology decision becomes a follow-up instead of expanding Spec 317. + +## Constitution Check + +*GATE: Must pass before implementation. Re-check after runtime changes.* + +- Inventory-first: no inventory/snapshot truth semantics change. +- Read/write separation: no Graph writes or destructive operations are added. +- Graph contract path: no Graph calls are introduced. +- Deterministic capabilities: existing capability checks remain; no new capability model. +- RBAC-UX: workspace/environment authorization remains authoritative. Non-member access remains 404/safe no-access; missing capability remains existing 403 behavior. +- Workspace isolation: Workspace remains primary context; cleanup must not infer Workspace/Environment from legacy Tenant state. +- Tenant isolation: provider-boundary Tenant identity must not become platform data scope. Existing environment-scoped records remain access-checked. +- Run observability: no OperationRun lifecycle behavior changes. +- Test governance (TEST-GOV-001): lane, fixture cost, static/browserscope, and reviewer handoff are explicit in spec/plan/tasks. +- Proportionality (PROP-001): cleanup artifacts and static guards are justified by current cross-surface drift risk. +- No premature abstraction (ABSTR-001): no generic context framework; replacement over layering. +- Persisted truth (PERSIST-001): no new persisted entity/table. Spec-local inventory/allowlist are implementation evidence, not product persistence. +- Behavioral state (STATE-001): no new status/state/reason family. +- UI semantics (UI-SEM-001): direct vocabulary cleanup only; no new semantic UI framework. +- Shared pattern first (XCUT-001): extend/rename existing Workspace hub helpers; do not create parallel Tenant/Environment helpers. +- Provider boundary (PROV-001): provider-specific Tenant semantics stay provider-owned and do not leak into platform-core contracts. +- V1 explicitness / few layers: direct hard cutover and guard tests. +- Spec discipline / bloat check: 317 absorbs the cleanup work that Specs 314-316 intentionally deferred; durable browser no-drift remains Spec 318. +- Filament-native UI (UI-FIL-001): no ad-hoc styling; visible copy changes remain on existing surfaces. +- Filament v5 / Livewire v4: Livewire v4.1.4 compliance is required; no Livewire v3 references. +- Provider registration: Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`; `AdminPanelProvider` and `SystemPanelProvider` remain the active providers. +- Global search: no global search behavior is intentionally changed. If any Resource is touched, verify its Edit/View page or disabled global search state still satisfies Filament v5 rules. +- Destructive actions: no destructive actions are added or changed. If a touched surface already has destructive actions, verify `->action(...)`, `->requiresConfirmation()`, authorization, and audit behavior remain intact. +- Asset strategy: no new Filament assets are planned; `filament:assets` deployment requirements do not change. + +## Test Governance Check + +- **Test purpose / classification by changed surface**: Static/Unit for source guards and allowlist matching; Feature/Livewire for Workspace hub query/route/helper behavior; Browser for visible copy and generated link/query verification. +- **Affected validation lanes**: fast-feedback, confidence, browser. +- **Why this lane mix is the narrowest sufficient proof**: Structural drift is best caught statically; query/route behavior requires feature tests; visible copy/link truth requires browser or rendered-view verification. +- **Narrowest proving command(s)**: + - `cd apps/platform && ./vendor/bin/sail artisan test --filter=LegacyTenantPlatformContext` + - `cd apps/platform && ./vendor/bin/sail artisan test --filter=WorkspaceHub` + - `cd apps/platform && ./vendor/bin/sail artisan test --filter=TenantPanel` + - `cd apps/platform && ./vendor/bin/sail artisan test --filter=EnvironmentCopy` + - focused browser smoke for Spec 317 surfaces + - `cd apps/platform && ./vendor/bin/sail pint --test` or scoped Pint check for touched PHP files + - `git diff --check` +- **Fixture / helper / factory / seed / context cost risks**: Static guards should avoid DB setup. Feature/browser tests may reuse existing Workspace, ManagedEnvironment, membership/capability, provider connection, evidence, review, finding, and operation helpers. Do not widen shared setup defaults. +- **Expensive defaults or shared helper growth introduced?**: no. +- **Heavy-family additions, promotions, or visibility changes**: Focused browser smoke only; durable browser no-drift family is Spec 318. +- **Surface-class relief / special coverage rule**: Existing native Filament pages/resources receive ordinary behavior tests plus focused UI copy/link smoke. +- **Closing validation and reviewer handoff**: Review `legacy-inventory.md`, `tenant-usage-allowlist.md`, renamed helper/class list, removed query handlers, route output, browser evidence, and Spec 314-316 regression results. +- **Budget / baseline / trend follow-up**: none expected if static scans stay bounded. +- **Review-stop questions**: Does any Workspace hub still parse `tenant` as Environment? Does any helper expose Tenant-named Environment behavior? Does any active UI say Tenant for Managed Environment? Does any provider-boundary allowlist entry hide platform context? Are completed specs left intact? +- **Escalation path**: follow-up-spec for durable browser infrastructure (318), broad schema rename, or unresolved `needs_product_decision` inventory items. +- **Active feature PR close-out entry**: Guardrail and Smoke Coverage. +- **Why no dedicated follow-up spec is needed**: 317 is the dedicated cleanup spec. Only durable browser regression/no-drift automation remains separate. + +## Project Structure + +### Documentation (this feature) + +```text +specs/317-legacy-tenant-environment-context-cleanup/ +|-- spec.md +|-- plan.md +|-- tasks.md +|-- legacy-inventory.md # created/populated during implementation +|-- tenant-usage-allowlist.md # created/populated during implementation +|-- 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/Navigation/WorkspaceHubFilterStateResetter.php +apps/platform/app/Support/Navigation/WorkspaceSidebarNavigation.php +apps/platform/app/Support/Navigation/CanonicalNavigationContext.php +apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php +apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php +apps/platform/app/Support/Tenants/TenantPageCategory.php +apps/platform/app/Support/Workspaces/WorkspaceContext.php +apps/platform/app/Support/OperateHub/OperateHubShell.php +apps/platform/app/Support/ManagedEnvironmentLinks.php +apps/platform/app/Support/Operations/OperationRunLinks.php +apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php +apps/platform/app/Providers/Filament/AdminPanelProvider.php +apps/platform/routes/web.php +apps/platform/resources/views/ +apps/platform/tests/Feature/ +apps/platform/tests/Browser/ +``` + +Critical Workspace/Environment surfaces: + +```text +apps/platform/app/Filament/Pages/EnvironmentDashboard.php +apps/platform/app/Filament/Pages/Monitoring/Operations.php +apps/platform/app/Filament/Pages/Governance/GovernanceInbox.php +apps/platform/app/Filament/Pages/Governance/DecisionRegister.php +apps/platform/app/Filament/Pages/Monitoring/FindingExceptionsQueue.php +apps/platform/app/Filament/Resources/ProviderConnectionResource.php +apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php +apps/platform/app/Filament/Pages/Reviews/ReviewRegister.php +apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php +apps/platform/app/Filament/Pages/EnvironmentRequiredPermissions.php +apps/platform/app/Filament/Pages/EnvironmentDiagnostics.php +``` + +Docs/current product truth candidates: + +```text +docs/product/spec-candidates.md +docs/product/roadmap.md +docs/product/principles.md +docs/product/standards/ +docs/ui/ +docs/architecture-guidelines.md +docs/filament-guidelines.md +docs/testing-guidelines.md +specs/313-workspace-environment-context-browser-verification/ +specs/314-workspace-hub-navigation-context-contract/ +specs/315-environment-cta-explicit-filter-contract/ +specs/316-workspace-hub-clear-filter-contract/ +``` + +**Structure Decision**: Laravel/Filament platform app under `apps/platform`. Renames stay inside existing support/page/resource/test/doc locations. No new base application folder is planned. + +## Complexity Tracking + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|---|---|---| +| Static guard with allowlist | Prevents legacy platform-context Tenant terminology from returning while preserving provider-boundary Tenant usage | Naive grep would block valid provider terms; no guard would let old aliases return | +| Spec-local inventory and allowlist artifacts | Cleanup spans many files and requires reviewable classification before broad edits | Direct rename without classification risks breaking provider-boundary Microsoft/Entra Tenant concepts | +| Class/helper renames | Old names encode the wrong platform concept and mislead future work | Keeping aliases would preserve the legacy mental model and violate hard cutover | + +## Phase 0: Discovery Completed During Preparation + +Relevant repository facts discovered before authoring this plan: + +- Current branch before creation was `platform-dev`, clean, at `9b097f97 Spec 316: implement workspace hub clear filter contract (#371)`. +- `specs/317-legacy-tenant-environment-context-cleanup` did not exist before this preparation. +- No local or remote branch matching `317-*` was present before creation. +- Specs 313-316 contain completion/checklist signals and are historical baseline context. +- Specs 314-316 explicitly identify Spec 317 as the cleanup follow-up for old Tenant aliases, stale Tenant naming, remembered context drift, and `Filament::getTenant()` Workspace hub usage. +- `docs/product/roadmap.md` has an unrelated numbering note that recommends "Spec 317" for External Support Desk / PSA Handoff; the user-supplied Spec 317 and Specs 314-316 follow-up chain supersede that numbering note for this branch. +- Initial discovery found active references to `TenantPageCategory`, `WorkspaceScopedTenantRoutes`, `CanonicalAdminTenantFilterState`, `tenantPrefilterUrl`, `lastTenantId`, `Filament::getTenant()`, `/admin/t`, `tenant_scope`, `managed_environment_id`, and Tenant-named tests in the expected app/test/doc areas. +- Laravel Boost application info confirms PHP 8.4.15, Laravel 12.52.0, Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, and PostgreSQL. + +## Technical Approach + +### 1. Build inventory and allowlist first + +Create: + +```text +specs/317-legacy-tenant-environment-context-cleanup/legacy-inventory.md +specs/317-legacy-tenant-environment-context-cleanup/tenant-usage-allowlist.md +``` + +Inventory schema: + +```text +File +Symbol / method / route / label +Current meaning +Correct meaning +Classification +Action +Risk +``` + +Classification values: + +```text +remove +rename_to_environment +rename_to_provider_tenant +allowed_provider_boundary +allowed_historical_archived_doc +dead_code_candidate +needs_product_decision +out_of_scope +``` + +Initial required search: + +```bash +cd apps/platform +rg "\btenant\b|\bTenant\b|\btenant_id\b|\btenant_scope\b|managed_environment_id|Filament::getTenant|getTenant\(|lastTenantId|TenantPageCategory|WorkspaceScopedTenantRoutes|CanonicalAdminTenantFilterState|tenantPrefilterUrl|TenantPanelProvider|/admin/t" app resources routes tests docs specs -n +``` + +### 2. Remove legacy Workspace hub query alias handling + +Workspace hubs must use `WorkspaceHubEnvironmentFilter` and accept only: + +```text +environment_id +``` + +Invalid as Environment filter state: + +```text +tenant +tenant_id +managed_environment_id +tenant_scope +environment +tableFilters +remembered Tenant +remembered Environment +Filament::getTenant() +lastTenantId +provider external tenant id +``` + +### 3. Rename or remove helper/class names + +Use actual responsibility, not mechanical rename. Expected candidates: + +```text +tenantPrefilterUrl() -> environmentFilterUrl() +TenantPageCategory -> EnvironmentPageCategory / AdminSurfaceScope / quarantine with reason +WorkspaceScopedTenantRoutes -> WorkspaceScopedEnvironmentRoutes / EnvironmentScopedAdminRoutes / quarantine with reason +CanonicalAdminTenantFilterState -> CanonicalAdminEnvironmentFilterState / WorkspaceHubTableFilterState / quarantine with reason +EnsureFilamentTenantSelected -> EnsureEnvironmentContextSelected / ResolveAdminContext / quarantine with reason +``` + +Rules: + +- no deprecated aliases +- no wrapper methods preserving old names +- no compatibility adapters +- tests updated to new names/contracts + +### 4. Remove Workspace hub `Filament::getTenant()` fallback paths + +Allowed: + +- truly Environment-scoped pages where Filament tenant represents the selected Managed Environment + +Disallowed: + +- Workspace hub data scope +- default Environment for Workspace hubs +- authorization fallback for Workspace hubs +- shell context for Workspace hubs +- URL parameter source + +### 5. Clean active UI copy and generated links + +Replace platform-context: + +```text +Tenant +Managed tenant +Current tenant +Selected tenant +Tenant context +Tenant scope +Tenant filter +``` + +with: + +```text +Environment +Managed environment +Current environment +Selected environment +Environment context +Environment scope +Environment filter +``` + +Provider-boundary copy can keep: + +```text +Microsoft tenant +Entra tenant +Provider tenant ID +``` + +### 6. Update current docs/spec truth only + +Update current docs/spec candidates where they still describe Tenant as TenantPilot platform context. Do not rewrite completed specs as implementation history. If a historical spec creates confusion in current docs, add a current-truth note in an active doc or the Spec 317 artifacts. + +## Implementation Phases + +1. Guardrails and inventory. +2. Tests first: static legacy guard, query alias guard, fallback guard, route guard, UI copy/helper guards. +3. Helper/class/query cleanup. +4. UI copy/current docs cleanup. +5. Spec 314-316 regression verification. +6. Focused browser verification. +7. Final report and no-compatibility confirmation. + +## Rollout / Deployment + +- **Environment variables**: none expected. +- **Migrations**: default none. Any discovered schema rename must be explicitly justified, tested, and documented before implementation. +- **Queues / scheduler**: no changes expected. +- **Storage / volumes**: no changes expected. +- **Filament assets**: no new assets expected; no new `filament:assets` deployment requirement unless implementation unexpectedly registers assets. +- **Staging/Production**: pre-production hard cutover. No legacy URL or data compatibility is required. + +## Risks and Controls + +- **Risk**: Naive Tenant cleanup breaks provider-boundary Microsoft/Entra identity. + - **Control**: inventory and allowlist before broad edits. +- **Risk**: Renaming a helper creates a hidden deprecated alias. + - **Control**: static helper/API guard and no-alias review. +- **Risk**: Workspace hub filtering regresses from Specs 314-316. + - **Control**: run existing 314/315/316 regression tests after cleanup. +- **Risk**: Static guard becomes noisy and blocks valid historical/provider references. + - **Control**: explicit allowlist categories and bounded scan paths. +- **Risk**: Route churn exceeds this cleanup. + - **Control**: prefer hard rename in pre-production; quarantine only with documented reason and follow-up. + +## Acceptance Review Checklist + +- `legacy-inventory.md` and `tenant-usage-allowlist.md` are complete enough for review. +- Workspace hubs use only `environment_id` for explicit Environment filtering. +- No legacy query aliases are accepted as Environment filters. +- No Workspace hub uses Filament tenant or remembered Tenant state as scope. +- No active `/admin/t` route or TenantPanelProvider registration remains. +- Active UI copy uses Environment where it means Managed Environment. +- Provider-boundary Tenant usage remains valid and documented. +- Current docs/spec candidates reflect Workspace-first / Environment-second terminology. +- Static, route, helper, UI copy, and regression tests pass. +- Browser smoke passes for required surfaces. +- No backwards compatibility layer was introduced. diff --git a/specs/317-legacy-tenant-environment-context-cleanup/spec.md b/specs/317-legacy-tenant-environment-context-cleanup/spec.md new file mode 100644 index 00000000..c0ee1465 --- /dev/null +++ b/specs/317-legacy-tenant-environment-context-cleanup/spec.md @@ -0,0 +1,313 @@ +# Feature Specification: Legacy Tenant / Environment Context Cleanup + +**Feature Branch**: `317-legacy-tenant-environment-context-cleanup` +**Created**: 2026-05-16 +**Status**: Draft +**Input**: User supplied Spec 317 draft for hard-cut cleanup of legacy Tenant platform-context names, aliases, helpers, routes, UI copy, tests, and current docs after Specs 314-316. + +## Spec Candidate Check *(mandatory - SPEC-GATE-001)* + +- **Problem**: Specs 314-316 stabilized Workspace hub entry, explicit `environment_id` filtering, and clear-filter reset behavior, but old Tenant-era platform context names and fallback paths still exist in code, tests, routes, helper APIs, UI copy, and docs. +- **Today's failure**: Future work can accidentally treat `tenant`, `tenant_id`, `tenant_scope`, `managed_environment_id`, remembered Tenant state, or `Filament::getTenant()` as valid Workspace hub Environment context, reintroducing hidden scope mismatch after the runtime lifecycle was fixed. +- **User-visible improvement**: Operators see Workspace and Environment terminology where the product means Managed Environment, Workspace hubs only accept `environment_id` as an explicit filter, and provider-boundary Tenant language remains clearly limited to Microsoft/Entra/provider identity. +- **Smallest enterprise-capable version**: Inventory and classify remaining Tenant occurrences, create an allowlist for provider-boundary usage, remove or rename active platform-context leftovers, add static/runtime/browser guards, and update only current product-truth docs/spec artifacts. +- **Explicit non-goals**: No UI redesign, no new product feature, no new Environment CTA coverage beyond Specs 315/316, no broad cosmetic DB rename, no compatibility redirects, no dual-param support, no migrations unless repo analysis proves a schema name is active wrong platform-context truth, and no rewriting completed historical specs. +- **Permanent complexity imported**: Two spec-local cleanup artifacts, one or more guard tests, renamed helpers/classes where repo truth requires it, and browser smoke evidence. No new persisted entity, enum/status family, queue, provider framework, Graph contract, or runtime compatibility layer is introduced. +- **Why now**: Specs 314-316 intentionally deferred broad naming/alias cleanup to Spec 317. The runtime contract is now stable enough to remove the old mental model without mixing cleanup with filter lifecycle fixes. +- **Why not local**: The legacy risk crosses navigation, Filament support helpers, Workspace hub pages, provider links, tests, route assumptions, session helpers, UI copy, and docs. A page-local fix would leave future drift paths open. +- **Approval class**: Cleanup. +- **Red flags triggered**: Many surfaces touched and static guard coverage. Defense: this removes existing dangerous compatibility/naming residue instead of adding a framework; provider-boundary Tenant terminology is explicitly preserved through an allowlist. +- **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**: Workspace hub routes and Environment-owned routes under the admin panel, including Workspace Overview, Environment Dashboard, Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Reviews, Customer Reviews, Provider Readiness, and Required Permissions. +- **Data Ownership**: No new data ownership model. Workspace remains the primary platform context. Managed Environment remains the secondary operational context. Provider Tenant remains provider-boundary identity only. Existing `managed_environment_id` DB columns remain unless implementation proves a schema name is active wrong platform-context truth. +- **RBAC**: Existing workspace membership, environment entitlement, page/resource policies, capabilities, and deny-as-not-found behavior remain authoritative. Cleanup must not weaken RBAC or use UI visibility as authorization. + +For canonical-view specs: + +- **Default filter behavior when tenant-context is active**: Workspace hubs must remain workspace-wide unless a valid canonical `environment_id` query parameter is present. Legacy Tenant query params, Filament tenant state, remembered Tenant/Environment state, and table filters must not create Workspace hub Environment context. +- **Explicit entitlement checks preventing cross-tenant leakage**: Any valid `environment_id` filter must resolve a `ManagedEnvironment` inside the selected Workspace for the current actor. Provider external tenant IDs, Entra tenant IDs, slugs, remembered state, `Filament::getTenant()`, and old `tenant` aliases are not valid filter sources. + +## Cross-Cutting / Shared Pattern Reuse *(mandatory)* + +- **Cross-cutting feature?**: yes. +- **Interaction class(es)**: navigation context, URL/query contracts, Environment filter chips, helper APIs, route helpers, UI copy, session/table state guardrails, static guard tests, docs/product terminology. +- **Systems touched**: `WorkspaceHubRegistry`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `WorkspaceSidebarNavigation`, `ManagedEnvironmentLinks`, `OperationRunLinks`, `WorkspaceContext`, `OperateHubShell`, `CanonicalAdminTenantFilterState`, `TenantPageCategory`, `WorkspaceScopedTenantRoutes`, Filament pages/resources/clusters, active Blade views, admin routes, tests, and current docs/spec truth. +- **Existing pattern(s) to extend**: Specs 314-316 Workspace hub registry/filter/reset contracts, existing Filament/Livewire page tests, existing browser smoke tests for Workspace hub context, existing route guards, and existing guard-test conventions under `apps/platform/tests/Feature/Guards`. +- **Shared contract / presenter / builder / renderer to reuse**: Reuse current Workspace hub registry/filter/reset helpers where they remain correctly named and scoped. Rename or replace Tenant-named helpers/classes only when they model Environment or Workspace hub platform context. Do not keep deprecated aliases. +- **Why the existing shared path is sufficient or insufficient**: The runtime path is now sufficient for canonical behavior, but old names and alias entry points remain structurally unsafe. Cleanup must remove those old seams rather than adding another resolver layer. +- **Allowed deviation and why**: Provider-boundary code may keep Tenant terminology where it clearly means Microsoft/Entra/provider identity. Historical completed specs may remain unchanged as history unless they are used as current product truth. +- **Consistency impact**: Workspace, Environment, and Provider Tenant vocabulary must align across URL keys, helper names, tests, UI labels, docs, and browser-visible state. +- **Review focus**: Verify no legacy alias support remains for Workspace hub Environment filtering, no deprecated helper aliases survive, provider-boundary Tenant terms are allowlisted, and completed historical specs are not rewritten. + +## OperationRun UX Impact *(mandatory)* + +- **Touches OperationRun start/completion/link UX?**: no start/completion behavior. Link helpers may be inspected or renamed only where old Tenant context naming leaks into Workspace hub URLs. +- **Shared OperationRun UX contract/layer reused**: Existing `OperationRunLinks` remains the relevant link helper if touched. +- **Delegated start/completion UX behaviors**: N/A. +- **Local surface-owned behavior that remains**: Existing operation start/completion/notification/progress behavior remains 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**: mixed. Platform-core Workspace/Environment context must be cleaned. Provider-owned Microsoft/Entra Tenant identity must be preserved. +- **Seams affected**: query keys, route/helper names, Filament tenant usage in Workspace hubs, provider connection context links, provider identity labels, tests, UI copy, docs/spec current truth, and static guard allowlists. +- **Neutral platform terms preserved or introduced**: `Workspace`, `Managed Environment`, `Environment`, `Provider Connection`, `Provider Scope`, `Workspace hub`, `Environment filter`, `All environments`, `environment_id`. +- **Provider-specific semantics retained and why**: `provider_tenant_id`, `external_tenant_id`, `microsoft_tenant_id`, `entra_tenant_id`, `azure_tenant_id`, `tenantId` in Microsoft payloads, OAuth authority tenant segments, and Microsoft/Entra Tenant display copy remain valid at provider boundaries. +- **Why this does not deepen provider coupling accidentally**: The allowlist limits Tenant terminology to provider identity and historical archived docs, while platform-context code uses Workspace/Environment terms. +- **Follow-up path**: Spec 318 adds durable browser regression/no-drift coverage. It must not be implemented inside Spec 317. + +## 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 | +|---|---|---|---|---|---|---| +| Workspace Overview | yes | Existing Filament/Blade surface | navigation, scope copy | visible copy, generated links | no | Must not say Tenant when it means Environment | +| Environment Dashboard | yes | Existing Filament page/widgets | Environment CTAs, provider readiness | visible copy, generated links | no | Provider-specific Tenant copy allowed only in provider detail context | +| Operations | yes | Existing Filament page/table | Workspace hub | URL/query, page state, copy, tests | no | Must use only `environment_id` for Environment filter | +| Governance Inbox | yes | Existing Filament page/list | governance queue | URL/query, page state, copy, tests | no | Remove old Tenant aliases/state language | +| Decision Register | yes | Existing Filament page/list | decision register | URL/query, page state, copy, tests | no | Clean Workspace hub remains valid | +| Finding Exceptions Queue | yes | Existing Filament page/table | exception queue | URL/query, table state, copy, tests | no | Old `tenant` state cannot be valid Environment filter | +| Provider Connections | yes | Existing Filament resource/table | provider registry | provider identity labels, Environment filter links | no | Distinguish Environment from Provider Tenant ID | +| Evidence | yes | Existing Filament page/resource | evidence viewer | copy, filters, links, tests | no | Workspace hub vocabulary must be clean | +| Reviews / Customer Reviews | yes | Existing Filament pages/resources | review workspace | copy, filters, links, tests | no | `tenantPrefilterUrl()` is disallowed | +| Provider Readiness / Required Permissions | yes | Existing Environment/provider surfaces | provider readiness | copy, labels | no | Microsoft Tenant wording allowed only when provider-owned | +| Historical specs/docs | no active UI | Spec/doc artifacts | current truth vs history | documentation | no | Do not rewrite completed history unless it states current product truth | + +## 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 | +|---|---|---|---|---|---|---|---| +| Workspace hub with optional Environment filter | Secondary Context | Decide whether listed rows are workspace-wide or intentionally narrowed to one Environment | Workspace context, optional Environment chip, clean copy, canonical URL | Existing row/detail diagnostics | Scope truth is prerequisite for every hub decision | Completes 314-316 lifecycle by removing legacy context drift | Removes need to infer whether Tenant means Environment or provider identity | +| Provider detail/readiness surfaces | Secondary Context | Understand provider identity without confusing it with platform Environment context | Environment scope plus provider-specific Tenant ID where relevant | Provider raw diagnostics where already available | Provider Tenant is evidence, not platform context | Keeps provider boundary explicit | Prevents wrong operational scope assumptions | + +## 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/Environment UI copy | operator-MSP, support-platform, customer-read-only where existing surfaces apply | Workspace and Environment terms only for platform context | Existing diagnostics | Existing support/raw sections | Inspect rows or clear/apply Environment filter | Raw provider IDs unless already shown by provider detail | Tenant appears only where provider-owned and labelled as Microsoft/Entra/provider | + +## 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 | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| Workspace hubs | List / Table / Bulk | Existing hub/list/queue/register | Inspect item or clear Environment filter | Existing inspect pattern | existing | Existing placement | Existing placement; no destructive changes | existing admin hub routes | existing | Workspace-wide or Environment chip | Existing page nouns | Whether the hub is workspace-wide or Environment-filtered | none | +| Provider readiness/details | Detail / Related Context | Provider diagnostics | Verify provider connection/readiness | Existing detail pattern | existing | Existing placement | Existing placement; no destructive changes | existing | existing | Workspace + Environment + provider identity | Provider Connection / Required Permissions | Provider Tenant terms are provider-boundary only | 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 | +|---|---|---|---|---|---|---|---|---|---|---| +| Active Workspace hub surfaces | TenantPilot operator | Trust the current data scope before inspecting or acting | Existing list/table/page | Am I looking at all Workspace data or one Environment? | Workspace label, optional Environment filter, clean Environment terminology | Existing page diagnostics | Existing page-specific dimensions only | TenantPilot navigation/filter state only | Inspect rows, clear/apply filter | None added or changed | + +## Proportionality Review *(mandatory when structural complexity is introduced)* + +- **New source of truth?**: no. +- **New persisted entity/table/artifact?**: no database/persistent domain entity. Two spec-local implementation artifacts are required: `legacy-inventory.md` and `tenant-usage-allowlist.md`. +- **New abstraction?**: no new generic framework planned. Existing Tenant-named helpers/classes may be renamed or replaced with narrower responsibility names if repo analysis confirms they model platform Environment/Workspace context. +- **New enum/state/reason family?**: no. +- **New cross-domain UI framework/taxonomy?**: no. +- **Current operator problem**: Legacy Tenant platform-context language can mislead operators and future code into treating Environment filters as hidden Tenant context. +- **Existing structure is insufficient because**: Specs 314-316 fixed runtime behavior but intentionally left old names and aliases for this cleanup. Leaving them in place preserves a known reintroduction path. +- **Narrowest correct implementation**: Inventory, allowlist, remove/rename/quarantine confirmed platform-context leftovers, add guard tests, update current docs/copy, and stop before durable browser infrastructure. +- **Ownership cost**: Static guard maintenance, explicit provider-boundary allowlist updates, review attention for broad rename diffs, and focused browser smoke. +- **Alternative intentionally rejected**: Keeping deprecated aliases or compatibility readers would violate the hard cutover policy; broad DB renames for provider-boundary fields are unnecessary churn. +- **Release truth**: Current-release cleanup before production data exists. + +### Compatibility posture + +This feature assumes a pre-production environment. + +Backward compatibility, legacy aliases, migration shims, historical fixtures, dual-read behavior, compatibility redirects, and compatibility-specific tests are out of scope. Canonical replacement is required. No old URL or query alias support should remain for platform Workspace hub Environment filtering. + +## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)* + +- **Test purpose / classification**: Feature/Unit guard tests for static legacy detection, route/link/query contracts, helper renames, and Workspace hub alias rejection; Browser smoke for active UI copy and generated links on critical surfaces. +- **Validation lane(s)**: fast-feedback for static/unit/feature guards, confidence for Filament/Livewire page/resource behavior, browser for focused active UI verification. +- **Why this classification and these lanes are sufficient**: The risk is structural drift plus visible UI terminology. Static/feature tests prevent reintroduction; browser smoke proves active pages no longer display platform Tenant context. +- **New or expanded test families**: Spec 317 legacy platform-context guard tests, Workspace hub legacy query alias tests, route guard tests, helper/API rename tests, UI copy guard, and focused browser smoke. +- **Fixture / helper cost impact**: Existing Workspace, ManagedEnvironment, membership/capability, provider connection, evidence/review/finding/operation factories may be reused. Static guards should avoid broad database setup. Any full-context helpers must stay opt-in. +- **Heavy-family visibility / justification**: Browser coverage is explicit and limited to required surfaces because UI copy and generated links are browser-visible truth. Durable no-drift coverage remains Spec 318. +- **Special surface test profile**: global-context-shell, standard-native-filament, provider-boundary-copy. +- **Standard-native relief or required special coverage**: Existing native Filament tests cover page/resource behavior; browser smoke covers rendered vocabulary and link/query output. +- **Reviewer handoff**: Reviewers must inspect `legacy-inventory.md`, `tenant-usage-allowlist.md`, static guard allowlist entries, renamed helper/class list, removed legacy query handlers, and browser results. +- **Budget / baseline / trend impact**: Static guards may scan many files but should remain bounded to relevant app/resources/routes/tests/current docs paths. Any material runtime cost must be documented. +- **Escalation needed**: follow-up-spec only for durable browser infrastructure (Spec 318) or if implementation discovers a schema rename too broad for this cleanup. +- **Active feature PR close-out entry**: Guardrail and Smoke Coverage. +- **Planned validation commands**: Focused Pest filters for Spec 317 guard/contract tests, existing Spec 314/315/316 regression tests, `cd apps/platform && ./vendor/bin/sail artisan test` if scope warrants, browser smoke for required pages, `cd apps/platform && ./vendor/bin/sail pint --test` or equivalent Pint check for touched PHP files, and `git diff --check`. + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Inventory and classify legacy Tenant usage before cleanup (Priority: P1) + +An implementer inventories remaining Tenant terminology and classifies each relevant occurrence before broad cleanup starts. + +**Why this priority**: Provider-boundary Tenant terminology must be preserved, while platform-context Tenant terminology must be removed. A naive grep cleanup would break valid provider identity code. + +**Independent Test**: Review `legacy-inventory.md` and `tenant-usage-allowlist.md`; every relevant occurrence is classified with an action and no high-risk ambiguous occurrence remains. + +**Acceptance Scenarios**: + +1. **Given** a Tenant occurrence is found in a provider identity class, **When** the inventory is reviewed, **Then** it is classified as provider-boundary or renamed only if it leaks into platform context. +2. **Given** a Tenant occurrence is found in a Workspace hub helper, **When** the inventory is reviewed, **Then** it is classified as remove, rename, or quarantine with a concrete action. + +--- + +### User Story 2 - Workspace hubs accept only canonical Environment filters (Priority: P1) + +An operator or stale URL attempts to use legacy Tenant query aliases on Workspace hubs. The page must ignore or normalize them without applying an Environment filter. + +**Why this priority**: This prevents the old hidden Tenant context model from returning after Specs 314-316. + +**Independent Test**: Open critical Workspace hubs with `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, `environment`, and `tableFilters` without `environment_id`; assert no Environment chip appears and no Environment filter is applied. Open with valid `environment_id`; assert canonical filtering still works. + +**Acceptance Scenarios**: + +1. **Given** Operations is opened with `?tenant=123`, **When** the page renders, **Then** no Environment chip is shown and rows are not scoped by that parameter. +2. **Given** Provider Connections is opened with valid `?environment_id={Environment A}`, **When** the page renders, **Then** the existing Spec 315/316 visible filter behavior still works. + +--- + +### User Story 3 - Legacy helper/class names are removed or renamed without aliases (Priority: P1) + +A developer working on Workspace/Environment context should see current domain names and no deprecated Tenant aliases for Environment behavior. + +**Why this priority**: Old helper names are the main reintroduction path for legacy context. + +**Independent Test**: Static guard/tests assert old helper names such as `tenantPrefilterUrl`, platform-context `TenantPageCategory`, `WorkspaceScopedTenantRoutes`, and `CanonicalAdminTenantFilterState` are gone, renamed, or explicitly quarantined with reason. + +**Acceptance Scenarios**: + +1. **Given** code tries to call `tenantPrefilterUrl()`, **When** tests/static guards run, **Then** the call is rejected and the canonical Environment helper is used instead. +2. **Given** `CanonicalAdminTenantFilterState` remains after implementation, **When** the inventory is reviewed, **Then** it is quarantined with a bounded reason or renamed to match current responsibility. + +--- + +### User Story 4 - Active UI uses Environment, not Tenant, for platform context (Priority: P1) + +An operator uses Workspace and Environment surfaces without seeing Tenant language where the product means Managed Environment. + +**Why this priority**: Visible vocabulary is part of scope safety and enterprise trust. + +**Independent Test**: Browser or rendered-view checks cover Workspace Overview, Environment Dashboard, Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Reviews, Customer Reviews, Provider Readiness, and Required Permissions. + +**Acceptance Scenarios**: + +1. **Given** Workspace Overview lists active Environment-related navigation, **When** the page renders, **Then** the copy says Environment/Managed Environment and not Tenant unless provider-boundary context is explicit. +2. **Given** Provider Connection details show a Microsoft tenant ID, **When** the page renders, **Then** the label makes it clear that it is a provider/Microsoft Tenant value, not TenantPilot platform context. + +--- + +### User Story 5 - Routes, tests, and current docs prevent legacy context drift (Priority: P2) + +Developers and reviewers should have guardrails that block `/admin/t`, Tenant Panel assumptions, and stale docs/spec candidate language from becoming current truth again. + +**Why this priority**: Cleanup must be durable enough for future specs, while full browser no-drift infrastructure remains Spec 318. + +**Independent Test**: Route guard confirms no active `/admin/t` panel routes or TenantPanelProvider registration; docs/spec updates remove current product-truth Tenant platform context; historical specs are left untouched or marked historical. + +**Acceptance Scenarios**: + +1. **Given** admin routes are listed, **When** the route guard runs, **Then** no active `/admin/t` route is present. +2. **Given** `docs/product/spec-candidates.md` is reviewed, **When** current product truth is read, **Then** it no longer describes Tenant as TenantPilot platform context for Workspace hubs. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: Implementation MUST create and maintain `specs/317-legacy-tenant-environment-context-cleanup/legacy-inventory.md` before broad cleanup edits. +- **FR-002**: `legacy-inventory.md` MUST classify relevant Tenant occurrences as `remove`, `rename_to_environment`, `rename_to_provider_tenant`, `allowed_provider_boundary`, `allowed_historical_archived_doc`, `dead_code_candidate`, `needs_product_decision`, or `out_of_scope`. +- **FR-003**: Implementation MUST create and maintain `specs/317-legacy-tenant-environment-context-cleanup/tenant-usage-allowlist.md`. +- **FR-004**: The allowlist MUST explicitly preserve provider-boundary Tenant usage such as Microsoft tenant, Entra tenant, provider tenant ID, external tenant ID, Graph tenant ID, OAuth tenant segment, and `tenantId` in provider payloads. +- **FR-005**: Workspace hubs MUST NOT treat `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, `environment`, or `tableFilters` as valid Environment filter sources. +- **FR-006**: Workspace hubs MUST accept only `environment_id` for explicit Environment filtering. +- **FR-007**: Generated Workspace hub links MUST NOT emit `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, `environment`, or `tableFilters` as Environment filter keys. +- **FR-008**: Environment-owned CTAs to Workspace hubs MUST continue to emit canonical `environment_id` where Spec 315/316 already support it. +- **FR-009**: `tenantPrefilterUrl()` MUST be removed or renamed to an Environment-named helper with no deprecated alias. +- **FR-010**: Platform-context helper properties and variables such as `tenantId`, `tenantLabel`, `tenantFilter`, and `tenantScope` MUST be renamed when they represent Managed Environment behavior. +- **FR-011**: `TenantPageCategory`, `WorkspaceScopedTenantRoutes`, `CanonicalAdminTenantFilterState`, `EnsureFilamentTenantSelected`, and any `TenantPanelProvider` reference MUST be renamed, removed, or explicitly quarantined based on actual responsibility. +- **FR-012**: No deprecated helper/class aliases may remain for compatibility. +- **FR-013**: Workspace hubs MUST NOT use `Filament::getTenant()` or `getTenant()` as data scope, default Environment, authorization fallback, shell context, or URL parameter source. +- **FR-014**: Workspace hubs MUST NOT use `lastTenantId`, remembered Tenant state, or session Tenant state as data scope, authorization scope, Environment fallback, or URL source. +- **FR-015**: `lastEnvironmentId`, if it remains, MUST be named and used only as switcher/convenience memory and MUST NOT affect Workspace hub filters, URLs, data queries, authorization, or shell context. +- **FR-016**: Active platform UI MUST use Workspace/Environment terminology when it means TenantPilot platform context. +- **FR-017**: Provider-specific UI MAY say Microsoft Tenant, Entra Tenant, or Provider Tenant ID only when clearly provider-owned. +- **FR-018**: No active `/admin/t` route or Tenant Panel provider registration may remain. +- **FR-019**: Workspace hub clean URLs MUST remain workspace-scoped and must not require Tenant route context. +- **FR-020**: Static guard tests MUST distinguish allowed provider-boundary Tenant usage from disallowed platform-context Tenant usage. +- **FR-021**: Guard tests MUST fail if legacy Tenant query aliases become valid Workspace hub Environment filters again. +- **FR-022**: Guard tests MUST fail if `Filament::getTenant()` or remembered Tenant fallback is used for Workspace hub scope. +- **FR-023**: Route guard tests MUST fail if active `/admin/t` routes or TenantPanelProvider registration return. +- **FR-024**: UI copy tests or browser smoke MUST cover required active Workspace/Environment surfaces. +- **FR-025**: Current docs/spec candidate artifacts MUST use Workspace/Environment terminology for current product truth and must not describe Tenant as platform context. +- **FR-026**: Completed historical specs MUST NOT be rewritten unless they are actively used as current product truth; otherwise leave them as history or mark the current-truth replacement. +- **FR-027**: No migration may be added unless repo analysis proves a schema name is actively part of the wrong platform-context contract and the rename is safe, bounded, and tested. +- **FR-028**: Provider-boundary columns such as `provider_tenant_id`, `external_tenant_id`, `microsoft_tenant_id`, `entra_tenant_id`, and provider payload `tenantId` MUST NOT be renamed for cosmetic reasons. +- **FR-029**: Existing Specs 314, 315, and 316 regression behavior MUST remain green. +- **FR-030**: No backwards compatibility layer, compatibility redirect, alias reader, dual-param support, or transitional adapter may be introduced. + +### Non-Functional Requirements + +- **NFR-001**: Cleanup must be direct and bounded; do not introduce a generic context framework. +- **NFR-002**: Static scanning must be maintainable through an allowlist and must not block valid provider-boundary Tenant terminology. +- **NFR-003**: Browser verification must be focused on required active surfaces and must not implement Spec 318's durable no-drift suite. +- **NFR-004**: All Runtime changes must preserve Laravel 12, Filament v5.2.1, Livewire v4.1.4, and Pest v4 conventions. +- **NFR-005**: No Graph calls, queue semantics, destructive actions, or audit behavior may change except where a renamed helper/link affects existing tests. + +### Key Entities / Concepts + +- **Workspace**: Primary TenantPilot platform context and data boundary. +- **Managed Environment / Environment**: Secondary operational context inside a Workspace and explicit filter target through `environment_id`. +- **Provider Tenant**: External provider identity, such as Microsoft/Entra/Graph tenant. Valid only at provider boundaries. +- **Workspace Hub**: Workspace-owned/canonical admin surface that may optionally accept an explicit Environment filter. +- **Legacy Tenant Platform Context**: Disallowed old model where Tenant acts as Workspace hub, shell, sidebar, Environment filter, or TenantPilot data-boundary context. + +### Edge Cases + +- Provider Connection pages that display provider tenant IDs must keep provider-specific labels while not using those IDs as Workspace hub filters. +- Historical tests/specs may contain Tenant language as preserved history; guard allowlists must not require rewriting completed implementation evidence. +- `managed_environment_id` is a valid internal DB relationship in many models but not a public Workspace hub CTA filter key. +- `Filament::getTenant()` may be valid on truly Environment-scoped pages, but not as Workspace hub scope. +- Some legacy names may remain quarantined for a bounded reason if renaming would exceed this spec; each quarantine requires inventory entry, reason, risk, and follow-up. + +## Out of Scope + +- UI redesign or new product workflows. +- Adding Environment CTA support to pages excluded from Specs 315/316. +- Retrofitting Audit Log, Alerts, Reports, or Support Requests unless required by cleanup discovery. +- Removing provider-specific Microsoft/Entra Tenant concepts. +- Broad database renames for cosmetic reasons. +- Compatibility redirects or legacy URL support. +- Durable browser regression infrastructure owned by Spec 318. +- Rewriting completed specs as if they were current preparation artifacts. + +## Success Criteria *(mandatory)* + +- **SC-001**: `legacy-inventory.md` exists and classifies all relevant high-risk Tenant occurrences. +- **SC-002**: `tenant-usage-allowlist.md` exists and documents provider-boundary Tenant usage. +- **SC-003**: Critical Workspace hubs ignore legacy Tenant aliases and accept only `environment_id` as explicit Environment filter. +- **SC-004**: Active UI surfaces use Environment terminology where they mean Managed Environment. +- **SC-005**: Provider-boundary Tenant wording remains only where clearly provider-owned. +- **SC-006**: No active `/admin/t` route or TenantPanelProvider registration remains. +- **SC-007**: Old helper/class aliases are removed, renamed, or explicitly quarantined without compatibility aliases. +- **SC-008**: Static guard, route guard, helper/API guard, UI copy guard, and Spec 314-316 regression tests pass. +- **SC-009**: Focused browser verification confirms no active platform UI says Tenant when it means Environment, and generated links do not emit legacy query aliases. +- **SC-010**: No backwards compatibility layer was introduced and no provider-boundary Tenant concepts were incorrectly removed. + +## Assumptions + +- There is no production data or external contract requiring legacy Tenant platform-context compatibility. +- Specs 313-316 are completed historical context and should not be rewritten. +- The default expectation is no migrations. +- The user-provided Spec 317 is the selected candidate and supersedes roadmap numbering notes that mention a different Spec 317 topic. +- Implementation may discover a small number of names that need quarantine rather than immediate rename; each requires explicit documentation. + +## Open Questions + +None blocking preparation. Any `needs_product_decision` inventory classification during implementation must stop that specific cleanup item or move it to a follow-up instead of guessing. diff --git a/specs/317-legacy-tenant-environment-context-cleanup/tasks.md b/specs/317-legacy-tenant-environment-context-cleanup/tasks.md new file mode 100644 index 00000000..64c6e21c --- /dev/null +++ b/specs/317-legacy-tenant-environment-context-cleanup/tasks.md @@ -0,0 +1,148 @@ +# Tasks: Legacy Tenant / Environment Context Cleanup + +**Input**: [spec.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/317-legacy-tenant-environment-context-cleanup/spec.md), [plan.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/317-legacy-tenant-environment-context-cleanup/plan.md) +**Prerequisites**: Specs 313, 314, 315, and 316 are completed historical baseline context. Do not rewrite them as preparation artifacts. + +**Important**: These tasks are for the later implementation loop. No runtime implementation was performed during preparation. + +## Test Governance Checklist + +- [x] Lane assignment is named and is the narrowest sufficient proof for static legacy terms, query aliases, routes, helper/API names, UI copy, and browser-visible links. +- [x] New or changed tests stay in the smallest honest family, with browser smoke explicit and limited. +- [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 cleanup without pulling in unrelated lane cost. +- [x] Provider-boundary Tenant terminology is protected through an allowlist instead of blocked by naive grep. +- [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 `317-legacy-tenant-environment-context-cleanup` with `git status --short --branch` and no unrelated user changes. +- [x] T002 Re-read `specs/313-workspace-environment-context-browser-verification/spec.md`, `specs/314-workspace-hub-navigation-context-contract/spec.md`, `specs/315-environment-cta-explicit-filter-contract/spec.md`, and `specs/316-workspace-hub-clear-filter-contract/spec.md` as completed historical baseline context only. +- [x] T003 Confirm Laravel/Filament/Livewire/Pest versions through Laravel Boost `application_info`. +- [x] T004 Confirm `AdminPanelProvider` and `SystemPanelProvider` remain registered through `apps/platform/bootstrap/providers.php` and no `TenantPanelProvider` is active. +- [x] T005 Confirm no migration, seeder, package, env var, queue, scheduler, storage, or deployment asset change is planned unless later inventory proves a bounded exception. + +## Phase 2: Inventory and Allowlist + +- [x] T006 Create `specs/317-legacy-tenant-environment-context-cleanup/legacy-inventory.md` with columns for file, symbol/method/route/label, current meaning, correct meaning, classification, action, and risk. +- [x] T007 Create `specs/317-legacy-tenant-environment-context-cleanup/tenant-usage-allowlist.md` with allowed provider-boundary examples, disallowed platform-context examples, scan paths, and review rules. +- [x] T008 Run the required Tenant/Environment search across `apps/platform/app`, `apps/platform/resources`, `apps/platform/routes`, `apps/platform/tests`, `docs`, and current/recent `specs`. +- [x] T009 Classify all high-risk occurrences of `TenantPageCategory`, `WorkspaceScopedTenantRoutes`, `CanonicalAdminTenantFilterState`, `EnsureFilamentTenantSelected`, `TenantPanelProvider`, `tenantPrefilterUrl`, `lastTenantId`, `remembered tenant`, `tenant_scope`, `tenant_id`, `managed_environment_id`, `Filament::getTenant()`, `getTenant()`, `tableFilters`, and `/admin/t`. +- [x] T010 Classify provider-boundary occurrences such as `provider_tenant_id`, `external_tenant_id`, `microsoft_tenant_id`, `entra_tenant_id`, Graph `tenantId`, and OAuth tenant authority segments as allowed provider-boundary when correct. +- [x] T011 Mark completed historical spec/doc occurrences as `allowed_historical_archived_doc` unless they are current product truth. +- [x] T012 Mark ambiguous cases as `needs_product_decision` and do not guess or implement those specific renames without a bounded decision. + +## Phase 3: Tests First - Static and Contract Guards + +- [x] T013 Add `it_platform_context_does_not_use_legacy_tenant_terms` in an appropriate guard test under `apps/platform/tests/Feature/Guards/` with allowlist support from the Spec 317 allowlist artifact. +- [x] T014 Add `it_workspace_hubs_do_not_accept_legacy_tenant_query_aliases` covering Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Review Register, and Customer Review Workspace. +- [x] T015 Add assertions that `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, `environment`, and `tableFilters` do not show an Environment chip or filter Workspace hub data without `environment_id`. +- [x] T016 Add assertions that valid `environment_id` still applies the Spec 315/316 canonical Environment filter behavior. +- [x] T017 Add `it_workspace_hubs_do_not_use_filament_tenant_or_remembered_tenant_as_scope` with static checks for `Filament::getTenant()`, `getTenant()`, `lastTenantId`, and remembered Tenant fallback in Workspace hub classes. +- [x] T018 Add `it_admin_has_no_active_legacy_tenant_panel_routes` proving no active `/admin/t` routes and no active `TenantPanelProvider` registration. +- [x] T019 Add helper/API rename coverage proving `tenantPrefilterUrl()` is gone and the Environment-named replacement is used. +- [x] T020 Add helper/class guard coverage for `TenantPageCategory`, `WorkspaceScopedTenantRoutes`, `CanonicalAdminTenantFilterState`, and `EnsureFilamentTenantSelected` once each is renamed, removed, or quarantined. +- [x] T021 Add or update rendered-view/UI copy guard `it_active_workspace_environment_ui_uses_environment_not_tenant` for active Workspace/Environment surfaces, with provider-boundary exceptions. + +## Phase 4: Query and URL Cleanup + +- [x] T022 Update `apps/platform/app/Support/Navigation/WorkspaceHubEnvironmentFilter.php` so only `environment_id` is a valid Environment filter source. +- [x] T023 Update `apps/platform/app/Support/Navigation/WorkspaceHubRegistry.php` and related clean URL helpers so generated Workspace hub links never emit legacy Tenant query params. +- [x] T024 Update `apps/platform/app/Support/Navigation/WorkspaceHubFilterStateResetter.php` so legacy Tenant-like keys can be cleared/neutralized but never become valid Environment filter sources. +- [x] T025 Remove Workspace hub handling for `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, `environment`, and `tableFilters` as Environment filter aliases. +- [x] T026 Confirm Environment-owned CTA helpers still generate `environment_id` and do not regress Spec 315 behavior. +- [x] T027 Confirm clear-filter links still remove legacy keys and do not regress Spec 316 behavior. + +## Phase 5: Helper, Class, and Fallback Cleanup + +- [x] T028 Rename or remove `tenantPrefilterUrl()` on `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` with no deprecated alias. +- [x] T029 Rename `tenantId`, `tenantLabel`, `tenantFilter`, and `tenantScope` variables/properties when they mean Managed Environment on Workspace hub code paths. +- [x] T030 Rename, remove, or explicitly quarantine `apps/platform/app/Support/Tenants/TenantPageCategory.php` based on actual responsibility after cleanup. +- [x] T031 Rename, remove, or explicitly quarantine `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php` based on actual route responsibility after cleanup. +- [x] T032 Rename, remove, or explicitly quarantine `apps/platform/app/Support/Filament/CanonicalAdminTenantFilterState.php` based on actual table/filter responsibility after cleanup. +- [x] T033 Rename, remove, or explicitly quarantine `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php` based on actual Environment context responsibility after cleanup. +- [x] T034 Remove Workspace hub use of `Filament::getTenant()` or `getTenant()` as scope, fallback, query source, or shell context. +- [x] T035 Remove Workspace hub use of `lastTenantId`, remembered Tenant state, or session Tenant state as data scope or Environment fallback. +- [x] T036 Verify any remaining `lastEnvironmentId` behavior is switcher convenience only and cannot affect Workspace hub URLs, filters, queries, authorization, or shell context. + +## Phase 6: Route and Provider Boundary Cleanup + +- [x] T037 Inspect `apps/platform/routes/web.php` and remove or neutralize active `/admin/t` route assumptions where they remain product-facing. +- [x] T038 Inspect `apps/platform/app/Providers/Filament/AdminPanelProvider.php` for Tenant page category, context, render hook, or navigation assumptions and rename/remove them. +- [x] T039 Inspect `apps/platform/app/Support/ManagedEnvironmentLinks.php` and `apps/platform/app/Support/Operations/OperationRunLinks.php` for generated legacy Tenant query params or route helpers. +- [x] T040 Inspect Provider Connections surfaces and preserve only provider-boundary Tenant labels such as Microsoft Tenant, Entra Tenant, and Provider Tenant ID. +- [x] T041 Confirm provider external tenant identifiers are never used as Workspace hub `environment_id` substitutes. + +## Phase 7: Active UI Copy Cleanup + +- [x] T042 Update Workspace Overview active copy and links so Tenant wording is not used for Managed Environment context. +- [x] T043 Update Environment Dashboard active copy, CTAs, and provider readiness snippets so Environment and Provider Tenant terms are distinct. +- [x] T044 Update Operations active copy, chips, filters, and empty states where Tenant wording means Environment. +- [x] T045 Update Governance Inbox, Decision Register, and Finding Exceptions Queue active copy where Tenant wording means Environment. +- [x] T046 Update Provider Connections active copy so provider-boundary Tenant terminology is explicit and Environment filters use Environment wording. +- [x] T047 Update Evidence, Reviews, Customer Reviews, Provider Readiness, and Required Permissions active copy where Tenant wording means Environment. + +## Phase 8: Docs and Current Spec Truth Cleanup + +- [x] T048 Update `docs/product/spec-candidates.md` current queue/follow-up wording so it reflects Workspace-first / Environment-second platform context and the selected Spec 317 numbering. +- [x] T049 Update `docs/product/roadmap.md` only where current product truth or numbering notes conflict with Spec 317; leave strategic historical context intact when not current truth. +- [x] T050 Update relevant current docs under `docs/product`, `docs/ui`, `docs/architecture-guidelines.md`, `docs/filament-guidelines.md`, and `docs/testing-guidelines.md` only where they describe Tenant as platform context. +- [x] T051 Do not rewrite completed Specs 313-316 except for an explicit current-truth note if implementation proves they are used as current docs rather than history. +- [x] T052 Document any intentionally quarantined legacy names in `legacy-inventory.md` with reason, risk, and follow-up. + +## Phase 9: Regression Safety + +- [x] T053 Re-run or update Spec 314 regression coverage proving sidebar/global Workspace hub entry remains clean and workspace-wide. +- [x] T054 Re-run or update Spec 315 regression coverage proving Environment-owned CTAs use `environment_id`, visible chip renders when filtered, legacy params remain invalid, and cross-workspace IDs are rejected. +- [x] T055 Re-run or update Spec 316 regression coverage proving clear removes URL/page/table/session state and does not restore stale Environment filters. +- [x] T056 Confirm no globally searchable resource behavior changed; if a Resource is touched, verify it has Edit/View page or global search disabled. +- [x] T057 Confirm no destructive action behavior changed; if a touched surface has destructive actions, verify `->action(...)`, `->requiresConfirmation()`, authorization, and audit behavior remain intact. +- [x] T058 Confirm no Graph calls, OperationRun lifecycle behavior, queue behavior, scheduler behavior, env var, storage, or asset registration changed. + +## Phase 10: Browser Verification + +- [x] T059 Start the local platform stack using Sail or the repo's platform dev command. +- [x] T060 Resolve the absolute local app URL using Laravel Boost `get_absolute_url`. +- [x] T061 Browser smoke Workspace Overview, Environment Dashboard, Operations, Governance Inbox, Decision Register, Finding Exceptions Queue, Provider Connections, Evidence, Reviews, Customer Reviews, Provider Readiness, and Required Permissions. +- [x] T062 Verify active platform UI does not say Tenant when it means Environment. +- [x] T063 Verify provider-specific details may still say Microsoft Tenant, Entra Tenant, or Provider Tenant ID only where provider-owned. +- [x] T064 Verify sidebar/global clean entry still works and no old query params appear in generated links. +- [x] T065 Verify Environment CTA filtered entry still uses `environment_id`. +- [x] T066 Verify clear filter still returns to clean Workspace hub state. +- [x] T067 Verify no `/admin/t` route appears in active navigation or generated links. +- [x] T068 Save screenshots where useful under `specs/317-legacy-tenant-environment-context-cleanup/artifacts/screenshots/`. + +## Phase 11: Final Validation and Report + +- [x] T069 Run focused Spec 317 Pest guard/contract tests. +- [x] T070 Run existing related Spec 314, Spec 315, and Spec 316 regression tests. +- [x] T071 Run formatting/static checks expected by touched files, including Pint if PHP files changed. +- [x] T072 Run `git diff --check`. +- [x] T073 Prepare the final implementation report with changed behavior, legacy cleanup summary, provider-boundary Tenant usages kept, removed/renamed legacy platform usages, files changed, tests, browser verification, known issues, and Spec 318 follow-up. +- [x] T074 Confirm the final report lists paths to `legacy-inventory.md` and `tenant-usage-allowlist.md`, renamed classes/helpers, removed query handlers, UI copy changes, docs/spec artifacts updated, and quarantined names with reasons. +- [x] T075 Confirm the final report states no backwards compatibility layer was introduced, no legacy query alias support was preserved, and no provider-boundary Tenant concepts were incorrectly removed. + +## Explicit Non-Tasks + +- [x] NT001 Do not implement durable browser no-drift infrastructure; leave to Spec 318. +- [x] NT002 Do not add compatibility redirects, dual-param support, alias readers, or adapter layers. +- [x] NT003 Do not perform broad cosmetic database renames. +- [x] NT004 Do not remove provider-boundary Microsoft/Entra/provider Tenant terminology. +- [x] NT005 Do not add Environment CTA support to pages excluded from Specs 315/316. +- [x] NT006 Do not rewrite completed historical specs or implementation close-out evidence. +- [x] NT007 Do not create migrations, seeders, packages, env vars, queues, scheduler changes, storage changes, or asset registration unless a bounded implementation finding explicitly requires and documents the exception. + +## Dependencies + +- T001-T005 block all runtime work. +- T006-T012 block cleanup edits. +- T013-T021 should be added before or alongside the corresponding cleanup changes. +- T022-T041 can proceed after inventory classifications are complete. +- T042-T052 can proceed after UI/docs inventory classification. +- T053-T058 run after code/test cleanup. +- T059-T068 run after local validation is green enough for browser smoke. +- T069-T075 close the implementation. + +## Suggested MVP Scope + +Complete Phases 1-5 plus focused guards first. That proves the core hard-cut cleanup: inventory, allowlist, canonical `environment_id`, no helper aliases, and no Workspace hub Tenant fallback. UI/docs/browser phases then verify and close the visible product-truth layer. diff --git a/specs/317-legacy-tenant-environment-context-cleanup/tenant-usage-allowlist.md b/specs/317-legacy-tenant-environment-context-cleanup/tenant-usage-allowlist.md new file mode 100644 index 00000000..26733568 --- /dev/null +++ b/specs/317-legacy-tenant-environment-context-cleanup/tenant-usage-allowlist.md @@ -0,0 +1,76 @@ +# Spec 317 Tenant Usage Allowlist + +Status: implementation allowlist +Updated: 2026-05-16 + +Spec 317 blocks legacy Tenant language only when it models TenantPilot platform context. Tenant remains allowed when it means external provider identity, historical evidence, or existing domain vocabulary explicitly outside this cleanup. + +## Scan Paths + +Guard tests should scan these current-truth paths: + +- `apps/platform/app` +- `apps/platform/resources` +- `apps/platform/routes` +- `apps/platform/tests/Feature/Guards` +- `apps/platform/tests/Feature/Navigation` +- `apps/platform/tests/Feature/Reviews` +- `docs/product/spec-candidates.md` +- `docs/product/roadmap.md` +- `docs/product/principles.md` +- `docs/ui` +- `docs/architecture-guidelines.md` +- `docs/filament-guidelines.md` +- `docs/testing-guidelines.md` + +Completed historical specs, audit archives, and old research reports are not scanned as current product truth unless a current doc links to them as the active contract. + +## Allowed Provider-Boundary Tenant Usage + +These terms are allowed when they clearly mean Microsoft, Entra, Graph, OAuth, or external provider identity: + +- `provider_tenant_id` +- `external_tenant_id` +- `microsoft_tenant_id` +- `entra_tenant_id` +- `azure_tenant_id` +- `tenantId` in Graph or provider payloads +- OAuth authority tenant segments +- `Microsoft tenant` +- `Entra tenant` +- `Provider tenant ID` +- Cross-tenant Microsoft/Entra feature names +- Tenant-level Microsoft 365 configuration when describing provider-native product concepts + +## Allowed Existing Domain Vocabulary + +These remain allowed in Spec 317 unless they become a Workspace hub filter/source: + +- `tenant` relationship methods on tenant-owned Eloquent models +- `tenant` variables in existing tests that represent `ManagedEnvironment` fixtures +- `TenantOperability*` services, questions, outcomes, and existing capability semantics +- tenant isolation / tenant-owned data wording in security, architecture, and testing guidelines +- `Filament::getTenant()` on environment-owned pages, widgets, and resources where the route is explicitly environment-bound + +## Disallowed Platform-Context Usage + +These are disallowed for Workspace hub Environment filtering, generated links, shell scope, and current product-truth UI/docs: + +- `tenant`, `tenant_id`, `managed_environment_id`, `tenant_scope`, `environment`, or `tableFilters` as public Workspace hub Environment filter keys +- `tenantPrefilterUrl()` +- `tenantScopedUrl()` +- `CanonicalAdminTenantFilterState` +- `WorkspaceScopedTenantRoutes` +- `TenantPageCategory` +- `EnsureFilamentTenantSelected` +- `lastTenantId`, `rememberedTenant`, `rememberTenantContext`, and related session key names +- active `/admin/t` route family or `TenantPanelProvider` +- `Filament::getTenant()` as a Workspace hub default scope, authorization fallback, URL source, or shell context + +## Review Rules + +- Provider-boundary Tenant language must be specific: use `Microsoft tenant`, `Entra tenant`, or `Provider tenant ID` when visible to operators. +- Platform context must use `Workspace`, `Environment`, or `Managed Environment`. +- Do not add compatibility aliases for renamed helpers, classes, routes, or query keys. +- Do not rename provider-boundary columns or Graph payload keys for cosmetic reasons. +- Add a new inventory row before allowing any active runtime Tenant-named platform seam to remain.