From cbeed74dc3a8477deff1684e0713ab29590319aa Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Thu, 14 May 2026 13:28:50 +0200 Subject: [PATCH 1/2] Sync project state --- docs/product/implementation-ledger.md | 16 +++++++++------- docs/product/roadmap.md | 13 +++++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/docs/product/implementation-ledger.md b/docs/product/implementation-ledger.md index eedc8674..cee5a89b 100644 --- a/docs/product/implementation-ledger.md +++ b/docs/product/implementation-ledger.md @@ -1,10 +1,10 @@ # TenantPilot Implementation Ledger -> **Status:** Active -> **Last reviewed:** 2026-05-06 -> **Use for:** Repo-based implementation status and product-surface maturity assessment +> **Status:** Active +> **Last reviewed:** 2026-05-12 +> **Use for:** Repo-based implementation status and product-surface maturity assessment > **Do not use for:** Roadmap priority, spec priority, or proof that tests were executed in the current branch -> **Scoped maintenance:** 2026-05-06 ledger conflict cleanup plus alignment with `docs/product/roadmap.md` and `docs/product/spec-candidates.md` after the cross-domain indicator candidate intake and the current manual-promotion backlog review. +> **Scoped maintenance:** 2026-05-12 roadmap/ledger alignment after the admin workspace navigation and tenant-owned surface repair candidate intake from the repo-verified navigation/panel audit; 2026-05-06 ledger conflict cleanup plus alignment with `docs/product/roadmap.md` and `docs/product/spec-candidates.md` after the cross-domain indicator candidate intake and the current manual-promotion backlog review. ## Purpose @@ -48,7 +48,7 @@ ## Roadmap Coverage Summary | R2 Tenant Reviews, Evidence & Control Foundation | fast sellable | strong | yes | repo tests, not run | yes | Reviews, Evidence, Review Packs, Customer Review Workspace, governance-package delivery, compliance interpretation overlays und Control-/Exception-Layer greifen als reale Governance-Surface zusammen; die letzte customer-safe self-serve productization bleibt offen. | | Alert escalation + notification routing | sellable | strong | partial | repo tests, not run | yes | Alert-Regeln, Dispatch, Cooldown und Quiet Hours sind real. | | Governance & Architecture Hardening | foundation-only | strong | partial | repo tests, not run | no | Viele Hardening-Slices sind bereits im Code, die Lane bleibt als platform seam work aktiv. | -| UI & Product Maturity Polish | implemented but not productized | strong | partial | partial repo tests, not run | no | Empty States, Navigation, Localization und read-only Review-Polish sind real, aber kein geschlossenes Theme-Completion-Signal. | +| UI & Product Maturity Polish | implemented but not productized | strong | partial | partial repo tests, not run | no | Empty States, Navigation, Localization und read-only Review-Polish sind real, aber kein geschlossenes Theme-Completion-Signal; admin workspace navigation drift remains explicit manual-promotion follow-through. | | Secret & Security Hardening | fast sellable | strong | yes | repo tests, not run | yes | Provider-Verifikation, Permission-Diagnostics und Redaction sind belastbar. | | Baseline Drift Engine (Cutover) | sellable | strong | yes | repo tests, not run | yes | Compare- und Drift-Workflow wirken als produktive Kernfunktion. | | R1.9 Platform Localization v1 | foundation-only | strong | yes | repo tests, not run | no | Locale-Resolver, Override/Praeferenz, Workspace-Default, Fallback und lokalisierte Notifications sind repo-real; `specs/252-platform-localization-v1/spec.md` ist die historische Foundation. | @@ -225,6 +225,7 @@ ## Open Gaps & Blockers | Decision register and approval workflow is still missing | Product blocker | Decision-based operating still lacks a bounded approval-ready closure and decision-record package with audit trail | Decision-based operating | `Decision Register & Approval Workflow v1` | | Governance-artifact lifecycle runtime is still missing | Trust / auditability blocker | Lifecycle taxonomy and point retention rules exist, but governance artifacts still lack immutable-reference, hold, export, delete, and suspended/read-only runtime semantics | Lifecycle governance / enterprise trust | `Governance Artifact Lifecycle & Retention v1` | | Cross-domain progress and indicator semantics guardrail is still missing | UX / trust guardrail | Bars, percentages, scores, readiness, risk, usage, and generation-state hints still lack one shared taxonomy and standards layer above the OperationRun-specific rules | UI semantics / product trust | `Cross-Domain Progress / Indicator Semantics candidate group` | +| Admin workspace navigation and tenant-owned surface contract drift remains open | UX / IA repair blocker | Inventory and adjacent tenant-owned admin surfaces still conflict between workspace-home clean-sidebar rules and valid environment-bound admin access, leaving active product breaks and stale hide-first assumptions in place | UI maturity / admin runtime contract | `admin-inventory-navigation-cutover` from the `Admin Workspace Navigation & Tenant-owned Surface Repair candidate group` | | Customer-facing localization adoption is incomplete | Productization blocker | Locale groundwork is repo-real, but customer-safe adoption remains incomplete | Localization / review productization | `Customer-Facing Localization Adoption v1` | | Billing and subscription truth is missing | Commercial blocker | Entitlements and lifecycle state handling stop short of a durable billing/subscription truth layer | Commercial readiness | `Billing & Subscription Truth Layer v1` | | Stored reports still lack a clear product surface | Product blocker | Retained evidence and review artifacts remain harder to consume than they should be | Reports / evidence consumption | `Stored Reports Surface v1` | @@ -235,6 +236,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`; promote `admin-inventory-navigation-cutover` first, then keep the route audit, groups cutover, contract split, and tenant-panel dead-code retirement as separate sequenced follow-through - `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 - `Decision Register & Approval Workflow v1` -> anchored by `specs/250-decision-governance-inbox/spec.md`, `specs/257-governance-decision-convergence/spec.md`, and `docs/product/roadmap.md` - `Governance Artifact Lifecycle & Retention v1` -> anchored by `specs/158-artifact-truth-semantics/spec.md`, `specs/262-lifecycle-governance-taxonomy/spec.md`, and `docs/product/standards/lifecycle-governance.md` @@ -247,8 +249,8 @@ ## Recommended Manual Promotions ## Roadmap Drift Notes -- `docs/product/roadmap.md` and `docs/product/spec-candidates.md` were corrected on 2026-05-06 to reflect the cross-domain indicator candidate intake, the current manual-promotion backlog, and the resolved ledger conflict state. -- The remaining documentation risk is no longer queue drift alone; it is overstating sellability on still-open follow-through slices such as auditor-ready export, promotion execution, governance decision workflow, cross-domain indicator semantics, billing/subscription truth, stored reports surface, and the first governed AI runtime consumer. +- `docs/product/roadmap.md` and `docs/product/spec-candidates.md` are aligned through 2026-05-12, including the admin workspace navigation / tenant-owned surface repair candidate intake, the earlier cross-domain indicator candidate intake, the current manual-promotion backlog, and the resolved ledger conflict state. +- The remaining documentation risk is no longer queue drift alone; it is understating or overstating still-open follow-through slices such as admin workspace navigation repair, auditor-ready export, promotion execution, governance decision workflow, cross-domain indicator semantics, billing/subscription truth, stored reports surface, and the first governed AI runtime consumer. - This ledger therefore treats review-driven governance and portfolio preparation as `fast sellable` or `implemented but not productized`, not `sellable`, until those explicit manual-promotion slices land. - Tests referenced here remain repo-present only. They were not executed for this ledger update. diff --git a/docs/product/roadmap.md b/docs/product/roadmap.md index f5b00775..99d22502 100644 --- a/docs/product/roadmap.md +++ b/docs/product/roadmap.md @@ -1,10 +1,10 @@ # Product Roadmap -> **Status:** Active -> **Last reviewed:** 2026-05-06 -> **Use for:** Current product roadmap, release themes, and prioritization context +> **Status:** Active +> **Last reviewed:** 2026-05-12 +> **Use for:** Current product roadmap, release themes, and prioritization context > **Do not use for:** Implementation truth, spec completion status, or delivery guarantees without repo verification -> **Scoped maintenance:** 2026-05-06 roadmap cleanup after ledger conflict resolution and cross-domain progress / indicator semantics candidate intake; 2026-05-02 repo-based roadmap drift correction, manual-promotion backlog alignment, and enterprise-SaaS deep-research calibration against current specs, standards, and product-truth docs. +> **Scoped maintenance:** 2026-05-12 roadmap alignment after the admin workspace navigation and tenant-owned surface repair candidate intake from the repo-verified navigation/panel audit; 2026-05-06 roadmap cleanup after ledger conflict resolution and cross-domain progress / indicator semantics candidate intake; 2026-05-02 repo-based roadmap drift correction, manual-promotion backlog alignment, and enterprise-SaaS deep-research calibration against current specs, standards, and product-truth docs. > > Strategic thematic blocks and release trajectory. > This is the "big picture" — not individual specs. @@ -67,6 +67,8 @@ ### UI & Product Maturity Polish Parallel manual-promotion guardrail: `docs/product/spec-candidates.md` now carries a dedicated Cross-Domain Progress / Indicator Semantics candidate group so progress, coverage, readiness, risk, usage, score, and generation-state surfaces do not keep drifting behind OperationRun-specific rules. Spec 278 is the docs-first audit slice; its follow-up lanes stay split into standards patch, metric/indicator contract foundation, shared indicator component system, quality gate, and domain migration. +Parallel immediate repair lane: `docs/product/spec-candidates.md` now also carries an Admin Workspace Navigation & Tenant-owned Surface Repair candidate group so workspace-home clean-sidebar rules stay separate from valid environment-bound admin navigation. The first promotion target is `admin-inventory-navigation-cutover`; the tenant-owned surface route audit, groups cutover, contract split, and tenant-panel dead-code retirement remain sequenced manual follow-through rather than one bundled migration. + **Active specs**: 122, 121, 112 ### Secret & Security Hardening @@ -442,6 +444,7 @@ ## Infrastructure & Platform Debt | No formal security trust pack yet | Enterprise sales and customer security reviews require repeated manual explanations | Covered by Solo-Founder SaaS Automation & Operating Readiness | | Auditor-ready executive export is not yet productized | Review truth still stops short of calm auditor-/executive-ready delivery even though the spec package now exists | Covered by `specs/263-auditor-pack-executive-export/spec.md` | | Cross-tenant promotion execution is missing | Compare preview and preflight stop short of the actual portfolio action even though the execution spec package now exists on this branch | Covered by `specs/264-cross-tenant-promotion-execution/spec.md` | +| Admin workspace navigation contract drift remains open | Inventory and adjacent tenant-owned admin surfaces can still conflict between workspace-home clean-sidebar rules and valid environment-bound admin routing, which risks product breaks and stale hide-first test assumptions | Covered by the Admin Workspace Navigation & Tenant-owned Surface Repair candidate group in `docs/product/spec-candidates.md` | | Governance decision register and approval workflow is missing | Decision-based operating still lacks a bounded approval-ready closure and decision-record package with explicit audit trail | Covered by the manual-promotion backlog in `docs/product/spec-candidates.md` | | Customer-facing localization adoption is incomplete | Repo-real locale groundwork is not yet fully productized across customer-safe governance surfaces | Covered by the manual-promotion backlog in `docs/product/spec-candidates.md` | | Billing and subscription truth is missing | Commercial readiness still stops short of a durable billing/subscription truth layer | Covered by the manual-promotion backlog in `docs/product/spec-candidates.md` | @@ -466,6 +469,8 @@ ## Priority Ranking (Current Manual Promotion Order) Parallel immediate guardrail lane: the Cross-Domain Progress / Indicator Semantics candidate group in `docs/product/spec-candidates.md` should be promoted alongside OperationRun maturity when UI semantic drift is the active concern. Spec 278 provides the audit inventory and standards-delta input; the remaining follow-up remains split across contract, component, quality-gate, and migration lanes. It stays outside the main sellability ordering below because it is a cross-cutting semantics and standards package rather than a standalone customer-facing delivery lane. +Parallel immediate repair lane: the Admin Workspace Navigation & Tenant-owned Surface Repair candidate group in `docs/product/spec-candidates.md` should be promoted when Inventory or adjacent tenant-owned admin surfaces drift behind stale hide-first navigation contracts. `admin-inventory-navigation-cutover` is the immediate repair slice; the repo-wide route audit, groups cutover, navigation-contract split, and tenant-panel dead-code retirement stay sequenced manual follow-through outside the main sellability ordering. + 1. Decision Register & Approval Workflow v1 2. Governance Artifact Lifecycle & Retention v1 3. Billing & Subscription Truth Layer v1 -- 2.45.2 From 41c4a14792406bddc654a61f882fc8a36dcf824f Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Thu, 14 May 2026 16:42:15 +0200 Subject: [PATCH 2/2] feat: implement inventory navigation cutover and enhance tenant scoping in admin panel --- .../Clusters/Inventory/InventoryCluster.php | 94 +++++- .../app/Filament/Pages/InventoryCoverage.php | 81 ++++- .../Providers/Filament/AdminPanelProvider.php | 14 + .../EnsureFilamentTenantSelected.php | 26 +- ...301InventoryNavigationCutoverSmokeTest.php | 59 ++++ ...urfacesRedirectToChooseEnvironmentTest.php | 8 +- ...InventoryCoverageAdminTenantParityTest.php | 89 ++++-- .../Feature/Filament/InventoryPagesTest.php | 4 +- .../PanelNavigationSegregationTest.php | 27 +- .../checklists/requirements.md | 89 ++++++ .../plan.md | 213 +++++++++++++ .../spec.md | 298 ++++++++++++++++++ .../tasks.md | 116 +++++++ 13 files changed, 1059 insertions(+), 59 deletions(-) create mode 100644 apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php create mode 100644 specs/301-admin-inventory-navigation-cutover/checklists/requirements.md create mode 100644 specs/301-admin-inventory-navigation-cutover/plan.md create mode 100644 specs/301-admin-inventory-navigation-cutover/spec.md create mode 100644 specs/301-admin-inventory-navigation-cutover/tasks.md diff --git a/apps/platform/app/Filament/Clusters/Inventory/InventoryCluster.php b/apps/platform/app/Filament/Clusters/Inventory/InventoryCluster.php index 53f7adec..866375d9 100644 --- a/apps/platform/app/Filament/Clusters/Inventory/InventoryCluster.php +++ b/apps/platform/app/Filament/Clusters/Inventory/InventoryCluster.php @@ -4,10 +4,16 @@ namespace App\Filament\Clusters\Inventory; +use App\Models\ManagedEnvironment; +use App\Models\Workspace; +use App\Support\Navigation\NavigationScope; +use App\Support\OperateHub\OperateHubShell; use BackedEnum; use Filament\Clusters\Cluster; use Filament\Facades\Filament; use Filament\Pages\Enums\SubNavigationPosition; +use Filament\Panel; +use Illuminate\Database\Eloquent\Model; use UnitEnum; class InventoryCluster extends Cluster @@ -22,10 +28,92 @@ class InventoryCluster extends Cluster public static function shouldRegisterNavigation(): bool { - if (Filament::getCurrentPanel()?->getId() === 'admin') { - return false; + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); + } + + public static function getSlug(?Panel $panel = null): string + { + $panelId = $panel?->getId() ?? Filament::getCurrentOrDefaultPanel()?->getId(); + + if ($panelId !== 'admin') { + return parent::getSlug($panel); } - return parent::shouldRegisterNavigation(); + return 'workspaces/{workspace}/environments/{environment}/inventory'; + } + + /** + * @param array $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); + } + + $resolvedTenant = static::resolveAdminUrlTenant($parameters, $tenant); + + if (! $resolvedTenant instanceof ManagedEnvironment) { + return url('/admin'); + } + + $workspace = static::resolveAdminUrlWorkspace($resolvedTenant, $parameters); + + if (! $workspace instanceof Workspace && ! is_string($workspace) && ! is_int($workspace)) { + return url('/admin'); + } + + $parameters['environment'] ??= $resolvedTenant; + $parameters['workspace'] ??= $workspace; + unset($parameters['tenant']); + + return parent::getUrl($parameters, $isAbsolute, $panelId, null); + } + + /** + * @param array $parameters + */ + private static function resolveAdminUrlTenant(array $parameters, ?Model $tenant = null): ?ManagedEnvironment + { + $parameterTenant = $parameters['tenant'] ?? $parameters['environment'] ?? null; + + if ($parameterTenant instanceof ManagedEnvironment) { + return $parameterTenant; + } + + if ($tenant instanceof ManagedEnvironment) { + return $tenant; + } + + $filamentTenant = Filament::getTenant(); + + if ($filamentTenant instanceof ManagedEnvironment) { + return $filamentTenant; + } + + return app(OperateHubShell::class)->tenantOwnedPanelContext(request()); + } + + /** + * @param array $parameters + */ + private static function resolveAdminUrlWorkspace(ManagedEnvironment $tenant, array $parameters): Workspace|string|int|null + { + $workspace = $parameters['workspace'] ?? null; + + if ($workspace instanceof Workspace || is_string($workspace) || is_int($workspace)) { + return $workspace; + } + + $tenantWorkspace = $tenant->workspace; + + if ($tenantWorkspace instanceof Workspace) { + return $tenantWorkspace; + } + + return $tenant->workspace()->first(); } } diff --git a/apps/platform/app/Filament/Pages/InventoryCoverage.php b/apps/platform/app/Filament/Pages/InventoryCoverage.php index ba985237..3c368bde 100644 --- a/apps/platform/app/Filament/Pages/InventoryCoverage.php +++ b/apps/platform/app/Filament/Pages/InventoryCoverage.php @@ -11,6 +11,7 @@ use App\Models\OperationRun; use App\Models\ManagedEnvironment; use App\Models\User; +use App\Models\Workspace; use App\Services\Auth\CapabilityResolver; use App\Support\Auth\Capabilities; use App\Support\Badges\BadgeCatalog; @@ -20,6 +21,8 @@ use App\Support\Badges\TagBadgeDomain; use App\Support\Inventory\TenantCoverageTruth; use App\Support\Inventory\TenantCoverageTruthResolver; +use App\Support\Navigation\NavigationScope; +use App\Support\OperateHub\OperateHubShell; use App\Support\OperationRunLinks; use App\Support\OperationRunType; use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration; @@ -38,6 +41,7 @@ use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\Str; use UnitEnum; @@ -77,11 +81,38 @@ public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration public static function shouldRegisterNavigation(): bool { - if (Filament::getCurrentPanel()?->getId() === 'admin') { - return false; + return NavigationScope::shouldRegisterEnvironmentNavigation() + && parent::shouldRegisterNavigation(); + } + + /** + * @param array $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); } - return parent::shouldRegisterNavigation(); + $resolvedTenant = static::resolveAdminUrlTenant($parameters, $tenant); + + if (! $resolvedTenant instanceof ManagedEnvironment) { + return url('/admin'); + } + + $workspace = static::resolveAdminUrlWorkspace($resolvedTenant, $parameters); + + if (! $workspace instanceof Workspace && ! is_string($workspace) && ! is_int($workspace)) { + return url('/admin'); + } + + $parameters['environment'] ??= $resolvedTenant; + $parameters['workspace'] ??= $workspace; + unset($parameters['tenant']); + + return parent::getUrl($parameters, $isAbsolute, $panelId, null); } public static function canAccess(): bool @@ -564,4 +595,48 @@ private function inventorySyncHistoryUrl(ManagedEnvironment $tenant): string { return OperationRunLinks::index($tenant, operationType: OperationRunType::InventorySync->value); } + + /** + * @param array $parameters + */ + private static function resolveAdminUrlTenant(array $parameters, ?Model $tenant = null): ?ManagedEnvironment + { + $parameterTenant = $parameters['tenant'] ?? $parameters['environment'] ?? null; + + if ($parameterTenant instanceof ManagedEnvironment) { + return $parameterTenant; + } + + if ($tenant instanceof ManagedEnvironment) { + return $tenant; + } + + $filamentTenant = Filament::getTenant(); + + if ($filamentTenant instanceof ManagedEnvironment) { + return $filamentTenant; + } + + return app(OperateHubShell::class)->tenantOwnedPanelContext(request()); + } + + /** + * @param array $parameters + */ + private static function resolveAdminUrlWorkspace(ManagedEnvironment $tenant, array $parameters): Workspace|string|int|null + { + $workspace = $parameters['workspace'] ?? null; + + if ($workspace instanceof Workspace || is_string($workspace) || is_int($workspace)) { + return $workspace; + } + + $tenantWorkspace = $tenant->workspace; + + if ($tenantWorkspace instanceof Workspace) { + return $tenantWorkspace; + } + + return $tenant->workspace()->first(); + } } diff --git a/apps/platform/app/Providers/Filament/AdminPanelProvider.php b/apps/platform/app/Providers/Filament/AdminPanelProvider.php index 60b491f7..e1eb7498 100644 --- a/apps/platform/app/Providers/Filament/AdminPanelProvider.php +++ b/apps/platform/app/Providers/Filament/AdminPanelProvider.php @@ -20,6 +20,7 @@ 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; @@ -37,6 +38,7 @@ use App\Services\Auth\WorkspaceRoleCapabilityMap; use App\Support\Auth\Capabilities; use App\Support\Filament\PanelThemeAsset; +use App\Support\Navigation\NavigationScope; use App\Support\OperationRunLinks; use App\Support\Workspaces\WorkspaceContext; use Filament\Http\Middleware\Authenticate; @@ -83,6 +85,18 @@ public function panel(Panel $panel): Panel ]) ->navigationItems([ WorkspaceOverview::navigationItem(), + NavigationItem::make('Items') + ->url(fn (): string => InventoryCluster::getUrl(panel: 'admin')) + ->icon('heroicon-o-squares-2x2') + ->group('Inventory') + ->sort(1) + ->visible(fn (): bool => NavigationScope::shouldRegisterEnvironmentNavigation() && InventoryCluster::canAccess()), + NavigationItem::make('Coverage') + ->url(fn (): string => InventoryCoverage::getUrl(panel: 'admin')) + ->icon('heroicon-o-table-cells') + ->group('Inventory') + ->sort(3) + ->visible(fn (): bool => NavigationScope::shouldRegisterEnvironmentNavigation() && InventoryCoverage::canAccess()), NavigationItem::make(fn (): string => __('localization.navigation.integrations')) ->url(fn (): string => route('filament.admin.resources.provider-connections.index')) ->icon('heroicon-o-link') diff --git a/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php b/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php index 571b2333..5832d241 100644 --- a/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php +++ b/apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php @@ -66,32 +66,32 @@ public function handle(Request $request, Closure $next): Response $refererPath = '/'.ltrim((string) $refererPath, '/'); if ($this->isCanonicalWorkspaceRecordViewerPath($refererPath)) { - $this->configureNavigationForRequest($panel); + $this->configureNavigationForRequest($panel, $request); return $next($request); } if ($this->isWorkspaceScopedPageWithTenant($refererPath)) { - $this->configureNavigationForRequest($panel); + $this->configureNavigationForRequest($panel, $request); return $next($request); } } if ($this->isCanonicalWorkspaceRecordViewerPath($path)) { - $this->configureNavigationForRequest($panel); + $this->configureNavigationForRequest($panel, $request); return $next($request); } if (in_array($path, ['/admin/findings/my-work', '/admin/findings/intake', '/admin/findings/hygiene'], true)) { - $this->configureNavigationForRequest($panel); + $this->configureNavigationForRequest($panel, $request); return $next($request); } if (preg_match('#^/admin/workspaces/[^/]+/operations(?:/[^/]+)?$#', $path) === 1) { - $this->configureNavigationForRequest($panel); + $this->configureNavigationForRequest($panel, $request); return $next($request); } @@ -141,35 +141,31 @@ public function handle(Request $request, Closure $next): Response str_starts_with($path, '/admin/workspaces/') || in_array($path, ['/admin', '/admin/choose-workspace', '/admin/choose-environment', '/admin/no-access', '/admin/alerts', '/admin/audit-log', '/admin/onboarding', '/admin/settings/workspace', '/admin/findings/my-work', '/admin/findings/intake', '/admin/findings/hygiene'], true) ) { - $this->configureNavigationForRequest($panel); + $this->configureNavigationForRequest($panel, $request); return $next($request); } if (filled(Filament::getTenant())) { - $this->configureNavigationForRequest($panel); + $this->configureNavigationForRequest($panel, $request); return $next($request); } if (! $user instanceof User) { - $this->configureNavigationForRequest($panel); + $this->configureNavigationForRequest($panel, $request); return $next($request); } - $this->configureNavigationForRequest($panel); + $this->configureNavigationForRequest($panel, $request); return $next($request); } - private function configureNavigationForRequest(\Filament\Panel $panel): void + private function configureNavigationForRequest(\Filament\Panel $panel, Request $request): void { - if (! $panel->hasTenancy()) { - return; - } - - if (NavigationScope::isEnvironmentSurface()) { + if (NavigationScope::isEnvironmentSurface($request)) { $panel->navigation(true); return; diff --git a/apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php b/apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php new file mode 100644 index 00000000..7bb0e629 --- /dev/null +++ b/apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php @@ -0,0 +1,59 @@ +browser()->timeout(15_000); + +it('smokes inventory navigation cutover between workspace and environment surfaces', function (): void { + $tenant = ManagedEnvironment::factory()->create([ + 'name' => 'Spec301 Inventory Navigation', + 'external_id' => 'spec301-inventory-navigation', + ]); + + [$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); + + $this->actingAs($user)->withSession([ + WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, + WorkspaceContext::LAST_TENANT_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, [ + (string) $tenant->workspace_id => (int) $tenant->getKey(), + ]); + + $workspaceHomeUrl = route('admin.workspace.home', ['workspace' => (int) $tenant->workspace_id]); + $environmentUrl = ManagedEnvironmentLinks::viewUrl($tenant); + $inventoryItemsUrl = InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant); + $coverageUrl = InventoryCoverage::getUrl(panel: 'admin', tenant: $tenant); + $inventoryClusterPathPattern = '\\/admin\\/workspaces\\/[^\\/]+\\/environments\\/[^\\/]+\\/inventory$'; + $coveragePath = (string) parse_url($coverageUrl, PHP_URL_PATH); + + visit($workspaceHomeUrl) + ->waitForText('Overview') + ->assertNoJavaScriptErrors() + ->assertScript("Array.from(document.querySelectorAll('a')).every((link) => ! new RegExp('{$inventoryClusterPathPattern}').test(link.pathname))", true) + ->assertScript("Array.from(document.querySelectorAll('a')).every((link) => link.pathname !== '{$coveragePath}')", true); + + visit($environmentUrl) + ->waitForText('Spec301 Inventory Navigation') + ->assertNoJavaScriptErrors() + ->assertScript("Array.from(document.querySelectorAll('a')).some((link) => new RegExp('{$inventoryClusterPathPattern}').test(link.pathname))", true); + + visit($inventoryItemsUrl) + ->waitForText('Inventory Items') + ->assertNoJavaScriptErrors() + ->assertScript("document.querySelector('a[href=\"{$coverageUrl}\"]') !== null", true) + ->click('Coverage') + ->waitForText('ManagedEnvironment coverage truth') + ->assertNoJavaScriptErrors() + ->assertScript("window.location.pathname === '{$coveragePath}'", true); +}); diff --git a/apps/platform/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseEnvironmentTest.php b/apps/platform/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseEnvironmentTest.php index b5729b2b..e7be25fb 100644 --- a/apps/platform/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseEnvironmentTest.php +++ b/apps/platform/tests/Feature/Filament/AdminTenantScopedSurfacesRedirectToChooseEnvironmentTest.php @@ -73,7 +73,13 @@ ->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get('/admin/inventory') - ->assertRedirect('/admin/workspaces/'.$workspace->getKey().'/environments'); + ->assertNotFound(); + + $this + ->actingAs($user) + ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) + ->get('/admin/inventory/inventory-coverage') + ->assertNotFound(); }); it('allows tenant-scoped admin surfaces to load from the remembered canonical tenant', function (string $path): void { diff --git a/apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php b/apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php index b4e187ea..5c56e7c7 100644 --- a/apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php +++ b/apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php @@ -8,6 +8,7 @@ use App\Support\Badges\BadgeCatalog; use App\Support\Badges\BadgeDomain; use App\Support\Inventory\InventoryCoverage as InventoryCoveragePayload; +use App\Support\OperationRunLinks; use App\Support\Workspaces\WorkspaceContext; use Filament\Facades\Filament; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -15,50 +16,37 @@ uses(RefreshDatabase::class); -it('loads inventory coverage from the remembered canonical tenant in the admin panel', function (): void { - $tenantA = ManagedEnvironment::factory()->create(); - [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner'); - $tenantB = ManagedEnvironment::factory()->create(['workspace_id' => (int) $tenantA->workspace_id]); - createUserWithTenant(tenant: $tenantB, user: $user, role: 'owner'); - - OperationRun::factory()->create([ - 'managed_environment_id' => (int) $tenantA->getKey(), - 'workspace_id' => (int) $tenantA->workspace_id, +function seedInventoryCoverageParityRun(ManagedEnvironment $tenant, string $status, int $itemCount): OperationRun +{ + return OperationRun::factory()->create([ + 'managed_environment_id' => (int) $tenant->getKey(), + 'workspace_id' => (int) $tenant->workspace_id, 'type' => 'inventory_sync', 'status' => 'completed', - 'outcome' => 'failed', + 'outcome' => $status === InventoryCoveragePayload::StatusSucceeded ? 'succeeded' : 'failed', 'context' => [ 'inventory' => [ 'coverage' => InventoryCoveragePayload::buildPayload([ 'deviceConfiguration' => [ - 'status' => InventoryCoveragePayload::StatusFailed, - 'item_count' => 0, - 'error_code' => 'graph_forbidden', + 'status' => $status, + 'item_count' => $itemCount, + ...($status === InventoryCoveragePayload::StatusFailed ? ['error_code' => 'graph_forbidden'] : []), ], ], []), ], ], 'completed_at' => now(), ]); +} - OperationRun::factory()->create([ - 'managed_environment_id' => (int) $tenantB->getKey(), - 'workspace_id' => (int) $tenantB->workspace_id, - 'type' => 'inventory_sync', - 'status' => 'completed', - 'outcome' => 'succeeded', - 'context' => [ - 'inventory' => [ - 'coverage' => InventoryCoveragePayload::buildPayload([ - 'deviceConfiguration' => [ - 'status' => InventoryCoveragePayload::StatusSucceeded, - 'item_count' => 1, - ], - ], []), - ], - ], - 'completed_at' => now()->subMinute(), - ]); +it('loads inventory coverage from the remembered canonical tenant in the admin panel', function (): void { + $tenantA = ManagedEnvironment::factory()->create(); + [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner'); + $tenantB = ManagedEnvironment::factory()->create(['workspace_id' => (int) $tenantA->workspace_id]); + createUserWithTenant(tenant: $tenantB, user: $user, role: 'owner'); + + $runA = seedInventoryCoverageParityRun($tenantA, InventoryCoveragePayload::StatusFailed, 0); + $runB = seedInventoryCoverageParityRun($tenantB, InventoryCoveragePayload::StatusSucceeded, 1); $this->actingAs($user); Filament::setCurrentPanel('admin'); @@ -79,3 +67,42 @@ 'policy:deviceConfiguration', ); }); + +it('generates the canonical workspace environment inventory coverage URL', function (): void { + $tenant = ManagedEnvironment::factory()->create(); + [, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner'); + + $url = InventoryCoverage::getUrl(panel: 'admin', tenant: $tenant); + $path = (string) parse_url($url, PHP_URL_PATH); + + expect($path) + ->toBe(sprintf( + '/admin/workspaces/%s/environments/%s/inventory/inventory-coverage', + (string) $tenant->workspace_id, + (string) $tenant->getRouteKey(), + )) + ->and($path)->not->toBe('/admin/inventory/inventory-coverage'); +}); + +it('loads inventory coverage from the canonical environment route before remembered context', function (): void { + $tenantA = ManagedEnvironment::factory()->create(); + [$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'owner'); + $tenantB = ManagedEnvironment::factory()->create(['workspace_id' => (int) $tenantA->workspace_id]); + createUserWithTenant(tenant: $tenantB, user: $user, role: 'owner'); + + $runA = seedInventoryCoverageParityRun($tenantA, InventoryCoveragePayload::StatusFailed, 0); + $runB = seedInventoryCoverageParityRun($tenantB, InventoryCoveragePayload::StatusSucceeded, 1); + + $this->actingAs($user) + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, + WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [ + (string) $tenantA->workspace_id => (int) $tenantB->getKey(), + ], + ]) + ->get(InventoryCoverage::getUrl(panel: 'admin', tenant: $tenantA)) + ->assertOk() + ->assertSee(BadgeCatalog::spec(BadgeDomain::InventoryCoverageState, 'failed')->label) + ->assertSee(OperationRunLinks::view($runA, $tenantA), false) + ->assertDontSee(OperationRunLinks::view($runB, $tenantB), false); +}); diff --git a/apps/platform/tests/Feature/Filament/InventoryPagesTest.php b/apps/platform/tests/Feature/Filament/InventoryPagesTest.php index 24f00368..a283e391 100644 --- a/apps/platform/tests/Feature/Filament/InventoryPagesTest.php +++ b/apps/platform/tests/Feature/Filament/InventoryPagesTest.php @@ -68,7 +68,7 @@ function seedInventoryCoverageBasis(ManagedEnvironment $tenant): OperationRun $basisRun = seedInventoryCoverageBasis($tenant); $itemsUrl = InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant); - $coverageUrl = InventoryCoverage::getUrl(panel: 'admin').'?tenant='.urlencode((string) $tenant->external_id); + $coverageUrl = InventoryCoverage::getUrl(panel: 'admin', tenant: $tenant); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) @@ -106,7 +106,7 @@ function seedInventoryCoverageBasis(ManagedEnvironment $tenant): OperationRun $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get(InventoryCoverage::getUrl(panel: 'admin').'?tenant='.urlencode((string) $tenant->external_id)) + ->get(InventoryCoverage::getUrl(panel: 'admin', tenant: $tenant)) ->assertOk() ->assertSee('No current coverage basis') ->assertSee('Run Inventory Sync from Inventory Items to establish current tenant coverage truth.') diff --git a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php index 91003772..69d51f4f 100644 --- a/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php +++ b/apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php @@ -27,12 +27,12 @@ }); dataset('admin hidden navigation classes', [ - InventoryCluster::class, - InventoryCoverage::class, EntraGroupResource::class, ]); dataset('environment visible navigation classes', [ + InventoryCluster::class, + InventoryCoverage::class, InventoryItemResource::class, PolicyResource::class, PolicyVersionResource::class, @@ -44,7 +44,12 @@ function bindNavigationRequestPath(string $path): void { - app()->instance('request', Request::create($path)); + $request = Request::create($path); + $route = app('router')->getRoutes()->match($request); + + $request->setRouteResolver(static fn () => $route); + + app()->instance('request', $request); } it('keeps admin-hidden tenant surfaces out of navigation registration', function (string $class): void { @@ -61,8 +66,13 @@ function bindNavigationRequestPath(string $path): void })->with('environment visible navigation classes'); it('registers environment-owned surfaces only on environment surfaces', function (string $class): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + $this->actingAs($user); + session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); + Filament::setCurrentPanel('admin'); - bindNavigationRequestPath('/admin/workspaces/workspace-alpha/environments/environment-alpha'); + bindNavigationRequestPath(ManagedEnvironmentLinks::viewUrl($tenant)); expect($class::shouldRegisterNavigation())->toBeTrue(); })->with('environment visible navigation classes'); @@ -165,6 +175,7 @@ function bindNavigationRequestPath(string $path): void $response->assertSeeText('Policies'); $response->assertSeeText('Policy Versions'); + $response->assertSeeText('Items'); $response->assertSeeText('Backup Schedules'); $response->assertSeeText('Backup Sets'); $response->assertSeeText('Restore Runs'); @@ -174,4 +185,12 @@ function bindNavigationRequestPath(string $path): void $response->assertSeeText('Baseline Compare'); $response->assertSeeText('Evidence'); $response->assertSeeText('Risk exceptions'); + + $this->actingAs($user) + ->withSession([ + WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, + ]) + ->get(InventoryItemResource::getUrl('index', panel: 'admin', tenant: $tenant)) + ->assertOk() + ->assertSeeText('Coverage'); }); diff --git a/specs/301-admin-inventory-navigation-cutover/checklists/requirements.md b/specs/301-admin-inventory-navigation-cutover/checklists/requirements.md new file mode 100644 index 00000000..11d55fa6 --- /dev/null +++ b/specs/301-admin-inventory-navigation-cutover/checklists/requirements.md @@ -0,0 +1,89 @@ +# Requirements Checklist: Admin Inventory Navigation Cutover + +**Purpose**: Validate that the Spec 301 preparation package is complete, bounded, and ready for implementation. +**Created**: 2026-05-14 +**Feature**: [spec.md](../spec.md) + +## Applicability And Low-Impact Gate + +- [x] The package explicitly changes an operator-facing navigation surface and does not use a false low-impact `N/A`. +- [x] `spec.md`, `plan.md`, and `tasks.md` carry the same native Filament navigation classification, shared-family relevance, and no-exception decision. + +## Candidate Selection + +- [x] The selected candidate exists in `docs/product/spec-candidates.md` as `admin-inventory-navigation-cutover`. +- [x] The candidate was explicitly manually promoted by the user and assigned number `301`. +- [x] The selected slice is the first recommended item in the Admin Workspace Navigation & Tenant-owned Surface Repair group. +- [x] Close alternatives are deferred: `tenant-owned-surface-route-audit`, `admin-directory-groups-cutover`, `navigation-contract-split`, and `tenant-panel-dead-code-retirement`. +- [x] Related completed or close-out specs `279` through `300` are context only and are not modified. + +## Scope And Requirements + +- [x] The package stays on Inventory Items, Inventory Coverage, and Inventory cluster navigation only. +- [x] Workspace-home sidebar cleanliness remains an explicit negative-control requirement. +- [x] Environment-bound Inventory visibility is explicit and testable. +- [x] Inventory Coverage context resolution remains tied to the current canonical route/remembered-context contract. +- [x] Inventory Coverage canonical URL generation is explicitly required and covered by tasks. +- [x] Entra Groups are explicitly out of scope. +- [x] No new persistence, migration, model, service, job, provider contract, route family, asset, or OperationRun lifecycle is introduced. + +## Native, Shared-Family, And State Ownership + +- [x] The surface remains native Filament cluster/resource/page navigation. +- [x] The shared path to reuse is named as `NavigationScope::shouldRegisterEnvironmentNavigation()` plus existing workspace/environment route helpers. +- [x] Shell, page, route context, and Livewire referer-path ownership are named without introducing a second state owner. +- [x] The likely next operator action is coherent: open Inventory Items or Coverage from an environment-bound sidebar. + +## Shared Pattern Reuse + +- [x] The navigation interaction class is explicitly marked. +- [x] The package extends the existing navigation scope instead of adding a local Inventory navigation framework. +- [x] The package does not create a parallel UX language for tenant-owned surface navigation. + +## OperationRun Start UX Contract + +- [x] The package explicitly says it does not create, queue, deduplicate, resume, block, complete, or deep-link to a new `OperationRun`. +- [x] Existing Inventory Sync OperationRun behavior remains unchanged. +- [x] No queued or terminal notification policy changes are introduced. + +## Provider Boundary And Vocabulary + +- [x] The package explicitly says no shared provider/platform boundary is touched. +- [x] Provider-specific Inventory item metadata remains unchanged and does not spread into navigation truth. +- [x] Operator-visible terms remain workspace, environment, Inventory, and Coverage. + +## Signals, Exceptions, And Test Depth + +- [x] The triggered repository signal is classified as a review-mandatory Inventory navigation repair. +- [x] No bounded exception is needed. +- [x] The required surface profile is `standard-native-filament`. +- [x] Planned proof stays focused on Filament Feature tests plus one optional Browser smoke. +- [x] Fixture/helper cost remains low and reuses existing workspace/environment test helpers. + +## Audience-Aware Disclosure And Decision Hierarchy + +- [x] Navigation stays decision-light and opens existing Inventory pages instead of duplicating status summaries. +- [x] Workspace home remains workspace-level and avoids tenant-owned default-visible content. +- [x] Raw/support diagnostics remain inside existing Inventory pages and are not exposed through navigation. +- [x] Exactly one dominant navigation intent is preserved: open Inventory within the active environment. + +## Filament v5 Checklist + +- [x] Filament v5 targets Livewire v4.0+; this repo currently uses Livewire 4.1.4. +- [x] No provider registration changes are planned; existing providers remain registered in `apps/platform/bootstrap/providers.php`. +- [x] `InventoryItemResource` has a View page, so global search destination eligibility remains satisfied if global search is enabled. +- [x] `InventoryCoverage` is a Page, not a globally searchable Resource. +- [x] No destructive actions are introduced or changed. +- [x] No assets are registered; deploy `filament:assets` posture remains unchanged. +- [x] Planned tests target Filament navigation/resource/page behavior using Feature tests and a bounded Browser smoke if rendered sidebar proof is needed. + +## Review Outcome + +- [x] Review outcome class: `acceptable-special-case`. +- [x] Workflow outcome: `document-in-feature`. +- [x] Final note location: active feature PR close-out entry `Guardrail / Exception / Smoke Coverage`. + +## Preparation Result + +- No application implementation was performed while preparing this package. +- Spec Readiness Gate is expected to pass after artifact analysis confirms no placeholders, contradictions, or scope leaks remain. diff --git a/specs/301-admin-inventory-navigation-cutover/plan.md b/specs/301-admin-inventory-navigation-cutover/plan.md new file mode 100644 index 00000000..b1ccdb87 --- /dev/null +++ b/specs/301-admin-inventory-navigation-cutover/plan.md @@ -0,0 +1,213 @@ +# Implementation Plan: Admin Inventory Navigation Cutover + +**Branch**: `301-admin-inventory-navigation-cutover` | **Date**: 2026-05-14 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/301-admin-inventory-navigation-cutover/spec.md` + +## Summary + +Restore Inventory as a discoverable environment-bound admin navigation surface. The implementation should remove or narrow Inventory-only blanket admin navigation suppression, reuse the current `NavigationScope` environment-surface rule, preserve workspace-home sidebar cleanliness, and update tests so they protect the correct split: workspace home hides tenant-owned navigation, environment routes show Inventory. + +No application implementation is performed in this preparation step. + +## Technical Context + +**Language/Version**: PHP 8.4.15, Laravel 12.52.0 +**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1 +**Storage**: PostgreSQL, no schema changes +**Testing**: Pest feature tests; optional Browser smoke for visible sidebar behavior +**Validation Lanes**: confidence, browser if smoke is added +**Target Platform**: Laravel Sail local development; Dokploy container deployment for staging/production +**Project Type**: Laravel application in `apps/platform` +**Performance Goals**: Navigation/page-load remains DB-only; no Graph calls or queued work are introduced +**Constraints**: Workspace and managed-environment isolation must remain enforced; workspace home must not show tenant-owned navigation; no Entra Groups decision +**Scale/Scope**: Two Inventory surfaces plus tests + +## UI / Surface Guardrail Plan + +- **Guardrail scope**: changed operator-facing navigation surfaces +- **Native vs custom classification summary**: native Filament navigation/cluster/page/resource +- **Shared-family relevance**: navigation entry points +- **State layers in scope**: shell, page, route context, Livewire update referer handling through existing `NavigationScope` +- **Audience modes in scope**: operator-MSP +- **Decision/diagnostic/raw hierarchy plan**: navigation-only entry points; diagnostics stay inside existing Inventory pages +- **Raw/support gating plan**: N/A +- **One-primary-action / duplicate-truth control**: Sidebar links open existing Inventory pages and do not add alternate status summaries or action cards +- **Handling modes by drift class or surface**: review-mandatory for Inventory navigation; report-only for unrelated tenant-owned surfaces +- **Repository-signal treatment**: `InventoryCluster` and `InventoryCoverage` blanket admin-hidden checks are active repair targets; Entra Groups remains follow-up +- **Special surface test profiles**: standard-native-filament +- **Required tests or manual smoke**: feature tests required; browser smoke recommended if sidebar rendering is touched visibly +- **Exception path and spread control**: none +- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage + +## Shared Pattern & System Fit + +- **Cross-cutting feature marker**: yes +- **Systems touched**: Filament navigation, Inventory cluster/page/resource, `NavigationScope`, panel/context tests +- **Shared abstractions reused**: `NavigationScope::shouldRegisterEnvironmentNavigation()`, `WorkspaceScopedTenantRoutes`, `ResolvesPanelTenantContext`, `OperateHubShell` +- **New abstraction introduced? why?**: none +- **Why the existing abstraction was sufficient or insufficient**: Existing environment-navigation detection is sufficient. Current Inventory cluster/page classes are insufficient because they bypass it with a blanket admin-panel hidden rule. +- **Bounded deviation / spread control**: none. Do not create a local Inventory-only replacement for `NavigationScope`. + +## OperationRun UX Impact + +- **Touches OperationRun start/completion/link UX?**: no +- **Central contract reused**: N/A +- **Delegated UX behaviors**: Existing Inventory Sync start behavior remains unchanged +- **Surface-owned behavior kept local**: Navigation visibility only +- **Queued DB-notification policy**: N/A +- **Terminal notification path**: N/A +- **Exception path**: none + +## Provider Boundary & Portability Fit + +- **Shared provider/platform boundary touched?**: no +- **Provider-owned seams**: N/A +- **Platform-core seams**: N/A +- **Neutral platform terms / contracts preserved**: workspace, environment, Inventory, Coverage +- **Retained provider-specific semantics and why**: Existing Inventory item provider metadata remains unchanged +- **Bounded extraction or follow-up path**: none + +## Constitution Check + +*GATE: Must pass before implementation starts. Re-check after implementation.* + +- Inventory-first: pass. Inventory remains last-observed managed-environment truth. +- Read/write separation: pass. Navigation repair is read-only; existing Inventory Sync action remains unchanged. +- Graph contract path: pass. No Graph calls or contract changes. +- Deterministic capabilities: pass. Existing capability resolver remains the authorization path. +- RBAC-UX: pass. UI visibility remains non-security; existing access checks stay authoritative. +- Workspace isolation: pass. Workspace home negative-control tests remain mandatory. +- Tenant/managed-environment isolation: pass. Wrong workspace/environment pairs remain not found. +- Run observability: N/A. No new OperationRun lifecycle. +- Test governance: pass if tasks keep feature/browser proof focused and avoid a route-audit sweep. +- Proportionality: pass. No new structures, persistence, states, or frameworks. +- Shared pattern first: pass. Reuse `NavigationScope`. +- Provider boundary: pass. No provider seam change. +- Filament-native UI: pass. Native Filament cluster/resource/page navigation only; no custom UI. +- Filament v5 / Livewire v4: pass. The repo uses Filament 5.2.1 and Livewire 4.1.4. +- Panel provider registration: unchanged. Laravel provider registration remains in `apps/platform/bootstrap/providers.php`; no provider is added to `bootstrap/app.php`. +- Global search: `InventoryItemResource` already has a View page. `InventoryCoverage` is a page, not a globally searchable resource. No new globally searchable resource is introduced. +- Destructive actions: none introduced or changed. Existing Inventory Sync is an operation start, not changed by this slice; any touched action must retain authorization and current confirmation/notification behavior. +- Asset strategy: no assets are registered; deploy `filament:assets` posture is unchanged. + +## Test Governance Check + +- **Test purpose / classification by changed surface**: Feature for navigation registration and context; Browser only for rendered sidebar proof +- **Affected validation lanes**: confidence, browser +- **Why this lane mix is the narrowest sufficient proof**: The behavior is mostly PHP navigation gating and route/context resolution. One browser smoke is enough to prove the visible sidebar if it changes. +- **Narrowest proving command(s)**: + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php tests/Feature/Filament/InventoryHubDbOnlyTest.php tests/Feature/Filament/InventoryPagesTest.php` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php` if added + - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` +- **Fixture / helper / factory / seed / context cost risks**: Low; reuse existing tenant/workspace helpers and Inventory factories +- **Expensive defaults or shared helper growth introduced?**: no +- **Heavy-family additions, promotions, or visibility changes**: none +- **Surface-class relief / special coverage rule**: standard-native-filament +- **Closing validation and reviewer handoff**: Verify the feature does not change Entra Groups, does not widen into route-audit, and does not show Inventory on workspace home. +- **Budget / baseline / trend follow-up**: none +- **Review-stop questions**: Has workspace-home cleanliness remained a test? Are Inventory cluster/page visible in environment context? Did any unrelated surface become visible? +- **Escalation path**: document-in-feature +- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage +- **Why no dedicated follow-up spec is needed**: The next follow-up specs are already named in `docs/product/spec-candidates.md`; this slice only repairs Inventory. + +## Project Structure + +### Documentation (this feature) + +```text +specs/301-admin-inventory-navigation-cutover/ +├── spec.md +├── plan.md +├── tasks.md +└── checklists/ + └── requirements.md +``` + +### Source Code (repository root) + +Likely affected runtime surfaces during later implementation: + +```text +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/Support/Navigation/NavigationScope.php +apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php +apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php +apps/platform/tests/Feature/Filament/InventoryHubDbOnlyTest.php +apps/platform/tests/Feature/Filament/InventoryPagesTest.php +apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php +``` + +**Structure Decision**: Use existing Laravel/Filament app structure. Do not create new base folders or new shared navigation framework. + +## Complexity Tracking + +No constitution violation or BLOAT-001 trigger is expected. If implementation introduces any new abstraction, route family, persistent state, status family, or UI framework, stop and update this plan before continuing. + +| Violation | Why Needed | Simpler Alternative Rejected Because | +|---|---|---| +| N/A | N/A | N/A | + +## Data Model + +No schema, model, migration, factory, seeder, or persisted entity changes are in scope. + +## Technical Approach + +1. Update proof first: + - Split current admin-hidden navigation expectations so workspace-home hidden behavior is preserved while environment-bound Inventory visibility is expected. + - Keep Entra Groups in the admin-hidden dataset or otherwise explicitly assert it remains out of scope. + - Add Coverage URL proof so `InventoryCoverage::getUrl(panel: 'admin', tenant: $environment)` resolves to the canonical workspace/environment route. +2. Converge Inventory navigation: + - Replace blanket `Filament::getCurrentPanel()?->getId() === 'admin'` false returns in Inventory cluster/page classes with the shared environment-navigation rule. + - Ensure `InventoryItemResource` remains aligned and no route-name workaround is regressed. +3. Preserve context behavior: + - Align `InventoryCoverage` route/URL behavior with the canonical environment route if the page still uses a flat admin path. + - Keep `InventoryCoverage` resolving context through `ResolvesPanelTenantContext` and `OperateHubShell`. + - Do not add query-only navigation or a new remembered-environment state. +4. Validate: + - Run focused Filament feature tests. + - Add/run a browser smoke only if the rendered sidebar proof is needed. + - Run Pint for touched platform files. + +## Risk Controls + +- Do not touch Entra Groups. +- Do not modify completed Specs `279-300`. +- Do not replace `NavigationScope` with a page-local helper. +- Do not expose environment-owned navigation on `/admin/workspaces/{workspace}`. +- Do not add Graph calls, queue work, assets, migrations, or OperationRun lifecycle changes. + +## Rollout Considerations + +- **Staging**: Run the focused feature tests and browser smoke before promotion. +- **Production**: No migration, env var, queue, cron, storage, or asset deployment impact is expected. +- **Rollback**: Revert the small navigation/test change if sidebar behavior regresses. No data rollback is required. + +## Implementation Phases + +### Phase 1 - Proof and Scope Lock + +Update tests so the current bug is expressed as a failing expectation: Inventory is visible in environment context and hidden at workspace home. + +### Phase 2 - Inventory Navigation Repair + +Adjust Inventory cluster/page navigation registration to use the existing environment-navigation contract. + +### Phase 3 - Context and DB-only Safety + +Re-run Inventory Coverage and Inventory hub tests to confirm context resolution, DB-only rendering, and coverage truth remain unchanged. + +### Phase 4 - Validation and Close-Out + +Run the focused validation commands, optional browser smoke, Pint, and final scope checks. Record the close-out note in the active PR. + +## Filament v5 Output Contract + +1. **Livewire v4.0+ compliance**: The repo uses Livewire 4.1.4 with Filament 5.2.1. This slice must not reference Livewire v3 APIs. +2. **Provider registration location**: No provider changes are planned. Existing Laravel provider registration remains in `apps/platform/bootstrap/providers.php`. +3. **Globally searchable resources**: `InventoryItemResource` has a View page. `InventoryCoverage` is a page, not a globally searchable resource. No new global search behavior is introduced. +4. **Destructive actions**: None are introduced or changed. If any touched action becomes destructive during implementation, it must use `Action::make(...)->action(...)`, `->requiresConfirmation()`, authorization, and current audit/notification rules. +5. **Asset strategy**: No assets are registered. Deployment `filament:assets` posture remains unchanged. +6. **Testing plan**: Cover Filament navigation registration, workspace-home negative control, environment-bound visibility, Inventory Coverage context, DB-only Inventory rendering, and a bounded browser smoke if rendered sidebar behavior changes. diff --git a/specs/301-admin-inventory-navigation-cutover/spec.md b/specs/301-admin-inventory-navigation-cutover/spec.md new file mode 100644 index 00000000..7b00fa23 --- /dev/null +++ b/specs/301-admin-inventory-navigation-cutover/spec.md @@ -0,0 +1,298 @@ +# Feature Specification: Admin Inventory Navigation Cutover + +**Feature Branch**: `301-admin-inventory-navigation-cutover` +**Created**: 2026-05-14 +**Status**: Draft +**Input**: User description: "mach als naechstes admin-inventory-navigation-cutover und mach mit 301 weiter" + +## Spec Candidate Check *(mandatory - SPEC-GATE-001)* + +- **Problem**: Inventory is already a repo-real admin surface under workspace and managed-environment routing, but `InventoryCluster` and `InventoryCoverage` still carry a blanket admin-hidden navigation rule. Operators can reach parts of Inventory through canonical URLs, links, or remembered environment context, while the sidebar still implies Inventory is not an admin-runtime surface. +- **Today's failure**: The admin sidebar hides Inventory even when an operator is inside a valid `/admin/workspaces/{workspace}/environments/{environment}` context. Current navigation tests protect both the intended workspace-home clean-sidebar rule and the stale blanket "admin can never see Inventory" rule, so a real product break is encoded as regression protection. +- **User-visible improvement**: Tenant operators can discover Inventory Items and Coverage from an environment-bound admin context without reopening tenant-owned navigation on the workspace home page. +- **Smallest enterprise-capable version**: Limit the change to Inventory Items, Inventory Coverage, and the Inventory cluster visibility contract. Keep workspace-home sidebar cleanliness intact and update tests to distinguish workspace-level navigation from environment-bound navigation. +- **Explicit non-goals**: No Entra Groups navigation decision, no generic tenant-owned surface audit, no navigation framework redesign, no system-panel changes, no workspace-home information-architecture rewrite, no tenant-panel dead-code retirement, and no new persisted state. +- **Permanent complexity imported**: No new models, tables, statuses, enums, services, registries, or UI frameworks. The lasting cost is a narrower navigation contract plus focused Filament feature/browser proof. +- **Why now**: `docs/product/spec-candidates.md` marks this as the immediate manual-promotion slice because Inventory is the clearest repo-verified navigation drift seam after the workspace-first managed-environment cutover work. +- **Why not local**: A one-line visibility flip would risk reintroducing tenant-owned navigation on workspace home. The repair must encode the distinction between workspace-scope and environment-scope navigation in tests. +- **Approval class**: Workflow Compression +- **Red flags triggered**: None. The slice changes no new truth model and introduces no framework or taxonomy. +- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 1 | **Gesamt: 11/12** +- **Decision**: approve + +## Spec Scope Fields *(mandatory)* + +- **Scope**: canonical-view +- **Primary Routes**: + - `/admin/workspaces/{workspace}/environments/{environment}/inventory` + - `/admin/workspaces/{workspace}/environments/{environment}/inventory/inventory-coverage` + - `/admin/workspaces/{workspace}` as the workspace-home negative-control surface +- **Data Ownership**: Inventory rows and coverage truth remain managed-environment-owned records with `workspace_id` and `managed_environment_id` enforced by existing runtime and policy paths. No data model changes are in scope. +- **RBAC**: Workspace membership plus managed-environment membership remain required. Inventory visibility requires the existing environment view capability (`Capabilities::TENANT_VIEW`) through current policy/capability paths. Non-members remain deny-as-not-found. Members missing capability remain denied by existing capability checks. + +For canonical-view specs, the spec MUST define: + +- **Default filter behavior when tenant-context is active**: Environment-bound routes resolve their environment from the canonical route. Workspace-scoped routes may use remembered environment context for recovery and links, but must not register environment-owned sidebar navigation on workspace home. +- **Explicit entitlement checks preventing cross-tenant leakage**: Existing `OperateHubShell`, `WorkspaceContext`, `WorkspaceScopedTenantRoutes`, `ResolvesPanelTenantContext`, and resource/page `canAccess` or `canViewAny` checks remain the entitlement boundary. This slice must not bypass those paths. + +## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)* + +- **Cross-cutting feature?**: yes +- **Interaction class(es)**: navigation entry points, Filament cluster navigation, environment-scoped sidebar visibility +- **Systems touched**: + - `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/Support/Navigation/NavigationScope.php` + - `apps/platform/app/Support/OperateHub/OperateHubShell.php` + - `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php` + - `apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php` +- **Existing pattern(s) to extend**: `NavigationScope::shouldRegisterEnvironmentNavigation()`, workspace-scoped resource routing via `WorkspaceScopedTenantRoutes`, and current `OperateHubShell` environment context resolution. +- **Shared contract / presenter / builder / renderer to reuse**: `NavigationScope` and the current environment route/context helpers. No new navigation contract is introduced. +- **Why the existing shared path is sufficient or insufficient**: The shared path is sufficient for resources that already call `NavigationScope::shouldRegisterEnvironmentNavigation()`. It is insufficient only where Inventory cluster/page classes still short-circuit on `panel === admin` before considering environment context. +- **Allowed deviation and why**: none. Inventory should converge on the shared environment-navigation path. +- **Consistency impact**: Inventory Items and Inventory Coverage must follow the same environment-scope visibility rule while workspace home stays clean. +- **Review focus**: Reviewers must verify the implementation removes the stale blanket admin-hidden rule only for Inventory, does not make Entra Groups visible, and does not show environment-owned navigation on workspace home. + +## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)* + +- **Touches OperationRun start/completion/link UX?**: no +- **Shared OperationRun UX contract/layer reused**: N/A +- **Delegated start/completion UX behaviors**: N/A +- **Local surface-owned behavior that remains**: Existing Inventory "Run Inventory Sync" behavior remains unchanged and continues to use current OperationRun UX paths. +- **Queued DB-notification policy**: N/A +- **Terminal notification path**: N/A +- **Exception required?**: none + +## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)* + +- **Shared provider/platform boundary touched?**: no +- **Boundary classification**: N/A +- **Seams affected**: N/A +- **Neutral platform terms preserved or introduced**: "workspace", "environment", "Inventory", and "Coverage" remain the operator-facing terms for this slice. +- **Provider-specific semantics retained and why**: Inventory item metadata may still include provider-specific policy type detail, but this slice does not change that provider-owned truth. +- **Why this does not deepen provider coupling accidentally**: The work is navigation visibility only and reuses current environment routing/context. +- **Follow-up path**: none + +## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)* + +| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note | +|---|---|---|---|---|---|---| +| Environment-bound admin sidebar Inventory cluster | yes | Native Filament cluster/resource navigation | navigation | shell, route context | no | Converges Inventory on existing environment-navigation rules | +| Inventory Coverage page navigation registration | yes | Native Filament page navigation | navigation | shell, page, route context | no | Visibility only; page behavior remains DB-only | +| Workspace home sidebar | yes, negative control | Native Filament panel navigation | navigation | shell | no | Must remain clean when no active environment context exists | + +## 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 | +|---|---|---|---|---|---|---|---| +| Environment-bound admin sidebar Inventory cluster | Secondary Context | Operator needs to inspect observed inventory or coverage after choosing an environment | Inventory entry points in the environment sidebar | Inventory item list and coverage table remain inside their pages | Secondary because the sidebar helps resume the workflow, not make the inventory decision itself | Follows Workspace -> Managed Environment -> Inventory | Removes need to know direct URLs | +| Workspace home sidebar | Secondary Context | Operator is reviewing workspace-wide state without an environment selected | Workspace-owned entries only | Environment-owned detail remains behind environment selection | It is a negative-control surface for scope clarity | Follows workspace overview IA | Prevents tenant-owned noise at workspace level | + +## 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 | +|---|---|---|---|---|---|---|---| +| Environment-bound Inventory navigation | operator-MSP | Inventory Items and Coverage entries | Existing page-level filters, tables, basis links | Existing diagnostics remain inside existing pages | Open Inventory | Raw payloads remain out of navigation | Navigation only links to existing Inventory truth | +| Workspace home sidebar | operator-MSP | Workspace-level entries only | N/A | N/A | Choose or open an environment through existing workspace flows | Inventory entries hidden without environment context | Workspace home does not duplicate environment inventory status | + +## 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 | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| Environment-bound Inventory sidebar | Navigation / Sidebar | Cluster navigation | Open Inventory Items or Coverage | Sidebar navigation item | N/A | Existing page actions only | N/A | `/admin/workspaces/{workspace}/environments/{environment}/inventory` | `/admin/workspaces/{workspace}/environments/{environment}/inventory/{record}` | Workspace and environment route path | Inventory | Entry availability reflects active environment context | none | +| Inventory Coverage page | List / Table / Read-only | Read-only derived report page | Review coverage gaps | Page navigation | no row inspect | Existing table/filter controls | N/A | `/admin/workspaces/{workspace}/environments/{environment}/inventory/inventory-coverage` | N/A | Workspace and environment route path | Coverage | Coverage state and follow-up guidance | 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 | +|---|---|---|---|---|---|---|---|---|---|---| +| Environment-bound Inventory navigation | Tenant operator | Continue to observed Inventory within selected environment | Sidebar navigation | Where do I inspect current inventory for this environment? | Inventory Items and Coverage navigation labels | None in navigation | Environment context only | Navigation only | Open Inventory / Open Coverage | none | +| Inventory Coverage page | Tenant operator | Decide which Inventory policy families need follow-up | Read-only report table | Which inventory types are covered or need follow-up? | Coverage state, type, guidance, observed count | Existing detailed basis links and type keys | coverage state, observed item count, restore/dependency metadata | Read-only page; existing Inventory Sync action remains on Inventory Items | Open inventory items | none | + +## Proportionality Review *(mandatory when structural complexity is introduced)* + +- **New source of truth?**: no +- **New persisted entity/table/artifact?**: no +- **New abstraction?**: no +- **New enum/state/reason family?**: no +- **New cross-domain UI framework/taxonomy?**: no +- **Current operator problem**: Inventory is discoverable only through direct or remembered routes in contexts where it should be first-class environment navigation. +- **Existing structure is insufficient because**: The shared `NavigationScope` rule exists but Inventory cluster/page classes bypass it with a blanket admin-panel block. +- **Narrowest correct implementation**: Remove or narrow the blanket admin-hidden rule for Inventory only and prove workspace-home remains clean. +- **Ownership cost**: Focused navigation tests plus one browser smoke if the sidebar behavior changes visibly. +- **Alternative intentionally rejected**: A generic tenant-owned surface audit is deferred to the next candidate because it would widen this immediate repair. +- **Release truth**: Current-release truth. + +### Compatibility posture + +This feature assumes a pre-production environment. + +Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec. + +Canonical replacement is preferred over preservation. + +## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)* + +- **Test purpose / classification**: Feature plus one Browser smoke if visible sidebar rendering is changed +- **Validation lane(s)**: confidence, browser +- **Why this classification and these lanes are sufficient**: Feature tests can prove navigation registration, route generation, workspace-home absence, environment-bound presence, and RBAC/context isolation. Browser proof is justified only for the actual sidebar rendering contract because this is a user-visible navigation repair. +- **New or expanded test families**: Focused expansion of `tests/Feature/Filament/PanelNavigationSegregationTest.php`, `InventoryCoverageAdminTenantParityTest.php`, and optionally one `tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php`. +- **Fixture / helper cost impact**: Low. Reuse current `createUserWithTenant`, `WorkspaceContext`, `ManagedEnvironmentLinks`, and Inventory fixtures. +- **Heavy-family visibility / justification**: No heavy-governance family. One bounded browser smoke is allowed because the product break is navigation visibility. +- **Special surface test profile**: standard-native-filament +- **Standard-native relief or required special coverage**: Native Filament navigation is sufficient; no custom UI proof is needed beyond the sidebar smoke if implemented. +- **Reviewer handoff**: Confirm lane fit, no hidden Entra Groups scope, no generic route audit implementation, and workspace-home negative controls remain explicit. +- **Budget / baseline / trend impact**: Low; one focused browser smoke at most. +- **Escalation needed**: document-in-feature +- **Active feature PR close-out entry**: Guardrail / Exception / Smoke Coverage +- **Planned validation commands**: + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php tests/Feature/Filament/InventoryHubDbOnlyTest.php tests/Feature/Filament/InventoryPagesTest.php` + - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php` if the browser smoke is added + - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Inventory appears in environment-bound admin navigation (Priority: P1) + +As a tenant operator working inside a selected managed environment, I need Inventory entry points in the admin sidebar so I can inspect observed items and coverage without knowing direct URLs. + +**Why this priority**: This is the product break named by the candidate. Inventory already has runtime access, but the sidebar hides it. + +**Independent Test**: In an admin request path matching `/admin/workspaces/{workspace}/environments/{environment}`, assert `InventoryCluster`, `InventoryItemResource`, and `InventoryCoverage` register navigation while preserving current authorization checks. + +**Acceptance Scenarios**: + +1. **Given** an authenticated operator with workspace and environment membership, **When** they open the canonical environment route, **Then** Inventory Items and Coverage are visible in the environment-bound sidebar. +2. **Given** a valid environment-bound route, **When** `InventoryCoverage::shouldRegisterNavigation()` is evaluated, **Then** it returns true through the shared environment-navigation contract. +3. **Given** `InventoryItemResource::getUrl('index', panel: 'admin', tenant: $environment)`, **When** the URL is generated, **Then** the path starts with `/admin/workspaces/{workspace}/environments/{environment}/` and not `/admin/t/`. +4. **Given** `InventoryCoverage::getUrl(panel: 'admin', tenant: $environment)`, **When** the URL is generated after this slice, **Then** the path resolves to the canonical workspace/environment Inventory Coverage route rather than a flat `/admin/inventory/inventory-coverage` route. + +--- + +### User Story 2 - Workspace home remains clean (Priority: P2) + +As an operator on the workspace home page, I need the sidebar to show workspace-level navigation only, even if the system remembers my last environment, so I do not mistake tenant-owned Inventory work for workspace-wide state. + +**Why this priority**: The intended workspace-home clean-sidebar rule is still valid and must not be undone while fixing Inventory. + +**Independent Test**: With a remembered environment in session, open the workspace home route and assert Inventory Items, Coverage, and other environment-owned entries are not present. + +**Acceptance Scenarios**: + +1. **Given** a workspace home route and a remembered environment in session, **When** the sidebar renders, **Then** Inventory navigation is absent. +2. **Given** no active environment context, **When** Inventory cluster/page navigation registration is evaluated, **Then** the result does not make Inventory visible on workspace home. + +--- + +### User Story 3 - Inventory Coverage follows canonical context (Priority: P3) + +As a tenant operator, I need Inventory Coverage to resolve the same managed environment as the Inventory Items list so coverage truth is scoped to the selected environment and not a stale remembered or query-hint target. + +**Why this priority**: `InventoryCoverage` currently has route/context proof, but its navigation registration still protects the wrong admin-hidden assumption. + +**Independent Test**: Seed two managed environments in one workspace, give the operator access to both, open Coverage through the canonical route for one environment, and assert coverage rows come from that environment only. + +**Acceptance Scenarios**: + +1. **Given** two environments with different coverage truth, **When** the operator opens Coverage from the first environment route, **Then** only the first environment's coverage is shown. +2. **Given** an invalid workspace/environment pair, **When** the operator opens Inventory or Coverage, **Then** the response remains deny-as-not-found. + +## Edge Cases + +- Workspace home with remembered environment must not register Inventory. +- Livewire update requests should use the referer path through `NavigationScope::effectivePath()` and preserve the same environment-bound vs workspace-bound decision. +- Invalid workspace/environment route pair must remain not found. +- Operators without environment membership must not see or access Inventory. +- Operators with environment membership but missing view capability must remain denied by existing capability checks. +- Entra Groups must stay out of scope even though it appears in the same candidate group. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-301-001**: Inventory cluster navigation MUST register in the admin panel when the current request is an environment-bound admin surface. +- **FR-301-002**: Inventory Coverage navigation MUST register in the admin panel when the current request is an environment-bound admin surface. +- **FR-301-003**: Inventory Items navigation MUST keep using the current environment-navigation contract and remain visible only on environment-bound admin surfaces. +- **FR-301-004**: Inventory navigation MUST remain absent from workspace-home sidebar routes where no active environment route context exists, even when a remembered environment exists in session. +- **FR-301-005**: Inventory Coverage MUST resolve managed-environment context through the canonical admin route or existing remembered environment contract without adding a new query/filter state. +- **FR-301-006**: Inventory Coverage URL generation MUST align to the canonical workspace/environment Inventory Coverage route when an environment context is supplied. +- **FR-301-007**: The implementation MUST update navigation tests so they no longer protect the stale blanket rule that Inventory can never register in the admin panel. +- **FR-301-008**: The implementation MUST preserve deny-as-not-found behavior for wrong workspace/environment pairs and non-members. +- **FR-301-009**: The implementation MUST NOT make Entra Groups visible or decide its navigation posture. +- **FR-301-010**: The implementation MUST NOT introduce a new navigation framework or route audit artifact. + +### Non-Functional Requirements + +- **NFR-301-001**: The slice must stay DB-only for Inventory page rendering and must not add outbound Graph calls or background work to navigation/page load. +- **NFR-301-002**: Filament v5 and Livewire v4 behavior must remain compatible with the current panel provider registration model. +- **NFR-301-003**: No new assets are registered. +- **NFR-301-004**: No new persistence, schema, queue, cron, provider, or OperationRun lifecycle change is introduced. +- **NFR-301-005**: Tests must stay focused and must not broaden into a tenant-owned surface route audit. + +### RBAC / Security Requirements + +- **SEC-301-001**: UI navigation visibility is not authorization. Server-side `canAccess`, `canViewAny`, policy, and capability checks remain the source of truth. +- **SEC-301-002**: Workspace non-members and environment non-members must receive not-found semantics through existing routes/context resolution. +- **SEC-301-003**: Member-without-capability denial must remain enforced by the existing capability resolver. + +### Auditability / Observability Requirements + +- **AUD-301-001**: This navigation-only slice introduces no new audit event. +- **AUD-301-002**: Existing Inventory Sync audit and OperationRun behavior remain unchanged. + +### Data / Truth-Source Requirements + +- **DATA-301-001**: Inventory remains last-observed managed-environment-owned truth. +- **DATA-301-002**: Coverage remains derived from the current Inventory Sync basis in `OperationRun.context`. +- **DATA-301-003**: No new snapshot, backup, or inventory source of truth is introduced. + +## Success Criteria *(mandatory)* + +- **SC-301-001**: Environment-bound admin navigation shows Inventory Items and Coverage for entitled operators. +- **SC-301-002**: Workspace-home navigation remains free of Inventory and other tenant-owned entries when no environment context is active. +- **SC-301-003**: The focused Filament navigation tests distinguish workspace-home absence from environment-bound presence. +- **SC-301-004**: Inventory Coverage continues to show only the selected environment's coverage truth. +- **SC-301-005**: No Entra Groups, route-audit, system-panel, provider-boundary, asset, schema, or OperationRun changes are included. + +## Assumptions + +- `301` is intentionally requested by the user and is free in `specs/`. +- The current branch may be ahead of `origin/platform-dev`, but the working tree was clean before Spec Kit execution. +- The existing `/admin/workspaces/{workspace}/environments/{environment}` route family is the canonical route family for this slice. +- The current `InventoryItemResource` already has a View page, so global search destination eligibility remains satisfied if global search is enabled. + +## Risks + +- A too-broad change could expose all tenant-owned navigation on workspace home. +- A too-local change could make the cluster visible while leaving Coverage hidden. +- Browser-only confidence would be weak without explicit feature tests for `shouldRegisterNavigation()`. +- Updating historical completed cutover specs would corrupt implementation history; they must remain context only. + +## Out of Scope + +- Entra Groups navigation. +- Tenant-owned surface route audit. +- Navigation contract split. +- Tenant-panel dead-code retirement. +- System panel changes. +- Workspace-home IA redesign. +- New provider capability semantics. +- New persisted state, migrations, models, or services. +- Any Graph API contract changes. + +## Follow-up Spec Candidates + +- `tenant-owned-surface-route-audit` +- `admin-directory-groups-cutover` +- `navigation-contract-split`, only if drift remains after the first three candidates +- `tenant-panel-dead-code-retirement` + +## Manual Promotion Notes + +- **Selected candidate title**: `admin-inventory-navigation-cutover` +- **Source location**: `docs/product/spec-candidates.md`, Admin Workspace Navigation & Tenant-owned Surface Repair candidate group. +- **Why selected**: User explicitly promoted it and requested number `301`; the candidate is the first recommended item in the group and is identified as the only immediately implementation-ready slice. +- **Close alternatives deferred**: `tenant-owned-surface-route-audit`, `admin-directory-groups-cutover`, `navigation-contract-split`, and `tenant-panel-dead-code-retirement` require separate product decisions or follow this repair. +- **Roadmap relationship**: UI and product maturity polish plus workspace-first managed-environment cutover follow-through. +- **Completed-spec guardrail result**: Specs `279` through `300` are treated as historical/contextual completed or in-flight cutover evidence and are not modified by this preparation. diff --git a/specs/301-admin-inventory-navigation-cutover/tasks.md b/specs/301-admin-inventory-navigation-cutover/tasks.md new file mode 100644 index 00000000..6b3f6d73 --- /dev/null +++ b/specs/301-admin-inventory-navigation-cutover/tasks.md @@ -0,0 +1,116 @@ +# Tasks: Admin Inventory Navigation Cutover + +**Input**: Design documents from `/specs/301-admin-inventory-navigation-cutover/` +**Prerequisites**: `spec.md`, `plan.md`, `checklists/requirements.md` + +**Tests**: Required. This is a runtime navigation repair with user-visible Filament sidebar behavior. + +**Review Outcome**: `acceptable-special-case` +**Workflow Outcome**: `document-in-feature` + +## Test Governance Checklist + +- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior. +- [x] New or changed tests stay in focused Filament Feature coverage plus one optional Browser smoke. +- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default. +- [x] Planned validation commands cover the change without pulling in unrelated route-audit or Entra Groups scope. +- [x] The declared surface test profile is `standard-native-filament`. +- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR. + +## Phase 1: Preparation and Scope Lock + +**Purpose**: Confirm the implementation starts from repo truth and does not reopen completed cutover specs. + +- [x] T001 Review `specs/301-admin-inventory-navigation-cutover/spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md` together before application code changes. +- [x] T002 Review `docs/product/spec-candidates.md` Admin Workspace Navigation & Tenant-owned Surface Repair candidate group and confirm this implementation is limited to `admin-inventory-navigation-cutover`. +- [x] T003 Review completed/context specs `279` through `300` only as historical cutover context; do not rewrite their close-out or validation artifacts. +- [x] T004 Confirm the current branch is `301-admin-inventory-navigation-cutover` or an isolated session branch derived from it. +- [x] T005 Run `git status --short --branch` and stop if unrelated uncommitted changes exist. +- [x] T006 Confirm `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/Support/Navigation/NavigationScope.php`, and `apps/platform/app/Support/OperateHub/OperateHubShell.php` still contain the seams described in the spec. + +--- + +## Phase 2: Tests First (Expected Failures) + +**Purpose**: Encode the correct navigation contract before changing implementation. + +- [x] T007 [P] Update `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php` so `InventoryCluster` and `InventoryCoverage` are no longer protected by the blanket admin-hidden dataset. +- [x] T008 [P] In `PanelNavigationSegregationTest.php`, add or update assertions proving Inventory cluster/page/resource navigation registers on `/admin/workspaces/{workspace}/environments/{environment}`. +- [x] T009 [P] In `PanelNavigationSegregationTest.php`, keep or strengthen assertions proving Inventory navigation remains absent from `/admin/workspaces/{workspace}` even when a remembered environment is present in session. +- [x] T010 [P] Keep `EntraGroupResource` explicitly out of scope in `PanelNavigationSegregationTest.php`; it must not become visible as part of this slice. +- [x] T011 [P] Extend `apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php` only as needed so Coverage context resolution is proven from the canonical environment route or current remembered-context contract. +- [x] T012 [P] Add or update focused URL proof so `InventoryCoverage::getUrl(panel: 'admin', tenant: $environment)` resolves under `/admin/workspaces/{workspace}/environments/{environment}/inventory/inventory-coverage` and not a flat `/admin/inventory/inventory-coverage` route. +- [x] T013 [P] Review `apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactAdminPanelRegistrationTest.php` and update only if Inventory-specific expectations need alignment with the new environment-bound visibility contract. + +--- + +## Phase 3: Inventory Navigation Repair (US1, US2) + +**Purpose**: Make Inventory visible in environment-bound admin navigation while preserving workspace-home cleanliness. + +- [x] T014 [US1] Update `apps/platform/app/Filament/Clusters/Inventory/InventoryCluster.php` so admin-panel navigation is not blanket-hidden and instead follows the shared environment-navigation contract. +- [x] T015 [US1] Update `apps/platform/app/Filament/Pages/InventoryCoverage.php` so `shouldRegisterNavigation()` uses the same environment-navigation rule as `InventoryItemResource`. +- [x] T016 [US1] Update `InventoryCoverage` route/URL behavior only as needed so the page lives under the canonical workspace/environment Inventory route when an environment context is supplied. +- [x] T017 [US1] Confirm `InventoryItemResource::shouldRegisterNavigation()` remains aligned with `NavigationScope::shouldRegisterEnvironmentNavigation()` and does not require a route-name rewrite. +- [x] T018 [US2] Confirm the workspace-home route still returns false for Inventory cluster/page/resource navigation when the effective request path is `/admin/workspaces/{workspace}`. +- [x] T019 [US2] Confirm Livewire update requests still rely on `NavigationScope` referer-path behavior and do not make workspace-home Inventory navigation visible accidentally. +- [x] T020 [US1] Do not introduce new navigation helper classes, new panel providers, new route families beyond the Inventory Coverage canonical path, new config keys, or new assets. + +--- + +## Phase 4: Inventory Coverage Context and DB-only Safety (US3) + +**Purpose**: Keep Inventory Coverage truthful and DB-only after navigation is restored. + +- [x] T021 [US3] Run or update `apps/platform/tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php` to prove the selected managed environment supplies coverage truth when multiple environments exist in the same workspace. +- [x] T022 [US3] Run or update `apps/platform/tests/Feature/Filament/InventoryHubDbOnlyTest.php` to confirm Inventory Items and Coverage page loads still dispatch no background jobs and make no outbound HTTP calls. +- [x] T023 [US3] Run or update `apps/platform/tests/Feature/Filament/InventoryPagesTest.php` to confirm Inventory Items and Coverage keep their coverage-first summaries, basis continuity, and no-basis fallback. +- [x] T024 [US3] Confirm wrong workspace/environment pairs still return deny-as-not-found through existing route/context tests or add a focused assertion if not already covered. + +--- + +## Phase 5: Browser Proof (If Needed) + +**Purpose**: Prove the visible sidebar contract when implementation changes rendered navigation. + +- [x] T025 [P] Add `apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php` if no existing browser smoke already proves the rendered environment sidebar. +- [x] T026 [P] In the browser smoke, open workspace home with a remembered environment and assert Inventory navigation is absent. +- [x] T027 [P] In the browser smoke, open the canonical environment route and assert Inventory Items and Coverage are visible and clickable. +- [x] T028 [P] Keep the smoke bounded to sidebar visibility and page reachability; do not add route-audit or Entra Groups coverage here. + +--- + +## Phase 6: Filament, RBAC, and Scope Review + +**Purpose**: Ensure the implementation stays inside Filament v5, RBAC, and roadmap boundaries. + +- [x] T029 Review touched Filament classes for Livewire v4 / Filament v5 compatibility and confirm no Livewire v3 APIs are introduced. +- [x] T030 Confirm no panel provider registration changed; Laravel provider registration remains in `apps/platform/bootstrap/providers.php`. +- [x] T031 Confirm `InventoryItemResource` still has a View page and no global-search destination rule is broken. +- [x] T032 Confirm no destructive actions were introduced or changed; if an action is touched unexpectedly, verify `Action::make(...)->action(...)`, `->requiresConfirmation()` where destructive, server-side authorization, and notifications/audit behavior remain intact. +- [x] T033 Confirm no assets were registered and deployment `filament:assets` requirements are unchanged. +- [x] T034 Confirm no migrations, models, providers, OperationRun lifecycle, Graph contracts, or runtime jobs were introduced. +- [x] T035 Confirm Entra Groups, tenant-owned surface route audit, navigation contract split, and tenant-panel dead-code retirement remain out of scope. + +--- + +## Phase 7: Validation + +**Purpose**: Run the narrowest proof commands. + +- [x] T036 Run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/PanelNavigationSegregationTest.php tests/Feature/Filament/InventoryCoverageAdminTenantParityTest.php tests/Feature/Filament/InventoryHubDbOnlyTest.php tests/Feature/Filament/InventoryPagesTest.php`. +- [x] T037 If `apps/platform/tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php` exists, run `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec301InventoryNavigationCutoverSmokeTest.php`. +- [x] T038 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`. +- [x] T039 Run `git diff --check`. +- [x] T040 Record validation results, skipped browser decision if applicable, and the Guardrail / Exception / Smoke Coverage close-out note in the active PR or implementation summary. + +--- + +## Explicit Non-Goals + +- [x] Do not change Entra Groups navigation. +- [x] Do not perform a generic tenant-owned surface route audit. +- [x] Do not split or redesign the navigation contract beyond Inventory. +- [x] Do not retire tenant-panel dead code in this slice. +- [x] Do not modify completed Specs `279-300`. +- [x] Do not add migrations, models, services, jobs, routes outside Inventory needs, provider contracts, assets, or OperationRun lifecycle changes. -- 2.45.2