parent
dd3f413e6a
commit
3108d521a5
|
|
@ -8,25 +8,27 @@ use App\Models\Setting;
|
||||||
class StorageProbe extends Command
|
class StorageProbe extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'health:probe-disk {target=/}';
|
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
|
public function handle(): int
|
||||||
{
|
{
|
||||||
$target = $this->argument('target') ?: '/';
|
$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', $data);
|
||||||
Setting::set('health.disk_updated_at', now()->toIso8601String());
|
Setting::set('health.disk_updated_at', now()->toIso8601String());
|
||||||
|
|
||||||
$this->info(sprintf(
|
$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['mount'],
|
||||||
$data['total_gb'],
|
$data['total_gb'],
|
||||||
$data['used_gb'],
|
$data['used_gb'],
|
||||||
$data['free_gb'],
|
$data['free_gb'],
|
||||||
$data['free_plus_reserve_gb'],
|
$data['free_plus_reserve_gb'],
|
||||||
$data['percent_used_total'],
|
$data['percent_used_total'],
|
||||||
|
$data['breakdown']['system_gb'],
|
||||||
|
$data['breakdown']['mails_gb'],
|
||||||
|
$data['breakdown']['backup_gb'],
|
||||||
));
|
));
|
||||||
|
|
||||||
return self::SUCCESS;
|
return self::SUCCESS;
|
||||||
|
|
@ -34,7 +36,8 @@ class StorageProbe extends Command
|
||||||
|
|
||||||
protected function probe(string $target): array
|
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 = '';
|
$device = $mount = '';
|
||||||
$totalKb = $usedKb = $availKb = 0;
|
$totalKb = $usedKb = $availKb = 0;
|
||||||
|
|
@ -50,16 +53,33 @@ class StorageProbe extends Command
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$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);
|
$totalGb = $toGiB_i($totalKb);
|
||||||
$freeGb = $toGiB($availKb); // user-verfügbar
|
$freeGb = $toGiB_i($availKb); // user-verfügbar
|
||||||
$usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve
|
$usedGb = max(0, $totalGb - $freeGb); // belegt inkl. Reserve
|
||||||
$res5Gb = (int)round($totalGb * 0.05); // 5% von Gesamt
|
$res5Gb = (int)round($totalGb * 0.05); // 5% von Gesamt
|
||||||
$freePlusReserveGb = min($totalGb, $freeGb + $res5Gb);
|
$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 [
|
return [
|
||||||
'device' => $device ?: 'unknown',
|
'device' => $device ?: 'unknown',
|
||||||
'mount' => $mount ?: $target,
|
'mount' => $mount ?: $target,
|
||||||
|
|
@ -68,9 +88,97 @@ class StorageProbe extends Command
|
||||||
'used_gb' => $usedGb, // inkl. Reserve
|
'used_gb' => $usedGb, // inkl. Reserve
|
||||||
'free_gb' => $freeGb, // User-sicht
|
'free_gb' => $freeGb, // User-sicht
|
||||||
'reserve5_gb' => $res5Gb, // Info
|
'reserve5_gb' => $res5Gb, // Info
|
||||||
'free_plus_reserve_gb' => $freePlusReserveGb, // ← das willst du anzeigen
|
'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)
|
||||||
|
// ],
|
||||||
|
// ];
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
|
||||||
namespace App\Livewire\Ui\System;
|
namespace App\Livewire\Ui\System;
|
||||||
|
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
@ -10,17 +11,22 @@ class StorageCard extends Component
|
||||||
{
|
{
|
||||||
public string $target = '/';
|
public string $target = '/';
|
||||||
|
|
||||||
|
// Summen
|
||||||
public ?int $diskTotalGb = null;
|
public ?int $diskTotalGb = null;
|
||||||
public ?int $diskUsedGb = null; // inkl. Reserve (passt zum Donut)
|
public ?int $diskUsedGb = null;
|
||||||
public ?int $diskFreeGb = null; // wird unten auf free_plus_reserve_gb gesetzt
|
public ?int $diskFreeGb = null;
|
||||||
|
|
||||||
|
// Donut
|
||||||
public array $diskSegments = [];
|
public array $diskSegments = [];
|
||||||
public int $diskSegOuterRadius = 92;
|
public int $diskSegOuterRadius = 92;
|
||||||
public int $diskInnerSize = 160;
|
public int $diskInnerSize = 160;
|
||||||
public array $diskCenterText = ['percent' => '–', 'label' => 'SPEICHER BELEGT'];
|
public array $diskCenterText = ['percent' => '–', 'label' => 'SPEICHER BELEGT'];
|
||||||
|
|
||||||
|
// Stacked-Bar + Legende
|
||||||
|
public array $barSegments = []; // [{label,color,gb,percent}]
|
||||||
public ?string $measuredAt = null;
|
public ?string $measuredAt = null;
|
||||||
|
|
||||||
protected int $segCount = 48;
|
protected int $segCount = 100;
|
||||||
|
|
||||||
public function mount(string $target = '/'): void
|
public function mount(string $target = '/'): void
|
||||||
{
|
{
|
||||||
|
|
@ -28,7 +34,10 @@ class StorageCard extends Component
|
||||||
$this->loadFromSettings();
|
$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
|
public function refresh(): void
|
||||||
{
|
{
|
||||||
|
|
@ -36,41 +45,277 @@ class StorageCard extends Component
|
||||||
$this->loadFromSettings();
|
$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
|
protected function loadFromSettings(): void
|
||||||
{
|
{
|
||||||
$disk = Setting::get('health.disk', []);
|
$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->diskTotalGb = self::intOrNull($disk['total_gb'] ?? null);
|
||||||
$this->diskUsedGb = $disk['used_gb'] ?? null;
|
$this->diskUsedGb = self::intOrNull($disk['used_gb'] ?? null);
|
||||||
$this->diskFreeGb = $disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? null);
|
$this->diskFreeGb = self::intOrNull($disk['free_gb'] ?? ($disk['free_plus_reserve_gb'] ?? null));
|
||||||
|
|
||||||
$percent = $disk['percent_used_total'] ?? null;
|
$percent = $disk['percent_used_total'] ?? null;
|
||||||
$this->diskCenterText = [
|
$this->diskCenterText = [
|
||||||
'percent' => is_numeric($percent) ? $percent.'%' : '–',
|
'percent' => is_numeric($percent) ? (string)round($percent).'%' : '–',
|
||||||
'label' => 'SPEICHER BELEGT',
|
'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);
|
$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 = [];
|
$total = (float)($disk['total_gb'] ?? 0);
|
||||||
$active = is_int($percent) ? (int) round($this->segCount * $percent / 100) : 0;
|
if ($total <= 0) return [];
|
||||||
|
|
||||||
$activeClass = match (true) {
|
// Breakdown lesen
|
||||||
!is_int($percent) => 'bg-white/15',
|
$bd = $disk['breakdown'] ?? [];
|
||||||
$percent >= 90 => 'bg-rose-400',
|
$sys = (float)($bd['system_gb'] ?? 0);
|
||||||
$percent >= 70 => 'bg-amber-300',
|
$mails = (float)($bd['mails_gb'] ?? 0);
|
||||||
default => 'bg-emerald-400',
|
$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++) {
|
for ($i = 0; $i < $this->segCount; $i++) {
|
||||||
$angle = (360 / $this->segCount) * $i - 90;
|
$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;
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
|
||||||
|
|
@ -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 --}}
|
{{-- Kopf --}}
|
||||||
<div class="flex items-center justify-between -mb-3">
|
<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">
|
||||||
|
|
@ -12,14 +12,12 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Inhalt --}}
|
{{-- Inhalt --}}
|
||||||
<div class="grid grid-cols-1 items-center">
|
<div class="grid grid-cols-1 items-center gap-4">
|
||||||
{{-- Donut --}}
|
{{-- 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;">
|
<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 --}}
|
|
||||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
<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">
|
<div class="text-2xl md:text-3xl font-semibold leading-none tracking-tight">
|
||||||
{{ $diskCenterText['percent'] }}
|
{{ $diskCenterText['percent'] }}
|
||||||
|
|
@ -27,41 +25,131 @@
|
||||||
<div class="text-[10px] md:text-[11px] text-white/60 mt-1 uppercase tracking-wide">
|
<div class="text-[10px] md:text-[11px] text-white/60 mt-1 uppercase tracking-wide">
|
||||||
{{ $diskCenterText['label'] }}
|
{{ $diskCenterText['label'] }}
|
||||||
</div>
|
</div>
|
||||||
@if($measuredAt)
|
|
||||||
<div class="absolute bottom-12 mt-2 text-[10px] text-white/45 text-center">
|
{{-- @if($measuredAt)--}}
|
||||||
zuletzt aktualisiert: <br> {{ \Carbon\Carbon::parse($measuredAt)->diffForHumans() }}
|
{{-- <div class="absolute bottom-12 mt-2 text-[10px] text-white/45 text-center">--}}
|
||||||
</div>
|
{{-- zuletzt aktualisiert:<br>{{ \Carbon\Carbon::parse($measuredAt)->diffForHumans() }}--}}
|
||||||
@endif
|
{{-- </div>--}}
|
||||||
|
{{-- @endif--}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Segment-Ring --}}
|
|
||||||
@foreach($diskSegments as $seg)
|
@foreach($diskSegments as $seg)
|
||||||
<span class="absolute top-1/2 left-1/2 block"
|
<span class="absolute top-1/2 left-1/2 block"
|
||||||
style="
|
style="transform: rotate({{ $seg['angle'] }}deg) translateX({{ $diskSegOuterRadius + 14 }}px);
|
||||||
transform: rotate({{ $seg['angle'] }}deg) translateX({{ $diskSegOuterRadius + 14 }}px);
|
width: 12px; height: 3px; margin:-3px 0 0 -6px;">
|
||||||
width: 12px; height: 6px; margin:-3px 0 0 -6px;">
|
|
||||||
<span class="block w-full h-full rounded-full {{ $seg['class'] }}"></span>
|
<span class="block w-full h-full rounded-full {{ $seg['class'] }}"></span>
|
||||||
</span>
|
</span>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Zahlen --}}
|
{{-- Zahlen + Stacked Bar --}}
|
||||||
<div class="md:pl-2">
|
<div class="md:pl-2 space-y-4">
|
||||||
<dl class="space-y-2">
|
{{-- Stacked-Bar (horizontale Leiste) --}}
|
||||||
<div class="flex items-center justify-between">
|
@if(!empty($barSegments))
|
||||||
<dt class="text-white/60 text-sm">Gesamt</dt>
|
<div class="space-y-2">
|
||||||
<dd class="font-medium tabular-nums text-base">{{ is_numeric($diskTotalGb) ? $diskTotalGb.' GB' : '–' }}</dd>
|
{{-- <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>
|
</div>
|
||||||
<div class="flex items-center justify-between">
|
@endforeach
|
||||||
<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>
|
||||||
<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>
|
</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>
|
||||||
</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>--}}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue