## Summary - Removes the legacy Tenant CRUD create page (`/admin/tenants/create`) so tenant creation is handled exclusively via the onboarding wizard. - Updates tenant selection flows and pages to prevent Livewire polling/notification-related 404s on workspace-scoped routes. - Aligns empty-state UX with enterprise patterns (avoid duplicate CTAs). ## Key changes - Tenant creation - Removed `CreateTenant` page + route from `TenantResource`. - `TenantResource::canCreate()` now returns `false` (CRUD creation disabled). - Tenants list now surfaces an **Add tenant** action that links to onboarding (`admin.onboarding`). - Onboarding wizard - Removed redundant legacy step-cards from the blade view (Wizard schema is the source of truth). - Disabled topbar on the onboarding page to avoid lazy-loaded notifications. - Choose tenant - Enterprise UI redesign + workspace context. - Uses Livewire `selectTenant()` instead of a form POST. - Disabled topbar and gated BODY_END hook to avoid background polling. - Baseline profiles - Hide header create action when table is empty to avoid duplicate CTAs. ## Tests - `vendor/bin/sail artisan test --compact --filter='Onboarding|ManagedTenantOnboarding'` - `vendor/bin/sail artisan test --compact --filter='ManagedTenantsLivewireUpdate'` - `vendor/bin/sail artisan test --compact --filter='TenantSetup|TenantResourceAuth|TenantAdminAuth|ListTenants'` - `vendor/bin/sail artisan test --compact --filter='BaselineProfile'` - `vendor/bin/sail artisan test --compact --filter='ChooseTenant|TenantMake|TenantScoping|AdminTenantScoped|AdminHomeRedirect|WorkspaceContext'` ## Notes - Filament v5 / Livewire v4 compatible. - No new assets introduced; no deploy pipeline changes required. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #131
4.0 KiB
Routes Contract: Workspace Chooser v1
Feature: 107-workspace-chooser | Date: 2026-02-22
Routes (existing, behavior changes)
GET /admin (named: admin.home)
Change: After workspace auto-resume, redirect uses the shared WorkspaceRedirectResolver instead of inline branching.
Middleware: web, panel:admin, ensure-correct-guard:web, FilamentAuthenticate, ensure-workspace-selected
Behavior (updated):
ensure-workspace-selectedmiddleware handles auto-resume (may set workspace + redirect before this handler runs).- If workspace is resolved, apply tenant-count branching.
- If no workspace, redirect to
/admin/choose-workspace.
GET /admin/choose-workspace (Filament Page: ChooseWorkspace)
Change: Page now displays metadata (role, tenant count), cleaner empty state, "Manage workspaces" link instead of "Create workspace" header action.
Middleware: Standard admin panel middleware. ensure-workspace-selected allows this path (exempted in isWorkspaceOptionalPath()).
Query params:
?choose=1— forces chooser display (bypasses auto-resume). The middleware redirects here when this param is present.
Response: Filament page with workspace cards.
Livewire actions:
selectWorkspace(int $workspaceId)— validates membership, sets workspace context, emits audit event, redirects via tenant-count branching.
POST /admin/switch-workspace (named: admin.switch-workspace)
Change: Redirect logic replaced with WorkspaceRedirectResolver. Audit logging added via WorkspaceAuditLogger::log() — emits workspace.selected with reason context_bar to satisfy FR-005 (every workspace selection must be audited).
Controller: SwitchWorkspaceController
Request body: workspace_id (required, integer)
Middleware: web, auth, ensure-correct-guard:web
Middleware Contract: ensure-workspace-selected
Algorithm (v1 — 7-step)
Step 1: If path is workspace-optional → ALLOW (no redirect)
Step 2: If query has `choose=1` → REDIRECT to /admin/choose-workspace?choose=1
Step 3: If session.current_workspace_id is set:
- If membership valid + not archived → ALLOW
- Else: clear session + flash warning → REDIRECT to chooser
Step 4: Load user's selectable workspace memberships (not archived)
Step 5: If exactly 1 → auto-select, audit log (single_membership) → REDIRECT via tenant branching
Step 6: If last_workspace_id set:
- If valid membership + selectable → auto-select, audit log (last_used) → REDIRECT via tenant branching
- Else: clear last_workspace_id + flash warning → REDIRECT to chooser
Step 7: Else → REDIRECT to chooser
Exempt Paths (workspace-optional)
/admin/workspaces*/admin/choose-workspace/admin/no-access/admin/onboarding/admin/settings/workspace/admin/operations/{id}(existing exemption)/admin/t/*(tenant-scoped routes)- Routes with
.auth.in name
User Menu Contract
"Switch workspace" menu item
Location: Admin panel user menu (registered via AdminPanelProvider::panel() → ->userMenuItems())
Visibility: Only when current user has > 1 workspace membership.
URL: /admin/choose-workspace?choose=1
Icon: heroicon-o-arrows-right-left
Audit Event Contracts
workspace.auto_selected
Trigger: Middleware auto-resume (steps 5 or 6).
Payload (in audit_logs.metadata):
{
"method": "auto",
"reason": "single_membership" | "last_used",
"prev_workspace_id": null
}
workspace.selected
Trigger: Manual selection from chooser (via selectWorkspace()).
Payload (in audit_logs.metadata):
{
"method": "manual",
"reason": "chooser",
"prev_workspace_id": 42
}
Both events use WorkspaceAuditLogger::log() with:
action:AuditActionId::WorkspaceAutoSelected->valueorAuditActionId::WorkspaceSelected->valueresource_type:'workspace'resource_id:(string) $workspace->getKey()