From 94972c78bd5946eece224b300837d9c819f8fe5d Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 12 Jun 2026 23:17:02 +0200 Subject: [PATCH] perf+ux: lazy-load pages (skeletons), fix modal, normalize buttons + spinners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Route switching was slow because each page did its SSH reads inside mount(), blocking the response. Now mount() only sets cheap shell state and the SSH read runs in load() via wire:init — the page shell + skeletons render instantly and content streams in. - Lazy-load Dashboard (services), Services, Files, Server-Details: SSH moved from mount() to load(); skeleton placeholders (new ) while !$ready. - Modal container: z-10 -> z-50 (was rendering *under* the z-40 sidebar) and a real max-w-lg (the dynamic modalWidth classes were never generated by Tailwind, so modals spanned full width). This also fixes the "flash + disappear". - Modal/action buttons: normalize size (min-h-11 uppercase font-display -> h-9 text-sm font-medium) and add a wire:loading spinner + disabled-while-running on the confirm/save actions (no double-submit, clear feedback). - Server-Details gauges keep wire:poll (live) but no longer block initial render. Verified: every page renders ready=false + skeleton with no SSH, then load() populates real data; modal buttons carry the spinner. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Livewire/Dashboard.php | 14 ++++++++- app/Livewire/Files/Index.php | 22 +++++++------ app/Livewire/Servers/Show.php | 17 +++++++--- app/Livewire/Services/Index.php | 31 ++++++++++++------- resources/css/safelist-tailwindcss.txt | 5 +++ resources/views/components/skeleton.blade.php | 2 ++ resources/views/livewire/dashboard.blade.php | 13 +++++++- .../views/livewire/files/index.blade.php | 14 +++++++-- .../livewire/modals/add-ssh-key.blade.php | 8 +++-- .../livewire/modals/confirm-action.blade.php | 8 +++-- .../views/livewire/servers/show.blade.php | 25 +++++++++++++-- .../views/livewire/services/index.blade.php | 12 ++++++- .../wire-elements-modal/modal.blade.php | 5 ++- 13 files changed, 133 insertions(+), 43 deletions(-) create mode 100644 resources/css/safelist-tailwindcss.txt create mode 100644 resources/views/components/skeleton.blade.php diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index f65cee6..1e71405 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -18,6 +18,18 @@ class Dashboard extends Component { use WithFleetContext; + public bool $ready = false; + + /** @var array notable systemd units (lazy-loaded over SSH) */ + public array $svcRows = []; + + /** Lazy: the SSH services read runs after the shell renders (wire:init). */ + public function load(FleetService $fleet): void + { + $this->svcRows = $this->liveServices($fleet, $this->activeServer()); + $this->ready = true; + } + /** Deterministic wavy series — fallback sparkline for metrics without history. */ private function series(float $base, int $n = 40, float $amp = 9): array { @@ -111,7 +123,7 @@ class Dashboard extends Component 'memTrend' => $this->trend($memSeries), 'diskTrend' => $this->trend($diskSeries), 'loadTrend' => $this->trend($loadSeries, ''), - 'services' => $this->liveServices($fleet, $active), + 'services' => $this->svcRows, 'events' => AuditEvent::with('server')->latest()->limit(6)->get(), ]); } diff --git a/app/Livewire/Files/Index.php b/app/Livewire/Files/Index.php index b6fa73a..e7ac2f0 100644 --- a/app/Livewire/Files/Index.php +++ b/app/Livewire/Files/Index.php @@ -21,31 +21,33 @@ class Index extends Component public bool $connected = false; + public bool $ready = false; + /** @var array */ public array $entries = []; public function mount(): void { - $this->load(); + // listing loads lazily via wire:init -> load() } /** Read the active server's directory over SSH; empty + offline on failure. */ - private function load(): void + public function load(): void { $this->entries = []; $this->connected = false; $active = $this->activeServer(); - if (! $active || ! $active->credential_exists) { - return; + if ($active && $active->credential_exists) { + try { + $this->entries = app(FleetService::class)->files($active, $this->path); + $this->connected = true; + } catch (Throwable) { + $this->connected = false; + } } - try { - $this->entries = app(FleetService::class)->files($active, $this->path); - $this->connected = true; - } catch (Throwable) { - $this->connected = false; - } + $this->ready = true; } public function open(string $name): void diff --git a/app/Livewire/Servers/Show.php b/app/Livewire/Servers/Show.php index de68062..51cedb0 100644 --- a/app/Livewire/Servers/Show.php +++ b/app/Livewire/Servers/Show.php @@ -19,6 +19,8 @@ class Show extends Component public bool $connected = false; + public bool $ready = false; + public array $volumes = []; public array $interfaces = []; @@ -32,7 +34,14 @@ class Show extends Component * On any failure (no credential, unreachable) the page renders an offline * state from the last persisted values instead of crashing. */ - public function mount(FleetService $fleet): void + public function mount(): void + { + // identity + gauges render from the persisted row immediately; + // the live snapshot loads lazily via wire:init -> load(). + } + + /** Lazy: pull the full SSH snapshot after the shell renders, behind skeletons. */ + public function load(FleetService $fleet): void { try { $snap = $fleet->snapshot($this->server); @@ -69,14 +78,14 @@ class Show extends Component $this->server->forceFill(['status' => 'offline'])->save(); } } + + $this->ready = true; } /** Refresh the live gauges from the poller-updated row (wire:poll). */ public function pollMetrics(): void { - if ($this->connected) { - $this->server->refresh(); - } + $this->server->refresh(); } /** diff --git a/app/Livewire/Services/Index.php b/app/Livewire/Services/Index.php index 842a81d..96240b1 100644 --- a/app/Livewire/Services/Index.php +++ b/app/Livewire/Services/Index.php @@ -21,6 +21,8 @@ class Index extends Component public bool $connected = false; + public bool $ready = false; + /** wire:model search over service name + description */ public string $search = ''; @@ -30,23 +32,28 @@ class Index extends Component /** @var array */ public array $journal = []; - public function mount(FleetService $fleet): void + public function mount(): void + { + $this->server = $this->activeServer()?->name ?? '—'; + } + + /** Lazy: the SSH read runs after the shell renders (wire:init), behind a skeleton. */ + public function load(FleetService $fleet): void { $active = $this->activeServer(); - $this->server = $active?->name ?? '—'; - if (! $active || ! $active->credential_exists) { - return; + if ($active && $active->credential_exists) { + try { + $data = $fleet->systemd($active); + $this->services = $data['services']; + $this->journal = $data['journal']; + $this->connected = true; + } catch (Throwable) { + $this->connected = false; + } } - try { - $data = $fleet->systemd($active); - $this->services = $data['services']; - $this->journal = $data['journal']; - $this->connected = true; - } catch (Throwable) { - $this->connected = false; - } + $this->ready = true; } /** diff --git a/resources/css/safelist-tailwindcss.txt b/resources/css/safelist-tailwindcss.txt new file mode 100644 index 0000000..c99c89b --- /dev/null +++ b/resources/css/safelist-tailwindcss.txt @@ -0,0 +1,5 @@ +sm:max-w-md +md:max-w-xl +lg:max-w-3xl +xl:max-w-5xl +2xl:max-w-7xl diff --git a/resources/views/components/skeleton.blade.php b/resources/views/components/skeleton.blade.php new file mode 100644 index 0000000..101c7d3 --- /dev/null +++ b/resources/views/components/skeleton.blade.php @@ -0,0 +1,2 @@ +{{-- Shimmer placeholder for lazy-loaded content (R4: animation via utility, no inline style). --}} +
merge(['class' => 'animate-pulse rounded bg-raised']) }}>
diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index ccbcfeb..e5b92e3 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -16,7 +16,7 @@ $memP = $chart($memSeries); @endphp -
+
{{-- Header --}}
@@ -93,6 +93,16 @@ + @if (! $ready) + @for ($i = 0; $i < 5; $i++) + + + + + + + @endfor + @else @forelse ($services as $svc) @@ -106,6 +116,7 @@ @empty Keine Dienstdaten — Server nicht verbunden. @endforelse + @endif
diff --git a/resources/views/livewire/files/index.blade.php b/resources/views/livewire/files/index.blade.php index 3306628..457d690 100644 --- a/resources/views/livewire/files/index.blade.php +++ b/resources/views/livewire/files/index.blade.php @@ -19,7 +19,7 @@ }; @endphp -
+
{{-- Header --}}
@@ -63,7 +63,16 @@ Aktionen
-
+
+ @if (! $ready) + @for ($i = 0; $i < 8; $i++) +
+ + +
+ @endfor + @else @foreach ($entries as $e) @php $isDir = $e['type'] === 'dir'; @endphp
@@ -123,6 +132,7 @@
@endforeach + @endif
diff --git a/resources/views/livewire/modals/add-ssh-key.blade.php b/resources/views/livewire/modals/add-ssh-key.blade.php index bd76898..9c05dd9 100644 --- a/resources/views/livewire/modals/add-ssh-key.blade.php +++ b/resources/views/livewire/modals/add-ssh-key.blade.php @@ -27,15 +27,17 @@
diff --git a/resources/views/livewire/modals/confirm-action.blade.php b/resources/views/livewire/modals/confirm-action.blade.php index 4c90045..4320489 100644 --- a/resources/views/livewire/modals/confirm-action.blade.php +++ b/resources/views/livewire/modals/confirm-action.blade.php @@ -20,19 +20,21 @@
diff --git a/resources/views/livewire/servers/show.blade.php b/resources/views/livewire/servers/show.blade.php index 9aea6bd..e5c4b08 100644 --- a/resources/views/livewire/servers/show.blade.php +++ b/resources/views/livewire/servers/show.blade.php @@ -17,7 +17,7 @@ ]; @endphp - diff --git a/resources/views/livewire/services/index.blade.php b/resources/views/livewire/services/index.blade.php index e2537d3..15733ee 100644 --- a/resources/views/livewire/services/index.blade.php +++ b/resources/views/livewire/services/index.blade.php @@ -10,7 +10,7 @@ $logTone = ['info' => 'text-ink-3', 'warn' => 'text-warning', 'error' => 'text-offline']; @endphp -
+
{{-- Header --}}
@@ -37,6 +37,15 @@
+ @if (! $ready) + @for ($i = 0; $i < 7; $i++) +
+ +
+
+ @endfor + @else @forelse ($list as $svc)
{{-- identity --}} @@ -88,6 +97,7 @@

Suche „{{ $search }}“ ergab keine Treffer.

@endforelse + @endif
diff --git a/resources/views/vendor/wire-elements-modal/modal.blade.php b/resources/views/vendor/wire-elements-modal/modal.blade.php index a7a20a2..85e8386 100644 --- a/resources/views/vendor/wire-elements-modal/modal.blade.php +++ b/resources/views/vendor/wire-elements-modal/modal.blade.php @@ -11,7 +11,7 @@ x-on:close.stop="setShowPropertyTo(false)" x-on:keydown.escape.window="show && closeModalOnEscape()" x-show="show" - class="fixed inset-0 z-10 overflow-y-auto" + class="fixed inset-0 z-50 overflow-y-auto" style="display: none;" >
@@ -39,8 +39,7 @@ x-transition:leave="ease-in duration-200" x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100" x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" - x-bind:class="modalWidth" - class="inline-block w-full align-bottom bg-surface border border-line rounded-lg text-left overflow-hidden shadow-pop transform transition-all sm:my-8 sm:align-middle sm:w-full" + class="inline-block w-full max-w-lg align-bottom bg-surface border border-line rounded-lg text-left overflow-hidden shadow-pop transform transition-all sm:my-8 sm:align-middle" id="modal-container" x-trap.noscroll.inert="show && showActiveComponent" aria-modal="true"