- Define Global Mode: /admin/workspaces is workspace-optional; allowlist in EnsureWorkspaceSelected
- Remove redundancy: no sidebar Switch workspace; no topbar Manage workspaces link; tenant context read-only on /admin/t/{tenant}
- Unify workspace creation auth via WorkspacePolicy + Gate enforcement
- Tests: vendor/bin/sail artisan test --compact tests/Feature/Workspaces tests/Feature/Monitoring tests/Feature/OpsUx tests/Feature/Filament/WorkspaceContextTopbarAndTenantSelectionTest.php
216 lines
9.8 KiB
Markdown
216 lines
9.8 KiB
Markdown
# 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=...`.
|