203 lines
7.5 KiB
PHP
203 lines
7.5 KiB
PHP
<?php
|
||
|
||
namespace App\Livewire\Ui\System;
|
||
|
||
use Carbon\Carbon;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Livewire\Component;
|
||
|
||
class HealthCard extends Component
|
||
{
|
||
// Inputquellen
|
||
public array $meta = [];
|
||
|
||
// UI: Balken/Segmente
|
||
public int $barSegments = 24;
|
||
public array $cpuSeg = [];
|
||
public array $ramSeg = [];
|
||
|
||
// Anzeige
|
||
public ?int $cpuPercent = null;
|
||
public ?int $ramPercent = null;
|
||
public ?string $ramSummary = null;
|
||
public ?string $loadText = null;
|
||
public array $loadDots = [];
|
||
public ?string $uptimeText = null;
|
||
public string $uptimeIcon = 'ph ph-clock';
|
||
public ?string $updatedAtHuman = null;
|
||
|
||
// NEW: IPs
|
||
public ?string $ip4 = null;
|
||
public ?string $ip6 = null;
|
||
|
||
public function mount(): void
|
||
{
|
||
$this->loadData();
|
||
}
|
||
|
||
public function loadData(): void
|
||
{
|
||
$this->meta = Cache::get('health:meta', []);
|
||
|
||
$this->hydrateSystem();
|
||
$this->hydrateIps();
|
||
$this->hydrateUpdatedAt();
|
||
}
|
||
|
||
public function render()
|
||
{
|
||
return view('livewire.ui.system.health-card');
|
||
}
|
||
|
||
/* --------------- Aufbereitung --------------- */
|
||
|
||
protected function hydrateSystem(): void
|
||
{
|
||
$sys = is_array($this->meta['system'] ?? null) ? $this->meta['system'] : [];
|
||
|
||
// RAM % + Summary
|
||
$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']));
|
||
}
|
||
$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)
|
||
$l1 = $this->numFloat($this->pick($sys, ['cpu_load_1','load1','load_1']));
|
||
$l5 = $this->numFloat($this->pick($sys, ['cpu_load_5','load5','load_5']));
|
||
$l15 = $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);
|
||
$l1 ??= $this->numFloat($vals[0] ?? null);
|
||
$l5 ??= $this->numFloat($vals[1] ?? null);
|
||
$l15 ??= $this->numFloat($vals[2] ?? null);
|
||
}
|
||
$this->loadText = $this->fmtLoad($l1, $l5, $l15, is_string($loadMixed) ? $loadMixed : null);
|
||
|
||
// CPU %
|
||
$this->cpuPercent = $this->numInt($this->pick($sys, ['cpu','cpu_percent','cpuUsage','cpu_usage']));
|
||
if ($this->cpuPercent === null && $l1 !== null) {
|
||
$cores = $this->numInt($this->pick($sys, ['cores','cpu_cores','num_cores'])) ?: 1;
|
||
$this->cpuPercent = (int) round(min(100, max(0, ($l1 / 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);
|
||
}
|
||
|
||
// Segmente
|
||
$this->cpuSeg = $this->buildSegments($this->cpuPercent);
|
||
$this->ramSeg = $this->buildSegments($this->ramPercent);
|
||
|
||
// Load Dots relativ zu Kernen
|
||
$cores = max(1, (int)($this->pick($sys, ['cores','cpu_cores','num_cores']) ?? 1));
|
||
$ratio1 = $l1 !== null ? $l1 / $cores : null;
|
||
$ratio5 = $l5 !== null ? $l5 / $cores : null;
|
||
$ratio15 = $l15 !== null ? $l15 / $cores : null;
|
||
$this->loadDots = [
|
||
['label'=>'1m', 'cls'=>$this->loadDotClass($ratio1)],
|
||
['label'=>'5m', 'cls'=>$this->loadDotClass($ratio5)],
|
||
['label'=>'15m', 'cls'=>$this->loadDotClass($ratio15)],
|
||
];
|
||
}
|
||
|
||
protected function hydrateIps(): void
|
||
{
|
||
$sys = is_array($this->meta['system'] ?? null) ? $this->meta['system'] : [];
|
||
// Versuche diverse mögliche Keys
|
||
$this->ip4 = $this->pick($sys, ['ipv4','ip4','public_ipv4','server_public_ipv4','lan_ipv4']);
|
||
$this->ip6 = $this->pick($sys, ['ipv6','ip6','public_ipv6','server_public_ipv6','lan_ipv6']);
|
||
|
||
// Fallback: aus installer.env lesen
|
||
if (!$this->ip4 || !$this->ip6) {
|
||
$env = @file('/etc/mailwolt/installer.env', FILE_IGNORE_NEW_LINES) ?: [];
|
||
foreach ($env as $ln) {
|
||
if (!$this->ip4 && str_starts_with($ln, 'SERVER_PUBLIC_IPV4=')) {
|
||
$this->ip4 = trim(substr($ln, strlen('SERVER_PUBLIC_IPV4=')));
|
||
}
|
||
if (!$this->ip6 && str_starts_with($ln, 'SERVER_PUBLIC_IPV6=')) {
|
||
$this->ip6 = trim(substr($ln, strlen('SERVER_PUBLIC_IPV6=')));
|
||
}
|
||
}
|
||
}
|
||
|
||
// Auf hübsch: dünne Zwischenräume (wir lassen Blade das stylen – monospaced + tracking)
|
||
$this->ip4 = $this->ip4 ?: '–';
|
||
$this->ip6 = $this->ip6 ?: '–';
|
||
}
|
||
|
||
protected function hydrateUpdatedAt(): void
|
||
{
|
||
$updated = $this->meta['updated_at'] ?? null;
|
||
try { $this->updatedAtHuman = $updated ? Carbon::parse($updated)->diffForHumans() : '–';
|
||
} catch (\Throwable) { $this->updatedAtHuman = '–'; }
|
||
}
|
||
|
||
/* --------------- 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';
|
||
}
|
||
}
|