parent
2fecb09984
commit
323c6032af
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\MailUser;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Console\Command;
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
|
|
@ -58,12 +59,11 @@ class UpdateMailboxStats extends Command
|
|||
$messageCount = $this->messageCountViaFilesystem($maildir);
|
||||
}
|
||||
|
||||
// Update DB
|
||||
$u->forceFill([
|
||||
'used_bytes' => $usedBytes,
|
||||
'message_count' => (int) $messageCount,
|
||||
'stats_refreshed_at' => now(),
|
||||
])->save();
|
||||
Setting::set("mailbox.{$email}", [
|
||||
'used_bytes' => $usedBytes,
|
||||
'message_count' => $messageCount,
|
||||
'updated_at' => now()->toDateTimeString(),
|
||||
]);
|
||||
|
||||
$this->line(sprintf(
|
||||
"%-35s %7.1f MiB %5d msgs",
|
||||
|
|
@ -127,7 +127,7 @@ class UpdateMailboxStats extends Command
|
|||
$count++;
|
||||
}
|
||||
}
|
||||
closedir($dh);
|
||||
closedir($dh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
namespace App\Livewire\Ui\Mail;
|
||||
|
||||
use App\Models\Domain;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Attributes\On;
|
||||
|
|
@ -72,6 +73,19 @@ class MailboxList extends Component
|
|||
);
|
||||
}
|
||||
|
||||
public function updateMailboxStatsOne(string $email)
|
||||
{
|
||||
Artisan::call('mail:update-stats', ['--user' => $email]);
|
||||
$this->dispatch('$refresh');
|
||||
$this->dispatch('toast',
|
||||
type: 'done',
|
||||
badge: 'Mailbox',
|
||||
title: 'Mailbox aktualisiert',
|
||||
text: 'Die Mailbox-Statistiken wurden aktualisiert.',
|
||||
duration: 6000
|
||||
);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$system = Domain::query()->where('is_system', true)->first();
|
||||
|
|
@ -82,17 +96,17 @@ class MailboxList extends Component
|
|||
$domains = Domain::query()
|
||||
->when($system, fn ($q) => $q->whereKeyNot($system->id))
|
||||
|
||||
// Domain selbst ODER MailUser/ Aliasse müssen matchen
|
||||
// Domain selbst ODER MailUser müssen matchen
|
||||
->when($hasTerm, function ($q) use ($needle) {
|
||||
$q->where(function ($w) use ($needle) {
|
||||
$w->where('domain', 'like', $needle)
|
||||
->orWhereHas('mailUsers', fn($u) => $u->where('localpart', 'like', $needle));
|
||||
->orWhereHas('mailUsers', fn($u) => $u->where('localpart', 'like', $needle));
|
||||
});
|
||||
})
|
||||
|
||||
->withCount(['mailUsers'])
|
||||
|
||||
// Beziehungen zunächst gefiltert laden (damit "test" nur passende Mailboxen zeigt)
|
||||
// Relationen zunächst ggf. gefiltert laden
|
||||
->with([
|
||||
'mailUsers' => function ($q) use ($hasTerm, $needle) {
|
||||
if ($hasTerm) $q->where('localpart', 'like', $needle);
|
||||
|
|
@ -103,32 +117,39 @@ class MailboxList extends Component
|
|||
->orderBy('domain')
|
||||
->get();
|
||||
|
||||
// Domains, deren NAME den Suchbegriff trifft → ALLE Mailboxen/Aliasse zeigen
|
||||
// Wenn der Domainname selbst matched → alle Mailboxen/Aliasse vollständig nachladen
|
||||
if ($hasTerm) {
|
||||
$lower = Str::lower($term);
|
||||
foreach ($domains as $d) {
|
||||
if (Str::contains(Str::lower($d->domain), $lower)) {
|
||||
// volle Relationen nachladen (überschreibt die gefilterten)
|
||||
$d->setRelation('mailUsers', $d->mailUsers()->orderBy('localpart')->get());
|
||||
$d->setRelation('mailAliases', $d->mailAliases()->orderBy('local')->get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vorbereitung für Blade (unverändert, arbeitet auf den ggf. gefilterten Relationen)
|
||||
// Vorbereitung für Blade
|
||||
foreach ($domains as $d) {
|
||||
$prepared = [];
|
||||
$domainActive = (bool)($d->is_active ?? true);
|
||||
|
||||
foreach ($d->mailUsers as $u) {
|
||||
$usedMB = (int) round(($u->used_bytes ?? 0) / 1024 / 1024);
|
||||
// E-Mail sicher bestimmen (Fallback aus localpart + Domain)
|
||||
$email = $u->email ?? ($u->localpart ? ($u->localpart.'@'.$d->domain) : null);
|
||||
|
||||
// Stats aus Redis/DB holen (kann array, json-string oder null sein)
|
||||
$statsRaw = $email ? Setting::get("mailbox.$email") : null;
|
||||
$stats = is_array($statsRaw)
|
||||
? $statsRaw
|
||||
: (is_string($statsRaw) ? (json_decode($statsRaw, true) ?: null) : null);
|
||||
|
||||
// Werte priorisieren: Setting → DB-Felder → 0
|
||||
$usedBytes = (int)($stats['used_bytes'] ?? ($u->used_bytes ?? 0));
|
||||
$messageCount = (int)($stats['message_count'] ?? ($u->message_count ?? 0));
|
||||
|
||||
$usedMB = (int) round($usedBytes / 1024 / 1024);
|
||||
$quota = (int)($u->quota_mb ?? 0);
|
||||
$usage = $quota > 0 ? min(100, (int) round($usedMB / max(1,$quota) * 100)) : 0;
|
||||
|
||||
|
||||
// $quota = (int)($u->quota_mb ?? 0);
|
||||
// $used = (int)($u->used_mb ?? 0);
|
||||
// $usage = $quota > 0 ? min(100, (int)round($used / max(1, $quota) * 100)) : 0;
|
||||
$usage = $quota > 0 ? min(100, (int) round($usedMB / max(1, $quota) * 100)) : 0;
|
||||
|
||||
$mailboxActive = (bool)($u->is_active ?? true);
|
||||
$effective = $domainActive && $mailboxActive;
|
||||
|
|
@ -144,17 +165,14 @@ class MailboxList extends Component
|
|||
'localpart' => (string)$u->localpart,
|
||||
'quota_mb' => $quota,
|
||||
'usage_percent' => $usage,
|
||||
'used_mb' => $usedMB,
|
||||
'message_count' => (int) ($u->message_count ?? 0),
|
||||
// 'used_mb' => $used,
|
||||
// 'message_count' => (int)($u->message_count ?? $u->mails_count ?? 0),
|
||||
'used_mb' => $usedMB, // MiB fürs UI
|
||||
'message_count' => $messageCount,
|
||||
'is_active' => $mailboxActive,
|
||||
'is_effective_active' => $effective,
|
||||
'inactive_reason' => $reason,
|
||||
];
|
||||
}
|
||||
|
||||
// für Blade
|
||||
$d->prepared_mailboxes = $prepared;
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +181,98 @@ class MailboxList extends Component
|
|||
'system' => $this->showSystemCard ? $system : null,
|
||||
]);
|
||||
}
|
||||
// public function render()
|
||||
// {
|
||||
// $system = Domain::query()->where('is_system', true)->first();
|
||||
// $term = trim($this->search);
|
||||
// $hasTerm = $term !== '';
|
||||
// $needle = '%'.str_replace(['%','_'], ['\%','\_'], $term).'%'; // LIKE-sicher
|
||||
//
|
||||
// $domains = Domain::query()
|
||||
// ->when($system, fn ($q) => $q->whereKeyNot($system->id))
|
||||
//
|
||||
// // Domain selbst ODER MailUser/ Aliasse müssen matchen
|
||||
// ->when($hasTerm, function ($q) use ($needle) {
|
||||
// $q->where(function ($w) use ($needle) {
|
||||
// $w->where('domain', 'like', $needle)
|
||||
// ->orWhereHas('mailUsers', fn($u) => $u->where('localpart', 'like', $needle));
|
||||
// });
|
||||
// })
|
||||
//
|
||||
// ->withCount(['mailUsers'])
|
||||
//
|
||||
// // Beziehungen zunächst gefiltert laden (damit "test" nur passende Mailboxen zeigt)
|
||||
// ->with([
|
||||
// 'mailUsers' => function ($q) use ($hasTerm, $needle) {
|
||||
// if ($hasTerm) $q->where('localpart', 'like', $needle);
|
||||
// $q->orderBy('localpart');
|
||||
// },
|
||||
// ])
|
||||
//
|
||||
// ->orderBy('domain')
|
||||
// ->get();
|
||||
//
|
||||
// // Domains, deren NAME den Suchbegriff trifft → ALLE Mailboxen/Aliasse zeigen
|
||||
// if ($hasTerm) {
|
||||
// $lower = Str::lower($term);
|
||||
// foreach ($domains as $d) {
|
||||
// if (Str::contains(Str::lower($d->domain), $lower)) {
|
||||
// // volle Relationen nachladen (überschreibt die gefilterten)
|
||||
// $d->setRelation('mailUsers', $d->mailUsers()->orderBy('localpart')->get());
|
||||
// $d->setRelation('mailAliases', $d->mailAliases()->orderBy('local')->get());
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // Vorbereitung für Blade (unverändert, arbeitet auf den ggf. gefilterten Relationen)
|
||||
// foreach ($domains as $d) {
|
||||
// $prepared = [];
|
||||
// $domainActive = (bool)($d->is_active ?? true);
|
||||
//
|
||||
// foreach ($d->mailUsers as $u) {
|
||||
// $stats = Setting::get("mailbox.{$u->email}");
|
||||
// $usedBytes = $stats['used_bytes'] ?? ($u->used_bytes ?? 0);
|
||||
// $messageCount = $stats['message_count'] ?? ($u->message_count ?? 0);
|
||||
// $usedMB = (int) round(($usedBytes) / 1024 / 1024);
|
||||
// $quota = (int)($u->quota_mb ?? 0);
|
||||
// $usage = $quota > 0 ? min(100, (int) round($usedMB / max(1,$quota) * 100)) : 0;
|
||||
//
|
||||
//
|
||||
//// $quota = (int)($u->quota_mb ?? 0);
|
||||
//// $used = (int)($u->used_mb ?? 0);
|
||||
//// $usage = $quota > 0 ? min(100, (int)round($used / max(1, $quota) * 100)) : 0;
|
||||
//
|
||||
// $mailboxActive = (bool)($u->is_active ?? true);
|
||||
// $effective = $domainActive && $mailboxActive;
|
||||
//
|
||||
// $reason = null;
|
||||
// if (!$effective) {
|
||||
// $reason = !$domainActive ? 'Domain inaktiv'
|
||||
// : (!$mailboxActive ? 'Postfach inaktiv' : null);
|
||||
// }
|
||||
//
|
||||
// $prepared[] = [
|
||||
// 'id' => $u->id,
|
||||
// 'localpart' => (string)$u->localpart,
|
||||
// 'quota_mb' => $quota,
|
||||
// 'usage_percent' => $usage,
|
||||
// 'used_mb' => $usedMB,
|
||||
// 'message_count' => $messageCount,
|
||||
// 'is_active' => $mailboxActive,
|
||||
// 'is_effective_active' => $effective,
|
||||
// 'inactive_reason' => $reason,
|
||||
// ];
|
||||
// }
|
||||
//
|
||||
// // für Blade
|
||||
// $d->prepared_mailboxes = $prepared;
|
||||
// }
|
||||
//
|
||||
// return view('livewire.ui.mail.mailbox-list', [
|
||||
// 'domains' => $domains,
|
||||
// 'system' => $this->showSystemCard ? $system : null,
|
||||
// ]);
|
||||
// }
|
||||
|
||||
// public function render()
|
||||
// {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Ui\System;
|
||||
|
||||
use Livewire\Component;
|
||||
|
||||
class UpdateManager extends Component
|
||||
{
|
||||
public bool $running = false;
|
||||
public string $log = '';
|
||||
public ?int $rc = null;
|
||||
public ?string $latest = null; // optional: von Cache o.ä.
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->refreshState();
|
||||
// Optional: latest Tag/Version aus Cache anzeigen
|
||||
$this->latest = cache('mailwolt.update_available');
|
||||
}
|
||||
|
||||
public function refreshState(): void
|
||||
{
|
||||
$state = @trim(@file_get_contents('/var/lib/mailwolt/update/state') ?: '');
|
||||
$this->running = ($state === 'running');
|
||||
|
||||
$rcRaw = @trim(@file_get_contents('/var/lib/mailwolt/update/rc') ?: '');
|
||||
$this->rc = is_numeric($rcRaw) ? (int)$rcRaw : null;
|
||||
|
||||
// letzte 200 Zeilen Log
|
||||
$this->log = @shell_exec('tail -n 200 /var/log/mailwolt-update.log 2>/dev/null') ?? '';
|
||||
}
|
||||
|
||||
public function runUpdate(): void
|
||||
{
|
||||
// Hinweis „Update verfügbar“ ausblenden
|
||||
cache()->forget('mailwolt.update_available');
|
||||
|
||||
// Update im Hintergrund starten
|
||||
@shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &');
|
||||
|
||||
$this->running = true;
|
||||
$this->dispatch('toast',
|
||||
type: 'info',
|
||||
badge: 'Update',
|
||||
title: 'Update gestartet',
|
||||
text: 'Das System wird aktualisiert …',
|
||||
duration: 6000
|
||||
);
|
||||
}
|
||||
|
||||
public function poll(): void
|
||||
{
|
||||
$before = $this->running;
|
||||
$this->refreshState();
|
||||
|
||||
if ($before && !$this->running && $this->rc !== null) {
|
||||
$ok = ($this->rc === 0);
|
||||
$this->dispatch('toast',
|
||||
type: $ok ? 'done' : 'warn',
|
||||
badge: 'Update',
|
||||
title: $ok ? 'Update abgeschlossen' : 'Update fehlgeschlagen',
|
||||
text: $ok ? 'Die neue Version ist aktiv.' : "Fehlercode: {$this->rc}. Log prüfen.",
|
||||
duration: 8000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.ui.system.update-manager');
|
||||
}
|
||||
}
|
||||
|
|
@ -9,20 +9,19 @@
|
|||
<button class="sidebar-toggle translate-0 right-5 block sm:hidden text-white/60 hover:text-white text-2xl">
|
||||
<i class="ph ph-list"></i>
|
||||
</button>
|
||||
|
||||
@if ($latest = cache('mailwolt.update_available'))
|
||||
<div class="bg-blue-900/40 text-blue-100 p-4 rounded-xl border border-blue-800">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<strong>Neue Version verfügbar:</strong> {{ $latest }}
|
||||
</div>
|
||||
<button wire:click="runUpdate"
|
||||
class="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded">
|
||||
Jetzt aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
{{-- @if ($latest = cache('mailwolt.update_available'))--}}
|
||||
{{-- <div class="bg-blue-900/40 text-blue-100 p-4 rounded-xl border border-blue-800">--}}
|
||||
{{-- <div class="flex justify-between items-center">--}}
|
||||
{{-- <div>--}}
|
||||
{{-- <strong>Neue Version verfügbar:</strong> {{ $latest }}--}}
|
||||
{{-- </div>--}}
|
||||
{{-- <button wire:click="runUpdate"--}}
|
||||
{{-- class="bg-blue-600 hover:bg-blue-500 text-white px-3 py-1 rounded">--}}
|
||||
{{-- Jetzt aktualisieren--}}
|
||||
{{-- </button>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- </div>--}}
|
||||
{{-- @endif--}}
|
||||
{{-- <button id="sidebar-toggle-btn"--}}
|
||||
{{-- class="action-button sidebar-toggle flex items-center justify-center p-1.5 shadow-2xl rounded">--}}
|
||||
{{-- <svg class="size-5 group-[.expanded]/side:rotate-180"--}}
|
||||
|
|
|
|||
|
|
@ -65,6 +65,15 @@
|
|||
<x-button.copy-btn :text="$r['value']" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-4 pb-3">
|
||||
<pre class="text-[12px] w-full rounded-lg bg-white/5 border border-white/10 text-white px-3 py-2 text-sm opacity-70 whitespace-pre-wrap break-all">{{ $r['value'] }}</pre>
|
||||
@if(!empty($r['helpUrl']))
|
||||
<a href="{{ $r['helpUrl'] }}" target="_blank" rel="noopener"
|
||||
class="mt-2 inline-flex items-center gap-1 text-[12px] text-sky-300 hover:text-sky-200">
|
||||
<i class="ph ph-arrow-square-out"></i>{{ $r['helpLabel'] }}
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -304,6 +304,8 @@
|
|||
|
||||
<td class="px-3 py-2 rounded-r-xl">
|
||||
<div class="flex items-center gap-2 justify-end">
|
||||
<button wire:click="updateMailboxStatsOne('{{ $u['localpart'].'@'.$domain->domain }}')">Jetzt aktualisieren</button>
|
||||
|
||||
<button wire:click="updateMailboxStats"
|
||||
class="px-3 py-1 text-sm rounded-md bg-blue-600 text-white hover:bg-blue-700 transition">
|
||||
Jetzt aktualisieren
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<div class="bg-white/5 border border-white/10 rounded-xl p-4"
|
||||
@if($running) wire:poll.3s="poll" @endif>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-white/80">
|
||||
<strong>MailWolt Update</strong>
|
||||
@if($latest)
|
||||
<span class="ml-2 px-2 py-0.5 rounded-full text-xs border border-blue-400/40 bg-blue-500/10 text-blue-200">
|
||||
verfügbar: {{ $latest }}
|
||||
</span>
|
||||
@endif
|
||||
@if($running)
|
||||
<span class="ml-2 px-2 py-0.5 rounded-full text-xs border border-white/20 bg-white/10">läuft …</span>
|
||||
@elseif(!is_null($rc))
|
||||
<span class="ml-2 px-2 py-0.5 rounded-full text-xs border {{ $rc===0 ? 'border-emerald-400/40 bg-emerald-500/10 text-emerald-200' : 'border-rose-400/40 bg-rose-500/10 text-rose-200' }}">
|
||||
{{ $rc===0 ? 'fertig' : 'fehler' }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button wire:click="runUpdate"
|
||||
@disabled($running)
|
||||
class="bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-3 py-1 rounded">
|
||||
Jetzt aktualisieren
|
||||
</button>
|
||||
<button wire:click="refreshState"
|
||||
class="border border-white/10 bg-white/5 text-white/80 px-3 py-1 rounded hover:border-white/20">
|
||||
Neu laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if($running || !empty($log))
|
||||
<pre class="mt-3 max-h-64 overflow-auto text-xs bg-black/40 rounded p-3 border border-white/10 text-white/80">{{ $log }}</pre>
|
||||
@endif
|
||||
</div>
|
||||
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto space-y-6 px-2 md:px-4">
|
||||
<div class="max-w-fit col-span-4 #lg:col-span-6">
|
||||
<livewire:ui.system.update-manager />
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 lg:col-span-6">
|
||||
<livewire:ui.dashboard.top-bar />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue