clupilot/app/resources/views/livewire/pages/sites/index.blade.php

233 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
use App\Models\Server;
use App\Models\Site;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Url;
use Livewire\Volt\Component;
use Livewire\WithPagination;
new #[Layout('layouts.app')] class extends Component {
use WithPagination;
#[Url(as: 'q')]
public string $search = '';
#[Url(as: 'f')]
public string $filter = 'all'; // all | updates | warnings | critical
#[Url(as: 'srv')]
public string $serverFilter = '';
#[Url(as: 'sort')]
public string $sort = 'visitors';
public function updatingSearch(): void { $this->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'),
];
}
}; ?>
<div>
{{-- ───── Topbar ───── --}}
<div class="clu-topbar">
<x-clu.burger />
<div>
<h1 class="clu-page-title">Sites</h1>
<div class="clu-page-sub">{{ $totalSites }} wordpress-installationen</div>
</div>
<span class="clu-status-chip"><span class="clu-pulse-dot"></span> {{ $totalSites }} online</span>
<div class="spacer"></div>
<div class="clu-search">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-(--color-muted-2)" aria-hidden="true">
<circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/>
</svg>
<input type="search" wire:model.live.debounce.300ms="search" placeholder="Domain suchen…" />
<span class="kbd">⌘ K</span>
</div>
<button class="clu-btn-primary" style="height:36px;padding:0 14px;">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true">
<path d="M12 5v14M5 12h14"/>
</svg>
Site hinzufügen
</button>
</div>
{{-- ───── Metric tiles ───── --}}
<div class="grid gap-[14px] mb-[18px]" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr));">
<div class="clu-metric success">
<div class="clu-metric-row">
<span class="clu-metric-label">Sites Online</span>
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="M9 12l2 2 4-4"/></svg></span>
</div>
<div class="clu-metric-value">{{ $totalSites }}<span class="unit">/{{ $totalSites }}</span></div>
<div class="clu-metric-delta">100% erreichbar</div>
</div>
<div class="clu-metric warning">
<div class="clu-metric-row">
<span class="clu-metric-label">Updates offen</span>
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M21 12a9 9 0 1 1-3-6.7M21 4v5h-5"/></svg></span>
</div>
<div class="clu-metric-value">{{ $updatesCount }}</div>
<div class="clu-metric-delta warn">{{ $criticalCount }} kritisch · {{ $warnCount }} warn</div>
</div>
<div class="clu-metric">
<div class="clu-metric-row">
<span class="clu-metric-label">Ø PageSpeed</span>
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M12 3a9 9 0 1 0 9 9M12 12l5-5"/></svg></span>
</div>
<div class="clu-metric-value">{{ $avgPagespeed }}<span class="unit">/100</span></div>
<div class="clu-metric-delta">Core Web Vitals OK</div>
</div>
<div class="clu-metric">
<div class="clu-metric-row">
<span class="clu-metric-label">Besucher / 24h</span>
<span class="clu-metric-ico"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/></svg></span>
</div>
<div class="clu-metric-value">{{ number_format($totalVisitors / 1000, 0, ',', '.') }}<span class="unit">k</span></div>
<div class="clu-metric-delta">↑ 12% vs Vorwoche</div>
</div>
</div>
{{-- ───── Sites table ───── --}}
<div class="clu-card">
<div class="clu-toolbar">
<div class="clu-seg">
<button wire:click="setFilter('all')" class="@if($filter==='all') active @endif">Alle <span class="count">{{ $totalSites }}</span></button>
<button wire:click="setFilter('updates')" class="@if($filter==='updates') active @endif">Updates <span class="count">{{ $updatesCount }}</span></button>
<button wire:click="setFilter('warnings')" class="@if($filter==='warnings') active @endif">Warnungen <span class="count">{{ $warnCount }}</span></button>
<button wire:click="setFilter('critical')" class="@if($filter==='critical') active @endif">Kritisch <span class="count">{{ $criticalCount }}</span></button>
</div>
<div class="flex gap-[8px]">
<select wire:model.live="serverFilter" class="clu-ghost-btn" style="padding-right:8px;appearance:auto;">
<option value="">Server: Alle</option>
@foreach ($servers as $server)
<option value="{{ $server->id }}">{{ $server->name }}</option>
@endforeach
</select>
<select wire:model.live="sort" class="clu-ghost-btn" style="padding-right:8px;appearance:auto;">
<option value="visitors">Besucher</option>
<option value="response">Response</option>
<option value="updates">Updates</option>
<option value="name">Name</option>
</select>
</div>
</div>
<div class="clu-card-body tight" style="overflow-x:auto;">
<table class="clu-table">
<thead>
<tr>
<th>Site</th>
<th>Server</th>
<th>WordPress</th>
<th>PHP</th>
<th>Besucher / 24h</th>
<th>Response</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
@forelse ($sites as $site)
<tr>
<td>
<a href="{{ route('sites.show', $site) }}" wire:navigate class="clu-site-cell" style="text-decoration:none;color:inherit;">
<div class="clu-favicon">{{ mb_substr($site->domain, 0, 1) }}</div>
<div class="clu-site-meta">
<span class="clu-site-domain">{{ $site->domain }}</span>
<span class="clu-site-server">ssl ✓ · {{ $site->cache_enabled ? 'cache an' : 'cache aus' }}</span>
</div>
</a>
</td>
<td><span class="clu-pill muted">{{ $site->server->name }}</span></td>
<td><span class="clu-pill @if($site->updates_pending > 0) warning @else success @endif">WP {{ $site->wp_version }}</span></td>
<td><span class="clu-pill @if(version_compare($site->php_version, '8.0', '<')) warning @else muted @endif">PHP {{ $site->php_version }}</span></td>
<td class="font-mono">{{ number_format($site->visitors_24h, 0, ',', '.') }}</td>
<td class="font-mono">{{ $site->response_ms }}ms</td>
<td>
<span class="clu-health @if($site->status === 'warn') warn @elseif($site->status === 'bad') bad @endif">
{{ match($site->status) {
'ok' => $site->updates_pending > 0 ? $site->updates_pending . ' Updates' : 'Optimal',
'warn' => 'Wartung nötig',
'bad' => 'Kritisch',
default => $site->status,
} }}
</span>
</td>
<td>
<div class="clu-row-actions">
<button title="Site öffnen" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<path d="M14 3h7v7M21 3l-9 9M19 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5"/>
</svg>
</button>
<button title="Mehr" type="button">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<circle cx="5" cy="12" r="1.5"/><circle cx="12" cy="12" r="1.5"/><circle cx="19" cy="12" r="1.5"/>
</svg>
</button>
</div>
</td>
</tr>
@empty
<tr><td colspan="8" style="text-align:center;padding:40px;color:var(--color-muted);">Keine Sites gefunden.</td></tr>
@endforelse
</tbody>
</table>
</div>
<div class="clu-pager">
<span class="clu-pager-meta">
{{ $sites->firstItem() ?? 0 }}{{ $sites->lastItem() ?? 0 }} von {{ $sites->total() }}
</span>
<div class="flex gap-[6px]">
<button class="clu-ghost-btn" wire:click="previousPage" @if(! $sites->onFirstPage()) @else disabled @endif>← Zurück</button>
<button class="clu-ghost-btn" wire:click="nextPage" @if($sites->hasMorePages()) @else disabled @endif>Weiter →</button>
</div>
</div>
</div>
</div>