290 lines
11 KiB
PHP
290 lines
11 KiB
PHP
<?php
|
||
|
||
namespace App\Livewire\Ui\Nx;
|
||
|
||
use App\Models\BackupJob;
|
||
use App\Models\BackupPolicy;
|
||
use App\Models\Domain;
|
||
use App\Models\MailUser;
|
||
use App\Models\SandboxRoute;
|
||
use App\Models\Setting as SettingModel;
|
||
use App\Support\CacheVer;
|
||
use Illuminate\Support\Facades\Cache;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Livewire\Attributes\Layout;
|
||
use Livewire\Attributes\Title;
|
||
use Livewire\Component;
|
||
|
||
#[Layout('layouts.dvx')]
|
||
#[Title('Dashboard · Mailwolt')]
|
||
class Dashboard extends Component
|
||
{
|
||
public function render()
|
||
{
|
||
$services = $this->loadServices();
|
||
|
||
[$cpu, $cpuCores, $cpuMhz] = $this->cpu();
|
||
[$ramPercent, $ramUsed, $ramTotal] = $this->ram();
|
||
[$load1, $load5, $load15] = $this->load();
|
||
[$uptimeDays, $uptimeHours] = $this->uptime();
|
||
[$diskUsedPercent, $diskUsedGb, $diskFreeGb, $diskTotalGb] = $this->disk();
|
||
|
||
$servicesActive = count(array_filter($services, fn($s) => $s['status'] === 'online'));
|
||
|
||
// Echte Zertifikatsdateien prüfen — nicht nur DB-Wert (kann veraltet sein)
|
||
$uiDomain = (string) SettingModel::get('ui_domain', '');
|
||
$sslConfigured = !$uiDomain
|
||
|| file_exists("/etc/letsencrypt/renewal/{$uiDomain}.conf")
|
||
|| is_dir("/etc/letsencrypt/live/{$uiDomain}");
|
||
// DB-Wert nachziehen damit Banner nach einmaligem Check dauerhaft weg ist
|
||
if ($sslConfigured && SettingModel::get('ssl_configured', '0') !== '1') {
|
||
SettingModel::set('ssl_configured', '1');
|
||
}
|
||
|
||
return view('livewire.ui.nx.dashboard', [
|
||
'sslConfigured' => $sslConfigured,
|
||
'domainCount' => Domain::where('is_system', false)->where('is_server', false)->count(),
|
||
'mailboxCount' => MailUser::where('is_system', false)->where('is_active', true)->count(),
|
||
'servicesActive' => $servicesActive,
|
||
'servicesTotal' => count($services),
|
||
'alertCount' => SandboxRoute::where('is_active', true)->count(),
|
||
'sandboxAlerts' => SandboxRoute::activeRoutes(),
|
||
'backup' => $this->backupData(),
|
||
'mailHostname' => gethostname() ?: 'mailserver',
|
||
'services' => $services,
|
||
'cpu' => $cpu,
|
||
'cpuCores' => $cpuCores,
|
||
'cpuMhz' => $cpuMhz,
|
||
'ramPercent' => $ramPercent,
|
||
'ramUsed' => $ramUsed,
|
||
'ramTotal' => $ramTotal,
|
||
'load1' => $load1,
|
||
'load5' => $load5,
|
||
'load15' => $load15,
|
||
'uptimeDays' => $uptimeDays,
|
||
'uptimeHours' => $uptimeHours,
|
||
'diskUsedPercent' => $diskUsedPercent,
|
||
'diskUsedGb' => $diskUsedGb,
|
||
'diskFreeGb' => $diskFreeGb,
|
||
'diskTotalGb' => $diskTotalGb,
|
||
'updateLatest' => Cache::get('updates:latest_raw') ?: (Cache::get('updates:latest') ? 'v' . Cache::get('updates:latest') : null),
|
||
...$this->mailSecurity(),
|
||
'ports' => $this->ports(),
|
||
]);
|
||
}
|
||
|
||
|
||
private function loadServices(): array
|
||
{
|
||
$allCards = config('woltguard.cards', []);
|
||
$dashKeys = config('woltguard.dashboard', array_keys($allCards));
|
||
|
||
// 1) Monit-Cache für nicht-optionale Dienste
|
||
$cached = Cache::get(CacheVer::k('health:services'), []);
|
||
$monitRows = $cached['rows'] ?? [];
|
||
$monitIndex = [];
|
||
foreach ($monitRows as $r) {
|
||
$monitIndex[strtolower($r['name'] ?? '')] = $r;
|
||
}
|
||
|
||
$rows = [];
|
||
foreach ($dashKeys as $key) {
|
||
$card = $allCards[$key] ?? null;
|
||
if (!$card) continue;
|
||
$optional = $card['optional'] ?? false;
|
||
|
||
// Optionale Dienste (z.B. ClamAV) immer live prüfen – Monit-Cache kann veraltet sein
|
||
if (!$optional && isset($monitIndex[strtolower($card['label'] ?? '')])) {
|
||
$rows[] = $monitIndex[strtolower($card['label'])];
|
||
continue;
|
||
}
|
||
|
||
$isOk = false;
|
||
foreach ($card['sources'] as $src) {
|
||
if ($this->probeSource($src)) { $isOk = true; break; }
|
||
}
|
||
if (!$isOk && $optional) continue;
|
||
$rows[] = ['label' => $card['label'], 'hint' => $card['hint'], 'ok' => $isOk];
|
||
}
|
||
|
||
// Fallback: wenn gar keine Daten, alle live proben
|
||
if (empty($rows) && !empty($monitRows)) {
|
||
$rows = $monitRows;
|
||
}
|
||
|
||
return array_map(fn($r) => [
|
||
'name' => $r['label'] ?? ucfirst($r['name'] ?? ''),
|
||
'type' => $r['hint'] ?? '',
|
||
'status' => ($r['ok'] ?? false) ? 'online' : 'offline',
|
||
], $rows);
|
||
}
|
||
|
||
private function probeSource(string $src): bool
|
||
{
|
||
if (str_starts_with($src, 'systemd:')) {
|
||
$unit = substr($src, 8);
|
||
$exit = null;
|
||
@exec("systemctl is-active --quiet " . escapeshellarg($unit) . " 2>/dev/null", $_, $exit);
|
||
return $exit === 0;
|
||
}
|
||
if (str_starts_with($src, 'tcp:')) {
|
||
[, $host, $port] = explode(':', $src, 3);
|
||
$fp = @fsockopen($host, (int)$port, $e1, $e2, 1);
|
||
if (is_resource($fp)) { fclose($fp); return true; }
|
||
return false;
|
||
}
|
||
if (str_starts_with($src, 'socket:')) {
|
||
return @file_exists(substr($src, 7));
|
||
}
|
||
if ($src === 'db') {
|
||
try { \Illuminate\Support\Facades\DB::connection()->getPdo(); return true; }
|
||
catch (\Throwable) { return false; }
|
||
}
|
||
return false;
|
||
}
|
||
|
||
private function ports(): array
|
||
{
|
||
$check = [25, 465, 587, 110, 143, 993, 995, 80, 443];
|
||
$out = trim(@shell_exec('ss -tlnH 2>/dev/null') ?? '');
|
||
$listening = [];
|
||
foreach (explode("\n", $out) as $line) {
|
||
if (preg_match('/:(\d+)\s/', $line, $m)) {
|
||
$listening[(int)$m[1]] = true;
|
||
}
|
||
}
|
||
$result = [];
|
||
foreach ($check as $port) {
|
||
$result[$port] = isset($listening[$port]);
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
private function mailSecurity(): array
|
||
{
|
||
// rspamd metrics from cache (populated by spamav:collect every 5 min)
|
||
$av = Cache::get('dash.spamav') ?? SettingModel::get('spamav.metrics', []);
|
||
|
||
$spam = (int)($av['spam'] ?? 0);
|
||
$reject = (int)($av['reject'] ?? 0);
|
||
$ham = (int)($av['ham'] ?? 0);
|
||
$clamVer = $av['clamVer'] ?? '—';
|
||
|
||
// Postfix queue counts (active + deferred)
|
||
$queueOut = trim(@shell_exec('postqueue -p 2>/dev/null') ?? '');
|
||
$qActive = preg_match_all('/^[A-F0-9]{9,}\*?\s+/mi', $queueOut);
|
||
$qDeferred = substr_count($queueOut, '(deferred)');
|
||
$qTotal = $qActive + $qDeferred;
|
||
|
||
return [
|
||
'spamBlocked' => $spam + $reject,
|
||
'spamTagged' => $spam,
|
||
'spamRejected'=> $reject,
|
||
'hamCount' => $ham,
|
||
'clamVer' => $clamVer,
|
||
'queueTotal' => $qTotal,
|
||
'queueDeferred' => $qDeferred,
|
||
];
|
||
}
|
||
|
||
private function cpu(): array
|
||
{
|
||
$cores = (int)(shell_exec("nproc 2>/dev/null") ?: 1);
|
||
$mhz = round((float)shell_exec("awk '/^cpu MHz/{s+=$4;n++}END{if(n)print s/n}' /proc/cpuinfo 2>/dev/null") / 1000, 1);
|
||
$s1 = $this->stat(); usleep(400000); $s2 = $this->stat();
|
||
$idle1 = $s1[3] + ($s1[4] ?? 0); $idle2 = $s2[3] + ($s2[4] ?? 0);
|
||
$dt = array_sum($s2) - array_sum($s1);
|
||
$cpu = $dt > 0 ? max(0, min(100, round(($dt - ($idle2 - $idle1)) / $dt * 100))) : 0;
|
||
return [$cpu, $cores, $mhz ?: '—'];
|
||
}
|
||
|
||
private function stat(): array
|
||
{
|
||
$p = preg_split('/\s+/', trim(shell_exec("head -1 /proc/stat 2>/dev/null") ?: ''));
|
||
array_shift($p);
|
||
return array_map('intval', $p);
|
||
}
|
||
|
||
private function ram(): array
|
||
{
|
||
preg_match('/MemTotal:\s+(\d+)/', shell_exec("cat /proc/meminfo") ?: '', $mt);
|
||
preg_match('/MemAvailable:\s+(\d+)/', shell_exec("cat /proc/meminfo") ?: '', $ma);
|
||
$total = (int)($mt[1] ?? 0); $avail = (int)($ma[1] ?? 0); $used = $total - $avail;
|
||
return [$total > 0 ? round($used / $total * 100) : 0, round($used / 1048576, 1), round($total / 1048576, 1)];
|
||
}
|
||
|
||
private function backupData(): array
|
||
{
|
||
$policy = BackupPolicy::first();
|
||
if (!$policy) {
|
||
return ['status' => 'unconfigured', 'last_at' => null, 'last_at_full' => null, 'size' => null, 'duration' => null, 'next_at' => null, 'enabled' => false];
|
||
}
|
||
|
||
$running = BackupJob::whereIn('status', ['queued', 'running'])->exists();
|
||
if ($running) {
|
||
$status = 'running';
|
||
} elseif ($policy->last_run_at === null) {
|
||
$status = 'pending';
|
||
} else {
|
||
$status = $policy->last_status ?? 'unknown';
|
||
}
|
||
|
||
$size = ($policy->last_size_bytes ?? 0) > 0 ? $this->fmtBytes($policy->last_size_bytes) : null;
|
||
|
||
$duration = null;
|
||
$lastJob = BackupJob::where('status', 'ok')->latest('finished_at')->first();
|
||
if ($lastJob && $lastJob->started_at && $lastJob->finished_at) {
|
||
$secs = $lastJob->started_at->diffInSeconds($lastJob->finished_at);
|
||
$duration = $secs >= 60
|
||
? round($secs / 60) . ' min'
|
||
: $secs . 's';
|
||
}
|
||
|
||
$next = null;
|
||
if ($policy->enabled && $policy->schedule_cron) {
|
||
try {
|
||
$cron = new \Cron\CronExpression($policy->schedule_cron);
|
||
$next = $cron->getNextRunDate()->format('d.m.Y H:i');
|
||
} catch (\Throwable) {}
|
||
}
|
||
|
||
return [
|
||
'status' => $status,
|
||
'last_at' => $policy->last_run_at?->diffForHumans(),
|
||
'last_at_full' => $policy->last_run_at?->format('d.m.Y H:i'),
|
||
'size' => $size,
|
||
'duration' => $duration,
|
||
'next_at' => $next,
|
||
'enabled' => (bool) $policy->enabled,
|
||
];
|
||
}
|
||
|
||
private function fmtBytes(int $bytes): string
|
||
{
|
||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||
$i = 0;
|
||
$v = (float) $bytes;
|
||
while ($v >= 1024 && $i < 4) { $v /= 1024; $i++; }
|
||
return number_format($v, $i <= 1 ? 0 : 1) . ' ' . $units[$i];
|
||
}
|
||
|
||
private function load(): array
|
||
{
|
||
$p = explode(' ', trim(shell_exec("cat /proc/loadavg") ?: ''));
|
||
return [$p[0] ?? '0.00', $p[1] ?? '0.00', $p[2] ?? '0.00'];
|
||
}
|
||
|
||
private function uptime(): array
|
||
{
|
||
$s = (int)(float)(shell_exec("awk '{print $1}' /proc/uptime") ?: 0);
|
||
return [intdiv($s, 86400), intdiv($s % 86400, 3600)];
|
||
}
|
||
|
||
private function disk(): array
|
||
{
|
||
$p = preg_split('/\s+/', trim(shell_exec("df -BG / 2>/dev/null | tail -1") ?: ''));
|
||
$total = (int)($p[1] ?? 0); $used = (int)($p[2] ?? 0); $free = (int)($p[3] ?? 0);
|
||
return [$total > 0 ? round($used / $total * 100) : 0, $used, $free, $total];
|
||
}
|
||
}
|