233 lines
12 KiB
PHP
233 lines
12 KiB
PHP
<?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>
|