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

19 KiB
Raw Blame History

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

Input: Design documents from /specs/107-workspace-chooser/ Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/routes.md, quickstart.md

Tests: REQUIRED (Pest) — all changes involve runtime behavior. 17 test cases from spec + additional integration tests. Operations: No OperationRun needed — workspace selection is synchronous, DB-only, < 2s. Audit entries via WorkspaceAuditLogger. RBAC:

  • Authorization plane: admin /admin
  • Membership = switch-right (any workspace member may select/switch)
  • Non-member selection attempt → 404 (deny-as-not-found) via WorkspaceContext::isMember()
  • Capabilities::WORKSPACE_MANAGE gates "Manage workspaces" link visibility (canonical registry constant)
  • Positive test: member selects workspace → success
  • Negative test: non-member attempt → 404 Filament UI Action Surfaces: ChooseWorkspace is a custom context-selector page (not CRUD Resource). UI Action Matrix in spec — no header actions (v1), "Open" per workspace, empty state with specific title + CTA. Exemption from UX-001 documented. Badges: Workspace membership role badge is a tag/category (owner/admin/member), exempt from BADGE-001 per R7 decision. Simple color-mapped <x-filament::badge>, no BadgeCatalog.

Organization: Tasks grouped by user story. Stories map to quickstart phases:

  • Foundation → Phase A (enum + redirect resolver)
  • US1+US2 → Phase B (middleware refactor, incremental)
  • US3 → Phase C (chooser page upgrade)
  • US4 → Phase B enhancement (stale detection)
  • US5 → Phase C+D (chooser audit + user menu)
  • US6 → Verification (audit payloads)

Format: [ID] [P?] [Story] Description

  • [P]: Can run in parallel (different files, no dependencies)
  • [Story]: Which user story this task belongs to (US1US6)
  • Exact file paths included in all descriptions

Phase 1: Setup

Purpose: No project initialization needed — existing Laravel monolith with Filament v5.

(No tasks — project structure, dependencies, and all target directories already exist.)


Phase 2: Foundational (Blocking Prerequisites)

Purpose: Core infrastructure that MUST be complete before ANY user story can be implemented.

⚠️ CRITICAL: No user story work can begin until this phase is complete.

  • T001 [P] Add WorkspaceAutoSelected and WorkspaceSelected enum cases to app/Support/Audit/AuditActionId.php
  • T002 [P] Create WorkspaceRedirectResolver service with tenant-count branching logic (0→managed tenants, 1→tenant dashboard, >1→choose tenant) in app/Support/Workspaces/WorkspaceRedirectResolver.php
  • T003 Write tests for WorkspaceRedirectResolver covering 0/1/>1 tenant branching in tests/Feature/Workspaces/WorkspaceRedirectResolverTest.php

Checkpoint: Foundation ready — AuditActionId enum extended, tenant-count branching deduplicated into resolver. User story implementation can now begin.


Phase 3: User Story 1 — Auto-Resume: Single Workspace (Priority: P1) 🎯 MVP

Goal: A user with exactly one workspace membership is taken directly to their workspace dashboard without seeing the chooser.

Independent Test: Create a user with one workspace membership, hit /admin, verify redirect to workspace dashboard without chooser.

Implementation for User Story 1

  • T004 [US1] Refactor EnsureWorkspaceSelected middleware: implement step 1 (workspace-optional bypass), step 2 (?choose=1 redirect), step 3 (basic session validation — allow if valid membership), step 4 (load selectable memberships), step 5 (single membership auto-resume with audit via WorkspaceAuditLogger), step 7 (fallback redirect to chooser) in app/Http/Middleware/EnsureWorkspaceSelected.php
  • T005 [US1] Write test it_skips_chooser_when_single_workspace_membership — verify direct redirect to workspace dashboard in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
  • T006 [US1] Write test it_emits_audit_event_on_auto_selection_single_membership — verify workspace.auto_selected audit log with reason single_membership in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
  • T007 [US1] Write test it_redirects_via_tenant_count_branching_after_single_auto_resume — verify 0/1/>1 tenant routing after auto-resume in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
  • T008 [US1] Write test it_allows_request_when_session_workspace_is_valid — verify middleware passes through when session has valid membership in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php

Checkpoint: Single-workspace users bypass the chooser entirely. Audit event emitted. Middleware skeleton (7 steps) in place with steps 15, 7 active.


Phase 4: User Story 2 — Auto-Resume: Last Used Workspace (Priority: P1)

Goal: A user with multiple workspaces who has a valid last_workspace_id is taken directly to that workspace without the chooser.

Independent Test: Create a user with 2+ workspaces and a valid last_workspace_id, hit /admin, verify direct entry.

Implementation for User Story 2

  • T009 [US2] Add step 6 to EnsureWorkspaceSelected middleware: last_workspace_id auto-resume with membership validation and audit logging in app/Http/Middleware/EnsureWorkspaceSelected.php
  • T010 [US2] Write test it_auto_resumes_to_last_used_workspace_when_membership_valid — verify direct redirect via last_workspace_id in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
  • T011 [US2] Write test it_emits_audit_event_on_auto_selection_last_used — verify workspace.auto_selected audit log with reason last_used in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
  • T012 [US2] Write test it_falls_back_to_chooser_when_multiple_workspaces_and_no_last_used — verify redirect to chooser when no default in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php

Checkpoint: Multi-workspace users with a valid last-used preference bypass the chooser. Both P1 auto-resume paths (single + last-used) are functional.


Phase 5: User Story 3 — Chooser Fallback: Multiple Workspaces, No Default (Priority: P1)

Goal: A user with multiple workspaces and no valid last_workspace_id sees the chooser with enterprise metadata (name, role badge, tenant count).

Independent Test: Create a user with 3 workspaces (varying roles, tenant counts), clear last_workspace_id, visit /admin, verify chooser renders with metadata.

Implementation for User Story 3

  • T013 [US3] Refactor ChooseWorkspace::getWorkspaces() to add withCount('tenants') and load membership roles keyed by workspace_id; expose $this->workspaceRoles for Blade in app/Filament/Pages/ChooseWorkspace.php
  • T014 [US3] Remove "Create workspace" header action from ChooseWorkspace page (FR-006) in app/Filament/Pages/ChooseWorkspace.php
  • T015 [US3] Update Blade template: add role badge (<x-filament::badge> with color mapping for owner/admin/member), tenant count display, "Manage workspaces" link (gated by Capabilities::WORKSPACE_MANAGE), updated empty state copy per spec terminology in resources/views/filament/pages/choose-workspace.blade.php
  • T016 [US3] Write test it_only_lists_workspaces_user_is_member_of — create workspaces user is and isn't a member of, verify only member workspaces shown in tests/Feature/Workspaces/ChooseWorkspacePageTest.php
  • T017 [US3] Write test it_shows_name_role_and_tenants_count_per_workspace — verify metadata rendered in chooser cards in tests/Feature/Workspaces/ChooseWorkspacePageTest.php
  • T018 [US3] Write test it_shows_empty_state_when_no_memberships — verify "You don't have access to any workspace yet." message in tests/Feature/Workspaces/ChooseWorkspacePageTest.php
  • T019 [US3] Write test it_hides_manage_link_without_workspace_manage_capability and it_shows_manage_link_with_workspace_manage_capability — positive + negative authorization in tests/Feature/Workspaces/ChooseWorkspacePageTest.php
  • T020 [US3] Write test it_has_no_n_plus_1_queries_in_chooser — assert query count with 5+ workspaces in tests/Feature/Workspaces/ChooseWorkspacePageTest.php

Checkpoint: Chooser page displays enterprise metadata. All three P1 stories are functional — auto-resume (single + last-used) and chooser fallback with metadata.


Phase 6: User Story 4 — Stale Session / Revoked Membership (Priority: P2)

Goal: A user whose workspace membership was revoked between sessions sees a clear warning and is redirected to the chooser.

Independent Test: Set session to a workspace, delete the membership, visit /admin, verify warning + chooser.

Implementation for User Story 4

  • T021 [US4] Enhance middleware step 3: detect stale session (revoked membership or archived workspace), clear session, emit Filament Notification::make()->danger() with "Your access to {workspace_name} was removed." flash, redirect to chooser in app/Http/Middleware/EnsureWorkspaceSelected.php
  • T022 [US4] Enhance middleware step 6 error path: detect stale last_workspace_id (revoked or archived), clear last_workspace_id on user record, emit flash warning, redirect to chooser in app/Http/Middleware/EnsureWorkspaceSelected.php
  • T023 [US4] Write test it_clears_session_when_active_workspace_membership_revoked — verify session cleared + warning notification + chooser redirect in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
  • T024 [US4] Write test it_redirects_to_chooser_when_last_workspace_membership_revoked_and_shows_warning — verify last_workspace_id cleared + warning + chooser, including archived workspace scenario (edge case EC2) in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
  • T025 [US4] Write test it_handles_archived_workspace_in_session — verify archived workspace treated as stale in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php

Checkpoint: Stale/revoked membership detection is active. Users see clear warning notifications instead of broken states.


Phase 7: User Story 5 — Manual Workspace Switch (Priority: P2)

Goal: A user can switch workspaces from within the app via the user menu, which takes them to the chooser.

Independent Test: As a logged-in user with active workspace, click "Switch workspace" in user menu, verify chooser loads with ?choose=1.

Implementation for User Story 5

  • T026 [US5] Add audit logging in ChooseWorkspace::selectWorkspace() — emit workspace.selected via WorkspaceAuditLogger with metadata {method: "manual", reason: "chooser", prev_workspace_id} in app/Filament/Pages/ChooseWorkspace.php
  • T027 [US5] Replace form POST with wire:click="selectWorkspace({{ $workspace->id }})" in chooser Blade template in resources/views/filament/pages/choose-workspace.blade.php
  • T028 [US5] Use WorkspaceRedirectResolver in ChooseWorkspace::redirectAfterWorkspaceSelected() for tenant-count branching in app/Filament/Pages/ChooseWorkspace.php
  • T029 [US5] Register "Switch workspace" user menu item via ->userMenuItems() with MenuItem::make()->url('/admin/choose-workspace?choose=1')->icon('heroicon-o-arrows-right-left') and ->visible() callback (>1 workspace membership) in app/Providers/Filament/AdminPanelProvider.php
  • T030 [US5] Write test it_forces_chooser_with_choose_param — verify ?choose=1 bypasses auto-resume, including the single-workspace sub-case (edge case EC3: forced chooser shown even with 1 membership) in tests/Feature/Workspaces/EnsureWorkspaceSelectedMiddlewareTest.php
  • T031 [US5] Write test it_persists_last_used_workspace_on_manual_selection and it_emits_audit_event_on_manual_selection — verify last_workspace_id update + workspace.selected audit log in tests/Feature/Workspaces/ChooseWorkspacePageTest.php
  • T032 [US5] Write test it_shows_switch_workspace_menu_when_multiple_workspaces and it_hides_switch_workspace_menu_when_single_workspace in tests/Feature/Workspaces/WorkspaceSwitchUserMenuTest.php
  • T033 [US5] Write test it_rejects_non_member_workspace_selection_with_404 — verify deny-as-not-found for non-member attempt in tests/Feature/Workspaces/ChooseWorkspacePageTest.php

Checkpoint: Manual switch flow complete — user menu entry, Livewire selection, audit logging, and 404 for non-members.


Phase 8: User Story 6 — Audit Trail for Workspace Context Changes (Priority: P2)

Goal: Every workspace selection (auto or manual) produces an audit log entry with correct payloads for compliance.

Independent Test: Trigger auto-resume and manual selection, verify audit log entries with correct payloads.

Implementation for User Story 6

  • T034 [US6] Write comprehensive audit payload verification test covering all four audit scenarios (auto/single_membership, auto/last_used, manual/chooser, manual/context_bar) with full metadata assertion (method, reason, prev_workspace_id, resource_type, resource_id) in tests/Feature/Workspaces/WorkspaceAuditTrailTest.php
  • T035 [US6] Write test it_includes_prev_workspace_id_when_switching_from_active_workspace — verify previous workspace context is captured in audit metadata in tests/Feature/Workspaces/WorkspaceAuditTrailTest.php

Checkpoint: All six user stories are implemented and tested. Audit trail is verified for compliance.


Phase 9: Polish & Cross-Cutting Concerns

Purpose: Deduplicate remaining tenant-branching copies, full suite validation, formatting.

  • T036 [US6] Replace inline tenant-count branching in SwitchWorkspaceController::__invoke() with WorkspaceRedirectResolver AND add WorkspaceAuditLogger::log() for workspace.selected (method: manual, reason: context_bar) to satisfy FR-005 audit coverage for the context-bar switch path, in app/Http/Controllers/SwitchWorkspaceController.php
  • T037 Replace inline tenant-count branching in /admin route handler with WorkspaceRedirectResolver in routes/web.php
  • T038 Run full test suite via vendor/bin/sail artisan test --compact and verify no regressions
  • T039 Run Pint formatting via vendor/bin/sail bin pint --dirty --format agent
  • T040 Final commit and push to branch 107-workspace-chooser

Dependencies & Execution Order

Phase Dependencies

  • Setup (Phase 1): N/A — project already initialized
  • Foundational (Phase 2): No dependencies — can start immediately. BLOCKS all user stories
  • US1 (Phase 3): Depends on Phase 2 (AuditActionId enum + WorkspaceRedirectResolver)
  • US2 (Phase 4): Depends on Phase 3 (middleware skeleton from US1)
  • US3 (Phase 5): Depends on Phase 2 only — can run in parallel with US1/US2
  • US4 (Phase 6): Depends on Phase 3 (middleware skeleton from US1)
  • US5 (Phase 7): Depends on Phase 5 (chooser page from US3) + Phase 3 (middleware ?choose=1)
  • US6 (Phase 8): Depends on Phases 37 (all audit-emitting code must exist)
  • Polish (Phase 9): Depends on all user stories being complete

User Story Dependencies

  • US1 (P1): After Foundational → provides middleware skeleton for US2 + US4
  • US2 (P1): After US1 → extends middleware with step 6
  • US3 (P1): After Foundational → independent from US1/US2 (different files)
  • US4 (P2): After US1 → enhances middleware error paths
  • US5 (P2): After US3 (chooser page) + US1 (middleware ?choose=1)
  • US6 (P2): After US1US5 → verifies audit payloads across all paths

Within Each User Story

  • Implementation before tests (refactoring existing code — not greenfield TDD)
  • Core changes before edge-case handling
  • Story complete before moving to next priority

Parallel Opportunities

  • Phase 2: T001 and T002 can run in parallel (different files)
  • After Phase 2: US3 (chooser page) can start in parallel with US1 (middleware)
  • After Phase 3: US2 and US4 can start in parallel (US4 enhances middleware error paths, US2 adds step 6)
  • Tests within same file: Sequential (same file), but different test files can run in parallel

Parallel Example: After Foundational

# Developer A: Middleware track (US1 → US2 → US4)
T004 Refactor EnsureWorkspaceSelected (US1)
T009 Add step 6 to middleware (US2)
T021T022 Enhance error paths (US4)

# Developer B: Chooser page track (US3 → US5)
T013T015 Upgrade ChooseWorkspace page + Blade (US3)
T026T029 Add audit + wire:click + user menu (US5)

# Both tracks converge at:
T034T035 Audit trail verification (US6)
T036T040 Polish & cross-cutting

Implementation Strategy

MVP First (US1 Only)

  1. Complete Phase 2: Foundational (T001T003)
  2. Complete Phase 3: US1 — Single workspace auto-resume (T004T008)
  3. STOP and VALIDATE: Single-workspace users bypass chooser, audit logged
  4. Deploy/demo if ready → immediate UX improvement for majority of users

Incremental Delivery

  1. Foundation (T001T003) → Core infrastructure ready
  2. US1 (T004T008) → Single workspace auto-resume → MVP!
  3. US2 (T009T012) → Last-used auto-resume → Multi-workspace friction reduced
  4. US3 (T013T020) → Enterprise metadata in chooser → Better selection UX
  5. US4 (T021T025) → Stale session handling → Governance safety net
  6. US5 (T026T033) → Manual switch via user menu → Full switch flow
  7. US6 (T034T035) → Audit verification → Compliance confidence
  8. Polish (T036T040) → DRY codebase, full suite green

Each story adds value without breaking previous stories.


Summary

Metric Value
Total tasks 40
Phase 2 (Foundational) 3 tasks
US1 (Auto-resume single) 5 tasks
US2 (Auto-resume last-used) 4 tasks
US3 (Chooser metadata) 8 tasks
US4 (Stale session) 5 tasks
US5 (Manual switch) 8 tasks
US6 (Audit verification) 2 tasks
Polish 5 tasks
Parallel opportunities 2 independent tracks (middleware + chooser page) after foundation
MVP scope Foundation + US1 (8 tasks)
New files 6 (1 service + 5 test files)
Modified files 6 (middleware, page, blade, enum, provider, controller + routes)

Notes

  • All tasks reference exact file paths from plan.md project structure
  • Audit logging uses direct WorkspaceAuditLogger::log() calls (decision R2)
  • Middleware is refactored in-place (decision R1) — alias ensure-workspace-selected unchanged
  • Chooser migrates to wire:click (decision R9) — SwitchWorkspaceController retained for context-bar
  • Flash warnings use Filament Notification::make()->danger() (decision R10)
  • Role badge uses simple color mapping, exempt from BadgeCatalog (decision R7)
  • WorkspaceRedirectResolver deduplicates 4 copies of tenant-count branching (decision R4)