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

main v1.0.73
boban 2025-10-27 19:57:15 +01:00
parent ddd96eb9f2
commit 09117fe1e9
2 changed files with 374 additions and 248 deletions

View File

@ -12,76 +12,59 @@ class BackupStatusCard extends Component
public string $lastDuration = ''; public string $lastDuration = '';
public string $statusText = 'unbekannt'; public string $statusText = 'unbekannt';
public string $statusColor = 'text-white/60 border-white/20 bg-white/5'; public string $statusColor = 'text-white/60 border-white/20 bg-white/5';
public string $progressText = ''; public string $progressText = '';
public string $progressPercent = '0'; public string $progressPercent = '0';
public bool $running = false; public string $progressVisibleClass = 'hidden'; // <- Sichtbarkeit
protected string $statusFile = '/var/lib/mailwolt/backup.status'; protected string $statusFile = '/var/lib/mailwolt/backup.status';
public function mount(): void public function mount(): void { $this->load(); }
{ public function render() { return view('livewire.ui.system.backup-status-card'); }
$this->load(); public function refresh(): 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 public function runNow(): void
{ {
@shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &'); @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
$this->running = true; // UI sofort auf "läuft" setzen
$this->progressText = 'Starte Backup...'; $this->progressText = 'Vorbereitung läuft...';
$this->progressPercent = '1'; $this->progressPercent = '1';
$this->progressVisibleClass = 'block';
} }
protected function load(bool $force = false): void protected function load(bool $force = false): void
{ {
if (!is_file($this->statusFile)) {
$this->running = false;
return;
}
$kv = []; $kv = [];
foreach (@file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [] as $ln) { if (is_file($this->statusFile)) {
$p = strpos($ln, '='); foreach (@file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
if ($p !== false) { $p = strpos($ln, '='); if ($p !== false) $kv[substr($ln,0,$p)] = substr($ln,$p+1);
$kv[substr($ln, 0, $p)] = substr($ln, $p + 1);
} }
} }
$state = $kv['state'] ?? null; $state = $kv['state'] ?? null; // running | done | failed
$step = $kv['step'] ?? null; $step = $kv['step'] ?? null;
$percent = isset($kv['percent']) ? (int)$kv['percent'] : 0; $percent = isset($kv['percent']) ? (int)$kv['percent'] : 0;
$ok = isset($kv['ok']) ? ((int)$kv['ok'] === 1) : null; $ok = isset($kv['ok']) ? ((int)$kv['ok'] === 1) : null;
// Formatierung // Anzeigeformatierungen wie gehabt …
$tz = config('app.timezone', 'Europe/Berlin'); // (deine bestehenden formatBytes/formatDuration/Timezone-Logik)
$finished = $kv['finished_at'] ?? $kv['start_at'] ?? null;
$this->lastAt = $finished
? Carbon::parse($finished)->setTimezone($tz)->format('d.m.Y H:i:s')
: '';
$this->lastSize = isset($kv['size']) $this->progressPercent = (string)max(0, min(100, $percent));
? $this->formatBytes((int)$kv['size']) $this->progressText = $this->mapStep($step);
: '';
$this->lastDuration = isset($kv['duration']) // Sichtbarkeit steuern KEINE Blade-Logik nötig
? $this->formatDuration((int)$kv['duration']) if ($state === 'running') {
: ''; $this->progressVisibleClass = 'block';
} else {
// Fortschritt // bei done/failed: Balken verstecken und auf 100% / finalen Text setzen
$this->progressPercent = (string)$percent; $this->progressVisibleClass = 'hidden';
$this->progressText = $this->mapStep($step); if ($percent >= 100 || $step === 'done') {
$this->progressPercent = '100';
// Status $this->progressText = 'Backup abgeschlossen.';
$this->running = ($state === 'running'); }
}
// Status-Badge (wie gehabt)
if ($ok === true) { if ($ok === true) {
$this->statusText = 'erfolgreich'; $this->statusText = 'erfolgreich';
$this->statusColor = 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'; $this->statusColor = 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10';
@ -94,58 +77,33 @@ class BackupStatusCard extends Component
} }
} }
private function formatBytes(int $b): string
{
if ($b >= 1024 * 1024 * 1024) return number_format($b / (1024 * 1024 * 1024), 1) . ' GB';
if ($b >= 1024 * 1024) return number_format($b / (1024 * 1024), 1) . ' MB';
if ($b >= 1024) return number_format($b / 1024, 0) . ' KB';
return $b . ' B';
}
private function formatDuration(int $s): string
{
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('%dh %02dm %02ds', $h, $m, $r);
}
private function mapStep(?string $step): string private function mapStep(?string $step): string
{ {
return match ($step) { return match($step) {
'mysqldump' => 'Datenbank wird gesichert...', 'mysqldump' => 'Datenbank wird gesichert...',
'maildir' => 'Mail-Verzeichnis wird archiviert...', 'maildir' => 'Mail-Verzeichnis wird archiviert...',
'app' => 'Anwendungsdaten werden gesichert...', 'app' => 'Anwendungsdaten werden gesichert...',
'configs' => 'Konfigurationen werden gesichert...', 'configs' => 'Konfigurationen werden gesichert...',
'compress' => 'Backup wird komprimiert...', 'compress' => 'Backup wird komprimiert...',
'retention' => 'Alte Backups werden gelöscht...', 'retention' => 'Alte Backups werden gelöscht...',
'done' => 'Backup abgeschlossen.', 'done' => 'Backup abgeschlossen.',
default => 'Vorbereitung läuft...', default => 'Vorbereitung läuft...',
}; };
} }
} }
//
//
//namespace App\Livewire\Ui\System;
//
//use Carbon\CarbonImmutable;
//use Livewire\Component;
// //
//class BackupStatusCard extends Component //class BackupStatusCard extends Component
//{ //{
// public ?string $lastAt = null; // finale Zeit // public string $lastAt = '';
// public ?string $lastSize = null; // menschenlesbar // public string $lastSize = '';
// public ?string $lastDuration = null; // menschenlesbar // public string $lastDuration = '';
// public ?bool $ok = null; // public string $statusText = 'unbekannt';
// // public string $statusColor = 'text-white/60 border-white/20 bg-white/5';
// // Live-Status // public string $progressText = '';
// public string $progressPercent = '0';
// public bool $running = false; // public bool $running = false;
// public ?string $step = null; // protected string $statusFile = '/var/lib/mailwolt/backup.status';
// public int $percent = 0;
// //
// public function mount(): void // public function mount(): void
// { // {
@ -164,112 +122,122 @@ class BackupStatusCard extends Component
// //
// public function runNow(): void // 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 &'); // @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
// // Sofort UI auf "läuft" stellen Poll holt echten Status nach
// $this->running = true; // $this->running = true;
// $this->step = 'start'; // $this->progressText = 'Starte Backup...';
// $this->percent = 1; // $this->progressPercent = '1';
// } // }
// //
// protected function load(bool $force = false): void // protected function load(bool $force = false): void
// { // {
// $f = '/var/lib/mailwolt/backup.status'; // if (!is_file($this->statusFile)) {
// if (!is_file($f)) { // $this->running = false;
// return; // return;
// } // }
// //
// $data = []; // $kv = [];
// foreach (@file($f, FILE_IGNORE_NEW_LINES) ?: [] as $ln) { // foreach (@file($this->statusFile, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
// if (strpos($ln, '=') !== false) { // $p = strpos($ln, '=');
// [$k, $v] = explode('=', $ln, 2); // if ($p !== false) {
// $data[$k] = $v; // $kv[substr($ln, 0, $p)] = substr($ln, $p + 1);
// } // }
// } // }
// //
// $state = $data['state'] ?? null; // $state = $kv['state'] ?? null;
// $step = $kv['step'] ?? null;
// $percent = isset($kv['percent']) ? (int)$kv['percent'] : 0;
// $ok = isset($kv['ok']) ? ((int)$kv['ok'] === 1) : null;
//
// // Formatierung
// $tz = config('app.timezone', 'Europe/Berlin');
// $finished = $kv['finished_at'] ?? $kv['start_at'] ?? null;
// $this->lastAt = $finished
// ? Carbon::parse($finished)->setTimezone($tz)->format('d.m.Y H:i:s')
// : '';
//
// $this->lastSize = isset($kv['size'])
// ? $this->formatBytes((int)$kv['size'])
// : '';
//
// $this->lastDuration = isset($kv['duration'])
// ? $this->formatDuration((int)$kv['duration'])
// : '';
//
// // Fortschritt
// $this->progressPercent = (string)$percent;
// $this->progressText = $this->mapStep($step);
//
// // Status
// $this->running = ($state === 'running'); // $this->running = ($state === 'running');
// //
// // Progress // if ($ok === true) {
// $this->step = $data['step'] ?? null; // $this->statusText = 'erfolgreich';
// $this->percent = (int)($data['percent'] ?? 0); // $this->statusColor = 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10';
// // } elseif ($ok === false) {
// // Finale Werte // $this->statusText = 'fehlgeschlagen';
// if ($state === 'done' || $state === 'failed') { // $this->statusColor = 'text-rose-300 border-rose-400/30 bg-rose-500/10';
// $this->ok = ($data['ok'] ?? '') === '1'; // } else {
// $ts = $data['finished_at'] ?? $data['start_at'] ?? null; // $this->statusText = 'unbekannt';
// $this->lastAt = $ts ? $this->fmtTs($ts) : null; // $this->statusColor = 'text-white/60 border-white/20 bg-white/5';
//
// $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 fmtTs(string $iso): string // private function formatBytes(int $b): string
// { // {
// return CarbonImmutable::parse($iso)->tz(config('app.timezone')) // if ($b >= 1024 * 1024 * 1024) return number_format($b / (1024 * 1024 * 1024), 1) . ' GB';
// ->format('d.m.Y H:i:s'); // if ($b >= 1024 * 1024) return number_format($b / (1024 * 1024), 1) . ' MB';
// if ($b >= 1024) return number_format($b / 1024, 0) . ' KB';
// return $b . ' B';
// } // }
// //
// protected function fmtBytes(int $b): string // private function formatDuration(int $s): string
// {
// $u = ['B', 'KB', 'MB', 'GB', 'TB'];
// $i = 0;
// while ($b >= 1024 && $i < count($u) - 1) {
// $b /= 1024;
// $i++;
// }
// return sprintf('%.1f %s', $b, $u[$i]);
// }
//
// protected function fmtDuration(int $s): string
// { // {
// if ($s < 60) return $s . 's'; // if ($s < 60) return $s . 's';
// $m = intdiv($s, 60); // $m = intdiv($s, 60);
// $r = $s % 60; // $r = $s % 60;
// if ($m < 60) return sprintf('%dm %02ds', $m, $r); // if ($m < 60) return sprintf('%dm %02ds', $m, $r);
// $h = intdiv($m, 60); // $h = intdiv($m, 60);
// $m %= 60; // $m = $m % 60;
// return sprintf('%dh %02dm', $h, $m); // return sprintf('%dh %02dm %02ds', $h, $m, $r);
// }
//
// private function mapStep(?string $step): string
// {
// return match ($step) {
// 'mysqldump' => 'Datenbank wird gesichert...',
// 'maildir' => 'Mail-Verzeichnis wird archiviert...',
// 'app' => 'Anwendungsdaten werden gesichert...',
// 'configs' => 'Konfigurationen werden gesichert...',
// 'compress' => 'Backup wird komprimiert...',
// 'retention' => 'Alte Backups werden gelöscht...',
// 'done' => 'Backup abgeschlossen.',
// default => 'Vorbereitung läuft...',
// };
// } // }
//} //}
// //
//// ////
////
////namespace App\Livewire\Ui\System; ////namespace App\Livewire\Ui\System;
//// ////
////use Carbon\CarbonImmutable; ////use Carbon\CarbonImmutable;
////use Illuminate\Support\Str;
////use Livewire\Component; ////use Livewire\Component;
//// ////
////class BackupStatusCard extends Component ////class BackupStatusCard extends Component
////{ ////{
//// public ?string $lastAt = null; // formatierte Zeit //// public ?string $lastAt = null; // finale Zeit
//// public ?string $lastSize = null; // human readable //// public ?string $lastSize = null; // menschenlesbar
//// public ?string $lastDuration = null; // human readable //// public ?string $lastDuration = null; // menschenlesbar
//// public ?bool $ok = null; //// public ?bool $ok = null;
//// ////
//// // Laufzeit/Progress //// // Live-Status
//// public string $state = 'idle'; // idle|running|done|error //// public bool $running = false;
//// public string $step = ''; // aktueller Schritt //// public ?string $step = null;
//// public array $steps = [ //// public int $percent = 0;
//// '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 //// public function mount(): void
//// { //// {
//// $this->load(true); //// $this->load();
//// } //// }
//// ////
//// public function render() //// public function render()
@ -284,125 +252,245 @@ class BackupStatusCard extends Component
//// ////
//// public function runNow(): void //// 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 &'); //// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &');
//// // Sofort in "running" gehen Poll übernimmt dann //// // Sofort UI auf "läuft" stellen Poll holt echten Status nach
//// $this->state = 'running'; //// $this->running = true;
//// $this->step = 'mysqldump'; //// $this->step = 'start';
//// $this->percent = 1;
//// } //// }
//// ////
//// public function load(bool $force = false): void //// protected function load(bool $force = false): void
//// { //// {
//// $raw = $this->readStatus(); //// $f = '/var/lib/mailwolt/backup.status';
//// $this->state = $raw['state'] ?? 'idle'; //// if (!is_file($f)) {
//// $this->step = $raw['step'] ?? ''; //// return;
////
//// // Datum/Zeit hübsch
//// if (!empty($raw['time'])) {
//// $this->lastAt = $this->fmtTime($raw['time']);
//// } else {
//// $this->lastAt = null;
//// } //// }
//// ////
//// // Größe/Dauer hübsch //// $data = [];
//// $bytes = isset($raw['size_bytes']) ? (int)$raw['size_bytes'] : null; //// foreach (@file($f, FILE_IGNORE_NEW_LINES) ?: [] as $ln) {
//// $secs = isset($raw['dur_seconds']) ? (int)$raw['dur_seconds'] : null; //// if (strpos($ln, '=') !== false) {
//// //// [$k, $v] = explode('=', $ln, 2);
//// $this->lastSize = $bytes !== null ? $this->humanBytes($bytes) : null; //// $data[$k] = $v;
//// $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) //// $state = $data['state'] ?? null;
//// if (isset($out['size']) && !isset($out['size_bytes'])) { //// $this->running = ($state === 'running');
//// $out['size_bytes'] = (int)$out['size'];
//// }
//// if (isset($out['dur']) && !isset($out['dur_seconds'])) {
//// $out['dur_seconds'] = (int)$out['dur'];
//// }
//// ////
//// return $out; //// // Progress
//// } //// $this->step = $data['step'] ?? null;
//// $this->percent = (int)($data['percent'] ?? 0);
//// ////
//// protected function fmtTime(string $iso): string //// // Finale Werte
//// { //// if ($state === 'done' || $state === 'failed') {
//// try { //// $this->ok = ($data['ok'] ?? '') === '1';
//// $tz = config('app.timezone', 'UTC'); //// $ts = $data['finished_at'] ?? $data['start_at'] ?? null;
//// // ISO aus Script ist idealerweise UTC (Z) //// $this->lastAt = $ts ? $this->fmtTs($ts) : null;
//// $dt = CarbonImmutable::parse($iso)->timezone($tz); ////
//// // z.B. 27.10.2025, 16:48:02 (CET) //// $bytes = (int)($data['size'] ?? 0);
//// return $dt->isoFormat('L, LTS') . ' ' . $dt->format('T'); //// $this->lastSize = $bytes ? $this->fmtBytes($bytes) : null;
//// } catch (\Throwable) { ////
//// return $iso; //// $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; //// $i = 0;
//// while ($bytes >= 1024 && $i < count($units) - 1) { //// while ($b >= 1024 && $i < count($u) - 1) {
//// $bytes /= 1024; //// $b /= 1024;
//// $i++; //// $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'; //// if ($s < 60) return $s . 's';
//// $m = intdiv($secs, 60); //// $m = intdiv($s, 60);
//// $s = $secs % 60; //// $r = $s % 60;
//// if ($m < 60) return sprintf('%d min %02d s', $m, $s); //// if ($m < 60) return sprintf('%dm %02ds', $m, $r);
//// $h = intdiv($m, 60); //// $h = intdiv($m, 60);
//// $m = $m % 60; //// $m %= 60;
//// return sprintf('%d h %02d min', $h, $m); //// return sprintf('%dh %02dm', $h, $m);
//// } //// }
////} ////}
//// ////
////// //////
//////namespace App\Livewire\Ui\System; //////namespace App\Livewire\Ui\System;
////// //////
//////use Carbon\CarbonImmutable;
//////use Illuminate\Support\Str;
//////use Livewire\Component; //////use Livewire\Component;
////// //////
//////class BackupStatusCard extends Component //////class BackupStatusCard extends Component
//////{ //////{
////// public ?string $lastAt = null; ////// public ?string $lastAt = null; // formatierte Zeit
////// public ?string $lastSize = null; ////// public ?string $lastSize = null; // human readable
////// public ?string $lastDuration = null; ////// public ?string $lastDuration = null; // human readable
////// public ?bool $ok = null; ////// public ?bool $ok = null;
////// //////
////// public function mount(): void { $this->load(); } ////// // Laufzeit/Progress
////// public function render() { return view('livewire.ui.system.backup-status-card'); } ////// public string $state = 'idle'; // idle|running|done|error
////// public function refresh(): void { $this->load(true); } ////// 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 ////// public function runNow(): void
////// { ////// {
////// @shell_exec('nohup sudo -n /usr/local/sbin/mailwolt-backup >/dev/null 2>&1 &'); ////// @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. ////// $raw = $this->readStatus();
////// $f = '/var/lib/mailwolt/backup.status'; ////// $this->state = $raw['state'] ?? 'idle';
////// if (is_file($f)) { ////// $this->step = $raw['step'] ?? '';
////// $lines = @file($f, FILE_IGNORE_NEW_LINES) ?: []; //////
////// foreach ($lines as $ln) { ////// // Datum/Zeit hübsch
////// if (str_starts_with($ln,'time=')) $this->lastAt = substr($ln,5); ////// if (!empty($raw['time'])) {
////// if (str_starts_with($ln,'size=')) $this->lastSize = substr($ln,5); ////// $this->lastAt = $this->fmtTime($raw['time']);
////// if (str_starts_with($ln,'dur=')) $this->lastDuration = substr($ln,4); ////// } else {
////// if (str_starts_with($ln,'ok=')) $this->ok = (substr($ln,3) === '1'); ////// $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,25 +1,19 @@
<div <div wire:key="backup-card" wire:poll.2s="refresh"
wire:key="backup-card" class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5">
@if($running) wire:poll.1s="refresh" @endif
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="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"> <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> <i class="ph ph-archive-box text-white/70 text-[13px]"></i>
<span class="text-[11px] uppercase text-white/70">Backups</span> <span class="text-[11px] uppercase text-white/70">Backups</span>
</div> </div>
<button wire:click="runNow"
<button class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20">
wire:click="runNow"
class="px-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20 disabled:opacity-50"
@disabled($running)
>
Jetzt sichern Jetzt sichern
</button> </button>
</div> </div>
{{-- Fortschritt --}} {{-- Fortschritt (sichtbar nur während run) --}}
<div class="mb-3"> <div class="mb-3 {{ $progressVisibleClass }}">
<div class="flex items-center justify-between mb-1 text-xs text-white/70"> <div class="flex items-center justify-between mb-1 text-xs text-white/70">
<span>{{ $progressText }}</span> <span>{{ $progressText }}</span>
<span>{{ $progressPercent }}%</span> <span>{{ $progressPercent }}%</span>
@ -29,19 +23,63 @@
</div> </div>
</div> </div>
{{-- Backup Infos --}} {{-- Infos --}}
<div class="space-y-1 text-sm"> <div class="space-y-1 text-sm">
<div class="text-white/70">Letztes Backup: <span class="text-white/90">{{ $lastAt }}</span></div> <div class="text-white/70">Letztes Backup: <span class="text-white/90">{{ $lastAt }}</span></div>
<div class="text-white/70">Größe: <span class="text-white/90">{{ $lastSize }}</span></div> <div class="text-white/70">Größe: <span class="text-white/90">{{ $lastSize }}</span></div>
<div class="text-white/70">Dauer: <span class="text-white/90">{{ $lastDuration }}</span></div> <div class="text-white/70">Dauer: <span class="text-white/90">{{ $lastDuration }}</span></div>
<div> <div>
<span class="px-2 py-0.5 rounded-full border text-xs {{ $statusColor }}"> <span class="px-2 py-0.5 rounded-full border text-xs {{ $statusColor }}">
{{ $statusText }} {{ $statusText }}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
{{--<div--}}
{{-- wire:key="backup-card"--}}
{{-- @if($running) wire:poll.1s="refresh" @endif--}}
{{-- 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">--}}
{{-- <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-2 py-1 text-xs rounded-lg border border-white/10 bg-white/5 hover:border-white/20 disabled:opacity-50"--}}
{{-- @disabled($running)--}}
{{-- >--}}
{{-- Jetzt sichern--}}
{{-- </button>--}}
{{-- </div>--}}
{{-- --}}{{-- Fortschritt --}}
{{-- <div class="mb-3">--}}
{{-- <div class="flex items-center justify-between mb-1 text-xs text-white/70">--}}
{{-- <span>{{ $progressText }}</span>--}}
{{-- <span>{{ $progressPercent }}%</span>--}}
{{-- </div>--}}
{{-- <div class="w-full h-2 rounded bg-white/10 overflow-hidden">--}}
{{-- <div class="h-2 bg-white/40" style="width: {{ $progressPercent }}%"></div>--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- --}}{{-- Backup Infos --}}
{{-- <div class="space-y-1 text-sm">--}}
{{-- <div class="text-white/70">Letztes Backup: <span class="text-white/90">{{ $lastAt }}</span></div>--}}
{{-- <div class="text-white/70">Größe: <span class="text-white/90">{{ $lastSize }}</span></div>--}}
{{-- <div class="text-white/70">Dauer: <span class="text-white/90">{{ $lastDuration }}</span></div>--}}
{{-- <div>--}}
{{-- <span class="px-2 py-0.5 rounded-full border text-xs {{ $statusColor }}">--}}
{{-- {{ $statusText }}--}}
{{-- </span>--}}
{{-- </div>--}}
{{-- </div>--}}
{{--</div>--}}
{{--<div--}} {{--<div--}}
{{-- class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5"--}} {{-- class="glass-card p-4 rounded-2xl border border-white/10 bg-white/5"--}}
{{-- @if($running) wire:poll.1500ms="refresh" @endif--}} {{-- @if($running) wire:poll.1500ms="refresh" @endif--}}