mailwolt/app/Livewire/Ui/Nx/Dashboard.php

212 lines
7.9 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\Setting as SettingModel;
use App\Support\CacheVer;
use Illuminate\Support\Facades\Cache;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.dvx')]
#[Title('Dashboard · Mailwolt')]
class Dashboard extends Component
{
public function render()
{
$cached = Cache::get(CacheVer::k('health:services'), []);
$rows = $cached['rows'] ?? [];
$services = array_map(fn($r) => [
'name' => $r['label'] ?? ucfirst($r['name']),
'type' => $r['hint'] ?? '',
'status' => ($r['ok'] ?? false) ? 'online' : 'offline',
], $rows);
[$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'));
return view('livewire.ui.nx.dashboard', [
'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' => 0,
'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,
...$this->mailSecurity(),
'ports' => $this->ports(),
]);
}
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];
}
}