293 lines
11 KiB
PHP
293 lines
11 KiB
PHP
<?php
|
||
|
||
namespace App\Livewire\Ui\Dashboard;
|
||
|
||
use Carbon\Carbon;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Livewire\Component;
|
||
|
||
class HealthCard extends Component
|
||
{
|
||
// Rohdaten
|
||
public array $services = [];
|
||
public array $meta = [];
|
||
|
||
// UI: Balken/Segmente
|
||
public int $barSegments = 24;
|
||
public array $cpuSeg = [];
|
||
public array $ramSeg = [];
|
||
|
||
// UI: Load & Uptime
|
||
public string $loadBadgeText = '';
|
||
public array $loadDots = [];
|
||
public array $uptimeChips = [];
|
||
public string $uptimeIcon = 'ph ph-clock';
|
||
|
||
// UI: Services kompakt
|
||
public array $servicesCompact = [];
|
||
|
||
// Storage
|
||
public ?int $diskUsedGb = null;
|
||
public ?int $diskTotalGb = null;
|
||
public ?int $diskPercent = null;
|
||
public ?int $diskFreeGb = null;
|
||
public array $diskSegments = [];
|
||
public int $diskInnerSize = 160; // grauer Kreis
|
||
public int $diskSegOuterRadius = 92; // Abstand der Ticks
|
||
public array $diskCenterText = ['percent'=>'–%','label'=>'Speicher belegt'];
|
||
|
||
// Anzeige oben
|
||
public ?string $ramSummary = null;
|
||
public ?string $loadText = null;
|
||
public ?string $uptimeText = null;
|
||
public ?int $cpuPercent = null;
|
||
public ?int $ramPercent = null;
|
||
public ?string $updatedAtHuman = null;
|
||
|
||
public function mount(): void
|
||
{
|
||
$this->loadData();
|
||
}
|
||
|
||
public function loadData(): void
|
||
{
|
||
$this->services = Cache::get('health:services', []);
|
||
$this->meta = Cache::get('health:meta', []);
|
||
|
||
$this->hydrateSystem();
|
||
$this->hydrateDisk();
|
||
$this->hydrateUpdatedAt();
|
||
$this->decorateServicesCompact();
|
||
$this->decorateDisk();
|
||
}
|
||
|
||
public function render()
|
||
{
|
||
return view('livewire.ui.dashboard.health-card');
|
||
}
|
||
|
||
/* ---------------- Aufbereitung ---------------- */
|
||
|
||
protected function hydrateSystem(): void
|
||
{
|
||
$sys = is_array($this->meta['system'] ?? null) ? $this->meta['system'] : [];
|
||
|
||
// RAM %
|
||
$this->ramPercent = $this->numInt($this->pick($sys, ['ram','ram_percent','mem_percent','memory']));
|
||
if ($this->ramPercent === null && is_array($sys['ram'] ?? null)) {
|
||
$this->ramPercent = $this->numInt($this->pick($sys['ram'], ['percent','used_percent']));
|
||
}
|
||
|
||
// RAM Summary "used / total"
|
||
$used = $this->numInt($this->pick($sys['ram'] ?? [], ['used_gb','used','usedGB']));
|
||
$tot = $this->numInt($this->pick($sys['ram'] ?? [], ['total_gb','total','totalGB']));
|
||
$this->ramSummary = ($used !== null && $tot !== null) ? "{$used} GB / {$tot} GB" : null;
|
||
|
||
// Load (1/5/15)
|
||
$load1 = $this->numFloat($this->pick($sys, ['cpu_load_1','load1','load_1']));
|
||
$load5 = $this->numFloat($this->pick($sys, ['cpu_load_5','load5','load_5']));
|
||
$load15 = $this->numFloat($this->pick($sys, ['cpu_load_15','load15','load_15']));
|
||
|
||
$loadMixed = $this->pick($sys, ['load','loadavg','load_avg','load_text']);
|
||
if (is_array($loadMixed)) {
|
||
$vals = array_values($loadMixed);
|
||
$load1 ??= $this->numFloat($vals[0] ?? null);
|
||
$load5 ??= $this->numFloat($vals[1] ?? null);
|
||
$load15 ??= $this->numFloat($vals[2] ?? null);
|
||
}
|
||
$this->loadText = $this->fmtLoad($load1, $load5, $load15, is_string($loadMixed) ? $loadMixed : null);
|
||
|
||
// CPU %
|
||
$this->cpuPercent = $this->numInt($this->pick($sys, ['cpu','cpu_percent','cpuUsage','cpu_usage']));
|
||
if ($this->cpuPercent === null && $load1 !== null) {
|
||
$cores = $this->numInt($this->pick($sys, ['cores','cpu_cores','num_cores'])) ?: 1;
|
||
$this->cpuPercent = (int) round(min(100, max(0, ($load1 / max(1,$cores)) * 100)));
|
||
}
|
||
|
||
// Uptime
|
||
$this->uptimeText = $this->pick($sys, ['uptime_human','uptimeText','uptime']);
|
||
if (!$this->uptimeText) {
|
||
$uptimeSec = $this->numInt($this->pick($sys, ['uptime_seconds','uptime_sec','uptime_s']));
|
||
if ($uptimeSec !== null) $this->uptimeText = $this->secondsToHuman($uptimeSec);
|
||
}
|
||
|
||
// Segment-Balken
|
||
$this->cpuSeg = $this->buildSegments($this->cpuPercent);
|
||
$this->ramSeg = $this->buildSegments($this->ramPercent);
|
||
|
||
// Load: Dots (1/5/15 relativ zu Kernen)
|
||
$cores = max(1, (int)($this->pick($sys, ['cores','cpu_cores','num_cores']) ?? 1));
|
||
$ratio1 = $load1 !== null ? $load1 / $cores : null;
|
||
$ratio5 = $load5 !== null ? $load5 / $cores : null;
|
||
$ratio15 = $load15 !== null ? $load15 / $cores : null;
|
||
|
||
$this->loadBadgeText = $this->loadText ?? '–';
|
||
$this->loadDots = [
|
||
['label'=>'1m', 'cls'=>$this->loadDotClass($ratio1)],
|
||
['label'=>'5m', 'cls'=>$this->loadDotClass($ratio5)],
|
||
['label'=>'15m', 'cls'=>$this->loadDotClass($ratio15)],
|
||
];
|
||
|
||
// Uptime Chips
|
||
$chips = [];
|
||
if ($this->uptimeText) {
|
||
$d = $h = $m = null;
|
||
if (preg_match('/(\d+)d/i', $this->uptimeText, $m1)) $d = (int)$m1[1];
|
||
if (preg_match('/(\d+)h/i', $this->uptimeText, $m2)) $h = (int)$m2[1];
|
||
if (preg_match('/(\d+)m/i', $this->uptimeText, $m3)) $m = (int)$m3[1];
|
||
if ($d !== null) $chips[] = ['v'=>$d, 'u'=>'Tage'];
|
||
if ($h !== null) $chips[] = ['v'=>$h, 'u'=>'Stunden'];
|
||
if ($m !== null) $chips[] = ['v'=>$m, 'u'=>'Minuten'];
|
||
}
|
||
$this->uptimeChips = $chips ?: [['v'=>'–','u'=>'']];
|
||
}
|
||
|
||
protected function hydrateDisk(): void
|
||
{
|
||
$disk = is_array($this->meta['disk'] ?? null) ? $this->meta['disk'] : [];
|
||
|
||
$this->diskPercent = $this->numInt($this->pick($disk, ['percent','usage']));
|
||
$this->diskFreeGb = $this->numInt($this->pick($disk, ['free_gb','free']));
|
||
|
||
// total/used berechnen, falls nicht geliefert
|
||
if ($this->diskFreeGb !== null && $this->diskPercent !== null && $this->diskPercent < 100) {
|
||
$p = max(0, min(100, $this->diskPercent)) / 100;
|
||
$estTotal = (int) round($this->diskFreeGb / (1 - $p));
|
||
$this->diskTotalGb = $estTotal > 0 ? $estTotal : null;
|
||
}
|
||
if ($this->diskTotalGb !== null && $this->diskFreeGb !== null) {
|
||
$u = $this->diskTotalGb - $this->diskFreeGb;
|
||
$this->diskUsedGb = $u >= 0 ? $u : null;
|
||
}
|
||
}
|
||
|
||
protected function hydrateUpdatedAt(): void
|
||
{
|
||
$updated = $this->meta['updated_at'] ?? null;
|
||
try { $this->updatedAtHuman = $updated ? Carbon::parse($updated)->diffForHumans() : '–';
|
||
} catch (\Throwable) { $this->updatedAtHuman = '–'; }
|
||
}
|
||
|
||
protected function decorateServicesCompact(): void
|
||
{
|
||
$nameMap = [
|
||
'postfix' => ['label' => 'Postfix', 'hint' => 'MTA'],
|
||
'dovecot' => ['label' => 'Dovecot', 'hint' => 'IMAP/POP3'],
|
||
'rspamd' => ['label' => 'Rspamd', 'hint' => 'Spamfilter'],
|
||
'db' => ['label' => 'Datenbank', 'hint' => 'MySQL/MariaDB'],
|
||
'127.0.0.1:6379' => ['label' => 'Redis', 'hint' => 'Cache/Queue'],
|
||
'127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket'],
|
||
];
|
||
|
||
$this->servicesCompact = collect($this->services)
|
||
->map(function ($srv) use ($nameMap) {
|
||
$key = (string)($srv['name'] ?? '');
|
||
$ok = (bool) ($srv['ok'] ?? false);
|
||
$label = $nameMap[$key]['label'] ?? ucfirst($key);
|
||
$hint = $nameMap[$key]['hint'] ?? (str_contains($key, ':') ? $key : '');
|
||
|
||
return [
|
||
'label' => $label,
|
||
'hint' => $hint,
|
||
'ok' => $ok,
|
||
'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400',
|
||
'pillText' => $ok ? 'ok' : 'down',
|
||
'pillClass' => $ok
|
||
? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
|
||
: 'text-rose-300 border-rose-400/30 bg-rose-500/10',
|
||
];
|
||
})
|
||
->values()
|
||
->all();
|
||
}
|
||
|
||
protected function decorateDisk(): void
|
||
{
|
||
$percent = (int) max(0, min(100, (int)($this->diskPercent ?? 0)));
|
||
$this->diskCenterText = [
|
||
'percent' => is_numeric($this->diskPercent) ? $this->diskPercent.'%' : '–%',
|
||
'label' => 'Speicher belegt',
|
||
];
|
||
|
||
$count = 48;
|
||
$activeN = is_numeric($this->diskPercent) ? (int) round($count * $percent / 100) : 0;
|
||
$step = 360 / $count;
|
||
|
||
$activeClass = match (true) {
|
||
$percent >= 90 => 'bg-rose-400',
|
||
$percent >= 70 => 'bg-amber-300',
|
||
default => 'bg-emerald-400',
|
||
};
|
||
|
||
$this->diskSegments = [];
|
||
for ($i = 0; $i < $count; $i++) {
|
||
$angle = ($i * $step) - 90; // Start bei 12 Uhr
|
||
$this->diskSegments[] = [
|
||
'angle' => $angle,
|
||
'class' => $i < $activeN ? $activeClass : 'bg-white/16',
|
||
];
|
||
}
|
||
}
|
||
|
||
/* ---------------- Helpers ---------------- */
|
||
|
||
protected function pick(array $arr, array $keys)
|
||
{
|
||
foreach ($keys as $k) {
|
||
if (array_key_exists($k, $arr) && $arr[$k] !== null && $arr[$k] !== '') {
|
||
return $arr[$k];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
protected function numInt($v): ?int { return is_numeric($v) ? (int) round($v) : null; }
|
||
protected function numFloat($v): ?float{ return is_numeric($v) ? (float)$v : null; }
|
||
|
||
protected function fmtLoad(?float $l1, ?float $l5, ?float $l15, ?string $fallback): ?string
|
||
{
|
||
if ($l1 !== null || $l5 !== null || $l15 !== null) {
|
||
$fmt = fn($x) => $x === null ? '–' : number_format($x, 2);
|
||
return "{$fmt($l1)} / {$fmt($l5)} / {$fmt($l15)}";
|
||
}
|
||
return $fallback ?: null;
|
||
}
|
||
|
||
protected function secondsToHuman(int $s): string
|
||
{
|
||
$d = intdiv($s, 86400); $s %= 86400;
|
||
$h = intdiv($s, 3600); $s %= 3600;
|
||
$m = intdiv($s, 60);
|
||
if ($d > 0) return "{$d}d {$h}h";
|
||
if ($h > 0) return "{$h}h {$m}m";
|
||
return "{$m}m";
|
||
}
|
||
|
||
protected function toneByPercent(?int $p): string {
|
||
if ($p === null) return 'white';
|
||
if ($p >= 90) return 'rose';
|
||
if ($p >= 70) return 'amber';
|
||
return 'emerald';
|
||
}
|
||
|
||
protected function buildSegments(?int $percent): array {
|
||
$n = max(6, $this->barSegments);
|
||
$filled = is_numeric($percent) ? (int) round($n * max(0, min(100, $percent)) / 100) : 0;
|
||
$tone = $this->toneByPercent($percent);
|
||
$fillCls = match($tone) {
|
||
'rose' => 'bg-rose-500/80',
|
||
'amber' => 'bg-amber-400/80',
|
||
'emerald'=> 'bg-emerald-500/80',
|
||
default => 'bg-white/20',
|
||
};
|
||
return array_map(fn($i) => $i < $filled ? $fillCls : 'bg-white/10', range(0, $n-1));
|
||
}
|
||
|
||
protected function loadDotClass(?float $ratio): string {
|
||
if ($ratio === null) return 'bg-white/25';
|
||
if ($ratio >= 0.9) return 'bg-rose-500';
|
||
if ($ratio >= 0.7) return 'bg-amber-400';
|
||
return 'bg-emerald-400';
|
||
}
|
||
}
|