Fix: Mailbox Stats über Dovecot mit config/mailpool.php

main v1.0.71
boban 2025-10-27 19:05:11 +01:00
parent 074d2da4ec
commit da30b80056
2 changed files with 299 additions and 141 deletions

View File

@ -1,37 +1,26 @@
<?php
namespace App\Livewire\Ui\System;
use Carbon\CarbonImmutable;
use Illuminate\Support\Str;
use Livewire\Component;
class BackupStatusCard extends Component
{
public ?string $lastAt = null; // formatierte Zeit
public ?string $lastSize = null; // human readable
public ?string $lastDuration = null; // human readable
public ?string $lastAt = null; // finale Zeit
public ?string $lastSize = null; // menschenlesbar
public ?string $lastDuration = null; // menschenlesbar
public ?bool $ok = null;
// Laufzeit/Progress
public string $state = 'idle'; // idle|running|done|error
public string $step = ''; // aktueller Schritt
public array $steps = [
'mysqldump' => 'Datenbank sichern',
'maildir' => 'Maildir kopieren',
'app' => 'App sichern',
'configs' => 'Configs sichern',
'archive' => 'Archiv erstellen',
'compress' => 'Komprimieren',
'retention' => 'Aufräumen',
'finish' => 'Abschluss',
];
protected string $statusFile = '/var/lib/mailwolt/backup.status';
// Live-Status
public bool $running = false;
public ?string $step = null;
public int $percent = 0;
public function mount(): void
{
$this->load(true);
$this->load();
}
public function render()
@ -46,125 +35,245 @@ class BackupStatusCard extends Component
public function runNow(): void
{
// Script asynchron starten (sudoers muss gesetzt sein)
@shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
// Sofort in "running" gehen Poll übernimmt dann
$this->state = 'running';
$this->step = 'mysqldump';
// Sofort UI auf "läuft" stellen Poll holt echten Status nach
$this->running = true;
$this->step = 'start';
$this->percent = 1;
}
public function load(bool $force = false): void
protected function load(bool $force = false): void
{
$raw = $this->readStatus();
$this->state = $raw['state'] ?? 'idle';
$this->step = $raw['step'] ?? '';
// Datum/Zeit hübsch
if (!empty($raw['time'])) {
$this->lastAt = $this->fmtTime($raw['time']);
} else {
$this->lastAt = null;
$f = '/var/lib/mailwolt/backup.status';
if (!is_file($f)) {
return;
}
// Größe/Dauer hübsch
$bytes = isset($raw['size_bytes']) ? (int)$raw['size_bytes'] : null;
$secs = isset($raw['dur_seconds']) ? (int)$raw['dur_seconds'] : null;
$this->lastSize = $bytes !== null ? $this->humanBytes($bytes) : null;
$this->lastDuration = $secs !== null ? $this->humanDuration($secs) : null;
$this->ok = isset($raw['ok']) ? ((string)$raw['ok'] === '1') : null;
}
protected function readStatus(): array
{
if (!is_file($this->statusFile)) return [];
$out = [];
foreach (@file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
if (!str_contains($ln, '=')) continue;
[$k, $v] = array_map('trim', explode('=', $ln, 2));
$out[$k] = $v;
$data = [];
foreach (@file($f, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
if (strpos($ln, '=') !== false) {
[$k, $v] = explode('=', $ln, 2);
$data[$k] = $v;
}
}
// Backward compatibility (alte Keys)
if (isset($out['size']) && !isset($out['size_bytes'])) {
$out['size_bytes'] = (int)$out['size'];
}
if (isset($out['dur']) && !isset($out['dur_seconds'])) {
$out['dur_seconds'] = (int)$out['dur'];
}
$state = $data['state'] ?? null;
$this->running = ($state === 'running');
return $out;
}
// Progress
$this->step = $data['step'] ?? null;
$this->percent = (int)($data['percent'] ?? 0);
protected function fmtTime(string $iso): string
{
try {
$tz = config('app.timezone', 'UTC');
// ISO aus Script ist idealerweise UTC (Z)
$dt = CarbonImmutable::parse($iso)->timezone($tz);
// z.B. 27.10.2025, 16:48:02 (CET)
return $dt->isoFormat('L, LTS') . ' ' . $dt->format('T');
} catch (\Throwable) {
return $iso;
// Finale Werte
if ($state === 'done' || $state === 'failed') {
$this->ok = ($data['ok'] ?? '') === '1';
$ts = $data['finished_at'] ?? $data['start_at'] ?? null;
$this->lastAt = $ts ? $this->fmtTs($ts) : null;
$bytes = (int)($data['size'] ?? 0);
$this->lastSize = $bytes ? $this->fmtBytes($bytes) : null;
$dur = (int)($data['duration'] ?? 0);
$this->lastDuration = $dur ? $this->fmtDuration($dur) : null;
}
}
protected function humanBytes(int $bytes): string
protected function fmtTs(string $iso): string
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
return CarbonImmutable::parse($iso)->tz(config('app.timezone'))
->format('d.m.Y H:i:s');
}
protected function fmtBytes(int $b): string
{
$u = ['B', 'KB', 'MB', 'GB', 'TB'];
$i = 0;
while ($bytes >= 1024 && $i < count($units) - 1) {
$bytes /= 1024;
while ($b >= 1024 && $i < count($u) - 1) {
$b /= 1024;
$i++;
}
return number_format($bytes, $i === 0 ? 0 : 1, ',', '.') . ' ' . $units[$i];
return sprintf('%.1f %s', $b, $u[$i]);
}
protected function humanDuration(int $secs): string
protected function fmtDuration(int $s): string
{
if ($secs < 60) return $secs . ' s';
$m = intdiv($secs, 60);
$s = $secs % 60;
if ($m < 60) return sprintf('%d min %02d s', $m, $s);
if ($s < 60) return $s . 's';
$m = intdiv($s, 60);
$r = $s % 60;
if ($m < 60) return sprintf('%dm %02ds', $m, $r);
$h = intdiv($m, 60);
$m = $m % 60;
return sprintf('%d h %02d min', $h, $m);
$m %= 60;
return sprintf('%dh %02dm', $h, $m);
}
}
//
//namespace App\Livewire\Ui\System;
//
//use Carbon\CarbonImmutable;
//use Illuminate\Support\Str;
//use Livewire\Component;
//
//class BackupStatusCard extends Component
//{
// public ?string $lastAt = null;
// public ?string $lastSize = null;
// public ?string $lastDuration = null;
// public ?string $lastAt = null; // formatierte Zeit
// public ?string $lastSize = null; // human readable
// public ?string $lastDuration = null; // human readable
// public ?bool $ok = null;
//
// public function mount(): void { $this->load(); }
// public function render() { return view('livewire.ui.system.backup-status-card'); }
// public function refresh(): void { $this->load(true); }
// // Laufzeit/Progress
// public string $state = 'idle'; // idle|running|done|error
// public string $step = ''; // aktueller Schritt
// public array $steps = [
// 'mysqldump' => 'Datenbank sichern',
// 'maildir' => 'Maildir kopieren',
// 'app' => 'App sichern',
// 'configs' => 'Configs sichern',
// 'archive' => 'Archiv erstellen',
// 'compress' => 'Komprimieren',
// 'retention' => 'Aufräumen',
// 'finish' => 'Abschluss',
// ];
//
// protected string $statusFile = '/var/lib/mailwolt/backup.status';
//
// public function mount(): void
// {
// $this->load(true);
// }
//
// public function render()
// {
// return view('livewire.ui.system.backup-status-card');
// }
//
// public function refresh(): void
// {
// $this->load(true);
// }
//
// public function runNow(): void
// {
// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
//// $this->dispatch('toast', type:'info', title:'Backup gestartet');
// // Sofort in "running" gehen Poll übernimmt dann
// $this->state = 'running';
// $this->step = 'mysqldump';
// }
//
// protected function load(bool $force=false): void
// public function load(bool $force = false): void
// {
// // Example: parse a tiny status file your backup script writes.
// $f = '/var/lib/mailwolt/backup.status';
// if (is_file($f)) {
// $lines = @file($f, FILE_IGNORE_NEW_LINES) ?: [];
// foreach ($lines as $ln) {
// if (str_starts_with($ln,'time=')) $this->lastAt = substr($ln,5);
// if (str_starts_with($ln,'size=')) $this->lastSize = substr($ln,5);
// if (str_starts_with($ln,'dur=')) $this->lastDuration = substr($ln,4);
// if (str_starts_with($ln,'ok=')) $this->ok = (substr($ln,3) === '1');
// }
// $raw = $this->readStatus();
// $this->state = $raw['state'] ?? 'idle';
// $this->step = $raw['step'] ?? '';
//
// // Datum/Zeit hübsch
// if (!empty($raw['time'])) {
// $this->lastAt = $this->fmtTime($raw['time']);
// } else {
// $this->lastAt = null;
// }
//
// // Größe/Dauer hübsch
// $bytes = isset($raw['size_bytes']) ? (int)$raw['size_bytes'] : null;
// $secs = isset($raw['dur_seconds']) ? (int)$raw['dur_seconds'] : null;
//
// $this->lastSize = $bytes !== null ? $this->humanBytes($bytes) : null;
// $this->lastDuration = $secs !== null ? $this->humanDuration($secs) : null;
// $this->ok = isset($raw['ok']) ? ((string)$raw['ok'] === '1') : null;
// }
//
// protected function readStatus(): array
// {
// if (!is_file($this->statusFile)) return [];
// $out = [];
// foreach (@file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
// if (!str_contains($ln, '=')) continue;
// [$k, $v] = array_map('trim', explode('=', $ln, 2));
// $out[$k] = $v;
// }
//
// // Backward compatibility (alte Keys)
// if (isset($out['size']) && !isset($out['size_bytes'])) {
// $out['size_bytes'] = (int)$out['size'];
// }
// if (isset($out['dur']) && !isset($out['dur_seconds'])) {
// $out['dur_seconds'] = (int)$out['dur'];
// }
//
// return $out;
// }
//
// protected function fmtTime(string $iso): string
// {
// try {
// $tz = config('app.timezone', 'UTC');
// // ISO aus Script ist idealerweise UTC (Z)
// $dt = CarbonImmutable::parse($iso)->timezone($tz);
// // z.B. 27.10.2025, 16:48:02 (CET)
// return $dt->isoFormat('L, LTS') . ' ' . $dt->format('T');
// } catch (\Throwable) {
// return $iso;
// }
// }
//
// protected function humanBytes(int $bytes): string
// {
// $units = ['B', 'KB', 'MB', 'GB', 'TB'];
// $i = 0;
// while ($bytes >= 1024 && $i < count($units) - 1) {
// $bytes /= 1024;
// $i++;
// }
// return number_format($bytes, $i === 0 ? 0 : 1, ',', '.') . ' ' . $units[$i];
// }
//
// protected function humanDuration(int $secs): string
// {
// if ($secs < 60) return $secs . ' s';
// $m = intdiv($secs, 60);
// $s = $secs % 60;
// if ($m < 60) return sprintf('%d min %02d s', $m, $s);
// $h = intdiv($m, 60);
// $m = $m % 60;
// return sprintf('%d h %02d min', $h, $m);
// }
//}
//
////
////namespace App\Livewire\Ui\System;
////
////use Livewire\Component;
////
////class BackupStatusCard extends Component
////{
//// public ?string $lastAt = null;
//// public ?string $lastSize = null;
//// public ?string $lastDuration = null;
//// public ?bool $ok = null;
////
//// public function mount(): void { $this->load(); }
//// public function render() { return view('livewire.ui.system.backup-status-card'); }
//// public function refresh(): void { $this->load(true); }
////
//// public function runNow(): void
//// {
//// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
////// $this->dispatch('toast', type:'info', title:'Backup gestartet');
//// }
////
//// protected function load(bool $force=false): void
//// {
//// // Example: parse a tiny status file your backup script writes.
//// $f = '/var/lib/mailwolt/backup.status';
//// if (is_file($f)) {
//// $lines = @file($f, FILE_IGNORE_NEW_LINES) ?: [];
//// foreach ($lines as $ln) {
//// if (str_starts_with($ln,'time=')) $this->lastAt = substr($ln,5);
//// if (str_starts_with($ln,'size=')) $this->lastSize = substr($ln,5);
//// if (str_starts_with($ln,'dur=')) $this->lastDuration = substr($ln,4);
//// if (str_starts_with($ln,'ok=')) $this->ok = (substr($ln,3) === '1');
//// }
//// }
//// }
////}

View File

@ -1,69 +1,118 @@
<div
@class([
'glass-card p-4 rounded-2xl border bg-white/5',
'border-white/10'
])
@if($state === 'running') wire:poll.1000ms="refresh" @endif
class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5"
@if($running) wire:poll.1500ms="refresh" @endif
>
<div class="flex items-center justify-between mb-3">
<div class="flex items-center justify-between mb-2">
<div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">
<i class="ph ph-archive-box text-white/70 text-[13px]"></i>
<span class="text-[11px] uppercase text-white/70">Backups</span>
</div>
<button
wire:click="runNow"
class="px-3 py-1.5 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20 disabled:opacity-50"
@disabled($state === 'running')
>
{{ $state === 'running' ? 'Läuft…' : 'Jetzt sichern' }}
<button wire:click="runNow"
@disabled($running)
class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20 disabled:opacity-50">
{{ $running ? 'Läuft…' : 'Jetzt sichern' }}
</button>
</div>
{{-- Laufender Fortschritt --}}
@if($state === 'running')
@if($running)
<div class="space-y-2">
<div class="text-white/80 text-sm">Sicherung läuft…</div>
<ul class="space-y-1">
@foreach($steps as $k => $label)
@php
$done = $k === 'finish' ? false : ($k === $step ? false : (array_key_first(array_flip($steps)) !== $k && $k !== $step && $step && array_search($k, array_keys($steps)) < array_search($step, array_keys($steps))));
$isCurrent = $k === $step;
@endphp
<li class="flex items-center gap-2 text-sm">
<span class="w-2 h-2 rounded-full
{{ $done ? 'bg-emerald-400' : ($isCurrent ? 'bg-amber-400 animate-pulse' : 'bg-white/20') }}"></span>
<span class="{{ $done ? 'text-white/80' : ($isCurrent ? 'text-white' : 'text-white/60') }}">
{{ $label }}
</span>
</li>
@endforeach
</ul>
<div class="flex items-center justify-between text-xs text-white/70">
<span>Schritt: <span class="text-white/90">{{ $step ?? 'start' }}</span></span>
<span>{{ $percent }}%</span>
</div>
<div class="w-full h-2 rounded-full bg-white/10 overflow-hidden">
<div class="h-2 bg-white/60" style="width: {{ max(2,$percent) }}%"></div>
</div>
</div>
@else
{{-- Letztes Ergebnis --}}
<div class="space-y-1 text-sm">
<div class="text-white/70">Letztes Backup:
<span class="text-white/90 font-medium">{{ $lastAt ?? '' }}</span>
<span class="text-white/90">{{ $lastAt ?? '' }}</span>
</div>
<div class="text-white/70">Größe:
<span class="text-white/90 font-medium">{{ $lastSize ?? '' }}</span>
<span class="text-white/90">{{ $lastSize ?? '' }}</span>
</div>
<div class="text-white/70">Dauer:
<span class="text-white/90 font-medium">{{ $lastDuration ?? '' }}</span>
<span class="text-white/90">{{ $lastDuration ?? '' }}</span>
</div>
<div class="pt-1">
<span class="px-2 py-0.5 rounded-full border text-xs
{{ $ok === null
? 'text-slate-300 border-slate-400/30 bg-slate-500/10'
: ($ok ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
: 'text-rose-300 border-rose-400/30 bg-rose-500/10') }}">
{{ $ok === null ? 'unbekannt' : ($ok ? 'erfolgreich' : 'fehlgeschlagen') }}
</span>
<div>
<span class="px-2 py-0.5 rounded-full border text-xs
{{ $ok ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
: 'text-rose-300 border-rose-400/30 bg-rose-500/10' }}">
{{ $ok === null ? 'unbekannt' : ($ok ? 'erfolgreich' : 'fehlgeschlagen') }}
</span>
</div>
</div>
@endif
</div>
{{--<div--}}
{{-- @class([--}}
{{-- 'glass-card p-4 rounded-2xl border bg-white/5',--}}
{{-- 'border-white/10'--}}
{{-- ])--}}
{{-- @if($state === 'running') wire:poll.1000ms="refresh" @endif--}}
{{-->--}}
{{-- <div class="flex items-center justify-between mb-3">--}}
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}
{{-- <i class="ph ph-archive-box text-white/70 text-[13px]"></i>--}}
{{-- <span class="text-[11px] uppercase text-white/70">Backups</span>--}}
{{-- </div>--}}
{{-- <button--}}
{{-- wire:click="runNow"--}}
{{-- class="px-3 py-1.5 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20 disabled:opacity-50"--}}
{{-- @disabled($state === 'running')--}}
{{-- >--}}
{{-- {{ $state === 'running' ? 'Läuft…' : 'Jetzt sichern' }}--}}
{{-- </button>--}}
{{-- </div>--}}
{{-- --}}{{-- Laufender Fortschritt --}}
{{-- @if($state === 'running')--}}
{{-- <div class="space-y-2">--}}
{{-- <div class="text-white/80 text-sm">Sicherung läuft…</div>--}}
{{-- <ul class="space-y-1">--}}
{{-- @foreach($steps as $k => $label)--}}
{{-- @php--}}
{{-- $done = $k === 'finish' ? false : ($k === $step ? false : (array_key_first(array_flip($steps)) !== $k && $k !== $step && $step && array_search($k, array_keys($steps)) < array_search($step, array_keys($steps))));--}}
{{-- $isCurrent = $k === $step;--}}
{{-- @endphp--}}
{{-- <li class="flex items-center gap-2 text-sm">--}}
{{-- <span class="w-2 h-2 rounded-full--}}
{{-- {{ $done ? 'bg-emerald-400' : ($isCurrent ? 'bg-amber-400 animate-pulse' : 'bg-white/20') }}"></span>--}}
{{-- <span class="{{ $done ? 'text-white/80' : ($isCurrent ? 'text-white' : 'text-white/60') }}">--}}
{{-- {{ $label }}--}}
{{-- </span>--}}
{{-- </li>--}}
{{-- @endforeach--}}
{{-- </ul>--}}
{{-- </div>--}}
{{-- @else--}}
{{-- --}}{{-- Letztes Ergebnis --}}
{{-- <div class="space-y-1 text-sm">--}}
{{-- <div class="text-white/70">Letztes Backup:--}}
{{-- <span class="text-white/90 font-medium">{{ $lastAt ?? '' }}</span>--}}
{{-- </div>--}}
{{-- <div class="text-white/70">Größe:--}}
{{-- <span class="text-white/90 font-medium">{{ $lastSize ?? '' }}</span>--}}
{{-- </div>--}}
{{-- <div class="text-white/70">Dauer:--}}
{{-- <span class="text-white/90 font-medium">{{ $lastDuration ?? '' }}</span>--}}
{{-- </div>--}}
{{-- <div class="pt-1">--}}
{{-- <span class="px-2 py-0.5 rounded-full border text-xs--}}
{{-- {{ $ok === null--}}
{{-- ? 'text-slate-300 border-slate-400/30 bg-slate-500/10'--}}
{{-- : ($ok ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'--}}
{{-- : 'text-rose-300 border-rose-400/30 bg-rose-500/10') }}">--}}
{{-- {{ $ok === null ? 'unbekannt' : ($ok ? 'erfolgreich' : 'fehlgeschlagen') }}--}}
{{-- </span>--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- @endif--}}
{{--</div>--}}
{{--<div wire:key="storage-{{ md5($target ?? '/') }}" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">--}}
{{-- <div class="flex items-center justify-between mb-2">--}}
{{-- <div class="inline-flex items-center gap-2 bg-white/5 border border-white/10 px-2.5 py-1 rounded-full">--}}