diff --git a/src/new/composer.json b/src/new/composer.json index 1d1b5c1d1..ef0486ed2 100755 --- a/src/new/composer.json +++ b/src/new/composer.json @@ -129,7 +129,6 @@ "symfony/stopwatch": "*", "symfony/web-profiler-bundle": "*", "symplify/config-transformer": "^11.1", - "tomasvotruba/symfony-config-generator": "^0.1.5", "vincentlanglet/twig-cs-fixer": "^3.5" }, "config": { diff --git a/src/new/src/PSC/Backend/DashboardBundle/Controller/DashboardController.php b/src/new/src/PSC/Backend/DashboardBundle/Controller/DashboardController.php index df2ef8061..9f2e77776 100755 --- a/src/new/src/PSC/Backend/DashboardBundle/Controller/DashboardController.php +++ b/src/new/src/PSC/Backend/DashboardBundle/Controller/DashboardController.php @@ -17,6 +17,7 @@ use Doctrine\ORM\EntityManagerInterface; use PSC\Shop\ContactBundle\Repository\ContactRepository; use PSC\Shop\OrderBundle\Service\Order; use PSC\Shop\QueueBundle\Service\Queue\Manager; +use PSC\System\SettingsBundle\Service\DiskUsage; use PSC\System\SettingsBundle\Service\Instance; use PSC\System\SettingsBundle\Service\Shop; use PSC\System\UpdateBundle\Service\Migration; @@ -59,6 +60,7 @@ class DashboardController extends AbstractController ChartBuilderInterface $chartBuilder, ContactRepository $contactRepository, Order $orderService, + DiskUsage $diskUsage, ) { // Muss vor dem ersten Laden des Shops geprüft werden: ausstehende // Migrationen können Spalten ergänzen, die das Shop-Entity bereits mappt @@ -170,6 +172,7 @@ class DashboardController extends AbstractController 'queueErrorCount' => $queueService->getErrorJobCount(), 'instance' => $instanceService->getInstance(), 'chart' => $chart, + 'diskUsage' => $this->isGranted('ROLE_ADMIN') ? $diskUsage->getUsage() : null, ]; } } diff --git a/src/new/src/PSC/Backend/DashboardBundle/Resources/views/dashboard/index.html.twig b/src/new/src/PSC/Backend/DashboardBundle/Resources/views/dashboard/index.html.twig index 14ff99678..1efdf8e17 100755 --- a/src/new/src/PSC/Backend/DashboardBundle/Resources/views/dashboard/index.html.twig +++ b/src/new/src/PSC/Backend/DashboardBundle/Resources/views/dashboard/index.html.twig @@ -8,6 +8,7 @@ {% block body %}
{% if is_granted('ROLE_ADMIN') %} +
@@ -28,23 +29,124 @@
-
-
-
-

+
+
+

Warnhinweise

+
- + + + + + +
Aktionen:

{% if queueErrorCount > 0 %}{{ queueErrorCount }}{% else %}Alles Ok{% endif %}

Mailserver:

{% if not instance.isSmtpOwn %}Kein eigener definiert{% else %}{% if instance.smtpHost == '' or instance.smtpPort == '' or instance.smtpUsername == '' or instance.smtpPassword == '' %}Smtp Einstellungen überprüfen{% else %}Alles Ok{% endif %}{% endif %}

- - - - - -
Aktionen:

{% if queueErrorCount > 0 %}{{ queueErrorCount }}{% else %}Alles Ok{% endif %}

Mailserver:

{% if not instance.isSmtpOwn %}Kein eigener definiert{% else %}{% if instance.smtpHost == '' or instance.smtpPort == '' or instance.smtpUsername == '' or instance.smtpPassword == '' %}Smtp Einstellungen überprüfen{% else %}Alles Ok{% endif %}{% endif %}

+ {% if diskUsage %} +
+
+

+ Speicherverbrauch +

+ +
+ + {# Obere Liste wird gegen das Daten-Volume (/dev/sdb) abgeglichen. + Fallback, falls der (evtl. ältere) Cache diese Keys nicht enthält. #} + {% set dataDisk = diskUsage.dataDisk|default({ 'total': null, 'free': null, 'used': null }) %} + {% set items = [{ label: 'Datenbank', size: diskUsage.database }] %} + {% for label, size in diskUsage.directories %} + {% set items = items|merge([{ label: label, size: size }]) %} + {% endfor %} + {% set colors = ['bg-psc-500', 'bg-sky-500', 'bg-emerald-500', 'bg-amber-500', 'bg-violet-500', 'bg-rose-500'] %} + {# Übrige Belegung = belegter Speicher des Daten-Volumes außerhalb der überwachten Verzeichnisse #} + {% if dataDisk.total %} + {% set trackedSize = 0 %} + {% for item in items %}{% if item.size is not null %}{% set trackedSize = trackedSize + item.size %}{% endif %}{% endfor %} + {% set otherUsed = dataDisk.used - trackedSize %} + {% if otherUsed < 0 %}{% set otherUsed = 0 %}{% endif %} + {% set items = items|merge([{ label: 'Übrige Belegung', size: otherUsed, color: 'bg-gray-400 dark:bg-gray-500' }]) %} + {% endif %} + {% set totalSize = 0 %} + {% for item in items %} + {% if item.size is not null %}{% set totalSize = totalSize + item.size %}{% endif %} + {% endfor %} + +
+ {% for item in items %} + {% set percent = (totalSize > 0 and item.size is not null) ? (item.size / totalSize * 100) : 0 %} +
+
+ {{ item.label }} + + {% if item.size is not null %}{{ item.size|readable_filesize }}({{ percent < 1 ? percent|round(2) : percent|round(1) }}%){% else %}n/a{% endif %} + +
+
+
+
+
+ {% endfor %} + + {# Dateisysteme: System (Overlay) und Data (Volume) jeweils mit Icon, Auslastungsbalken und Belegt/Frei/Gesamt #} + {% set systemDisk = diskUsage.systemDisk|default({ 'total': null, 'free': null, 'used': null }) %} + {% if systemDisk.total %} +
+
+
+ + + +
+
+
+ System (Overlay) + {{ systemDisk.used|readable_filesize }} / {{ systemDisk.total|readable_filesize }} +
+
+
+
+
+ Belegt {{ systemDisk.used|readable_filesize }} ({{ (systemDisk.used / systemDisk.total * 100)|round(1) }}%) + Frei {{ systemDisk.free|readable_filesize }} +
+
+
+
+ {% endif %} + {% set dataDisk = diskUsage.dataDisk|default({ 'total': null, 'free': null, 'used': null }) %} + {% if dataDisk.total %} +
+
+
+ + + +
+
+
+ Data (Volume) + {{ dataDisk.used|readable_filesize }} / {{ dataDisk.total|readable_filesize }} +
+
+
+
+
+ Belegt {{ dataDisk.used|readable_filesize }} ({{ (dataDisk.used / dataDisk.total * 100)|round(1) }}%) + Frei {{ dataDisk.free|readable_filesize }} +
+
+
+
+ {% endif %} +
+
+ {% endif %} +
diff --git a/src/new/src/PSC/System/SettingsBundle/Resources/config/services.yml b/src/new/src/PSC/System/SettingsBundle/Resources/config/services.yml index 3091e6abe..c3dbbac68 100755 --- a/src/new/src/PSC/System/SettingsBundle/Resources/config/services.yml +++ b/src/new/src/PSC/System/SettingsBundle/Resources/config/services.yml @@ -8,6 +8,19 @@ services: PSC\System\SettingsBundle\: resource: '../../*/*' + PSC\System\SettingsBundle\Service\DiskUsage: + arguments: + $directories: + 'Uploads (Data Volume)': '/data/www/old/public/uploads' + 'Pakete (Data Volume)': '/data/www/old/data/packages' + 'Templateprint Layouter (Data Volume)': '/data/www/old/market/templateprint' + 'Form Based Layouter (Data Volume)': '/data/www/old/market/collectlayouter' + 'Creative Layouter (Data Volume)': '/data/www/old/market/steplayouter' + # Daten-Volume (/dev/sdb): Bezug für die obere Liste inkl. "Übrige Belegung" + $dataPath: '/data/www/new/watch' + # System-/Overlay-Dateisystem: Bezug für Belegt/Frei/Gesamt unten + $systemPath: '/' + PSC\System\SettingsBundle\Form\Backend\CopyType: tags: - { name: form.type } @@ -64,4 +77,4 @@ services: PSC\System\SettingsBundle\Resolver\AbsolutePathResolver: tags: - - { name: liip_imagine.cache.resolver, resolver: absolutePathResolver } \ No newline at end of file + - { name: liip_imagine.cache.resolver, resolver: absolutePathResolver } diff --git a/src/new/src/PSC/System/SettingsBundle/Service/DiskUsage.php b/src/new/src/PSC/System/SettingsBundle/Service/DiskUsage.php new file mode 100644 index 000000000..ffe2d27fd --- /dev/null +++ b/src/new/src/PSC/System/SettingsBundle/Service/DiskUsage.php @@ -0,0 +1,144 @@ + $directories Label => absoluter Pfad + * @param string $dataPath Pfad auf dem Daten-Volume (z.B. /dev/sdb), gegen das + * die überwachten Verzeichnisse abgeglichen werden + * @param string $systemPath Pfad auf dem System-/Overlay-Dateisystem + */ + public function __construct( + private Connection $connection, + private CacheInterface $cache, + private array $directories, + private string $dataPath = '/data/www/old', + private string $systemPath = '/', + private int $cacheTtl = 3600, + ) { + } + + /** + * @return array{ + * database: ?int, + * directories: array, + * dataDisk: array{total: ?int, free: ?int, used: ?int}, + * systemDisk: array{total: ?int, free: ?int, used: ?int} + * } + */ + public function getUsage(): array + { + // Versionssuffix: ändert sich die Struktur, werden alte Cache-Einträge ignoriert + return $this->cache->get('psc_dashboard_disk_usage_v2', function (ItemInterface $item): array { + $item->expiresAfter($this->cacheTtl); + + return [ + 'database' => $this->getDatabaseSize(), + 'directories' => $this->getDirectorySizes(), + 'dataDisk' => $this->getDiskSpace($this->dataPath), + 'systemDisk' => $this->getDiskSpace($this->systemPath), + ]; + }); + } + + /** + * Gesamtkapazität, freier und belegter Speicher des Dateisystems am + * angegebenen Pfad in Bytes. + * + * @return array{total: ?int, free: ?int, used: ?int} + */ + private function getDiskSpace(string $path): array + { + if (!is_dir($path)) { + return ['total' => null, 'free' => null, 'used' => null]; + } + + $total = @disk_total_space($path); + $free = @disk_free_space($path); + + if ($total === false || $free === false) { + return ['total' => null, 'free' => null, 'used' => null]; + } + + return [ + 'total' => (int) $total, + 'free' => (int) $free, + 'used' => (int) ($total - $free), + ]; + } + + /** + * Größe der aktuellen Datenbank in Bytes (Daten + Indizes). + */ + private function getDatabaseSize(): ?int + { + try { + $database = $this->connection->getDatabase(); + + if ($database === null) { + return null; + } + + $size = $this->connection->fetchOne( + 'SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema = :db', + ['db' => $database], + ); + + return $size !== false && $size !== null ? (int) $size : null; + } catch (\Throwable) { + return null; + } + } + + /** + * @return array Label => Größe in Bytes (oder null) + */ + private function getDirectorySizes(): array + { + $result = []; + + foreach ($this->directories as $label => $path) { + $result[$label] = $this->getDirectorySize($path); + } + + return $result; + } + + /** + * Größe eines Verzeichnisses in Bytes via "du" (schnell). Liefert null, + * wenn das Verzeichnis fehlt oder nicht ermittelt werden kann. + */ + private function getDirectorySize(string $path): ?int + { + if (!is_dir($path)) { + return null; + } + + $output = []; + $exitCode = 0; + @exec('du -sb ' . escapeshellarg($path) . ' 2>/dev/null', $output, $exitCode); + + if ($exitCode === 0 && isset($output[0])) { + $parts = preg_split('/\s+/', trim($output[0])); + + if (isset($parts[0]) && is_numeric($parts[0])) { + return (int) $parts[0]; + } + } + + return null; + } +} diff --git a/src/new/var/plugins/Custom/Ahrweiler/Harry/Controller/Backend/StartController.php b/src/new/var/plugins/Custom/Ahrweiler/Harry/Controller/Backend/StartController.php index 5fee781af..af94e1542 100644 --- a/src/new/var/plugins/Custom/Ahrweiler/Harry/Controller/Backend/StartController.php +++ b/src/new/var/plugins/Custom/Ahrweiler/Harry/Controller/Backend/StartController.php @@ -197,7 +197,7 @@ class StartController extends AbstractController $docData = []; if ($contactEntity->getAbteilung() != '') { $docData[] = [ - 'name' => 'data[grad][enable]', + 'name' => 'data[betrieb][enable]', 'value' => '1', ]; } diff --git a/src/new/var/tailwind/backend.built.css b/src/new/var/tailwind/backend.built.css index ae6cf8351..9f1319538 100644 --- a/src/new/var/tailwind/backend.built.css +++ b/src/new/var/tailwind/backend.built.css @@ -1587,6 +1587,10 @@ html { grid-template-columns: repeat(2, minmax(0, 1fr)); } +.grid-cols-3{ + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + .grid-cols-4{ grid-template-columns: repeat(4, minmax(0, 1fr)); } @@ -1974,6 +1978,11 @@ html { border-color: rgb(250 204 21 / var(--tw-border-opacity)); } +.bg-amber-500{ + --tw-bg-opacity: 1; + background-color: rgb(245 158 11 / var(--tw-bg-opacity)); +} + .bg-black\/70{ background-color: rgb(0 0 0 / 0.7); } @@ -2003,6 +2012,11 @@ html { background-color: rgb(29 78 216 / var(--tw-bg-opacity)); } +.bg-emerald-500{ + --tw-bg-opacity: 1; + background-color: rgb(16 185 129 / var(--tw-bg-opacity)); +} + .bg-gray-100{ --tw-bg-opacity: 1; background-color: rgb(243 244 246 / var(--tw-bg-opacity)); @@ -2110,6 +2124,16 @@ html { background-color: rgb(220 38 38 / var(--tw-bg-opacity)); } +.bg-rose-500{ + --tw-bg-opacity: 1; + background-color: rgb(244 63 94 / var(--tw-bg-opacity)); +} + +.bg-sky-500{ + --tw-bg-opacity: 1; + background-color: rgb(14 165 233 / var(--tw-bg-opacity)); +} + .bg-slate-100{ --tw-bg-opacity: 1; background-color: rgb(241 245 249 / var(--tw-bg-opacity)); @@ -2125,6 +2149,11 @@ html { background-color: rgb(245 245 244 / var(--tw-bg-opacity)); } +.bg-violet-500{ + --tw-bg-opacity: 1; + background-color: rgb(139 92 246 / var(--tw-bg-opacity)); +} + .bg-white{ --tw-bg-opacity: 1; background-color: rgb(255 255 255 / var(--tw-bg-opacity)); @@ -2312,10 +2341,18 @@ html { padding-right: 1rem; } +.pt-1{ + padding-top: 0.25rem; +} + .pt-2{ padding-top: 0.5rem; } +.pt-3{ + padding-top: 0.75rem; +} + .text-left{ text-align: left; } @@ -2453,6 +2490,11 @@ html { color: rgb(30 64 175 / var(--tw-text-opacity)); } +.text-emerald-600{ + --tw-text-opacity: 1; + color: rgb(5 150 105 / var(--tw-text-opacity)); +} + .text-gray-400{ --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); @@ -2777,6 +2819,10 @@ html { transition-duration: 300ms; } +.duration-500{ + transition-duration: 500ms; +} + .duration-75{ transition-duration: 75ms; } @@ -4511,6 +4557,11 @@ html { background-color: rgb(30 58 138 / 0.2); } +:is(.dark .dark\:bg-gray-500){ + --tw-bg-opacity: 1; + background-color: rgb(107 114 128 / var(--tw-bg-opacity)); +} + :is(.dark .dark\:bg-gray-600){ --tw-bg-opacity: 1; background-color: rgb(75 85 99 / var(--tw-bg-opacity)); @@ -4581,6 +4632,11 @@ html { color: rgb(59 130 246 / var(--tw-text-opacity)); } +:is(.dark .dark\:text-emerald-400){ + --tw-text-opacity: 1; + color: rgb(52 211 153 / var(--tw-text-opacity)); +} + :is(.dark .dark\:text-gray-100){ --tw-text-opacity: 1; color: rgb(243 244 246 / var(--tw-text-opacity)); @@ -4968,6 +5024,12 @@ html { gap: 2rem; } + .lg\:space-y-8 > :not([hidden]) ~ :not([hidden]){ + --tw-space-y-reverse: 0; + margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(2rem * var(--tw-space-y-reverse)); + } + .lg\:border-r{ border-right-width: 1px; } diff --git a/src/new/version.yaml b/src/new/version.yaml index 9b712381f..54b5fe56e 100755 --- a/src/new/version.yaml +++ b/src/new/version.yaml @@ -1,11 +1,12 @@ info: - datum: 02.06.2026 + datum: 10.06.2026 release: 2.3.6 changelog: - version: 2.3.6 - datum: 02.06.2026 + datum: 10.06.2026 changes: + - "Verbrauchsanzeige im Dashboard" - "Shop kann eigene SMTP Einstellungen haben" - "Passwort Start und Finish Aktion Absender und Empfänger Bug behoben" - "Form Based Layouter speichert jetzt die Firma vom angemeldeten Benutzer"