User accepted 5-step plan: Workspace → Sub-Sidebar → Switcher → Plan/
Quota → Billing.
This step adds the workspace layer underneath everything:
- Workspace model with HasUuids + relations (sites/servers/clusters/
backups/members/invitations/activities) + plan-based quotas + feature
flags + isOverQuota() + hasFeature() helpers.
- Plan presets: free/starter/pro/agency/enterprise with sites,
servers, team, storage_gb, features map (audit/monitoring/
email_relay/multi_workspace/white_label/reports/sso).
- BelongsToWorkspace trait: global scope filters by app('current
Workspace'), creating-hook auto-fills workspace_id from container.
Falls back to no-scope when no workspace bound (tests, console).
- Migrations: workspaces table + workspace_id foreignUuid added to
all 10 domain tables (clusters/servers/sites/backups/team_members/
invitations/security_events/certificates/activities/crons).
- users gets current_workspace_id nullable FK.
- team_members unique changed to composite (workspace_id, user_id) so
one user can belong to multiple workspaces.
- ResolveCurrentWorkspace middleware: resolves active workspace from
?workspace=slug → session → user.current_workspace_id → first owned.
Wired into web middleware group via bootstrap/app.php.
- WorkspaceFactory + workspace_id added to all 10 domain factories
so tests/seed work without explicit workspace context.
- DatabaseSeeder rewritten: creates Marie Weber as owner of 'Acme
Agency' (Pro plan) workspace + 'Marie · Personal' (Free) workspace
for switcher demo. Binds app('currentWorkspace') so seeded sites/
servers/backups/etc. land in the right workspace.
- Sidebar component reads auth user's current workspace for the ws-
switch panel — no more hard-coded 'Acme Cluster' string.
Full Pest regression: 47/47 green. 2 workspaces seeded:
- Acme Agency (pro): 87 sites, 6 servers, 128 sec events, 102 backups
- Marie · Personal (free): 0 sites
Sidebar layout (per user request: header+footer fix, only nav scrollable):
- Restructured clu-sidebar into 3 flex-children: clu-sidebar-head (brand +
ws-switch fixed at top), clu-sidebar-scroll (nav, overflow-y:auto in
middle), clu-sidebar-foot (plan-box fixed at bottom).
Mobile sidebar (per user request: drawer + hamburger):
- <920px: sidebar position fixed, translateX(-110%) hidden, .open class
slides in. clu-sidebar-backdrop overlay dimms main + click-to-close.
- New x-clu.burger component with hamburger icon, visible only <920px
via display:grid in media query. Injected into all 9 topbars.
- Alpine state sidebarOpen on <body>, nav-item click closes drawer
after navigation.
Linking (per user request: sites detail unreachable):
- Sites index table + dashboard top-sites table: domain cell now <a>
to route('sites.show', $site) with wire:navigate.
Charts (per user request: chart.js):
- composer/npm: chart.js ^4.5.
- resources/js/app.js: register all chart.js plugins, expose window.Chart
for inline page scripts.
- Dashboard: added Traffic-24h line chart (Chart.js line+gradient fill)
between metrics and 2-col row. Init re-runs on livewire:navigated for
wire:navigate compatibility.
Modal (per user request: wire-elements):
- @livewire('wire-elements-modal') already in layouts/app.blade.php
master layout per wire-elements/modal v3 docs (single mount). Open
pattern: wire:click="$dispatch('openModal', { component: 'name' })".
Full Pest regression: 47/47 green.