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

main v1.0.75
boban 2025-10-28 18:07:37 +01:00
parent dd3f413e6a
commit 3108d521a5
3 changed files with 517 additions and 76 deletions

View File

@ -8,25 +8,27 @@ use App\Models\Setting;
class StorageProbe extends Command
{
protected $signature = 'health:probe-disk {target=/}';
protected $description = 'Speichert Storage-Werte (inkl. Frei+5%) in settings:health.disk';
protected $description = 'Speichert Storage-Werte (inkl. Breakdown) in settings:health.disk';
public function handle(): int
{
$target = $this->argument('target') ?: '/';
$data = $this->probe($target);
$data = $this->probe($target);
// Persistiert (DB + Redis) über dein Settings-Model
Setting::set('health.disk', $data);
Setting::set('health.disk_updated_at', now()->toIso8601String());
$this->info(sprintf(
'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d)',
'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d) breakdown: system=%.1fGB mails=%.1fGB backups=%.1fGB',
$data['mount'],
$data['total_gb'],
$data['used_gb'],
$data['free_gb'],
$data['free_plus_reserve_gb'],
$data['percent_used_total'],
$data['breakdown']['system_gb'],
$data['breakdown']['mails_gb'],
$data['breakdown']['backup_gb'],
));
return self::SUCCESS;
@ -34,7 +36,8 @@ class StorageProbe extends Command
protected function probe(string $target): array
{
$line = trim((string) @shell_exec('df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1'));
// --- df: Gesamtdaten des Filesystems (inkl. Reserve) -----------------
$line = trim((string)@shell_exec('LC_ALL=C df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1'));
$device = $mount = '';
$totalKb = $usedKb = $availKb = 0;
@ -42,35 +45,140 @@ class StorageProbe extends Command
if ($line !== '') {
$p = preg_split('/\s+/', $line);
if (count($p) >= 6) {
$device = $p[0];
$totalKb = (int) $p[1]; // TOTAL (inkl. Reserve)
$usedKb = (int) $p[2]; // Used
$availKb = (int) $p[3]; // Avail (User-sicht)
$mount = $p[5];
$device = $p[0];
$totalKb = (int)$p[1]; // TOTAL (inkl. Reserve)
$usedKb = (int)$p[2]; // Used
$availKb = (int)$p[3]; // Avail (User-sicht)
$mount = $p[5];
}
}
$toGiB = static fn($kb) => (int) round(max(0, (int)$kb) / (1024*1024));
$toGiB_i = static fn($kb) => (int)round(max(0, (int)$kb) / (1024 * 1024)); // ganzzahlig (UI: Gesamt/Genutzt/Frei)
$toGiB_f = static fn($kb) => round(max(0, (int)$kb) / (1024 * 1024), 1); // eine Nachkommastelle (Breakdown/Legende)
$totalGb = $toGiB($totalKb);
$freeGb = $toGiB($availKb); // user-verfügbar
$usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve
$res5Gb = (int) round($totalGb * 0.05); // 5% von Gesamt
$totalGb = $toGiB_i($totalKb);
$freeGb = $toGiB_i($availKb); // user-verfügbar
$usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve
$res5Gb = (int)round($totalGb * 0.05); // 5% von Gesamt
$freePlusReserveGb = min($totalGb, $freeGb + $res5Gb);
$percentUsed = $totalGb > 0 ? (int)round($usedGb * 100 / $totalGb) : 0;
$percentUsed = $totalGb > 0 ? (int) round($usedGb * 100 / $totalGb) : 0;
// --- du: reale Verbräuche bestimmter Bäume (KB) -----------------------
$duKb = function (string $path): int {
if (!is_dir($path)) return 0;
$kb = (int)trim((string)@shell_exec('LC_ALL=C du -sk --apparent-size ' . escapeshellarg($path) . ' 2>/dev/null | cut -f1'));
return max(0, $kb);
};
$kbMails = $duKb('/var/mail/vhosts');
$kbBackup = $duKb('/var/backups/mailwolt');
// „System“ = alles übrige, was nicht Mails/Backups ist (OS, App, Logs, DB-Daten, …)
$gbMails = $toGiB_f($kbMails);
$gbBackup = $toGiB_f($kbBackup);
// system_gb aus „usedGb (mails+backup)“, nie negativ
$gbSystem = max(0, round($usedGb - ($gbMails + $gbBackup), 1));
return [
'device' => $device ?: 'unknown',
'mount' => $mount ?: $target,
'device' => $device ?: 'unknown',
'mount' => $mount ?: $target,
'total_gb' => $totalGb,
'used_gb' => $usedGb, // inkl. Reserve
'free_gb' => $freeGb, // User-sicht
'reserve5_gb' => $res5Gb, // Info
'free_plus_reserve_gb' => $freePlusReserveGb, // ← das willst du anzeigen
'total_gb' => $totalGb,
'used_gb' => $usedGb, // inkl. Reserve
'free_gb' => $freeGb, // User-sicht
'reserve5_gb' => $res5Gb, // Info
'free_plus_reserve_gb' => $freePlusReserveGb, // Anzeige „Frei“
'percent_used_total' => $percentUsed, // fürs Donut (~15%)
'percent_used_total' => $percentUsed,
// Reale Breakdown-Werte
'breakdown' => [
'system_gb' => $gbSystem,
'mails_gb' => $gbMails,
'backup_gb' => $gbBackup,
],
];
}
}
//
//namespace App\Console\Commands;
//
//use Illuminate\Console\Command;
//use App\Models\Setting;
//
//class StorageProbe extends Command
//{
// protected $signature = 'health:probe-disk {target=/}';
// protected $description = 'Speichert Storage-Werte (inkl. Frei+5%) in settings:health.disk';
//
// public function handle(): int
// {
// $target = $this->argument('target') ?: '/';
// $data = $this->probe($target);
//
// // Persistiert (DB + Redis) über dein Settings-Model
// Setting::set('health.disk', $data);
// Setting::set('health.disk_updated_at', now()->toIso8601String());
//
// $this->info(sprintf(
// 'Storage %s → total:%dGB used:%dGB free_user:%dGB free+5%%:%dGB (%%used:%d)',
// $data['mount'],
// $data['total_gb'],
// $data['used_gb'],
// $data['free_gb'],
// $data['free_plus_reserve_gb'],
// $data['percent_used_total'],
// ));
//
// return self::SUCCESS;
// }
//
// protected function probe(string $target): array
// {
// $line = trim((string) @shell_exec('df -kP ' . escapeshellarg($target) . ' 2>/dev/null | tail -n1'));
//
// $device = $mount = '';
// $totalKb = $usedKb = $availKb = 0;
//
// if ($line !== '') {
// $p = preg_split('/\s+/', $line);
// if (count($p) >= 6) {
// $device = $p[0];
// $totalKb = (int) $p[1]; // TOTAL (inkl. Reserve)
// $usedKb = (int) $p[2]; // Used
// $availKb = (int) $p[3]; // Avail (User-sicht)
// $mount = $p[5];
// }
// }
//
// $toGiB = static fn($kb) => (int) round(max(0, (int)$kb) / (1024*1024));
//
// $totalGb = $toGiB($totalKb);
// $freeGb = $toGiB($availKb); // user-verfügbar
// $usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve
// $res5Gb = (int) round($totalGb * 0.05); // 5% von Gesamt
// $freePlusReserveGb = min($totalGb, $freeGb + $res5Gb);
//
// $percentUsed = $totalGb > 0 ? (int) round($usedGb * 100 / $totalGb) : 0;
//
// return [
// 'device' => $device ?: 'unknown',
// 'mount' => $mount ?: $target,
//
// 'total_gb' => $totalGb,
// 'used_gb' => $usedGb, // inkl. Reserve
// 'free_gb' => $freeGb, // User-sicht
// 'reserve5_gb' => $res5Gb, // Info
// 'free_plus_reserve_gb' => $freePlusReserveGb, // ← das willst du anzeigen
//
// 'percent_used_total' => $percentUsed, // fürs Donut (~15%)
// 'breakdown' => [
// 'system_gb' => 5.2, // OS, App, Logs …
// 'mails_gb' => 2.8, // /var/mail/vhosts
// 'backup_gb' => 1.0, // /var/backups/mailwolt (oder wohin du sicherst)
// ],
// ];
// }
//}

View File

@ -1,5 +1,6 @@
<?php
namespace App\Livewire\Ui\System;
use Livewire\Component;
@ -10,17 +11,22 @@ class StorageCard extends Component
{
public string $target = '/';
// Summen
public ?int $diskTotalGb = null;
public ?int $diskUsedGb = null; // inkl. Reserve (passt zum Donut)
public ?int $diskFreeGb = null; // wird unten auf free_plus_reserve_gb gesetzt
public ?int $diskUsedGb = null;
public ?int $diskFreeGb = null;
// Donut
public array $diskSegments = [];
public int $diskSegOuterRadius = 92;
public int $diskInnerSize = 160;
public array $diskCenterText = ['percent' => '', 'label' => 'SPEICHER BELEGT'];
public int $diskSegOuterRadius = 92;
public int $diskInnerSize = 160;
public array $diskCenterText = ['percent' => '', 'label' => 'SPEICHER BELEGT'];
// Stacked-Bar + Legende
public array $barSegments = []; // [{label,color,gb,percent}]
public ?string $measuredAt = null;
protected int $segCount = 48;
protected int $segCount = 100;
public function mount(string $target = '/'): void
{
@ -28,7 +34,10 @@ class StorageCard extends Component
$this->loadFromSettings();
}
public function render() { return view('livewire.ui.system.storage-card'); }
public function render()
{
return view('livewire.ui.system.storage-card');
}
public function refresh(): void
{
@ -36,41 +45,277 @@ class StorageCard extends Component
$this->loadFromSettings();
}
// protected function loadFromSettings(): void
// {
// $disk = Setting::get('health.disk', []);
// if (!is_array($disk) || empty($disk)) {
// $this->resetUi();
// return;
// }
//
// $this->diskTotalGb = self::intOrNull($disk['total_gb'] ?? null);
// $this->diskUsedGb = self::intOrNull($disk['used_gb'] ?? null);
// $this->diskFreeGb = self::intOrNull($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? null));
//
// $percent = $disk['percent_used_total'] ?? null;
// $this->diskCenterText = [
// 'percent' => is_numeric($percent) ? (string)round($percent) . '%' : '',
// 'label' => 'SPEICHER BELEGT',
// ];
// $this->diskSegments = $this->buildSegments(is_numeric($percent) ? (int)$percent : null);
//
// $this->barSegments = $this->buildBar($disk);
// $this->measuredAt = Setting::get('health.disk_updated_at', null);
// }
protected function loadFromSettings(): void
{
$disk = Setting::get('health.disk', []);
if (!is_array($disk) || empty($disk)) return;
if (!is_array($disk) || empty($disk)) { $this->resetUi(); return; }
$this->diskTotalGb = $disk['total_gb'] ?? null;
$this->diskUsedGb = $disk['used_gb'] ?? null;
$this->diskFreeGb = $disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? null);
$this->diskTotalGb = self::intOrNull($disk['total_gb'] ?? null);
$this->diskUsedGb = self::intOrNull($disk['used_gb'] ?? null);
$this->diskFreeGb = self::intOrNull($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? null));
$percent = $disk['percent_used_total'] ?? null;
$this->diskCenterText = [
'percent' => is_numeric($percent) ? $percent.'%' : '',
'percent' => is_numeric($percent) ? (string)round($percent).'%' : '',
'label' => 'SPEICHER BELEGT',
];
$this->diskSegments = $this->buildSegments($percent);
// Donut farbig aus Breakdown
$this->diskSegments = $this->buildDonutSegmentsFromBreakdown($disk);
// Legende unten (gleiche Farben wie Donut)
$total = max(1, (float)($disk['total_gb'] ?? 1));
$bd = $disk['breakdown'] ?? [];
$sys = (float)($bd['system_gb'] ?? 0);
$mails = (float)($bd['mails_gb'] ?? 0);
$backup = (float)($bd['backup_gb'] ?? 0);
$free = (float)($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? 0));
$p = fn(float $gb) => max(0, min(100, round($gb * 100 / $total)));
$this->barSegments = [
['label' => 'System', 'gb' => round($sys,1), 'percent' => $p($sys), 'color' => 'bg-emerald-400'],
['label' => 'Mails', 'gb' => round($mails,1), 'percent' => $p($mails), 'color' => 'bg-rose-400'],
['label' => 'Backups', 'gb' => round($backup,1),'percent' => $p($backup), 'color' => 'bg-sky-400'],
['label' => 'Frei', 'gb' => round($free,1), 'percent' => $p($free), 'color' => 'bg-white/20'],
];
$this->barSegments = array_values(array_filter(
$this->barSegments,
fn($b) => ($b['gb'] ?? 0) > 0 // nur Einträge mit realer Größe
));
$this->measuredAt = Setting::get('health.disk_updated_at', null);
}
protected function buildSegments(?int $percent): array
// NEU: ersetzt buildSegments() + nutzt Breakdown
protected function buildDonutSegmentsFromBreakdown(array $disk): array
{
$segments = [];
$active = is_int($percent) ? (int) round($this->segCount * $percent / 100) : 0;
$total = (float)($disk['total_gb'] ?? 0);
if ($total <= 0) return [];
$activeClass = match (true) {
!is_int($percent) => 'bg-white/15',
$percent >= 90 => 'bg-rose-400',
$percent >= 70 => 'bg-amber-300',
default => 'bg-emerald-400',
// Breakdown lesen
$bd = $disk['breakdown'] ?? [];
$sys = (float)($bd['system_gb'] ?? 0);
$mails = (float)($bd['mails_gb'] ?? 0);
$backup = (float)($bd['backup_gb'] ?? 0);
$free = (float)($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? 0));
// Robust machen: falls used aus Settings größer ist als Breakdown-Summe → auf System draufschlagen
$usedReported = (float)($disk['used_gb'] ?? ($total - $free));
$sumUsedBd = $sys + $mails + $backup;
if ($usedReported > $sumUsedBd && $usedReported <= $total) {
$sys += ($usedReported - $sumUsedBd);
}
// Grenzen
$sys = max(0, min($sys, $total));
$mails = max(0, min($mails, $total));
$backup = max(0, min($backup, $total));
$free = max(0, min($free, $total));
// Segmente verteilen
$mkCount = function (float $gb) use ($total) {
return (int) round($this->segCount * $gb / $total);
};
$segments = [];
$add = function (int $count, string $class) use (&$segments) {
for ($i = 0; $i < $count; $i++) {
$segments[] = ['class' => $class];
}
};
$add($mkCount($sys), 'bg-emerald-400'); // System
$add($mkCount($mails), 'bg-rose-400'); // Mails
$add($mkCount($backup), 'bg-sky-400'); // Backups
// Rest = Frei (grau)
while (count($segments) < $this->segCount) {
$segments[] = ['class' => 'bg-white/15'];
}
// Winkel setzen (gleich wie vorher)
$out = [];
for ($i = 0; $i < $this->segCount; $i++) {
$angle = (360 / $this->segCount) * $i - 90;
$segments[] = ['angle' => $angle, 'class' => $i < $active ? $activeClass : 'bg-white/15'];
$out[] = ['angle' => $angle, 'class' => $segments[$i]['class']];
}
return $segments;
return $out;
}
// protected function buildSegments(?int $percent): array
// {
// $segments = [];
// $active = is_int($percent) ? (int)round($this->segCount * $percent / 100) : 0;
//
// $activeClass = match (true) {
// !is_int($percent) => 'bg-white/15',
// $percent >= 90 => 'bg-rose-400',
// $percent >= 70 => 'bg-amber-300',
// default => 'bg-emerald-400',
// };
//
// for ($i = 0; $i < $this->segCount; $i++) {
// $angle = (360 / $this->segCount) * $i - 90;
// $segments[] = [
// 'angle' => $angle,
// 'class' => $i < $active ? $activeClass : 'bg-white/15'
// ];
// }
// return $segments;
// }
protected function buildBar(array $disk): array
{
$total = (float)($disk['total_gb'] ?? 0);
if ($total <= 0) return [];
// Breakdown lesen + normalisieren
[$sys, $mails, $backups] = $this->readBreakdown($disk);
$free = max(0.0, (float)($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? 0)));
$used = min($total, $sys + $mails + $backups); // robust bei Messrauschen
// Falls Breakdown kleiner ist als used_gb: Rest als “System” draufschlagen,
// damit die Prozent-Summe 100 ergibt.
$usedReported = (float)($disk['used_gb'] ?? ($total - $free));
if ($usedReported > 0 && $used < $usedReported) {
$sys += ($usedReported - $used);
$used = $usedReported;
}
// Prozent berechnen
$p = fn(float $gb) => max(0, min(100, round($gb * 100 / $total)));
return [
['label' => 'System', 'gb' => round($sys, 1), 'percent' => $p($sys), 'color' => 'bg-emerald-400'],
['label' => 'Mails', 'gb' => round($mails, 1), 'percent' => $p($mails), 'color' => 'bg-rose-400'],
['label' => 'Backups', 'gb' => round($backups, 1), 'percent' => $p($backups), 'color' => 'bg-sky-400'],
['label' => 'Frei', 'gb' => round($free, 1), 'percent' => $p($free), 'color' => 'bg-white/20'],
];
}
protected function readBreakdown(array $disk): array
{
// Deine Keys ggf. hier mappen
$bd = $disk['breakdown'] ?? [];
$sys = (float)($bd['system_gb'] ?? 0);
$mails = (float)($bd['mails_gb'] ?? 0);
$backup = (float)($bd['backup_gb'] ?? 0);
return [$sys, $mails, $backup];
}
protected function resetUi(): void
{
$this->diskTotalGb = null;
$this->diskUsedGb = null;
$this->diskFreeGb = null;
$this->diskSegments = [];
$this->barSegments = [];
$this->diskCenterText = ['percent' => '', 'label' => 'SPEICHER BELEGT'];
$this->measuredAt = null;
}
protected static function intOrNull($v): ?int
{
return is_numeric($v) ? (int)round($v) : null;
}
}
//namespace App\Livewire\Ui\System;
//
//use Livewire\Component;
//use Illuminate\Support\Facades\Artisan;
//use App\Models\Setting;
//
//class StorageCard extends Component
//{
// public string $target = '/';
//
// public ?int $diskTotalGb = null;
// public ?int $diskUsedGb = null; // inkl. Reserve (passt zum Donut)
// public ?int $diskFreeGb = null; // wird unten auf free_plus_reserve_gb gesetzt
//
// public array $diskSegments = [];
// public int $diskSegOuterRadius = 92;
// public int $diskInnerSize = 160;
// public array $diskCenterText = ['percent' => '', 'label' => 'SPEICHER BELEGT'];
// public ?string $measuredAt = null;
//
// protected int $segCount = 48;
//
// public function mount(string $target = '/'): void
// {
// $this->target = $target ?: '/';
// $this->loadFromSettings();
// }
//
// public function render() { return view('livewire.ui.system.storage-card'); }
//
// public function refresh(): void
// {
// Artisan::call('health:probe-disk', ['target' => $this->target]);
// $this->loadFromSettings();
// }
//
// protected function loadFromSettings(): void
// {
// $disk = Setting::get('health.disk', []);
// if (!is_array($disk) || empty($disk)) return;
//
// $this->diskTotalGb = $disk['total_gb'] ?? null;
// $this->diskUsedGb = $disk['used_gb'] ?? null;
// $this->diskFreeGb = $disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? null);
//
// $percent = $disk['percent_used_total'] ?? null;
// $this->diskCenterText = [
// 'percent' => is_numeric($percent) ? $percent.'%' : '',
// 'label' => 'SPEICHER BELEGT',
// ];
// $this->diskSegments = $this->buildSegments($percent);
//
// $this->measuredAt = Setting::get('health.disk_updated_at', null);
// }
//
// protected function buildSegments(?int $percent): array
// {
// $segments = [];
// $active = is_int($percent) ? (int) round($this->segCount * $percent / 100) : 0;
//
// $activeClass = match (true) {
// !is_int($percent) => 'bg-white/15',
// $percent >= 90 => 'bg-rose-400',
// $percent >= 70 => 'bg-amber-300',
// default => 'bg-emerald-400',
// };
//
// for ($i = 0; $i < $this->segCount; $i++) {
// $angle = (360 / $this->segCount) * $i - 90;
// $segments[] = ['angle' => $angle, 'class' => $i < $active ? $activeClass : 'bg-white/15'];
// }
// return $segments;
// }
//}

View File

@ -1,4 +1,4 @@
<div class="glass-card relative p-4 max-h-fit border border-white/10 bg-white/5 rounded-2xl">
<div class="glass-card relative p-4 max-h-fit border border-white/10 bg-white/5 rounded-2xl" wire:poll.30s="refresh">
{{-- Kopf --}}
<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">
@ -12,14 +12,12 @@
</div>
{{-- Inhalt --}}
<div class="grid grid-cols-1 items-center">
<div class="grid grid-cols-1 items-center gap-4">
{{-- Donut --}}
<div class="flex items-center justify-center">
<div class="flex items-center justify-center mt-2">
<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>
{{-- Prozentanzeige im Zentrum --}}
<div class="absolute inset-0 flex flex-col items-center justify-center">
<div class="text-2xl md:text-3xl font-semibold leading-none tracking-tight">
{{ $diskCenterText['percent'] }}
@ -27,41 +25,131 @@
<div class="text-[10px] md:text-[11px] text-white/60 mt-1 uppercase tracking-wide">
{{ $diskCenterText['label'] }}
</div>
@if($measuredAt)
<div class="absolute bottom-12 mt-2 text-[10px] text-white/45 text-center">
zuletzt aktualisiert: <br> {{ \Carbon\Carbon::parse($measuredAt)->diffForHumans() }}
</div>
@endif
{{-- @if($measuredAt)--}}
{{-- <div class="absolute bottom-12 mt-2 text-[10px] text-white/45 text-center">--}}
{{-- zuletzt aktualisiert:<br>{{ \Carbon\Carbon::parse($measuredAt)->diffForHumans() }}--}}
{{-- </div>--}}
{{-- @endif--}}
</div>
{{-- Segment-Ring --}}
@foreach($diskSegments as $seg)
<span class="absolute top-1/2 left-1/2 block"
style="
transform: rotate({{ $seg['angle'] }}deg) translateX({{ $diskSegOuterRadius + 14 }}px);
width: 12px; height: 6px; margin:-3px 0 0 -6px;">
style="transform: rotate({{ $seg['angle'] }}deg) translateX({{ $diskSegOuterRadius + 14 }}px);
width: 12px; height: 3px; margin:-3px 0 0 -6px;">
<span class="block w-full h-full rounded-full {{ $seg['class'] }}"></span>
</span>
@endforeach
</div>
</div>
{{-- Zahlen --}}
<div class="md:pl-2">
<dl class="space-y-2">
<div class="flex items-center justify-between">
<dt class="text-white/60 text-sm">Gesamt</dt>
<dd class="font-medium tabular-nums text-base">{{ is_numeric($diskTotalGb) ? $diskTotalGb.' GB' : '' }}</dd>
{{-- Zahlen + Stacked Bar --}}
<div class="md:pl-2 space-y-4">
{{-- Stacked-Bar (horizontale Leiste) --}}
@if(!empty($barSegments))
<div class="space-y-2">
{{-- <div class="w-full h-3 rounded-full bg-white/10 overflow-hidden ring-1 ring-white/10">--}}
{{-- <div class="flex h-full">--}}
{{-- @foreach($barSegments as $b)--}}
{{-- <div class="{{ $b['color'] }} h-full" style="width: {{ $b['percent'] }}%"></div>--}}
{{-- @endforeach--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- Legende --}}
<div class="flex flex-wrap gap-3 text-[12px] text-white/70">
@foreach($barSegments as $b)
<div class="inline-flex items-center gap-2">
<span class="inline-block w-2.5 h-2.5 rounded-full {{ $b['color'] }}"></span>
<span>{{ $b['label'] }} {{ $b['gb'] }} GB ({{ $b['percent'] }}%)</span>
</div>
@endforeach
</div>
</div>
<div class="flex items-center justify-between">
<dt class="text-white/60 text-sm">Genutzt</dt>
<dd class="font-medium tabular-nums text-base">{{ is_numeric($diskUsedGb) ? $diskUsedGb.' GB' : '' }}</dd>
</div>
<div class="flex items-center justify-between">
<dt class="text-white/60 text-sm">Frei</dt>
<dd class="font-medium tabular-nums text-base">{{ is_numeric($diskFreeGb) ? $diskFreeGb.' GB' : '' }}</dd>
</div>
</dl>
@endif
{{-- Zahlen unten rechts --}}
{{-- <dl class="space-y-2">--}}
{{-- <div class="flex items-center justify-between">--}}
{{-- <dt class="text-white/60 text-sm">Gesamt</dt>--}}
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskTotalGb) ? $diskTotalGb.' GB' : '' }}</dd>--}}
{{-- </div>--}}
{{-- <div class="flex items-center justify-between">--}}
{{-- <dt class="text-white/60 text-sm">Genutzt</dt>--}}
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskUsedGb) ? $diskUsedGb.' GB' : '' }}</dd>--}}
{{-- </div>--}}
{{-- <div class="flex items-center justify-between">--}}
{{-- <dt class="text-white/60 text-sm">Frei</dt>--}}
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskFreeGb) ? $diskFreeGb.' GB' : '' }}</dd>--}}
{{-- </div>--}}
{{-- </dl>--}}
</div>
</div>
</div>
{{--<div class="glass-card relative p-4 max-h-fit border border-white/10 bg-white/5 rounded-2xl">--}}
{{-- --}}{{-- Kopf --}}
{{-- <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>--}}
{{-- <button wire:click="refresh"--}}
{{-- 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">--}}
{{-- Update <i class="ph ph-arrows-clockwise text-[12px]"></i>--}}
{{-- </button>--}}
{{-- </div>--}}
{{-- --}}{{-- Inhalt --}}
{{-- <div class="grid grid-cols-1 items-center">--}}
{{-- --}}{{-- Donut --}}
{{-- <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>--}}
{{-- --}}{{-- Prozentanzeige im Zentrum --}}
{{-- <div class="absolute inset-0 flex flex-col items-center justify-center">--}}
{{-- <div class="text-2xl md:text-3xl font-semibold leading-none tracking-tight">--}}
{{-- {{ $diskCenterText['percent'] }}--}}
{{-- </div>--}}
{{-- <div class="text-[10px] md:text-[11px] text-white/60 mt-1 uppercase tracking-wide">--}}
{{-- {{ $diskCenterText['label'] }}--}}
{{-- </div>--}}
{{-- @if($measuredAt)--}}
{{-- <div class="absolute bottom-12 mt-2 text-[10px] text-white/45 text-center">--}}
{{-- zuletzt aktualisiert: <br> {{ \Carbon\Carbon::parse($measuredAt)->diffForHumans() }}--}}
{{-- </div>--}}
{{-- @endif--}}
{{-- </div>--}}
{{-- --}}{{-- Segment-Ring --}}
{{-- @foreach($diskSegments as $seg)--}}
{{-- <span class="absolute top-1/2 left-1/2 block"--}}
{{-- style="--}}
{{-- transform: rotate({{ $seg['angle'] }}deg) translateX({{ $diskSegOuterRadius + 14 }}px);--}}
{{-- width: 12px; height: 6px; margin:-3px 0 0 -6px;">--}}
{{-- <span class="block w-full h-full rounded-full {{ $seg['class'] }}"></span>--}}
{{-- </span>--}}
{{-- @endforeach--}}
{{-- </div>--}}
{{-- </div>--}}
{{-- --}}{{-- Zahlen --}}
{{-- <div class="md:pl-2">--}}
{{-- <dl class="space-y-2">--}}
{{-- <div class="flex items-center justify-between">--}}
{{-- <dt class="text-white/60 text-sm">Gesamt</dt>--}}
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskTotalGb) ? $diskTotalGb.' GB' : '' }}</dd>--}}
{{-- </div>--}}
{{-- <div class="flex items-center justify-between">--}}
{{-- <dt class="text-white/60 text-sm">Genutzt</dt>--}}
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskUsedGb) ? $diskUsedGb.' GB' : '' }}</dd>--}}
{{-- </div>--}}
{{-- <div class="flex items-center justify-between">--}}
{{-- <dt class="text-white/60 text-sm">Frei</dt>--}}
{{-- <dd class="font-medium tabular-nums text-base">{{ is_numeric($diskFreeGb) ? $diskFreeGb.' GB' : '' }}</dd>--}}
{{-- </div>--}}
{{-- </dl>--}}
{{-- </div>--}}
{{-- </div>--}}
{{--</div>--}}