# 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=...`.
