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

main v1.0.18
boban 2025-10-22 03:12:02 +02:00
parent a8e7aedadf
commit 529979f078
19 changed files with 855 additions and 186 deletions

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class AuthenticatedMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (!Auth::check()) {
if ($request->expectsJson()) {
return response()->json(['message' => 'Unauthenticated'], 401);
}
return redirect()->route('login');
}
return $next($request);
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class GuestOnlyMiddleware
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
if (Auth::check()) {
// Eingeloggt → z. B. Dashboard weiterleiten
return redirect()->route('dashboard');
}
return $next($request);
}
}

View File

@ -44,6 +44,7 @@ class HealthCard extends Component
public ?int $ramPercent = null;
public ?string $updatedAtHuman = null;
public $guardOk = [];
public function mount(): void
{
$this->loadData();
@ -179,19 +180,52 @@ class HealthCard extends Component
'127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket'],
];
$this->servicesCompact = collect($this->services)
->map(function ($srv) use ($nameMap) {
$key = (string)($srv['name'] ?? '');
$ok = (bool) ($srv['ok'] ?? false);
$label = $nameMap[$key]['label'] ?? ucfirst($key);
$hint = $nameMap[$key]['hint'] ?? (str_contains($key, ':') ? $key : '');
// $nameMap = [
// // --- Mail ---
// 'postfix' => ['label' => 'Postfix', 'hint' => 'MTA / Versand'],
// 'dovecot' => ['label' => 'Dovecot', 'hint' => 'IMAP / POP3'],
// 'rspamd' => ['label' => 'Rspamd', 'hint' => 'Spamfilter'],
// 'clamav' => ['label' => 'ClamAV', 'hint' => 'Virenscanner'],
//
// // --- Daten & Cache ---
// 'db' => ['label' => 'Datenbank', 'hint' => 'MySQL / MariaDB'],
// '127.0.0.1:6379' => ['label' => 'Redis', 'hint' => 'Cache / Queue'],
//
// // --- Web / PHP ---
// 'php8.2-fpm' => ['label' => 'PHP-FPM', 'hint' => 'PHP Runtime'],
// 'nginx' => ['label' => 'Nginx', 'hint' => 'Webserver'],
//
// // --- MailWolt spezifisch ---
// 'mailwolt-queue' => ['label' => 'MailWolt Queue', 'hint' => 'Job Worker'],
// 'mailwolt-schedule'=> ['label' => 'MailWolt Schedule','hint' => 'Task Scheduler'],
//
// // --- Sonstige Infrastruktur ---
// 'fail2ban' => ['label' => 'Fail2Ban', 'hint' => 'SSH / Mail Protection'],
// 'systemd-journald'=> ['label' => 'System Logs', 'hint' => 'Logging / Journal'],
//
// // --- WebSocket & Echtzeit ---
// '127.0.0.1:8080' => ['label' => 'Reverb', 'hint' => 'WebSocket Server'],
// ];
$existing = collect($this->services)->keyBy('name');
$this->servicesCompact = collect($nameMap)
->map(function ($meta, $key) use ($existing) {
$srv = $existing->get($key, []);
$ok = (bool)($srv['ok'] ?? false);
return [
'label' => $label,
'hint' => $hint,
'label' => $meta['label'],
'hint' => $meta['hint'],
'ok' => $ok,
// Punktfarbe
'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400',
'pillText' => $ok ? 'ok' : 'down',
// ✅ Bessere Status-Texte
'pillText' => $ok ? 'Aktiv' : 'Offline',
// Farbe für Pill
'pillClass' => $ok
? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
: 'text-rose-300 border-rose-400/30 bg-rose-500/10',
@ -199,6 +233,31 @@ class HealthCard extends Component
})
->values()
->all();
$this->guardOk = collect($this->services)->every(
fn($s) => (bool)($s['ok'] ?? false)
);
// $this->servicesCompact = collect($this->services)
// ->map(function ($srv) use ($nameMap) {
// $key = (string)($srv['name'] ?? '');
// $ok = (bool) ($srv['ok'] ?? false);
// $label = $nameMap[$key]['label'] ?? ucfirst($key);
// $hint = $nameMap[$key]['hint'] ?? (str_contains($key, ':') ? $key : '');
//
// var_dump($srv);
// return [
// 'label' => $label,
// 'hint' => $hint,
// 'ok' => $ok,
// 'dotClass' => $ok ? 'bg-emerald-400' : 'bg-rose-400',
// 'pillText' => $ok ? 'Läuft' : 'Down',
// 'pillClass' => $ok
// ? 'text-emerald-300 border-emerald-400/30 bg-emerald-500/10'
// : 'text-rose-300 border-rose-400/30 bg-rose-500/10',
// ];
// })
// ->values()
// ->all();
}
protected function decorateDisk(): void

View File

@ -28,7 +28,7 @@ class TopBar extends Component
public function mount(): void
{
// Domains + Zertifikate (passe an deinen Storage an)
$this->domainsCount = Domain::count(); // oder aus Cache/Repo
$this->domainsCount = Domain::where('is_server', false)->where('is_system', false)->count(); // oder aus Cache/Repo
// Beispiel: Domain::select('cert_expires_at','cert_issuer')...
// -> hier simple Annahme: im Cache 'domains:certs' liegt [{domain, days_left, type}]

View File

@ -0,0 +1,324 @@
<?php
namespace App\Livewire\Ui\System;
use Livewire\Component;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache;
class UpdateCard extends Component
{
public ?string $current = null; // installierte Version
public ?string $latest = null; // verfügbare Version (oder null)
public string $state = 'idle'; // idle | running
// UI-Textausgabe nach einer Prüfung / Aktion
public ?string $message = null; // z.B. "Du bist auf dem neuesten Stand (v1.0.16)"
public ?bool $messagePositive = null; // true = grün, false = neutral/weiß
// low-level (falls du sie später brauchst)
public bool $running = false; // aus /var/lib/mailwolt/update/state
public ?int $rc = null;
public function mount(): void
{
$this->current = $this->readCurrentVersion();
$this->latest = Cache::get('mailwolt.update_available');
$this->refreshLowLevelState();
// Starttext, falls nichts geprüft wurde
if ($this->message === null) {
$this->message = $this->latest && $this->hasUpdate()
? "Neue Version verfügbar: {$this->latest}"
: ($this->current ? "Du bist auf dem neuesten Stand ({$this->current})" : "Status unbekannt");
$this->messagePositive = !$this->hasUpdate();
}
}
public function render()
{
return view('livewire.ui.system.update-card');
}
/**
* „Erneut prüfen“ ohne Toast:
* - Progress anzeigen
* - Check-Command laufen lassen
* - Message in der Box aktualisieren
*/
public function refreshState(): void
{
$this->state = 'running';
$this->message = 'Prüfe auf Updates …';
$this->messagePositive = null;
try {
// Passe den Namen hier an dein tatsächliches Command an:
Artisan::call('mailwolt:check-updates');
} catch (\Throwable $e) {
// weich fallen
}
// Daten neu einlesen
$this->current = $this->readCurrentVersion();
$this->latest = Cache::get('mailwolt.update_available');
if ($this->hasUpdate()) {
$this->message = "Neue Version verfügbar: {$this->latest}";
$this->messagePositive = false; // neutral
} else {
$cur = $this->current ?: '';
$this->message = "Du bist auf dem neuesten Stand ({$cur})";
$this->messagePositive = true; // grün
}
$this->refreshLowLevelState();
$this->state = 'idle';
}
/**
* „Jetzt aktualisieren“ ohne Toast:
* - Hinweis sofort aus Cache entfernen (Badge weg)
* - Update-Wrapper starten
* - Running + Text in der Box anzeigen
*/
public function runUpdate(): void
{
Cache::forget('mailwolt.update_available');
@shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &');
$this->latest = null;
$this->state = 'running';
$this->running = true;
$this->message = 'Update läuft …';
$this->messagePositive = null;
}
/** --------------------- helpers --------------------- */
protected function hasUpdate(): bool
{
if (!$this->latest) return false;
$cur = $this->current ?: '0.0.0';
return version_compare($this->latest, $cur, '>');
}
protected function refreshLowLevelState(): 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;
}
protected function readCurrentVersion(): ?string
{
// bevorzugt /etc/mailwolt/build.info (wird im Installer/Updater gepflegt)
$build = @file_get_contents('/etc/mailwolt/build.info');
if ($build) {
foreach (preg_split('/\R+/', $build) as $line) {
if (str_starts_with($line, 'version=')) {
$v = trim(substr($line, 8));
if ($v !== '') return $v;
}
}
}
$v = config('app.version');
return $v !== '' ? $v : null;
}
}
//
//
//namespace App\Livewire\Ui\System;
//
//use Livewire\Component;
//use Illuminate\Support\Facades\Artisan;
//use Illuminate\Support\Facades\Cache;
//
//class UpdateCard extends Component
//{
// public ?string $current = null; // z.B. v1.0.16 (aktuell installiert)
// public ?string $latest = null; // z.B. v1.0.17 (verfügbar) oder null
// public string $state = 'idle'; // idle | running
//
// // optional: Low-level-Infos, falls du sie irgendwo anders brauchst
// public bool $running = false; // Wrapper-/Updater läuft gerade (aus State-Datei)
// public string $log = '';
// public ?int $rc = null;
//
// public function mount(): void
// {
// $this->current = $this->readCurrentVersion();
// $this->latest = Cache::get('mailwolt.update_available');
// $this->refreshLowLevelState(); // liest /var/lib/mailwolt/update/*
// }
//
// public function render()
// {
// return view('livewire.ui.system.update-card');
// }
//
// /**
// * „Erneut prüfen“:
// * - zeigt Running-Progress
// * - ruft den Checker (dein bestehendes Artisan-Command)
// * - aktualisiert $latest / $current
// * - dispatcht passenden Toast
// */
// public function refreshState(): void
// {
// $this->state = 'running';
//
// // Falls vorhanden: dein Command, der cache('mailwolt.update_available') setzt
// // -> Namen ggf. anpassen (du hattest z.B. mailwolt:check-updates erwähnt)
// try {
// Artisan::call('mailwolt:check-updates');
// } catch (\Throwable $e) {
// // Wenn das Command (noch) nicht existiert, nicht crashen state einfach zurücksetzen
// }
//
// // App-Status neu einlesen
// $this->current = $this->readCurrentVersion();
// $this->latest = Cache::get('mailwolt.update_available');
//
// $hasUpdate = $this->hasUpdate();
//
// // Toast ausspielen
// $this->dispatch('toast',
// type: $hasUpdate ? 'info' : 'done',
// badge: 'Update',
// title: $hasUpdate ? 'Neue Version verfügbar' : 'Alles aktuell',
// text: $hasUpdate
// ? "Verfügbar: {$this->latest} installiert: {$this->current}"
// : "Du bist auf dem neuesten Stand ({$this->current}).",
// duration: 6000
// );
//
// // Low-level-State (optional) und UI-State zurücksetzen
// $this->refreshLowLevelState();
// $this->state = 'idle';
// }
//
// /**
// * „Jetzt aktualisieren“:
// * - blendet die verfügbare-Version sofort aus
// * - startet den Root-Wrapper im Hintergrund
// * - zeigt einen Toast „Update gestartet“
// */
// public function runUpdate(): void
// {
// Cache::forget('mailwolt.update_available'); // Badge sofort weg
// @shell_exec('nohup sudo -n /usr/local/sbin/mw-update >/dev/null 2>&1 &');
//
// $this->state = 'running';
// $this->running = true;
//
// $this->dispatch('toast',
// type: 'info',
// badge: 'Update',
// title: 'Update gestartet',
// text: 'Das System wird aktualisiert …',
// duration: 6000
// );
// }
//
// /** ---- Helpers ------------------------------------------------------- */
//
// protected function hasUpdate(): bool
// {
// if (!$this->latest) return false;
// $cur = $this->current ?: '0.0.0';
// return version_compare($this->latest, $cur, '>');
// }
//
// protected function refreshLowLevelState(): 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;
//
// // Log bewusst NICHT angezeigt, aber verfügbar, falls du es später brauchst
// $this->log = @shell_exec('tail -n 200 /var/log/mailwolt-update.log 2>/dev/null') ?? '';
// }
//
// protected function readCurrentVersion(): ?string
// {
// // bevorzugt build.info, sonst config('app.version')
// $build = @file_get_contents('/etc/mailwolt/build.info');
// if ($build) {
// foreach (preg_split('/\R+/', $build) as $line) {
// if (str_starts_with($line, 'version=')) {
// $v = trim(substr($line, 8));
// if ($v !== '') return $v;
// }
// }
// }
// $v = config('app.version');
// return $v !== '' ? $v : null;
// }
//}
////
////namespace App\Livewire\Ui\System;
////
////use Livewire\Component;
////use Illuminate\Support\Facades\Artisan;
////use Illuminate\Support\Facades\Cache;
////
////class UpdateCard extends Component
////{
//// public ?string $current = null;
//// public ?string $latest = null;
//// public string $state = 'idle'; // idle|running
////
//// public bool $running = false;
//// public string $log = '';
//// public ?int $rc = null;
////
//// public function mount(): void
//// {
////// $this->refreshState();
//// $this->latest = cache('mailwolt.update_available');
////
//// }
////
//// public function render()
//// {
//// return view('livewire.ui.system.update-card');
//// }
////
//// 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
//// );
//// }
////}

View File

@ -23,13 +23,8 @@ class AppServiceProvider extends ServiceProvider
*/
public function boot(\App\Support\SettingsRepository $settings): void
{
Domain::observe(DomainObserver::class);
// $ver = trim(@file_get_contents(base_path('VERSION'))) ?: 'dev';
// config(['app.version' => $ver]);
config(['app.version' => trim(@file_get_contents('/var/lib/mailwolt/version')) ?: 'dev']);
if (file_exists(base_path('.git/HEAD'))) {
$ref = trim(file_get_contents(base_path('.git/HEAD')));
@ -39,6 +34,7 @@ class AppServiceProvider extends ServiceProvider
} else {
$commit = $ref;
}
config(['app.git_commit' => substr($commit ?? '', 0, 7)]);
}

View File

@ -0,0 +1,37 @@
<?php
namespace App\Providers;
use App\Support\BuildMeta;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class BuildMetaServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
$this->app->singleton(BuildMeta::class, fn () => BuildMeta::detect());
}
/**
* Bootstrap services.
*/
public function boot(): void
{
$meta = $this->app->make(BuildMeta::class);
// zentral verfügbar
config([
'app.version' => $meta->version,
'app.git_rev' => $meta->rev,
'app.git_short' => $meta->short,
'app.build_time' => $meta->updated,
]);
// optional: in allen Views als $build
View::share('build', $meta);
}
}

77
app/Support/BuildMeta.php Normal file
View File

@ -0,0 +1,77 @@
<?php
namespace App\Support;
use Illuminate\Support\Str;
final class BuildMeta
{
public string $version = 'dev';
public string $rev = '';
public string $short = '';
public ?string $updated = null;
public static function detect(
string $buildFile = '/etc/mailwolt/build.info',
string $basePath = null
): self {
$m = new self();
$basePath ??= base_path();
// 1) /etc/mailwolt/build.info (vom Updater)
if (is_file($buildFile) && is_readable($buildFile)) {
$lines = @file($buildFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
foreach ($lines as $line) {
if (!str_contains($line, '=')) continue;
[$k, $v] = explode('=', $line, 2);
$k = trim($k); $v = trim($v);
if ($k === 'version') $m->version = $v;
if ($k === 'rev') $m->rev = $v;
if ($k === 'updated') $m->updated = $v;
}
}
// 2) Fallback: .env APP_VERSION
if ($m->version === 'dev' && ($envVer = env('APP_VERSION'))) {
$m->version = trim($envVer);
}
// 3) Fallback: Git (ohne "-dirty")
if ($m->rev === '' || $m->version === 'dev') {
$head = $basePath.'/.git/HEAD';
if (is_file($head)) {
$ref = trim((string)@file_get_contents($head));
if (Str::startsWith($ref, 'ref:')) {
$refFile = $basePath.'/.git/'.substr($ref, 5);
$commit = @file_get_contents($refFile);
} else {
$commit = $ref;
}
$m->rev = $m->rev ?: trim((string)$commit);
if ($m->version === 'dev') {
$tag = @shell_exec('git -C '.escapeshellarg($basePath).' describe --tags --abbrev=0 2>/dev/null');
$m->version = $tag ? trim($tag) : 'dev';
}
}
}
// Sauber: "-dirty" entfernen
$m->version = preg_replace('/-dirty$/', '', $m->version);
// Kurz-Commit
$m->short = $m->rev ? substr($m->rev, 0, 7) : '';
return $m;
}
public function toArray(): array
{
return [
'version' => $this->version,
'rev' => $this->rev,
'short' => $this->short,
'updated' => $this->updated,
];
}
}

View File

@ -15,6 +15,8 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->alias([
'ensure.setup' => \App\Http\Middleware\EnsureSetupCompleted::class,
'signup.open' => \App\Http\Middleware\SignupOpen::class,
'auth.user' => \App\Http\Middleware\AuthenticatedMiddleware::class,
'guest.only' => \App\Http\Middleware\GuestOnlyMiddleware::class,
]);
})

View File

@ -2,4 +2,5 @@
return [
App\Providers\AppServiceProvider::class,
App\Providers\BuildMetaServiceProvider::class,
];

View File

@ -13,7 +13,7 @@ return [
|
*/
'name' => env('APP_NAME', 'Laravel'),
'name' => env('APP_NAME', 'MailWolt'),
/*
|--------------------------------------------------------------------------
@ -123,6 +123,8 @@ return [
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
'version' => env('APP_VERSION', '1.0.0'),
'git_commit' => env('APP_GIT_COMMIT', ''),
'version' => env('APP_VERSION', 'dev'),
'git_rev' => null,
'git_short' => null,
'build_time' => null,
];

View File

@ -18,25 +18,25 @@ return [
[
'title' => 'Security',
'icon' => 'icons.icon-security',
'route' => 'logout',
'route' => 'ui.logout',
'roles' => ['super_admin', 'admin', 'employee', 'user'],
],
[
'title' => 'Team',
'icon' => 'icons.icon-team',
'route' => 'logout',
'route' => 'ui.logout',
'roles' => ['super_admin', 'admin', 'employee', 'user'],
],
[
'title' => 'Settings',
'icon' => 'icons.icon-settings',
'route' => 'logout',
'route' => 'ui.logout',
'roles' => ['super_admin', 'admin', 'employee', 'user'],
],
[
'title' => 'Logout',
'icon' => 'icons.icon-logout',
'route' => 'logout',
'route' => 'ui.logout',
'roles' => ['super_admin', 'admin', 'employee', 'user'],
],
]

View File

@ -32,6 +32,14 @@
}
@keyframes pulse-slow {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
.animate-pulse-slow {
animation: pulse-slow 4s ease-in-out infinite;
}
.safe-pads {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);

View File

@ -1,36 +1,13 @@
{{--resources/views/components/partials/header.blade.php--}}
<div
class="#sticky top-0 w-full border-b hr rounded-lg">
class="#sticky top-0 w-full border-b hr #rounded-lg max-w-9xl mx-auto">
<header id="header" class="header w-full rounded-r-2xl rounded-l-none">
<nav class="flex h-[71px] #h-[74px] items-center justify-between #p-3">
<div class="#flex-1 md:w-3/5 w-full">
<div class="relative flex items-center gap-5">
<button class="sidebar-toggle translate-0 right-5 block sm:hidden text-white/60 hover:text-white text-2xl">
<button class="sidebar-toggle translate-0 right-5 block s#m: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--}}
{{-- <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"--}}
{{-- fill="none" stroke="currentColor" stroke-width="1.5"--}}
{{-- viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">--}}
{{-- <path stroke-linecap="round" stroke-linejoin="round"--}}
{{-- d="M9 5l7 7-7 7"></path>--}}
{{-- </svg>--}}
{{-- </button>--}}
<div class="flex items-center gap-3">
<h1 class="font-bold text-2xl">@yield('header_title')</h1>
</div>

View File

@ -83,20 +83,19 @@
</nav>
{{-- Footer Toggle (Desktop/Tablet) --}}
<div class="p-3 border-t hr flex justify-between items-center">
{{-- Version (z.B. aus config/app.php 'version' oder env) --}}
<div class="sidebar-label text-[10px] text-slate-300/70">
<span class="text-xs text-white/40">v{{ config('app.version') }}</span>
<div class="p-5 border-t hr flex justify-center items-center">
<div class="sidebar-label text-[10px] text-slate-300/70 whitespace-nowrap">
<span class="text-xs text-white/40">{{ config('app.name') }} | v{{ config('app.version') }}</span>
</div>
<button type="button"
class="action-button sidebar-toggle flex items-center justify-center p-2 shadow-2xl rounded"
aria-label="Sidebar ein/ausklappen">
<svg class="size-6" fill="none" stroke="currentColor" stroke-width="1.5"
viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"></path>
</svg>
</button>
{{-- <button type="button"--}}
{{-- class="action-button sidebar-toggle flex items-center justify-center p-2 shadow-2xl rounded"--}}
{{-- aria-label="Sidebar ein/ausklappen">--}}
{{-- <svg class="size-6" fill="none" stroke="currentColor" stroke-width="1.5"--}}
{{-- viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">--}}
{{-- <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"></path>--}}
{{-- </svg>--}}
{{-- </button>--}}
</div>
</div>
</div>

View File

@ -11,7 +11,8 @@
<div class="glass-card p-4 flex flex-col justify-between min-h-[140px]">
<div>
<div class="flex items-center justify-between mb-3">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<div
class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="ph ph-cpu text-white/70 text-[13px]"></i>
<span class="text-[11px] tracking-wide uppercase text-white/70">CPU</span>
</div>
@ -31,7 +32,8 @@
<div class="glass-card p-4 flex flex-col justify-between min-h-[140px]">
<div>
<div class="flex items-center justify-between mb-3">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<div
class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="ph ph-memory text-white/70 text-[13px]"></i>
<span class="text-[11px] tracking-wide uppercase text-white/70">RAM</span>
</div>
@ -54,7 +56,8 @@
<div class="glass-card p-4 flex flex-col justify-between min-h-[140px]">
<div>
<div class="flex items-center justify-between mb-3">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<div
class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="ph ph-activity text-white/70 text-[13px]"></i>
<span class="text-[11px] tracking-wide uppercase text-white/70">Load</span>
</div>
@ -78,7 +81,8 @@
<div class="glass-card p-4 flex flex-col justify-between min-h-[140px]">
<div>
<div class="flex items-center justify-between mb-3">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<div
class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="{{ $uptimeIcon }} text-white/70 text-[13px]"></i>
<span class="text-[11px] tracking-wide uppercase text-white/70">Uptime</span>
</div>
@ -96,68 +100,90 @@
</div>
</div>
{{-- Dienste & Storage: kompakt & bündig --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{{-- Dienste kompakt --}}
<div class="glass-card p-4">
<div class="flex items-center justify-between mb-3">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="ph ph-gear-six text-white/70 text-[13px]"></i>
<span class="text-[11px] tracking-wide uppercase text-white/70">Dienste</span>
<div class="grid grid-cols-2 #items-center justify-between gap-3">
{{-- MailGuard Status Card --}}
<div
class="glass-card p-4 flex flex-row items-start justify-between gap-4 relative overflow-hidden mb-4">
{{-- Linke Seite: Icon + Titel --}}
<div class="flex #items-start gap-3 relative z-10">
<div class="shrink-0">
{{-- Modernes Shield-Icon --}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<linearGradient id="shieldGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#4ade80"/>
<stop offset="1" stop-color="#15803d"/>
</linearGradient>
<radialGradient id="shine" cx="30%" cy="20%" r="70%">
<stop offset="0%" stop-color="rgba(255,255,255,0.4)"/>
<stop offset="100%" stop-color="rgba(255,255,255,0)"/>
</radialGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="#22c55e"
flood-opacity="0.6"/>
</filter>
</defs>
<path d="M32 6l20 8v12c0 13.5-8.7 22.7-20 27-11.3-4.3-20-13.5-20-27V14l20-8z"
fill="url(#shieldGradient)" filter="url(#glow)"/>
<path d="M32 6l20 8v12c0 13.5-8.7 22.7-20 27-11.3-4.3-20-13.5-20-27V14l20-8z"
fill="url(#shine)"/>
<path d="M23 33l7 7 11-14" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"
stroke-linejoin="round"/>
</svg>
</div>
<div>
<h3 class="text-lg font-semibold text-white/90">WoltGuard</h3>
<p class="text-sm text-white/50">System-Wächter aktiv und fehlerfrei</p>
</div>
<span class="inline-flex items-center gap-1 rounded-full bg-white/5 border border-white/10 px-2 py-0.5 text-[10px] text-white/60">
systemctl / TCP
</span>
</div>
<ul class="overflow-auto divide-y divide-white/5">
@forelse($servicesCompact as $s)
<li class="grid grid-cols-[auto,1fr,auto] items-center gap-3 py-2">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full {{ $s['dotClass'] }}"></span>
<div class="min-w-0">
<div class="text-white/90 truncate">{{ $s['label'] }}</div>
</div>
</div>
@if($s['hint'])
<div class="text-[11px] text-white/45 truncate">{{ $s['hint'] }}</div>
@endif
</div>
<span
class="justify-self-end inline-flex items-center px-2.5 py-0.5 rounded-full text-xs border {{ $s['pillClass'] }}">
{{ $s['pillText'] }}
</span>
</div>
</li>
@empty
<li class="py-2 text-white/50 text-sm">Keine Daten.</li>
@endforelse
</ul>
{{-- Rechte Seite: Status & Avatar --}}
<div class="flex items-start gap-3 relative z-10">
{{-- Status Badge --}}
@if($guardOk ?? false)
<span
class="inline-flex #items-center gap-1 px-3 py-1 rounded-full text-sm border border-emerald-400/30 text-emerald-300 bg-emerald-500/10">
<i class="ph ph-check-circle text-[14px]"></i>
<span class="text-[11px]">alle&nbsp;Dienste&nbsp;OK</span>
</span>
@else
<span
class="inline-flex #items-center gap-1 px-3 py-1 rounded-full text-sm border border-rose-400/30 text-rose-300 bg-rose-500/10">
<i class="ph ph-warning-circle text-[11px]"></i>
Störung erkannt
</span>
@endif
</div>
</div>
<livewire:ui.system.update-card/>
</div>
{{-- 2-Spalten Abschnitt: links Dienste, rechts Storage --}}
<div class="glass-card relative p-4">
{{-- Kopf: Titel + Link oben links --}}
<div class="flex items-center justify-between mb-3">
<div class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="ph ph-hard-drives text-white/70 text-[13px]"></i>
<span class="text-[11px] tracking-wide uppercase text-white/70">Storage</span>
</div>
<a href="#"
class="inline-flex items-center gap-1 rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-white/70 hover:text-white hover:border-white/20 transition">
Details <i class="ph ph-caret-right text-[12px]"></i>
</a>
</div>
{{-- Dienste & Storage: kompakt & bündig --}}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="glass-card relative p-4 max-h-fit">
{{-- Inhalt: Donut links, Zahlen rechts stacked auf kleineren Screens --}}
<div class="grid grid-cols-1 #md:grid-cols-[minmax(220px,1fr)_minmax(220px,1fr)] #gap-6 items-center">
<div class="grid grid-cols-1 items-center">
{{-- Donut --}}
<div class="flex items-center justify-between -mb-3">
<div
class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="ph ph-hard-drives text-white/70 text-[13px]"></i>
<span class="text-[11px] tracking-wide uppercase text-white/70">Storage</span>
</div>
<a href="#"
class="inline-flex items-center gap-1 rounded-full border border-white/10 bg-white/5 px-2 py-0.5 text-[10px] text-white/70 hover:text-white hover:border-white/20 transition">
Details <i class="ph ph-caret-right text-[12px]"></i>
</a>
</div>
<div class="flex items-center justify-center">
<div class="relative"
style="width: {{ $diskInnerSize + 80 }}px; height: {{ $diskInnerSize + 80 }}px;">
{{-- Innerer grauer Kreis --}}
<div class="absolute inset-[36px] rounded-full bg-white/[0.04] backdrop-blur-sm ring-1 ring-white/10"></div>
<div
class="absolute inset-[36px] rounded-full bg-white/[0.04] backdrop-blur-sm ring-1 ring-white/10"></div>
{{-- Prozentanzeige im Zentrum leicht kleiner & feiner --}}
<div class="absolute inset-0 flex flex-col items-center justify-center">
@ -206,5 +232,47 @@
</div>
</div>
</div>
<div>
<div class="glass-card p-4">
<div class="flex items-center justify-between mb-3">
<div
class="inline-flex items-center gap-2 rounded-full bg-white/5 border border-white/10 px-2.5 py-1">
<i class="ph ph-gear-six text-white/70 text-[13px]"></i>
<span class="text-[11px] tracking-wide uppercase text-white/70">Dienste</span>
</div>
<span
class="inline-flex items-center gap-1 rounded-full bg-white/5 border border-white/10 px-2 py-0.5 text-[10px] text-white/60">
systemctl / TCP
</span>
</div>
<ul class="overflow-auto divide-y divide-white/5">
@forelse($servicesCompact as $s)
<li class="grid grid-cols-[auto,1fr,auto] items-center gap-3 py-2">
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2">
<span class="h-2 w-2 rounded-full {{ $s['dotClass'] }}"></span>
<div class="min-w-0">
<div class="text-white/90 truncate">{{ $s['label'] }}</div>
</div>
</div>
@if($s['hint'])
<div class="text-[11px] text-white/45 truncate">{{ $s['hint'] }}</div>
@endif
</div>
<span
class="justify-self-end inline-flex items-center px-2.5 py-0.5 rounded-full text-xs border {{ $s['pillClass'] }}">
{{ $s['pillText'] }}
</span>
</div>
</li>
@empty
<li class="py-2 text-white/50 text-sm">Keine Daten.</li>
@endforelse
</ul>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,97 @@
@php
$hasUpdate = $latest && $current && version_compare($latest, $current, '>');
@endphp
<div class="glass-card rounded-2xl p-4 border border-white/10 bg-white/5 max-h-fit">
<div class="flex items-start gap-2">
{{-- Shield-Bot --}}
<div class="relative shrink-0">
<div class="shrink-0 relative">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
<defs>
<linearGradient id="shieldGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#4ade80"></stop>
<stop offset="1" stop-color="#15803d"></stop>
</linearGradient>
<radialGradient id="shine" cx="30%" cy="20%" r="70%">
<stop offset="0%" stop-color="rgba(255,255,255,0.4)"></stop>
<stop offset="100%" stop-color="rgba(255,255,255,0)"></stop>
</radialGradient>
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="0" stdDeviation="3" flood-color="#22c55e" flood-opacity="0.6"></feDropShadow>
</filter>
</defs>
<path d="M32 6l20 8v12c0 13.5-8.7 22.7-20 27-11.3-4.3-20-13.5-20-27V14l20-8z" fill="url(#shieldGradient)" filter="url(#glow)"></path>
<path d="M32 6l20 8v12c0 13.5-8.7 22.7-20 27-11.3-4.3-20-13.5-20-27V14l20-8z" fill="url(#shine)"></path>
<i class="ph-bold ph-arrows-clockwise absolute top-1/2 left-1/2 -translate-1/2 text-2xl {{ $state === 'running' ? 'animate-spin' : '' }}"></i>
</svg>
</div>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-center justify-between gap-3">
<div class="min-w-0">
<div class="text-white/90 font-semibold">MailWolt Update</div>
{{-- kleine Statuszeile mit Versionen, wenn vorhanden --}}
<div class="text-xs text-white/70">
@if($current)
aktuell: <span class="text-white/90">v{{ $current }}</span>
@else
aktuell: <span class="text-white/60"></span>
@endif
@if($latest)
<span class="mx-1 text-white/30"></span>
verfügbar: <span class="text-emerald-200">v{{ $latest }}</span>
@endif
</div>
</div>
<div>
{{-- Badge rechts --}}
<span class="shrink-0 inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-[11px] border px-3 py-1
{{ $hasUpdate
? 'text-yellow-200 bg-yellow-500/10 border-yellow-400/30'
: 'text-emerald-200 bg-emerald-500/10 border-emerald-400/30' }}">
<i class="ph {{ $hasUpdate ? 'ph-arrow-fat-line-up' : 'ph-check-circle' }} text-[12px]"></i>
{{ $hasUpdate ? 'Update verfügbar' : 'Aktuell' }}
</span>
{{-- <button wire:click="refreshState"--}}
{{-- @disabled($state==='running')--}}
{{-- class="inline-flex items-center gap-2 rounded p-1.5--}}
{{-- text-white/75 bg-white/5 border border-white/10 hover:border-white/20--}}
{{-- disabled:opacity-60">--}}
{{-- <i class="ph ph-arrow-clockwise text-[11px]"></i>--}}
{{-- </button>--}}
</div>
</div>
<div class="mt-4 flex items-center gap-2">
@if($hasUpdate)
<button wire:click="runUpdate"
@disabled($state==='running')
class="inline-flex items-center gap-2 rounded-lg px-3 py-1.5
text-emerald-200 bg-emerald-500/10 border border-emerald-400/30
hover:bg-emerald-500/15 hover:border-emerald-300/50
disabled:opacity-60">
<i class="ph ph-arrow-fat-lines-up text-[14px]"></i> Jetzt aktualisieren
</button>
@endif
</div>
{{-- Progress-Bar, wenn running --}}
@if($state === 'running')
<div class="mt-3 h-1.5 rounded bg-white/10 overflow-hidden">
<div class="h-full bg-emerald-400/70 animate-[progress_1.4s_infinite]" style="width:40%"></div>
</div>
<style>@keyframes progress {
0% {
transform: translateX(-100%)
}
100% {
transform: translateX(260%)
}
}</style>
@endif
</div>
</div>
</div>

View File

@ -5,10 +5,10 @@
@section('header_title', 'Dashboard')
@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="max-w-9xl 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 />
@ -18,20 +18,9 @@
<livewire:ui.dashboard.health-card />
</div>
{{-- <div class="grid grid-cols-1 lg:grid-cols-3 gap-4">--}}
{{-- @livewire('ui.dashboard.services-health') --}}{{----}}{{-- NEU --}}
{{-- @livewire('ui.dashboard.mail-kpis-card', ['key'=>'outgoing_queue'])--}}
{{-- @livewire('ui.dashboard.app-updates') --}}{{-- NEU --}}
{{-- </div>--}}
{{-- <div class="grid grid-cols-1 lg:grid-cols-2 gap-4">--}}
{{-- @livewire('ui.dashboard.mail-trend-chart')--}}
{{-- @livewire('ui.dashboard.alerts-feed') --}}{{-- NEU --}}
{{-- </div>--}}
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
@livewire('ui.dashboard.domains-panel')
@livewire('ui.dashboard.recent-logins-table')
<livewire:ui.dashboard.domains-panel />
<livewire:ui.dashboard.recent-logins-table />
</div>
</div>

View File

@ -2,57 +2,55 @@
use App\Http\Controllers\Api\TaskFeedController;
use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Setup\SetupWizard;
use App\Http\Controllers\Auth\SignUpController;
use App\Http\Controllers\UI\Domain\DomainDnsController;
use App\Http\Controllers\UI\Mail\AliasController;
use App\Http\Controllers\UI\Mail\MailboxController;
use App\Http\Controllers\UI\Security\RecoveryCodeDownloadController;
use App\Http\Controllers\UI\Security\SecurityController;
use App\Http\Controllers\UI\System\SettingsController;
use App\Livewire\PingButton;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
return Auth::check()
? redirect()->route('ui.dashboard')
: redirect()->route('login');
});
Route::middleware('auth.user')->name('ui.')->group(function () {
#DASHBOARD ROUTE
Route::get('/dashboard', [\App\Http\Controllers\UI\DashboardController::class, 'index'])->name('dashboard');
Route::get('/dashboard', [\App\Http\Controllers\UI\DashboardController::class, 'index'])
->middleware(['auth']) // falls gewünscht
->name('ui.dashboard');
//Route::middleware(['auth']) // falls du auth nutzt
//->get('/system/settings', [SettingsController::class, 'index'])
// ->name('ui.system.settings');
Route::middleware(['auth'])
->prefix('system')
->name('ui.system.')
->group(function () {
Route::get('/settings', [\App\Http\Controllers\UI\System\SettingsController::class, 'index'])
->name('settings');
#SYSTEM ROUTES
Route::prefix('system')->name('system.')->group(function () {
Route::get('/settings', [\App\Http\Controllers\UI\System\SettingsController::class, 'index'])->name('settings');
});
Route::middleware(['auth'])
->prefix('security')
->name('ui.security.')
->group(function () {
#SECURITY ROUTES
Route::prefix('security')->name('security.')->group(function () {
Route::get('/', [SecurityController::class, 'index'])->name('index');
Route::get('/ssl', [SecurityController::class, 'ssl'])->name('ssl');
Route::get('/fail2ban', [SecurityController::class, 'fail2ban'])->name('fail2ban');
Route::get('/rspamd', [SecurityController::class, 'rspamd'])->name('rspamd');
Route::get('/tls-ciphers', [SecurityController::class, 'tlsCiphers'])->name('tls');
Route::get('/audit-logs', [SecurityController::class, 'auditLogs'])->name('audit');
});
#DOMAIN ROUTES
Route::name('domain.')->group(function () {
Route::get('/domains', [DomainDnsController::class, 'index'])->name('index');
});
#MAIL ROUTES
Route::name('mail.')->group(function () {
Route::get('/mailboxes', [MailboxController::class, 'index'])->name('mailbox.index');
Route::get('/aliases', [AliasController::class, 'index'])->name('aliases.index');
});
#LOGOUT ROUTE
Route::post('/logout', [LoginController::class, 'logout'])->name('logout');
});
Route::middleware(['auth'])->name('ui.domain.')->group(function () {
Route::get('/domains', [DomainDnsController::class, 'index'])->name('index');
});
Route::middleware(['auth'])->name('ui.mail.')->group(function () {
Route::get('/mailboxes', [MailboxController::class, 'index'])->name('mailbox.index');
Route::get('/aliases', [AliasController::class, 'index'])->name('aliases.index');
});
//Route::middleware(['auth'])
@ -61,32 +59,12 @@ Route::middleware(['auth'])->name('ui.mail.')->group(function () {
// ->middleware('signed'); // wichtig: signierte URL
Route::middleware(['web','auth']) // nutzt Session, kein Token nötig
Route::middleware(['web', 'auth']) // nutzt Session, kein Token nötig
->get('/ui/tasks/active', [TaskFeedController::class, 'active'])
->name('ui.tasks.active');
//Route::middleware(['auth'])->group(function () {
// Route::get('/tasks/active', [TaskFeedController::class, 'active'])
// ->name('tasks.active');
// Route::post('/tasks/ack', [TaskFeedController::class, 'ack'])
// ->name('tasks.ack');
//});
//Route::middleware(['web','auth']) // gleiche Session wie im Dashboard
//->get('/ui/tasks/active', [\App\Http\Controllers\Api\TaskStatusController::class, 'index'])
// ->name('ui.tasks.active');
Route::middleware('guest.only')->group(function () {
Route::get('/login', [LoginController::class, 'show'])->name('login');
Route::get('/signup', [SignUpController::class, 'show'])->middleware('signup.open')->name('signup');
});
//Route::get('/dashboard', [DashboardController::class, 'show'])->name('dashboard');
Route::get('/login', [LoginController::class, 'show'])->name('login');
Route::get('/signup', [\App\Http\Controllers\Auth\SignUpController::class, 'show' ])->middleware('signup.open')->name('signup');
Route::post('/logout', [\App\Http\Controllers\Auth\LoginController::class, 'logout'])->name('logout');
//Route::middleware(['auth', 'ensure.setup'])->group(function () {
//// Route::get('/dashboard', Dashboard::class)->name('dashboard');
// Route::get('/setup', [SetupWizard::class, 'show'])->name('setup.wizard');
//});
//Route::middleware(['auth', 'ensure.setup'])->group(function () {
// Route::get('/dashboard', fn() => view('dashboard'))->name('dashboard');
//});