9.1 KiB
Research: Workspace Chooser v1
Feature: 107-workspace-chooser | Date: 2026-02-22
R1: Middleware Refactor Strategy
Question: Should we create a new EnsureActiveWorkspace middleware or refactor the existing EnsureWorkspaceSelected?
- Decision: Refactor the existing
EnsureWorkspaceSelectedmiddleware in-place. - Rationale: The existing middleware is already registered in both
AdminPanelProviderandTenantPanelProvidermiddleware stacks as'ensure-workspace-selected', and referenced by alias inbootstrap/app.php. Creating a new class would require changing all registration points and updating existing tests. The current class already handles the same responsibilities — it just doesn't implement them according to the spec. - Alternatives considered:
- New
EnsureActiveWorkspaceclass: rejected because it would require renaming the middleware alias everywhere, with no functional benefit beyond a name change. The alias can remainensure-workspace-selectedfor backward compatibility.
- New
R2: Audit Event Integration Pattern
Question: How should workspace selection audit events be emitted?
- Decision: Call
WorkspaceAuditLogger::log()directly from the middleware (for auto-selections) and fromChooseWorkspace::selectWorkspace()(for manual selections). No events/listeners needed. - Rationale:
WorkspaceAuditLoggeris a simple synchronous service — no queue, no listener. The codebase pattern (used in workspace membership, settings, alert destinations, baselines, etc.) is direct$logger->log(...)calls at the mutation point. Workspace selection audit is similarly < 1ms DB insert. - Alternatives considered:
- Laravel Events + Listeners: rejected — overkill for a synchronous log write. No other systems need to react to workspace selection events.
- Observer on
Usermodel (last_workspace_idchange): rejected — would miss cases where only the session changes (auto-resume from session), and would conflate preference persistence with audit semantics.
R3: AuditActionId Enum Values
Question: What enum values and string representations to use?
- Decision: Add two cases:
WorkspaceAutoSelected = 'workspace.auto_selected'— for auto-resume (single membership or last-used).WorkspaceSelected = 'workspace.selected'— for manual selection from chooser.
- Rationale: Follows the existing naming pattern (
case CamelName = 'snake.dotted_value'). Themethod(auto/manual) andreason(single_membership/last_used/chooser) are stored in the audit log'smetadataJSONB, not in separate enum values. - Alternatives considered:
- Three separate enum values (one per reason): rejected — metadata provides sufficient granularity; enum values should represent the action type, not the trigger.
R4: Redirect After Selection (Tenant-Count Branching)
Question: Where does redirect logic live? Should it be deduplicated?
-
Decision: Extract the tenant-count branching logic into a shared helper method on
WorkspaceContextor a dedicatedWorkspaceRedirectResolverto avoid duplicating it across:EnsureWorkspaceSelectedmiddleware (auto-resume redirects)ChooseWorkspace::selectWorkspace()(manual selection redirect)SwitchWorkspaceController::__invoke()(POST switch redirect)routes/web.php/adminroute handler
Currently, the same branching logic (0 tenants → managed-tenants, 1 → tenant dashboard, >1 → choose-tenant) is copy-pasted in all four locations.
-
Rationale: DRY — the branching is identical in all cases and is the single authority for "where to go after workspace is set." A single method eliminates the risk of divergence as new conditions are added.
-
Alternatives considered:
- Leave duplicated: rejected — 4 copies of the same logic is a maintenance hazard.
- Put on
ChooseWorkspacepage: rejected — the middleware and controller both need it but don't have access to the page class.
R5: ?choose=1 Handling Location
Question: Should the ?choose=1 forced-chooser parameter be handled in the middleware or in the page?
- Decision: Handle in the middleware — step 2 of the algorithm. If
choose=1is present, redirect to/admin/choose-workspace?choose=1and skip auto-resume logic. - Rationale: The middleware is the single entry point for all
/admin/*requests. Handling it there prevents auto-resume from overriding the explicit user intent to see the chooser. - Alternatives considered:
- Handle in
ChooseWorkspacepage: rejected — the middleware would auto-resume BEFORE the page loads, so the user would never see the chooser.
- Handle in
R6: User Menu Integration
Question: How to add "Switch workspace" to the Filament user menu?
- Decision: Register via
->userMenuItems()inAdminPanelProvider::panel(). UseFilament\Navigation\MenuItem::make()with->url('/admin/choose-workspace?choose=1')and a->visible()callback that checks workspace membership count > 1. - Rationale: This is the documented Filament v5 pattern from the constitution + blueprint. The menu item is a navigation-only action (URL link), not a destructive action, so no confirmation needed.
- Alternatives considered:
- Context bar link only: rejected — specification explicitly requires a user menu entry (FR-008).
- Adding to both user menu and context bar: the context bar already has "Switch workspace" — the user menu entry provides an additional discovery point per spec.
R7: Badge Rendering for Workspace Role
Question: Should workspace membership role badges use BadgeCatalog/BadgeDomain?
- Decision: No. Workspace membership role is a tag/category (owner, admin, member), not a status-like value. Per constitution (BADGE-001), tag/category chips are not governed by
BadgeCatalog. Use a simple Filament<x-filament::badge>with a color mapping (e.g., owner → primary, admin → warning, member → gray). - Rationale: The role is static metadata, not a state transition. No existing
BadgeDomainfor workspace roles. Adding one would be over-engineering for 3 static values. - Alternatives considered:
- Create a
WorkspaceRoleBadgeDomain: rejected — violates the "tag, not status" exemption in BADGE-001.
- Create a
R8: Blade Template vs. Livewire Component for Chooser
Question: Should the chooser cards remain as a Blade template or be converted to a Livewire component?
- Decision: Keep as Blade template rendered by the existing Livewire-backed
ChooseWorkspaceFilament Page. The page class already extendsFilament\Pages\Page(which is Livewire). Thewire:clickfor "Open" calls the existingselectWorkspace()method. No separate Livewire component needed. - Rationale: The existing pattern works. The page is already a Livewire component (all Filament Pages are). Converting to a separate Livewire component adds complexity with no benefit — the chooser has no real-time reactive needs.
- Alternatives considered:
- Separate
WorkspaceChooserCardLivewire component: rejected — unnecessary abstraction for a simple card grid.
- Separate
R9: Existing SwitchWorkspaceController Coexistence
Question: The chooser currently uses POST to SwitchWorkspaceController. Should we switch to Livewire wire:click or keep the POST?
- Decision: Migrate the chooser page to use Livewire
wire:clickcallingselectWorkspace($workspaceId)(which already exists). TheSwitchWorkspaceControlleris still needed for theworkspace-switcher.blade.phppartial (context-bar dropdown) which uses a form POST. Both paths converge onWorkspaceContext::setCurrentWorkspace(). - Rationale: The Livewire path already exists in
ChooseWorkspace::selectWorkspace()— the blade template just needs to call it viawire:clickinstead of a form POST. This simplifies the chooser page and makes audit integration easier (audit logging happens in the PHP method, not in a controller). - Alternatives considered:
- Keep form POST from chooser: rejected — the
selectWorkspace()method is where we add audit logging. Usingwire:clickmeans a single code path. - Remove
SwitchWorkspaceControllerentirely: deferred — the context-bar dropdown still uses it. Can be unified in a future PR.
- Keep form POST from chooser: rejected — the
R10: Flash Warning Implementation
Question: How to show "Your access to {workspace_name} was removed" when stale membership detected?
- Decision: Use Filament database notifications or session flash +
Filament\Notifications\Notification::make()->danger(). Since the middleware redirects to the chooser, and the chooser is a Filament page, Filament's notification system renders flash/database notifications automatically. - Rationale: The chooser is a Filament page — Filament's notification toast system is already wired. Session-based
Notification::make()works for redirect→page scenarios. - Alternatives considered:
- Custom Blade banner: rejected — Filament notifications already solve this and are consistent with the rest of the app.
- Session flash only (no Filament notification): rejected — the Filament notification system provides better UX (auto-dismiss, consistent styling).