From 46924135e644e1a8ad90f14c08e5beb2c3a9eeab Mon Sep 17 00:00:00 2001 From: boban Date: Sun, 5 Jul 2026 17:10:19 +0200 Subject: [PATCH] perf(fleet,wg) + i18n(dashboard,update) + a11y(btn): re-audit design/perf cleanups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit From the re-audit's design/perf pass (Codex-reviewed): - WithFleetContext::fleet() memoises per request (protected, not serialised by Livewire) — activeServer() calls it several times per render (Files ~6x, Services ~4x), so it was re-running the fleet query each time. - Wireguard: cache WgTraffic->series() (60s) keyed on the window + a data fingerprint (MAX(id) + COUNT) so a new/pruned sample busts it, but the 5s wire:poll no longer re-buckets the whole sample history every tick (samples only land once a minute). - dashboard: the systemd table headers (Unit/Status/Boot) were hard-coded English while a sibling column used __(); move all four through lang keys (de+en parity). - update-progress: localise the phase-list aria-label. - btn: the compact `sm` size grows to the 44px R7 touch target on a coarse pointer only, so dense row/toolbar buttons are tappable on mobile while desktop density is unchanged. Co-Authored-By: Claude Fable 5 --- app/Livewire/Concerns/WithFleetContext.php | 10 +++- app/Livewire/Wireguard/Index.php | 16 +++++- lang/de/dashboard.php | 3 ++ lang/de/update.php | 1 + lang/en/dashboard.php | 3 ++ lang/en/update.php | 1 + resources/views/components/btn.blade.php | 6 ++- resources/views/livewire/dashboard.blade.php | 6 +-- resources/views/update-progress.blade.php | 2 +- tests/Feature/WithFleetContextTest.php | 57 ++++++++++++++++++++ 10 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 tests/Feature/WithFleetContextTest.php diff --git a/app/Livewire/Concerns/WithFleetContext.php b/app/Livewire/Concerns/WithFleetContext.php index 822d87f..527b371 100644 --- a/app/Livewire/Concerns/WithFleetContext.php +++ b/app/Livewire/Concerns/WithFleetContext.php @@ -12,9 +12,17 @@ use Illuminate\Support\Collection; */ trait WithFleetContext { + /** + * Per-request memo. activeServer() calls fleet() and is itself called several times per + * render (Files ~6x, Services ~4x), so without this each call re-ran the fleet query. + * Protected, so Livewire does not serialise it into the snapshot; it resets on every + * request (the component is re-hydrated), and no page in scope mutates the fleet mid-request. + */ + protected ?Collection $fleetMemo = null; + public function fleet(): Collection { - return Server::withExists('credential')->orderBy('name')->get(); + return $this->fleetMemo ??= Server::withExists('credential')->orderBy('name')->get(); } public function activeServer(): ?Server diff --git a/app/Livewire/Wireguard/Index.php b/app/Livewire/Wireguard/Index.php index c3a6381..128222f 100644 --- a/app/Livewire/Wireguard/Index.php +++ b/app/Livewire/Wireguard/Index.php @@ -3,12 +3,14 @@ namespace App\Livewire\Wireguard; use App\Models\AuditEvent; +use App\Models\WgTrafficSample; use App\Services\WgBridge; use App\Services\WgStatus; use App\Services\WgTraffic; use App\Support\Confirm\ConfirmToken; use App\Support\Confirm\InvalidConfirmToken; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\RateLimiter; use Livewire\Attributes\Layout; use Livewire\Attributes\On; @@ -507,9 +509,21 @@ class Index extends Component { $window = $this->clampWindow($this->window); + // Traffic samples land at most once per minute (clusev:wg-sample), but the page polls + // every 5s (wire:poll) — so re-bucketing the whole WgTrafficSample history on every poll + // is ~11/12 wasted work. Cache the aggregate, keyed on the window AND a data fingerprint + // (MAX(id) busts on any insert, COUNT busts on a prune) so new/changed data invalidates it + // immediately — MAX(sampled_at) alone could collide for two sample sets in the same second. + $fp = WgTrafficSample::query()->selectRaw('COALESCE(MAX(id), 0) AS mid, COUNT(*) AS cnt')->first(); + $series = Cache::remember( + "wg:traffic:series:{$window}:{$fp->mid}:{$fp->cnt}", + 60, + fn () => $traffic->series($window), + ); + return view('livewire.wireguard.index', [ 'status' => $wg->read(), - 'traffic' => $traffic->series($window), + 'traffic' => $series, 'windows' => self::WINDOWS, ])->title(__('wireguard.title')); } diff --git a/lang/de/dashboard.php b/lang/de/dashboard.php index 3c950d9..1f3d7b3 100644 --- a/lang/de/dashboard.php +++ b/lang/de/dashboard.php @@ -19,7 +19,10 @@ return [ // systemd services panel 'services_title' => 'systemd-Dienste', + 'col_unit' => 'Unit', + 'col_status' => 'Status', 'col_state' => 'Zustand', + 'col_boot' => 'Autostart', 'no_service_data' => 'Keine Dienstdaten — Server nicht verbunden.', // Audit panel diff --git a/lang/de/update.php b/lang/de/update.php index d98d0ff..ed4eae0 100644 --- a/lang/de/update.php +++ b/lang/de/update.php @@ -23,4 +23,5 @@ return [ 'error_heading' => 'Update fehlgeschlagen', 'error_hint' => 'Der Stack konnte nicht aktualisiert werden — die bisherige Version läuft weiter. Ursache im Host-Log prüfen (journalctl -u clusev-update.service, oder run/update.log), beheben und das Update erneut über das Dashboard auslösen.', 'back_button' => 'Zurück', + 'phases_aria' => 'Update-Phasen', ]; diff --git a/lang/en/dashboard.php b/lang/en/dashboard.php index bbedd01..41b395b 100644 --- a/lang/en/dashboard.php +++ b/lang/en/dashboard.php @@ -19,7 +19,10 @@ return [ // systemd services panel 'services_title' => 'systemd services', + 'col_unit' => 'Unit', + 'col_status' => 'Status', 'col_state' => 'State', + 'col_boot' => 'Boot', 'no_service_data' => 'No service data — server not connected.', // Audit panel diff --git a/lang/en/update.php b/lang/en/update.php index 2f664ed..66eeeea 100644 --- a/lang/en/update.php +++ b/lang/en/update.php @@ -23,4 +23,5 @@ return [ 'error_heading' => 'Update failed', 'error_hint' => 'The stack could not be updated — your previous version is still running. Check the host log for the cause (journalctl -u clusev-update.service, or run/update.log), fix it, then re-trigger the update from the dashboard.', 'back_button' => 'Back', + 'phases_aria' => 'Update phases', ]; diff --git a/resources/views/components/btn.blade.php b/resources/views/components/btn.blade.php index 80b9e84..37a89bf 100644 --- a/resources/views/components/btn.blade.php +++ b/resources/views/components/btn.blade.php @@ -12,8 +12,12 @@ 'danger-soft' => 'border border-offline/25 bg-offline/10 text-offline hover:bg-offline/15', ]; // sm = compact toolbar/row buttons; lg = full-height primary CTA (≥44px touch target, R7). + // sm stays 32px visually on a fine pointer (mouse) for density, but grows to the 44px R7 + // minimum on a COARSE pointer (touch) so row actions are tappable — desktop is unaffected. $sizes = [ - 'sm' => ($icon ? 'h-8 w-8' : 'h-8 px-3').' text-xs', + 'sm' => ($icon + ? 'h-8 w-8 [@media(pointer:coarse)]:h-11 [@media(pointer:coarse)]:w-11' + : 'h-8 px-3 [@media(pointer:coarse)]:min-h-11').' text-xs', 'lg' => ($icon ? 'h-11 w-11' : 'h-11 px-4').' text-sm', ]; $classes = 'inline-flex items-center justify-center gap-1.5 rounded-md font-medium transition-colors ' diff --git a/resources/views/livewire/dashboard.blade.php b/resources/views/livewire/dashboard.blade.php index c098249..594d604 100644 --- a/resources/views/livewire/dashboard.blade.php +++ b/resources/views/livewire/dashboard.blade.php @@ -98,10 +98,10 @@ - - + + - + diff --git a/resources/views/update-progress.blade.php b/resources/views/update-progress.blade.php index 7d7fc59..72f9568 100644 --- a/resources/views/update-progress.blade.php +++ b/resources/views/update-progress.blade.php @@ -84,7 +84,7 @@ {{-- Phase checklist --}}
-
    +
      @php // Real order of the host updater: git pull → docker build → stack up → migrate. $phases = [ diff --git a/tests/Feature/WithFleetContextTest.php b/tests/Feature/WithFleetContextTest.php new file mode 100644 index 0000000..0e3c218 --- /dev/null +++ b/tests/Feature/WithFleetContextTest.php @@ -0,0 +1,57 @@ + 'box', 'ip' => '10.0.0.1', 'ssh_port' => 22, 'status' => 'online']); + $server->credential()->create(['username' => 'root', 'auth_type' => 'password', 'secret' => 'x']); + + $component = new class + { + use WithFleetContext; + }; + + DB::enableQueryLog(); + $component->fleet(); + $component->fleet(); + $component->activeServer(); // internally calls fleet() again + + $serverSelects = collect(DB::getQueryLog()) + ->filter(fn (array $q): bool => str_contains($q['query'], 'from "servers"')) + ->count(); + DB::disableQueryLog(); + + $this->assertSame(1, $serverSelects); // memoised: one query despite three fleet() reads + } + + public function test_fleet_still_returns_the_fleet(): void + { + $a = Server::create(['name' => 'alpha', 'ip' => '10.0.0.1', 'ssh_port' => 22, 'status' => 'online']); + $b = Server::create(['name' => 'bravo', 'ip' => '10.0.0.2', 'ssh_port' => 22, 'status' => 'online']); + + $component = new class + { + use WithFleetContext; + }; + + $ids = $component->fleet()->pluck('id'); + $this->assertTrue($ids->contains($a->id)); + $this->assertTrue($ids->contains($b->id)); + } +}
UnitStatus{{ __('dashboard.col_unit') }}{{ __('dashboard.col_status') }} {{ __('dashboard.col_state') }}