Bug 1 (visual): .clu-toggle is a <span> which is inline by default;
width:36px height:20px were being ignored so toggles rendered as zero-
width inline bars. Added display:inline-block + vertical-align:middle.
Bug 2 (no state): toggle-rows in vhost / ssl / access / plugins /
redirects / staging tabs were pure static markup with no wire:click.
Clicking did nothing.
Fix: generic $settings array on the Volt component (one key per toggle)
+ toggleSetting(key) action + isOn(key) helper. All toggle-rows now
emit wire:click=toggleSetting('namespace.key') and read the live state
via @if($this->isOn('namespace.key')) on @endif.
Per-plugin auto-update toggles read settings['plugin.<slug>.auto'] so
each row flips independently.
Tabs covered: vHost (server_tokens, http2_push), SSL (ocsp/auto_renew/
caa_check/ct_log), SSH-FTP access policy (password_auth/no_root/
fail2ban/ip_allowlist/ssh_2fa), Plugins (per-plugin auto + 5 policy
toggles), Redirects (keep_qs/case_insens), Staging (robots_block/
email_sandbox/payments_test).
Browser-verified: clicking Yoast SEO auto-update toggle flips from
blue ON to grey OFF instantly via Livewire. 0 console errors. 5/5
SiteDetailsTest still green.
Bug: When .clu-sidebar uses position:fixed it's removed from grid flow,
so the only remaining grid child (main) gets auto-placed into column 1
(the 264px reserved column), making main render in a tiny strip with
sidebar floating on top — exactly what the user screenshot showed.
Fix: Drop grid-template-columns. Use .clu-app-grid { padding-left: 264px }
so the fixed sidebar sits in the reserved gutter and main becomes a
plain block that fills the remaining width.
Mobile (≤920px): padding-left reset to 0 since sidebar slides in as
overlay drawer instead of occupying space.
User reported sidebar scrolling with content + missing bottom padding +
unresponsive site details/header.
Sidebar:
- Changed from position:sticky (which moved with grid context) to
position:fixed top:14 left:14 width:236 height:calc(100vh-28px).
Sidebar now genuinely never moves with scroll.
- Grid template adjusted to 264px first column to reserve the space the
fixed sidebar occupies (236 + 14 margin + 14 gap).
Main:
- layouts/app.blade.php + placeholder.blade.php padding bumped from
'14px 14px 0' to '14px' all-around — content now has equal bottom
breathing room.
Mobile (≤920px):
- Sidebar already had drawer behaviour, kept.
- Topbar: flex-wrap, smaller padding (10/12), shrunk title/sub typography,
search takes full row at order:99.
- Site-hero (site details): flex:1 1 100% forces title onto its own row,
smaller favicon (34px) + smaller h1 (15px) + flex-wrap sub-line.
- Status chip in topbar hidden on mobile (was clipping behind site title).
- clu-dtabs (6-tab bar): wrap with smaller pad/font so all 6 fit on 420px.
- File manager: tree+editor stack vertically; tree max-height 280px,
editor 460px.
- Inline two-col grids (Installation/Schnellaktionen, Sites+Activity,
WP+Salts, OPcache+Extensions) force to single column.
Per user request: switch from auto-increment IDs to UUID v4 across all
models.
Models — added HasUuids trait:
- User, Cluster, Server, Site, Backup, TeamMember, Invitation,
SecurityEvent, Certificate, Activity, Cron.
Migrations — $table->id() → $table->uuid('id')->primary(),
$table->foreignId(...) → $table->foreignUuid(...) across stock laravel
users/sessions/jobs migrations + all 10 domain migrations.
Routes: sites/{site} (and any other implicit route-model binding) keeps
working since Laravel resolves by primary key as a string. Tested:
clicking a site row → URL now /sites/019e800c-520a-7022-badd-2ef9d9ed5c61.
Re-seeded: 6 users, 87 sites, 6 servers, 128 security events, 30
activities, 102 backups, 47 crons, 87 certs, 5 team members, 3 invites.
Full Pest regression: 47/47 green. Browser-verified login + sites
index + site details + WP/PHP/Files tabs with template-exact chrome.
User-reported issues + browser-verified:
1. **Chart.js race condition** — inline @push('scripts') ran before
the @vite type=module script that imports Chart. Wrapped Chart init
in a poll loop that retries every 30ms until window.Chart resolves.
Re-fires on livewire:navigated for SPA navigation.
2. **Hamburger always visible on desktop** — .clu-icon-btn defined
display:grid AFTER .clu-burger {display:none}. Chained selector
'.clu-burger.clu-icon-btn' (specificity 0,2,0) now wins both at
default and inside the <920px media query.
3. **Modal in CluPilot glass style** —
- app/Livewire/Modals/ConfirmDelete.php (class extends ModalComponent)
with title/message/confirmLabel/confirmEvent/payload props,
confirm() dispatches the named event + closes the modal.
- resources/views/livewire/modals/confirm-delete.blade.php
renders clu-modal-card with icon + title/message head + foot
containing cancel (clu-ghost-btn) + delete (clu-btn-primary
clu-btn-danger with red gradient).
- CSS overrides for the wire-elements default Tailwind v2 markup:
bg-gray-500 backdrop dimmer, transparent modal-container, z-index
bumped to 100 so it sits above sticky topbar + sidebar.
- @source hints in app.css so Tailwind v4 scans
vendor/wire-elements/modal blade templates and generates the
sm:inline-block / sm:h-screen utilities the modal needs to center.
- Demo: Settings → Danger Zone → 'Workspace löschen' opens modal.
4. **AppServiceProvider Vite::usePreloadTagAttributes(false)** kept.
5. Browser console clean across dashboard / sites / sites/{id} /
settings + open modal (verified via Chrome MCP).
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.
- docker-compose.yml: bind-mount ./app:/var/www/html on nginx + all PHP
services. CLI dev workflow now uses host code directly (no rebuild
needed for code or CSS changes). Portainer-stack untouched (uses
docker-compose.portainer.yml without bind-mounts).
- AppServiceProvider: Vite::usePreloadTagAttributes(false) drops the
<link rel=preload> tags that triggered 'preloaded but not used within
a few seconds' warnings under wire:navigate.
- vendor/ + composer.lock synced from container to host so bind-mount
exposes a complete tree.
- app.css: CSS quote chars in .clu-quote-text::before/::after were
literal smart quotes which broke as terminating string delimiters in
the tailwind v4 parser. Use unicode escapes \201E and \201C.
- tests/TestCase.php: force DB_CONNECTION=sqlite + :memory: in setUp.
phpunit.xml env section was being ignored by pest 3 runner, so
RefreshDatabase truncated the real MySQL between turns.
- .env.testing for clarity (sqlite memory).