TenantAtlas/specs/107-workspace-chooser/plan.md

6.9 KiB

Implementation Plan: Workspace Chooser v1 (Enterprise) + In-App Switch Entry Point

Branch: 107-workspace-chooser | Date: 2026-02-22 | Spec: spec.md Input: Feature specification from /specs/107-workspace-chooser/spec.md

Summary

Refactor the workspace resolution flow to provide an enterprise-grade auto-resume, explicit switch/manage separation, enhanced metadata in the chooser, and audit events for all workspace selection transitions. The primary changes are:

  1. Refactor EnsureWorkspaceSelected middleware to implement the spec's 7-step auto-resume algorithm with stale-membership detection and flash warnings.
  2. Upgrade the ChooseWorkspace page with role badges, tenant counts, "Manage workspaces" link (capability-gated), and cleaned-up empty state (no "Create workspace" header action).
  3. Add audit events for workspace auto-selection and manual selection via new AuditActionId enum cases + WorkspaceAuditLogger calls.
  4. Add "Switch workspace" user menu entry visible only when user has >1 workspace membership.
  5. Support ?choose=1 forced chooser bypass parameter in middleware.

No new tables, no new columns, no Microsoft Graph calls. All changes are DB-only, session-based, and synchronous.

Technical Context

Language/Version: PHP 8.4 / Laravel 12 Primary Dependencies: Filament v5, Livewire v4, Tailwind CSS v4 Storage: PostgreSQL (existing tables: workspaces, workspace_memberships, users, audit_logs) Testing: Pest v4 (feature tests as Livewire component tests + HTTP tests) Target Platform: Web (Sail/Docker locally, Dokploy for staging/production) Project Type: Web application (Laravel monolith) Performance Goals: Chooser page < 200ms DB time with 50 workspace memberships; no N+1 queries Constraints: Session-based workspace context (all tabs share); no new tables/columns Scale/Scope: Single Filament page refactor + 1 middleware refactor + 2 enum values + user menu entry + ~17 tests

Constitution Check

GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.

  • Inventory-first: N/A — this feature does not interact with Inventory. Workspace selection is a session context operation.
  • Read/write separation: The only write is updating users.last_workspace_id (convenience preference) and creating audit log entries. No destructive mutations — no preview/confirmation needed for preference persistence. Audit events fire on every selection.
  • Graph contract path: N/A — no Microsoft Graph calls in this feature. All data is local (workspaces, memberships, session).
  • Deterministic capabilities: Capabilities::WORKSPACE_MANAGE is referenced via the canonical registry constant. No new capabilities introduced.
  • RBAC-UX: Feature operates in the /admin plane only. Non-member workspace selection returns 404 (deny-as-not-found) via WorkspaceContext::isMember(). "Manage workspaces" link gated by workspace.manage capability. No cross-plane access introduced.
  • Workspace isolation: Middleware ensures workspace membership on every /admin/* request. Stale sessions are cleared and redirected. Non-members get 404.
  • Destructive actions: No destructive actions in this feature. The re-selection is a non-destructive context switch.
  • Global search: No changes to global search behavior.
  • Tenant isolation: Not directly affected. After workspace selection, the existing tenant-count branching routes to tenant-scoped flows.
  • Run observability: N/A — workspace selection is a synchronous, DB-only, < 2s session operation. No OperationRun needed. Selection events are audit-logged.
  • Automation: N/A — no queued/scheduled operations.
  • Data minimization: Audit log stores only actor_id, workspace_id, method, reason, prev_workspace_id — no secrets/tokens/PII.
  • Badge semantics (BADGE-001): Role badge in chooser renders the workspace membership role. Simple color-mapped Filament badge (no status-like semantics, just a label). The workspace membership role is a tag/category, not a status — exempt from BadgeCatalog. Verified: no BadgeDomain exists for workspace roles.
  • Filament UI Action Surface Contract: ChooseWorkspace is a custom context-selector page, not a CRUD Resource. Spec includes UI Action Matrix with explicit exemption documented. No header actions (v1), "Open" per workspace, empty state with specific title + CTA.
  • Filament UI UX-001: This is a context-selector page, not a Create/Edit/View resource page. UX-001 Main/Aside layout does not apply. Exemption documented in spec.

Project Structure

Documentation (this feature)

specs/107-workspace-chooser/
├── plan.md              # This file
├── research.md          # Phase 0 output
├── data-model.md        # Phase 1 output
├── quickstart.md        # Phase 1 output
├── contracts/           # Phase 1 output
│   └── routes.md
└── tasks.md             # Phase 2 output (created by /speckit.tasks)

Source Code (repository root)

app/
├── Http/
│   ├── Controllers/
│   │   └── SwitchWorkspaceController.php      # MODIFY — WorkspaceRedirectResolver + audit (context_bar)
│   └── Middleware/
│       └── EnsureWorkspaceSelected.php       # MODIFY — refactor to spec algorithm
├── Filament/
│   └── Pages/
│       └── ChooseWorkspace.php               # MODIFY — metadata, remove Create action, audit
├── Providers/
│   └── Filament/
│       └── AdminPanelProvider.php            # MODIFY — add user menu item
├── Support/
│   ├── Audit/
│   │   └── AuditActionId.php                # MODIFY — add 2 enum cases
│   └── Workspaces/
│       └── WorkspaceRedirectResolver.php     # NEW — tenant-count branching helper (R4)

resources/
└── views/
    └── filament/
        └── pages/
            └── choose-workspace.blade.php    # MODIFY — metadata cards, empty state, manage link

routes/
└── web.php                                    # MODIFY — WorkspaceRedirectResolver integration

tests/
└── Feature/
    └── Workspaces/
        ├── EnsureWorkspaceSelectedMiddlewareTest.php  # NEW
        ├── ChooseWorkspacePageTest.php                # NEW
        ├── WorkspaceSwitchUserMenuTest.php            # NEW
        ├── WorkspaceRedirectResolverTest.php           # NEW
        └── WorkspaceAuditTrailTest.php                # NEW

Structure Decision: Standard Laravel monolith structure. All changes are in existing directories. No new folders needed.

Complexity Tracking

No Constitution Check violations. No justifications needed.

Violation Why Needed Simpler Alternative Rejected Because