mailwolt/app/Livewire/Ui/System/HealthCard.php

275 lines
10 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
namespace App\Livewire\Ui\System;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\On;
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 ?int $uptimeDays = null;
public ?int $uptimeHours = null;
public ?string $uptimeDaysLabel = null;
public ?string $uptimeHoursLabel = 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 getListeners(): array
{
// exakt auf die Event-Klasse hören (kein Alias)
return [
'echo-private:health,HealthUpdated' => 'onHealthEcho',
];
}
public function onHealthEcho($payload): void
{
// ① Bei manchen Treibern kommt $payload als JSON-String
if (is_string($payload)) {
$decoded = json_decode($payload, true);
if (json_last_error() === JSON_ERROR_NONE) {
$payload = $decoded;
}
}
// ② Manche Treiber kapseln erneut in "data"
if (isset($payload['data']) && is_string($payload['data'])) {
$decoded = json_decode($payload['data'], true);
if (json_last_error() === JSON_ERROR_NONE) {
$payload = $decoded;
}
} elseif (isset($payload['data']) && is_array($payload['data'])) {
$payload = $payload['data'];
}
$meta = $payload['meta'] ?? [];
if (!is_array($meta)) $meta = [];
$this->meta = $meta;
$this->hydrateSystem();
$this->hydrateIps();
$this->hydrateUpdatedAt();
// optionales Debug-Log (kein JS nötig)
logger('Health WS received', ['meta' => $this->meta]);
}
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);
// }
$uptimeSec = $this->numInt($this->pick($sys, ['uptime_seconds','uptime_sec','uptime_s']));
if ($uptimeSec !== null) {
$parsed = $this->parseUptime($uptimeSec);
$this->uptimeText = "{$parsed['days']}d {$parsed['hours']}h"; // bestehende Anzeige
$this->uptimeDays = $parsed['days'];
$this->uptimeHours = $parsed['hours'];
$this->uptimeDaysLabel = $parsed['days_text'];
$this->uptimeHoursLabel = $parsed['hours_text'];
}
// 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 parseUptime(int $seconds): array
{
$days = intdiv($seconds, 86400);
$hours = intdiv($seconds % 86400, 3600);
$minutes = intdiv($seconds % 3600, 60);
return [
'days' => $days,
'hours' => $hours,
'minutes' => $minutes,
'days_text' => $days === 1 ? 'Tag' : 'Tage', // string
'hours_text' => $hours === 1 ? 'Stunde' : 'Stunden', // string
];
}
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';
}
}