# Implementation Plan: Workspace-first Navigation & Monitoring Hub **Branch**: `077-workspace-nav-monitoring-hub` | **Date**: 2026-02-06 | **Spec**: [specs/077-workspace-nav-monitoring-hub/spec.md](spec.md) **Input**: Feature specification from [specs/077-workspace-nav-monitoring-hub/spec.md](spec.md) **Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts. ## Summary Resolve workspace navigation ambiguity and formalize a workspace-first context model: - Unambiguous labels: **Switch workspace** (`/admin/choose-workspace`) vs **Manage workspaces** (`/admin/workspaces`). - Monitoring → **Operations** remains canonical and tenantless (`/admin/operations`, `/admin/operations/{run}`). - Tenant context influences Operations only via **server-side default filter state** (removable), never via routing. - Strict non-leaking security semantics: - Non-member workspace scope → 404 (deny-as-not-found) - Workspace member missing capability (protected actions/screens) → 403 - Accessing a workspace record outside membership → 404 (deny-as-not-found) Supporting artifacts: - [research.md](research.md) - [data-model.md](data-model.md) - [contracts/routes.md](contracts/routes.md) - [quickstart.md](quickstart.md) ## Technical Context **Language/Version**: PHP 8.4.x **Primary Dependencies**: Laravel 12, Filament v5, Livewire v4 **Storage**: PostgreSQL (Sail) **Testing**: Pest v4 **Target Platform**: Web (Filament admin panels) **Project Type**: Laravel monolith **Performance Goals**: Operations pages remain DB-only at render; list/detail stay fast on large run tables (pagination + indexed filters) **Constraints**: Filament-native patterns only; canonical URLs must not depend on tenant context; strict 404/403 non-leakage semantics **Scale/Scope**: Multi-workspace MSP use; many tenants and many operation runs ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* - Inventory-first: N/A (no inventory semantics changes) - Read/write separation: PASS (no write operations introduced) - Graph contract path: N/A (no Graph calls) - Deterministic capabilities: PASS (capability gating uses existing resolver/registry patterns) - RBAC-UX: PASS (explicit 404 vs 403 rules) - RBAC-UX destructive confirmation: N/A (no destructive actions introduced) - RBAC-UX global search: N/A (no new searchable resources; no changes to global search) - Tenant isolation: PASS (workspace membership is isolation boundary; tenant context auto-cleared when invalid) - Run observability: N/A (no new operations/jobs) - Automation: N/A - Data minimization: N/A - Badge semantics (BADGE-001): N/A ## Project Structure ### Documentation (this feature) ```text specs/077-workspace-nav-monitoring-hub/ ├── spec.md ├── plan.md ├── research.md ├── data-model.md ├── quickstart.md ├── contracts/ │ └── routes.md └── checklists/ └── requirements.md ``` ### Source Code (repository root) ```text app/ ├── Filament/ │ ├── Pages/ │ │ └── ChooseWorkspace.php │ └── Resources/ │ ├── OperationRunResource.php │ └── OperationRunResource/ │ └── Pages/ │ └── ListOperationRuns.php ├── Http/ │ └── Middleware/ │ └── EnsureWorkspaceSelected.php ├── Providers/ │ └── Filament/ │ └── AdminPanelProvider.php └── Support/ └── Middleware/ └── EnsureFilamentTenantSelected.php resources/ └── views/ └── filament/ └── partials/ └── workspace-switcher.blade.php routes/ └── web.php tests/ └── Feature/ └── (new tests for navigation labels + 404/403 + operations default filter) ``` **Structure Decision**: Laravel monolith using Filament resources/pages and Laravel middleware. ## Complexity Tracking No constitution violations. ## Phase 0 — Outline & Research (complete) All unknowns/decisions have been resolved and recorded: - Repo reality + ambiguity sources + decisions D1–D4: [research.md](research.md) - No remaining NEEDS CLARIFICATION items in the spec. ## Phase 1 — Design & Contracts (complete) - Data model: no new tables/columns required; behavior is implemented via middleware + Filament config: [data-model.md](data-model.md) - Route/security contracts: [contracts/routes.md](contracts/routes.md) - Manual validation steps + suggested test filters: [quickstart.md](quickstart.md) ## Phase 2 — Implementation Plan (ready for tasks) ### Step 1 — Navigation labels: “one label, one meaning” - Update admin navigation to include: - **Switch workspace** (topbar context switcher) → `/admin/choose-workspace` - **Manage workspaces** (sidebar Settings) → `/admin/workspaces` - Remove/replace any navigation items labeled only “Workspaces”. Implementation targets: - Update [app/Support/Middleware/EnsureFilamentTenantSelected.php](../../app/Support/Middleware/EnsureFilamentTenantSelected.php) navigation builder: - Change the label from `Workspaces` to `Switch workspace` for the choose-workspace link. - Ensure this fallback navigation does not accidentally imply CRUD management. - Update [app/Providers/Filament/AdminPanelProvider.php](../../app/Providers/Filament/AdminPanelProvider.php) nav item label for workspace CRUD to `Manage workspaces`. - Update [resources/views/filament/partials/workspace-switcher.blade.php](../../resources/views/filament/partials/workspace-switcher.blade.php) text/links to consistently say “Switch workspace”. - Add reserved Monitoring navigation surfaces for **Alerts** and **Audit Log** as placeholder pages (non-functional “coming soon”) to satisfy FR-011. ### Step 2 — Enforce workspace-scoped RBAC semantics for `/admin/workspaces` - `/admin/workspaces` stays tenantless and is **Global Mode** (workspace-optional). - Enforce strict non-leakage semantics: - Non-member attempting to access a workspace record → **404** (deny-as-not-found) - Member missing required capability for protected actions/screens → **403** Implementation targets: - Scope the Workspaces query (index) to only workspaces the user is a member of. - Ensure `WorkspacePolicy` returns 404 semantics for non-members (record access). - Workspace creation is self-serve (policy-driven). Gate edit/membership-management behind canonical workspace capabilities (no raw strings). - Hide “Manage workspaces” navigation unless the user can manage something workspace-admin related (capability-based). ### Step 3 — Workspace selection redirect + return-to-intended Requirement: visiting any workspace-scoped page without a selected workspace MUST redirect to `/admin/choose-workspace` and then return to the originally requested URL. Implementation targets: - Update [app/Http/Middleware/EnsureWorkspaceSelected.php](../../app/Http/Middleware/EnsureWorkspaceSelected.php): - When redirecting to `/admin/choose-workspace`, store the intended URL (path + query) in session. - Preserve the existing exemptions for auth routes and for `/admin/operations/{run}` and Livewire update referers. - Update both workspace-selection entrypoints to honor intended URLs: - [app/Filament/Pages/ChooseWorkspace.php](../../app/Filament/Pages/ChooseWorkspace.php) - [app/Http/Controllers/SwitchWorkspaceController.php](../../app/Http/Controllers/SwitchWorkspaceController.php) - After setting the workspace, redirect to the stored intended URL (if present and safe), otherwise keep the existing behavior (onboarding / choose-tenant / tenant dashboard). ### Step 4 — Auto-clear invalid tenant context on workspace change Requirement: if tenant context is active but does not belong to the current workspace, auto-clear tenant context and continue on tenantless workspace pages. Implementation targets: - In [app/Support/Middleware/EnsureFilamentTenantSelected.php](../../app/Support/Middleware/EnsureFilamentTenantSelected.php) (or a dedicated middleware used for tenantless pages): - Detect a persisted Filament tenant that does not match `WorkspaceContext::currentWorkspaceId()`. - Clear the persisted Filament tenant context (confirm the correct Filament v5 mechanism during implementation). ### Step 5 — Operations: move tenant scoping from query to removable default filter Requirement: `/admin/operations` stays canonical; if tenant context is active, default to that tenant using server-side default filter state with a visible removable chip. Implementation targets: - Update [app/Filament/Resources/OperationRunResource.php](../../app/Filament/Resources/OperationRunResource.php): - Remove tenant-context filtering from `getEloquentQuery()`. - Update [app/Filament/Resources/OperationRunResource/Pages/ListOperationRuns.php](../../app/Filament/Resources/OperationRunResource/Pages/ListOperationRuns.php): - Add a tenant filter (select) over available tenants in the current workspace. - Default the filter state from the current tenant context when valid. - Ensure the filter chip is visible and can be cleared to view workspace-wide operations. ### Step 6 — Tests (Pest) + formatting Add/adjust tests to cover the strict semantics: - Navigation labels: “Switch workspace” vs “Manage workspaces” (no ambiguous “Workspaces”). - `/admin/workspaces`: - non-member record access → 404 - member missing capability for a protected action/screen → 403 - EnsureWorkspaceSelected: - visiting `/admin/operations` without workspace → redirects to choose-workspace - after selecting workspace → returns to intended URL - Operations default filter: - with tenant context active → tenant filter default set - clearing filter → shows workspace-wide results Tooling: - Run `./vendor/bin/sail bin pint --dirty`. - Run focused tests via `./vendor/bin/sail artisan test --compact --filter=...`.