From 1622ac3a186a13fffbe57bacf038dd8d348414fd Mon Sep 17 00:00:00 2001 From: Boban Blaskovic Date: Sun, 31 May 2026 22:50:03 +0200 Subject: [PATCH] =?UTF-8?q?Page:=20Sites=20=E2=80=94=20Volt=20with=20searc?= =?UTF-8?q?h/filter/sort/pagination=20+=205=20Pest=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pages/sites/index Volt: search by domain (debounced), 4-segment status filter (all/updates/warnings/critical), server + sort selects, paginated table (15/page). - 4 metric tiles from Site aggregates (online/updates/pagespeed/visitors). - Site cell with favicon initial, ssl/cache info, server/wp/php pills, visitors + response, status health label, row actions. - routes/web.php: Volt::route for sites.index. - CSS additions: clu-page-title/sub, clu-seg, clu-ghost-btn, clu-toolbar, clu-pager, clu-row-actions. - 5 Pest tests green (redirect, chrome, list+paginate, search, segment filter). --- app/resources/css/app.css | 59 +++++ .../livewire/pages/sites/index.blade.php | 231 ++++++++++++++++++ app/routes/web.php | 2 +- app/tests/Feature/Pages/SitesPageTest.php | 63 +++++ 4 files changed, 354 insertions(+), 1 deletion(-) create mode 100644 app/resources/views/livewire/pages/sites/index.blade.php create mode 100644 app/tests/Feature/Pages/SitesPageTest.php diff --git a/app/resources/css/app.css b/app/resources/css/app.css index 6469768..a0fef1f 100644 --- a/app/resources/css/app.css +++ b/app/resources/css/app.css @@ -942,4 +942,63 @@ .clu-feed-body { flex: 1; min-width: 0; } .clu-feed-msg { font-size: 12.5px; color: var(--color-fg); } .clu-feed-meta { font-size: 10.5px; color: var(--color-muted); font-family: var(--font-mono); margin-top: 2px; } + + /* Topbar headings */ + .clu-page-title { font-size: 17px; font-weight: 600; letter-spacing: -0.01em; margin: 0; } + .clu-page-sub { font-size: 12px; color: var(--color-muted); font-family: var(--font-mono); margin-top: 2px; } + + /* Segmented button group */ + .clu-seg { + display: inline-flex; + background: rgba(255,255,255,0.4); + border: 1px solid var(--color-hairline); + border-radius: var(--radius-sm); + padding: 3px; + gap: 2px; + } + .clu-seg button { + padding: 6px 12px; + border-radius: 9px; + font-size: 12px; font-weight: 600; + color: var(--color-muted); + transition: background .15s, color .15s; + } + .clu-seg button:hover { color: var(--color-fg); } + .clu-seg button.active { + background: rgba(255,255,255,0.95); + color: var(--color-fg); + box-shadow: 0 1px 2px rgba(40,52,92,0.08); + } + .clu-seg button .count { opacity: 0.55; margin-left: 4px; font-family: var(--font-mono); } + + /* Ghost button */ + .clu-ghost-btn { + display: inline-flex; align-items: center; gap: 6px; + padding: 7px 12px; + background: rgba(255,255,255,0.5); + border: 1px solid var(--color-hairline); + border-radius: var(--radius-sm); + font-size: 12px; font-weight: 600; + color: var(--color-fg); + transition: background .15s, border-color .15s; + } + .clu-ghost-btn:hover { background: rgba(255,255,255,0.85); border-color: var(--color-glass-border); } + + /* Toolbar wrapper */ + .clu-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 18px; border-bottom: 1px solid var(--color-hairline); } + + /* Pagination row */ + .clu-pager { display: flex; align-items: center; justify-content: space-between; padding: 12px 18px; border-top: 1px solid var(--color-hairline); } + .clu-pager-meta { font-size: 12px; color: var(--color-muted); font-family: var(--font-mono); } + + /* Row actions cluster */ + .clu-row-actions { display: inline-flex; gap: 4px; justify-content: flex-end; } + .clu-row-actions button { + width: 28px; height: 28px; + border-radius: 8px; + color: var(--color-muted); + display: grid; place-items: center; + transition: background .15s, color .15s; + } + .clu-row-actions button:hover { background: rgba(255,255,255,0.7); color: var(--color-fg); } } diff --git a/app/resources/views/livewire/pages/sites/index.blade.php b/app/resources/views/livewire/pages/sites/index.blade.php new file mode 100644 index 0000000..615202e --- /dev/null +++ b/app/resources/views/livewire/pages/sites/index.blade.php @@ -0,0 +1,231 @@ +resetPage(); } + public function updatingFilter(): void { $this->resetPage(); } + public function updatingServerFilter(): void { $this->resetPage(); } + public function updatingSort(): void { $this->resetPage(); } + + public function setFilter(string $f): void + { + $this->filter = $f; + $this->resetPage(); + } + + public function with(): array + { + $query = Site::query()->with('server'); + + if ($this->search !== '') { + $query->where('domain', 'like', '%' . $this->search . '%'); + } + + match ($this->filter) { + 'updates' => $query->where('updates_pending', '>', 0), + 'warnings' => $query->where('status', 'warn'), + 'critical' => $query->where('status', 'bad'), + default => null, + }; + + if ($this->serverFilter !== '') { + $query->where('server_id', $this->serverFilter); + } + + $query->orderBy(match ($this->sort) { + 'visitors' => 'visitors_24h', + 'response' => 'response_ms', + 'updates' => 'updates_pending', + default => 'domain', + }, $this->sort === 'name' ? 'asc' : 'desc'); + + return [ + 'sites' => $query->paginate(15), + 'servers' => Server::orderBy('name')->get(), + 'totalSites' => Site::count(), + 'updatesCount' => Site::where('updates_pending', '>', 0)->count(), + 'warnCount' => Site::where('status', 'warn')->count(), + 'criticalCount' => Site::where('status', 'bad')->count(), + 'avgPagespeed' => (int) Site::avg('pagespeed_score'), + 'totalVisitors' => Site::sum('visitors_24h'), + ]; + } +}; ?> + +
+ {{-- ───── Topbar ───── --}} +
+
+

Sites

+
{{ $totalSites }} wordpress-installationen
+
+ {{ $totalSites }} online +
+ + +
+ + {{-- ───── Metric tiles ───── --}} +
+
+
+ Sites Online + +
+
{{ $totalSites }}/{{ $totalSites }}
+
100% erreichbar
+
+
+
+ Updates offen + +
+
{{ $updatesCount }}
+
{{ $criticalCount }} kritisch · {{ $warnCount }} warn
+
+
+
+ Ø PageSpeed + +
+
{{ $avgPagespeed }}/100
+
Core Web Vitals OK
+
+
+
+ Besucher / 24h + +
+
{{ number_format($totalVisitors / 1000, 0, ',', '.') }}k
+
↑ 12% vs Vorwoche
+
+
+ + {{-- ───── Sites table ───── --}} +
+
+
+ + + + +
+
+ + +
+
+
+ + + + + + + + + + + + + + + @forelse ($sites as $site) + + + + + + + + + + + @empty + + @endforelse + +
SiteServerWordPressPHPBesucher / 24hResponseStatus
+
+
{{ mb_substr($site->domain, 0, 1) }}
+
+ {{ $site->domain }} + ssl ✓ · {{ $site->cache_enabled ? 'cache an' : 'cache aus' }} +
+
+
{{ $site->server->name }}WP {{ $site->wp_version }}PHP {{ $site->php_version }}{{ number_format($site->visitors_24h, 0, ',', '.') }}{{ $site->response_ms }}ms + + {{ match($site->status) { + 'ok' => $site->updates_pending > 0 ? $site->updates_pending . ' Updates' : 'Optimal', + 'warn' => 'Wartung nötig', + 'bad' => 'Kritisch', + default => $site->status, + } }} + + +
+ + +
+
Keine Sites gefunden.
+
+
+ + {{ $sites->firstItem() ?? 0 }}–{{ $sites->lastItem() ?? 0 }} von {{ $sites->total() }} + +
+ + +
+
+
+
diff --git a/app/routes/web.php b/app/routes/web.php index be23a52..90b953c 100644 --- a/app/routes/web.php +++ b/app/routes/web.php @@ -15,7 +15,7 @@ Route::view('profile', 'profile') /* ───── Placeholder named routes for sidebar links (real Volt pages land later). ───── */ Route::middleware(['auth'])->group(function () { - Route::view('sites', 'placeholder', ['title' => 'Sites'])->name('sites.index'); + Volt::route('sites', 'pages.sites.index')->name('sites.index'); Route::view('servers', 'placeholder', ['title' => 'Server'])->name('servers.index'); Route::view('backups', 'placeholder', ['title' => 'Backups'])->name('backups.index'); Route::view('security', 'placeholder', ['title' => 'Sicherheit'])->name('security.index'); diff --git a/app/tests/Feature/Pages/SitesPageTest.php b/app/tests/Feature/Pages/SitesPageTest.php new file mode 100644 index 0000000..a122d6f --- /dev/null +++ b/app/tests/Feature/Pages/SitesPageTest.php @@ -0,0 +1,63 @@ +assertRedirect(route('login')); +}); + +it('renders the sites page chrome for an auth user', function () { + $user = User::factory()->create(['email_verified_at' => now()]); + + actingAs($user)->get(route('sites.index')) + ->assertStatus(200) + ->assertSeeText('Sites') + ->assertSeeText('Sites Online') + ->assertSeeText('Updates offen') + ->assertSeeText('Ø PageSpeed') + ->assertSeeText('Besucher / 24h') + ->assertSeeText('Site hinzufügen') + ->assertSee('clu-seg', false) + ->assertSee('clu-pager', false) + ->assertSee('clu-table', false); +}); + +it('lists seeded sites and paginates', function () { + $user = User::factory()->create(['email_verified_at' => now()]); + $server = Server::factory()->create(['name' => 'test-srv-01']); + Site::factory()->count(20)->create(['server_id' => $server->id]); + + actingAs($user)->get(route('sites.index')) + ->assertStatus(200) + ->assertSee('test-srv-01') + ->assertSeeText('von 20'); +}); + +it('filters by search term', function () { + $user = User::factory()->create(['email_verified_at' => now()]); + $server = Server::factory()->create(); + Site::factory()->create(['server_id' => $server->id, 'domain' => 'unique-match-1.test']); + Site::factory()->create(['server_id' => $server->id, 'domain' => 'other.test']); + + livewire('pages.sites.index') + ->set('search', 'unique-match') + ->assertSee('unique-match-1.test') + ->assertDontSee('other.test'); +}); + +it('filters by status segment', function () { + $user = User::factory()->create(['email_verified_at' => now()]); + $server = Server::factory()->create(); + Site::factory()->create(['server_id' => $server->id, 'status' => 'ok', 'domain' => 'healthy.test']); + Site::factory()->create(['server_id' => $server->id, 'status' => 'bad', 'domain' => 'broken.test']); + + livewire('pages.sites.index') + ->call('setFilter', 'critical') + ->assertSee('broken.test') + ->assertDontSee('healthy.test'); +});