dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
dbxDashboard.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxAdmin;
3
4dbx()->use_system_class('dbxReport');
5dbx()->use_system_class('dbxForm');
6dbx()->use_system_class('dbxDD');
7
8class dbxDashboard extends \dbxObj {
9
10 private const HISTORY_DD = 'dbxAdmin|dbxAdminDashboardMetric';
11 private const PERF_REQUEST_DD = 'dbx|dbxPerformanceRequest';
12 private const PERF_TIMER_DD = 'dbx|dbxPerformanceTimer';
13 private const HISTORY_BUCKET_MINUTES = 15;
14
15 private $metricCache = array();
16 private $historyReady = null;
17 private $performanceRequestAverage = null;
18 private $performanceTimerAverages = null;
19 private $performanceModuleAverages = null;
20 private $dashboardMessageKey = '';
21 private $dashboardMessageError = false;
22 private $updateStatusCache = null;
23
27 private function default_admin_password_warning(): string {
28 try {
29 $db = dbx()->get_system_obj('dbxDB');
30 if (!is_object($db)) {
31 return '';
32 }
33 $admin = $db->select1(
34 'dbx|dbxUser',
35 array('uname' => 'admin'),
36 array('id', 'pass'),
37 0
38 );
39 } catch (\Throwable $exception) {
40 return '';
41 }
42
43 return $this->default_admin_password_warning_html(
44 (int)($admin['id'] ?? 0),
45 (string)($admin['pass'] ?? '')
46 );
47 }
48
49 private function default_admin_password_warning_html(
50 int $adminId,
51 string $hash
52 ): string {
53 if ($adminId <= 0 || $hash === '' || !password_verify('123456', $hash)) {
54 return '';
55 }
56
57 $url = '?dbx_modul=dbxAdmin&amp;dbx_run1=user'
58 . '&amp;dbx_run2=edit_user&amp;rid=' . $adminId
59 . '&amp;dbx_page=admin';
60 return '<div class="alert alert-warning dbx-admin-dashboard-password-warning" role="alert">'
61 . '<div class="dbx-admin-dashboard-password-warning-icon">'
62 . '<i class="bi bi-shield-exclamation" aria-hidden="true"></i></div>'
63 . '<div class="dbx-admin-dashboard-password-warning-copy">'
64 . '<strong>Unsicheres Installationspasswort aktiv</strong>'
65 . '<span>Der Administrator verwendet noch den öffentlichen Standardzugang '
66 . '<code>admin / 123456</code>. Ändern Sie das Passwort jetzt.</span></div>'
67 . '<a class="btn btn-warning fw-semibold" href="' . $url . '">'
68 . '<i class="bi bi-key me-1" aria-hidden="true"></i>Passwort ändern</a>'
69 . '</div>';
70 }
71
72 private function fmt($value) {
73 $value = (int) $value;
74 return number_format($value, 0, ',', '.');
75 }
76
77 private function percent($value, $max) {
78 $value = (float) $value;
79 $max = (float) $max;
80
81 if ($max <= 0) {
82 return 0;
83 }
84
85 return max(0, min(100, (int) round(($value / $max) * 100)));
86 }
87
88 private function health_reason_label(int $inventoryCount, int $existingCount, int $sysmsgRisk, int $missing): string {
89 $reasons = array();
90
91 if ($missing > 0) {
92 $reasons[] = 'Missing';
93 }
94
95 if ($sysmsgRisk > 0) {
96 $reasons[] = 'SysMsg';
97 }
98
99 if ($existingCount < $inventoryCount) {
100 $reasons[] = 'DB';
101 }
102
103 return count($reasons) ? implode('/', $reasons) : 'OK';
104 }
105
112 private function process_error_log_action(): void {
113 $work = dbx()->get_modul_var('dbx_do', '', 'parameter');
114 if ($work !== 'delete_error_log') {
115 return;
116 }
117
118 $sysMsg = dbx()->get_include_obj('dbxSysMsg');
119 $result = $sysMsg->delete_error_log();
120
121 if ($result === 'deleted') {
122 $this->dashboardMessageKey = 'error_log_deleted';
123 } elseif ($result === 'empty') {
124 $this->dashboardMessageKey = 'error_log_empty';
125 } else {
126 $this->dashboardMessageKey = 'error_log_delete_error';
127 $this->dashboardMessageError = true;
128 }
129
130 $this->metricCache = array();
131 }
132
139 private function error_log_panel(\dbxForm $texts): string {
140 $sysMsg = dbx()->get_include_obj('dbxSysMsg');
141 if (!$sysMsg->error_log_exists()) {
142 return '';
143 }
144
145 $file = $sysMsg->get_error_log_file();
146 $content = @file_get_contents($file);
147 if ($content === false) {
148 $content = $texts->get_fd_message('error_log_read_error');
149 }
150
151 clearstatcache(true, $file);
152 $size = @filesize($file);
153 $size = $size === false ? strlen($content) : (int)$size;
154
155 $action = dbx()->action_url(
156 '?dbx_modul=dbxAdmin&dbx_run1=dashboard&dbx_run2=delete_error_log'
157 . '&dbx_do=delete_error_log&rid=error_log'
158 );
159
160 return dbx()->get_system_obj('dbxTPL')->get_tpl(
161 'dbxAdmin|admin-dashboard-error-log',
162 array(
163 'error_log_title' => dbx()->esc($texts->get_fd_message('error_log_title')),
164 'error_log_subtitle' => dbx()->esc($texts->get_fd_message('error_log_subtitle')),
165 'file_label' => dbx()->esc($texts->get_fd_message('error_log_file_label')),
166 'file' => 'files/dbxError.log',
167 'size' => $this->fmt($size),
168 'bytes_label' => dbx()->esc($texts->get_fd_message('error_log_bytes')),
169 'content' => htmlspecialchars(
170 $content,
171 ENT_QUOTES | ENT_SUBSTITUTE,
172 'UTF-8'
173 ),
174 'delete_action' => dbx()->esc($action),
175 'delete_label' => dbx()->esc($texts->get_fd_message('error_log_delete_label')),
176 'delete_title' => dbx()->esc($texts->get_fd_message('error_log_delete_title')),
177 'delete_confirm' => dbx()->esc($texts->get_fd_message('error_log_delete_confirm')),
178 'delete_hint' => dbx()->esc($texts->get_fd_message('error_log_delete_hint')),
179 )
180 );
181 }
182
193 private function update_status(): array {
194 if (is_array($this->updateStatusCache)) {
195 return $this->updateStatusCache;
196 }
197
198 try {
199 dbx()->get_include_obj('dbxUpdateService', 'dbxAdmin');
200 $this->updateStatusCache = dbxUpdateService::configured()->status();
201 $this->updateStatusCache['status_available'] = true;
202 } catch (\Throwable $exception) {
203 $this->updateStatusCache = array(
204 'current_version' => '',
205 'available_version' => '',
206 'update_available' => false,
207 'checked_at' => '',
208 'staged_version' => '',
209 'stop_available' => false,
210 'status_available' => false,
211 );
212 }
213
214 return $this->updateStatusCache;
215 }
216
223 private function update_state(array $status): array {
224 $current = (string)($status['current_version'] ?? '');
225 $staged = (string)($status['staged_version'] ?? '');
226 $ready = $staged !== ''
227 && ($current === '' || version_compare($staged, $current, '>'));
228
229 if ($ready) {
230 return array(
231 'class' => 'ready',
232 'icon' => 'bi-shield-check',
233 'status_message' => 'update_status_ready',
234 'action_message' => 'update_action_ready',
235 'nav_message' => 'update_nav_ready',
236 'action_class' => 'btn-primary',
237 );
238 }
239
240 if (!empty($status['update_available'])) {
241 return array(
242 'class' => 'available',
243 'icon' => 'bi-cloud-arrow-down-fill',
244 'status_message' => 'update_status_available',
245 'action_message' => 'update_action_available',
246 'nav_message' => 'update_nav_available',
247 'action_class' => 'btn-warning',
248 );
249 }
250
251 if (!empty($status['status_available'])
252 && (string)($status['checked_at'] ?? '') !== '') {
253 return array(
254 'class' => 'current',
255 'icon' => 'bi-check-circle-fill',
256 'status_message' => 'update_status_current',
257 'action_message' => 'update_action_current',
258 'nav_message' => 'update_nav_current',
259 'action_class' => 'btn-outline-primary',
260 );
261 }
262
263 return array(
264 'class' => 'unknown',
265 'icon' => 'bi-question-circle-fill',
266 'status_message' => 'update_status_unknown',
267 'action_message' => 'update_action_unknown',
268 'nav_message' => 'update_nav_unknown',
269 'action_class' => 'btn-outline-primary',
270 );
271 }
272
276 private function update_status_panel(\dbxForm $texts): string {
277 $status = $this->update_status();
278 $state = $this->update_state($status);
279 $current = trim((string)($status['current_version'] ?? ''));
280 $available = trim((string)($status['available_version'] ?? ''));
281 $staged = trim((string)($status['staged_version'] ?? ''));
282
283 if ($state['class'] === 'ready') {
284 $statusText = $texts->format_fd_message(
285 $state['status_message'],
286 array('version' => $staged)
287 );
288 } elseif ($state['class'] === 'available') {
289 $statusText = $texts->format_fd_message(
290 $state['status_message'],
291 array('version' => $available)
292 );
293 } else {
294 $statusText = $texts->get_fd_message($state['status_message']);
295 }
296
297 $versionText = $available !== ''
298 ? $texts->format_fd_message(
299 'update_versions',
300 array('current' => $current, 'available' => $available)
301 )
302 : $texts->format_fd_message(
303 'update_version_current',
304 array('current' => $current !== '' ? $current : '–')
305 );
306 $checkedAt = strtotime((string)($status['checked_at'] ?? ''));
307 $checkedText = $checkedAt
308 ? $texts->format_fd_message(
309 'update_checked',
310 array('date' => date('d.m.Y H:i', $checkedAt))
311 )
312 : $texts->get_fd_message('update_not_checked');
313
314 return dbx()->get_system_obj('dbxTPL')->get_tpl(
315 'dbxAdmin|admin-dashboard-update-status',
316 array(
317 'update_title' => dbx()->esc($texts->get_fd_message('update_title')),
318 'state_class' => dbx()->esc($state['class']),
319 'state_icon' => dbx()->esc($state['icon']),
320 'status_text' => dbx()->esc($statusText),
321 'version_text' => dbx()->esc($versionText),
322 'checked_text' => dbx()->esc($checkedText),
323 'action_class' => dbx()->esc($state['action_class']),
324 'action_label' => dbx()->esc(
325 $texts->get_fd_message($state['action_message'])
326 ),
327 )
328 );
329 }
330
331 private function request_runtime_ms() {
332 $runtime = dbx()->get_system_obj('dbxRuntime');
333 return max(0, (int)round($runtime->current_php_runtime() * 1000));
334 }
335
336 private function memory_peak_kb() {
337 $bytes = (int)dbx()->get_system_obj('dbxRuntime')->current_memory_bytes();
338
339 if ($bytes <= 0) {
340 global $dbx_run_timer;
341 $startMemory = isset($dbx_run_timer['system']['start_memory']) && is_numeric($dbx_run_timer['system']['start_memory'])
342 ? (int) $dbx_run_timer['system']['start_memory']
343 : 0;
344
345 if ($startMemory > 0) {
346 $bytes = max(0, (int) memory_get_peak_usage() - $startMemory);
347 }
348 }
349
350 if ($bytes <= 0) {
351 $bytes = (int) memory_get_usage();
352 }
353
354 return max(1, (int) ceil($bytes / 1024));
355 }
356
357 private function card_action($href, $label) {
358 $tpl = dbx()->get_system_obj('dbxTPL');
359
360 return $tpl->get_tpl('dbxAdmin|admin-dashboard-card-action', array(
361 'href' => $href,
362 'label' => $label,
363 ));
364 }
365
366 private function collapse_action($target, $label = 'Aufklappen', $expanded = false) {
367 $tpl = dbx()->get_system_obj('dbxTPL');
368
369 return $tpl->get_tpl('dbxAdmin|admin-dashboard-collapse-action', array(
370 'target' => $target,
371 'label' => $label,
372 'expanded' => $expanded ? 'true' : 'false',
373 ));
374 }
375
376 private function help_action(string $topic): string {
377 try {
378 $help = dbx()->get_include_obj('dbxAdminHelp', 'dbxAdmin');
379 return (string) $help->button($topic);
380 } catch (\Throwable $e) {
381 return '';
382 }
383 }
384
385 private function card_bar_data($title, $icon, $subtitle = '', $action = '') {
386 return array(
387 'bar_class' => 'dbx-module-bar',
388 'bar_title_class' => 'dbx-module-bar-titleblock',
389 'bar_actions_class' => 'dbx-module-bar-actions',
390 'bar_title' => $title,
391 'bar_icon' => $icon,
392 'bar_subtitle' => $subtitle,
393 'bar_title_pre' => '',
394 'bar_title_heading_attrs' => '',
395 'bar_actions' => $action,
396 'bar_extra' => '',
397 'title' => $title,
398 'icon' => $icon,
399 'subtitle' => $subtitle,
400 'action' => $action,
401 );
402 }
403
404 private function card_bar($title, $icon, $subtitle = '', $action = '') {
405 $tpl = dbx()->get_system_obj('dbxTPL');
406
407 return $tpl->get_tpl('dbx|component-bar', $this->card_bar_data($title, $icon, $subtitle, $action));
408 }
409
410 private function safe_count($dd, $where = '') {
411 $count = 0;
412
413 try {
414 $db = dbx()->get_system_obj('dbxDB');
415 $res = $db->count($dd, $where);
416 if (is_numeric($res) && (int) $res > 0) {
417 $count = (int) $res;
418 }
419 } catch (\Throwable $e) {
420 $count = 0;
421 }
422
423 return $count;
424 }
425
426 private function safe_select($dd, $where = '', $columns = '*', $orderby = '', $asc_desc = 'ASC', $groupby = '', $max = 0, $offset = 0) {
427 $rows = array();
428
429 try {
430 $db = dbx()->get_system_obj('dbxDB');
431 $res = $db->select($dd, $where, $columns, $orderby, $asc_desc, $groupby, $max, $offset, 0);
432 if (is_array($res)) {
433 $rows = $res;
434 }
435 } catch (\Throwable $e) {
436 $rows = array();
437 }
438
439 return $rows;
440 }
441
442 private function ensure_performance_tables() {
443 try {
444 $dd = dbx()->get_system_obj('dbxDD');
445 return $dd->create_db_tab(self::PERF_REQUEST_DD) && $dd->create_db_tab(self::PERF_TIMER_DD);
446 } catch (\Throwable $e) {
447 return false;
448 }
449 }
450
451 private function ensure_history_table() {
452 if ($this->historyReady !== null) {
453 return $this->historyReady;
454 }
455
456 $this->historyReady = false;
457
458 try {
459 $dd = dbx()->get_system_obj('dbxDD');
460 $this->historyReady = $dd->create_db_tab(self::HISTORY_DD) ? $this->ensure_history_columns() : false;
461 } catch (\Throwable $e) {
462 $this->historyReady = false;
463 }
464
465 return $this->historyReady;
466 }
467
468 private function ensure_history_columns() {
469 $server = 'dbxAdmin|dbxAdmin.db3';
470 $table = 'dbx_admin_dashboard_metric';
471
472 try {
473 $db = dbx()->get_system_obj('dbxDB');
474 $rows = $db->select_query($server, 'PRAGMA table_info(' . $table . ')');
475
476 if (!is_array($rows)) {
477 return false;
478 }
479
480 $existing = array();
481 foreach ($rows as $row) {
482 $name = (string) ($row['name'] ?? '');
483 if ($name !== '') {
484 $existing[$name] = 1;
485 }
486 }
487
488 $fields = array(
489 'request_runtime_ms' => array('name' => 'request_runtime_ms', 'type' => 'int', 'length' => '11', 'default' => '0', 'index' => ''),
490 'memory_peak_mb' => array('name' => 'memory_peak_mb', 'type' => 'int', 'length' => '11', 'default' => '0', 'index' => ''),
491 'memory_peak_kb' => array('name' => 'memory_peak_kb', 'type' => 'int', 'length' => '11', 'default' => '0', 'index' => ''),
492 );
493
494 $dd = dbx()->get_system_obj('dbxDD');
495 foreach ($fields as $name => $field) {
496 if (!isset($existing[$name]) && !$dd->add_db_field_from_dd($server, $table, $field)) {
497 return false;
498 }
499 }
500
501 return true;
502 } catch (\Throwable $e) {
503 return false;
504 }
505 }
506
507 private function history_bucket_time() {
508 $bucketSeconds = self::HISTORY_BUCKET_MINUTES * 60;
509 return (int) (floor(time() / $bucketSeconds) * $bucketSeconds);
510 }
511
512 private function history_snapshot_record($metrics, $bucketTime) {
513 $now = date('Y-m-d H:i:s');
514 $uid = (int) dbx()->user();
515
516 return array(
517 'bucket_key' => date('YmdHi', $bucketTime),
518 'snapshot_date' => date('Y-m-d H:i:s', $bucketTime),
519 'create_date' => $now,
520 'create_uid' => $uid,
521 'update_date' => $now,
522 'update_uid' => $uid,
523 'owner' => $uid,
524 'users' => (int) ($metrics['users'] ?? 0),
525 'online' => (int) ($metrics['online'] ?? 0),
526 'modules' => (int) ($metrics['modules'] ?? 0),
527 'records' => (int) ($metrics['records'] ?? 0),
528 'databases' => (int) ($metrics['databases'] ?? 0),
529 'health_percent' => (int) ($metrics['health_percent'] ?? 0),
530 'active_users' => (int) ($metrics['active_users'] ?? 0),
531 'sessions' => (int) ($metrics['sessions'] ?? 0),
532 'tables' => (int) ($metrics['tables'] ?? 0),
533 'sysmsg_risk' => (int) ($metrics['sysmsg_risk'] ?? 0),
534 'missing' => (int) ($metrics['missing'] ?? 0),
535 'request_runtime_ms' => (int) ($metrics['request_runtime_ms'] ?? 0),
536 'memory_peak_mb' => (int) ceil(((int) ($metrics['memory_peak_kb'] ?? 0)) / 1024),
537 'memory_peak_kb' => (int) ($metrics['memory_peak_kb'] ?? 0),
538 );
539 }
540
541 private function store_history_snapshot($metrics) {
542 if (!$this->ensure_history_table()) {
543 return false;
544 }
545
546 try {
547 $db = dbx()->get_system_obj('dbxDB');
548 $bucketTime = $this->history_bucket_time();
549 $record = $this->history_snapshot_record($metrics, $bucketTime);
550 $rows = $db->select(self::HISTORY_DD, array('bucket_key' => $record['bucket_key']), array('id'), 'id', 'DESC', '', 1, 0, 0);
551 $id = is_array($rows) ? (int) ($rows[0]['id'] ?? 0) : 0;
552
553 if ($id > 0) {
554 unset($record['create_date'], $record['create_uid'], $record['owner']);
555 return $db->update(self::HISTORY_DD, $record, array('id' => $id), 0, 1, 1, 0) > 0;
556 }
557
558 return $db->insert(self::HISTORY_DD, $record, 0, 1, 1, 0) > 0;
559 } catch (\Throwable $e) {
560 return false;
561 }
562 }
563
564 private function metric_history_rows($metrics, $max = 48) {
565 if (!$this->ensure_history_table()) {
566 return array($this->history_snapshot_record($metrics, $this->history_bucket_time()));
567 }
568
569 $rows = $this->safe_select(
570 self::HISTORY_DD,
571 '',
572 array('snapshot_date', 'users', 'online', 'modules', 'records', 'databases', 'health_percent', 'request_runtime_ms', 'memory_peak_kb'),
573 'snapshot_date',
574 'DESC',
575 '',
576 $max,
577 0
578 );
579
580 if (!$rows) {
581 return array($this->history_snapshot_record($metrics, $this->history_bucket_time()));
582 }
583
584 return array_reverse($rows);
585 }
586
587 private function metric_series_definitions() {
588 return array(
589 'users' => array('label' => 'Benutzer', 'tone' => 'teal'),
590 'online' => array('label' => 'Online', 'tone' => 'green'),
591 'modules' => array('label' => 'Module', 'tone' => 'navy'),
592 'records' => array('label' => 'Datensaetze', 'tone' => 'amber'),
593 'databases' => array('label' => 'Datenbanken', 'tone' => 'cyan'),
594 'health_percent' => array('label' => 'Systemzustand', 'tone' => 'red'),
595 'request_runtime_ms' => array('label' => 'Speed', 'tone' => 'purple'),
596 'memory_peak_kb' => array('label' => 'DBX Memory', 'tone' => 'slate'),
597 );
598 }
599
600 private function metric_history($metrics) {
601 $rows = $this->metric_history_rows($metrics);
602 $labels = array();
603 $series = array();
604
605 foreach ($this->metric_series_definitions() as $key => $def) {
606 $series[$key] = array(
607 'key' => $key,
608 'label' => $def['label'],
609 'tone' => $def['tone'],
610 'values' => array(),
611 );
612 }
613
614 foreach ($rows as $row) {
615 $time = strtotime((string) ($row['snapshot_date'] ?? ''));
616 $labels[] = $time ? date('d.m. H:i', $time) : '';
617
618 foreach ($series as $key => $def) {
619 $series[$key]['values'][] = (int) ($row[$key] ?? 0);
620 }
621 }
622
623 return array(
624 'labels' => $labels,
625 'rows' => $rows,
626 'series' => array_values($series),
627 );
628 }
629
630 private function history_values($history, $key, $current = 0) {
631 $values = array();
632
633 foreach (($history['rows'] ?? array()) as $row) {
634 $values[] = (int) ($row[$key] ?? 0);
635 }
636
637 if (!$values) {
638 $values[] = (int) $current;
639 }
640
641 if (count($values) === 1) {
642 $values[] = $values[0];
643 }
644
645 return $values;
646 }
647
648 private function spark_values($history, $key, $current = 0) {
649 return implode(',', $this->history_values($history, $key, $current));
650 }
651
652 private function trend_text($history, $key, $suffix = '') {
653 $values = $this->history_values($history, $key, 0);
654
655 if (count($values) < 2) {
656 return '1 Messpunkt';
657 }
658
659 $first = (int) reset($values);
660 $last = (int) end($values);
661 $delta = $last - $first;
662
663 if ($delta === 0) {
664 return 'unveraendert';
665 }
666
667 return ($delta > 0 ? '+' : '') . $this->fmt($delta) . $suffix . ' im Verlauf';
668 }
669
670 private function fmt_ms($value) {
671 $value = max(0, (int) $value);
672 return number_format($value / 1000, 3, ',', '.') . ' Sec';
673 }
674
675 private function fmt_ms_precision($value, $precision = 3, $minSeconds = 0) {
676 $value = max(0, (float) $value);
677 $precision = max(0, min(6, (int) $precision));
678 $seconds = $value / 1000;
679
680 if ((float) $minSeconds > 0 && $seconds < (float) $minSeconds) {
681 $seconds = (float) $minSeconds;
682 }
683
684 return number_format($seconds, $precision, ',', '.') . ' Sec';
685 }
686
687 private function fmt_memory_kb($value) {
688 $value = max(0, (int) $value);
689
690 if ($value >= 1024) {
691 return number_format($value / 1024, 1, ',', '.') . ' MB';
692 }
693
694 return $this->fmt($value) . ' KB';
695 }
696
697 private function fmt_memory_delta_kb($value) {
698 $value = max(0, (int) $value);
699 return '+' . $this->fmt_memory_kb($value);
700 }
701
702 private function performance_dd_info($dd) {
703 try {
704 $db = dbx()->get_system_obj('dbxDB');
705 $table = (string) $db->get_dd_table($dd);
706 $server = (string) $db->get_dd_server($dd);
707
708 if (!preg_match('/^[A-Za-z0-9_]+$/', $table)) {
709 return array('', '');
710 }
711
712 return array($server, $table);
713 } catch (\Throwable $e) {
714 return array('', '');
715 }
716 }
717
718 private function performance_table_count(string $dd): int {
719 try {
720 $db = dbx()->get_system_obj('dbxDB');
721 $count = $db->count($dd, '');
722 return is_numeric($count) ? max(0, (int) $count) : 0;
723 } catch (\Throwable $e) {
724 return 0;
725 }
726 }
727
728 private function performance_now_record_base(): array {
729 $uid = (int) dbx()->user();
730 $now = date('Y-m-d H:i:s');
731
732 return array(
733 'create_date' => $now,
734 'create_uid' => $uid,
735 'update_date' => $now,
736 'update_uid' => $uid,
737 'owner' => $uid,
738 );
739 }
740
741 private function optimize_performance_db(): array {
742 $result = array('ok' => true, 'messages' => array());
743 $db = dbx()->get_system_obj('dbxDB');
744 $groups = array();
745
746 foreach (array(self::PERF_REQUEST_DD, self::PERF_TIMER_DD) as $dd) {
747 list($server, $table) = $this->performance_dd_info($dd);
748 if ($server === '' || $table === '') {
749 continue;
750 }
752 }
753
754 foreach ($groups as $server => $tables) {
755 $tables = array_values(array_unique($tables));
756 $type = strtolower((string) $db->get_db_type($server));
757
758 if ($type === 'mysql') {
759 $quoted = array();
760 foreach ($tables as $table) {
761 $quoted[] = '`' . str_replace('`', '``', $table) . '`';
762 }
763 $ok = $db->exec($server, 'OPTIMIZE TABLE ' . implode(', ', $quoted));
764 $result['ok'] = $result['ok'] && (bool) $ok;
765 $result['messages'][] = $ok ? 'MySQL OPTIMIZE TABLE ausgefuehrt.' : 'MySQL OPTIMIZE TABLE fehlgeschlagen.';
766 continue;
767 }
768
769 if ($type === 'sqlite') {
770 $vacuum = $db->exec($server, 'VACUUM');
771 $analyze = $db->exec($server, 'ANALYZE');
772 $result['ok'] = $result['ok'] && (bool) $vacuum;
773 $result['messages'][] = $vacuum ? 'SQLite VACUUM ausgefuehrt.' : 'SQLite VACUUM fehlgeschlagen.';
774 if ($analyze) {
775 $result['messages'][] = 'SQLite ANALYZE ausgefuehrt.';
776 }
777 continue;
778 }
779
780 $result['messages'][] = 'Keine Optimierung fuer DB-Typ ' . $type . ' definiert.';
781 }
782
783 return $result;
784 }
785
786 private function compress_performance_db(): array {
787 $stats = array(
788 'request_before' => $this->performance_table_count(self::PERF_REQUEST_DD),
789 'timer_before' => $this->performance_table_count(self::PERF_TIMER_DD),
790 'request_after' => 0,
791 'timer_after' => 0,
792 'inserted' => 0,
793 'ok' => false,
794 'messages' => array(),
795 );
796
797 if (!$this->ensure_performance_tables()) {
798 $stats['messages'][] = 'Performance-Tabellen konnten nicht vorbereitet werden.';
799 return $stats;
800 }
801
802 list($requestServer, $requestTable) = $this->performance_dd_info(self::PERF_REQUEST_DD);
803 list($timerServer, $timerTable) = $this->performance_dd_info(self::PERF_TIMER_DD);
804 if ($requestServer === '' || $requestTable === '' || $timerServer === '' || $timerTable === '') {
805 $stats['messages'][] = 'Performance-Tabellen konnten nicht ermittelt werden.';
806 return $stats;
807 }
808
809 $db = dbx()->get_system_obj('dbxDB');
810 $requestRows = $db->select_query($requestServer, "SELECT COUNT(*) AS row_count, AVG(total_time_ms) AS total_time_ms, AVG(total_memory_kb) AS total_memory_kb, AVG(peak_memory_mb) AS peak_memory_mb, AVG(timer_count) AS timer_count FROM $requestTable");
811 $timerRows = $db->select_query($timerServer, "SELECT section, MAX(info) AS info, MIN(sort_order) AS sort_order, COUNT(*) AS row_count, AVG(time_ms) AS time_ms, AVG(memory_kb) AS memory_kb, AVG(start_memory_kb) AS start_memory_kb, AVG(end_memory_kb) AS end_memory_kb FROM $timerTable WHERE section <> '' AND section <> 'system' GROUP BY section ORDER BY MIN(sort_order) ASC, section ASC");
812
813 $db->empty(self::PERF_TIMER_DD);
814 $db->empty(self::PERF_REQUEST_DD);
815
816 $base = $this->performance_now_record_base();
817 $requestId = 0;
818 $requestDate = date('Y-m-d H:i:s');
819 $requestCount = is_array($requestRows) ? (int) ($requestRows[0]['row_count'] ?? 0) : 0;
820
821 if ($requestCount > 0) {
822 $request = array_merge($base, array(
823 'request_date' => $requestDate,
824 'uid' => (int) dbx()->user(),
825 'session_id' => 'compressed',
826 'modul' => 'compressed',
827 'run1' => 'performance',
828 'run2' => 'compress',
829 'ajax' => 0,
830 'sync' => 0,
831 'method' => 'COMPRESS',
832 'uri' => 'Performance DB komprimiert aus ' . $requestCount . ' Request-Datensaetzen',
833 'total_time_ms' => (int) round((float) ($requestRows[0]['total_time_ms'] ?? 0)),
834 'total_memory_kb' => (int) round((float) ($requestRows[0]['total_memory_kb'] ?? 0)),
835 'peak_memory_mb' => (int) round((float) ($requestRows[0]['peak_memory_mb'] ?? 0)),
836 'timer_count' => (int) round((float) ($requestRows[0]['timer_count'] ?? 0)),
837 'sample_rate' => max(1, $requestCount),
838 ));
839 if ($db->insert(self::PERF_REQUEST_DD, $request, 0, 1, 1, 0) === 1) {
840 $requestId = $db->get_insert_id();
841 $stats['inserted']++;
842 }
843 }
844
845 if (is_array($timerRows)) {
846 $sort = 0;
847 foreach ($timerRows as $row) {
848 $section = trim((string) ($row['section'] ?? ''));
849 if ($section === '') {
850 continue;
851 }
852
853 $count = max(1, (int) ($row['row_count'] ?? 0));
854 $timer = array_merge($base, array(
855 'request_id' => $requestId,
856 'request_date' => $requestDate,
857 'sort_order' => $sort,
858 'section' => substr($section, 0, 80),
859 'info' => substr(trim((string) ($row['info'] ?? '')) . ' | komprimiert aus ' . $count . ' Messungen', 0, 160),
860 'time_ms' => (int) round((float) ($row['time_ms'] ?? 0)),
861 'memory_kb' => (int) round((float) ($row['memory_kb'] ?? 0)),
862 'start_memory_kb' => (int) round((float) ($row['start_memory_kb'] ?? 0)),
863 'end_memory_kb' => (int) round((float) ($row['end_memory_kb'] ?? 0)),
864 ));
865 if ($db->insert(self::PERF_TIMER_DD, $timer, 0, 1, 1, 0) === 1) {
866 $stats['inserted']++;
867 $sort++;
868 }
869 }
870 }
871
872 $optimize = $this->optimize_performance_db();
873 $stats['messages'] = array_merge($stats['messages'], $optimize['messages']);
874 $stats['request_after'] = $this->performance_table_count(self::PERF_REQUEST_DD);
875 $stats['timer_after'] = $this->performance_table_count(self::PERF_TIMER_DD);
876 $stats['ok'] = (bool) ($optimize['ok'] ?? false);
877
878 return $stats;
879 }
880
881 private function clear_performance_db(): array {
882 $stats = array(
883 'request_before' => $this->performance_table_count(self::PERF_REQUEST_DD),
884 'timer_before' => $this->performance_table_count(self::PERF_TIMER_DD),
885 'request_after' => 0,
886 'timer_after' => 0,
887 'inserted' => 0,
888 'ok' => false,
889 'messages' => array(),
890 );
891
892 if (!$this->ensure_performance_tables()) {
893 $stats['messages'][] = 'Performance-Tabellen konnten nicht vorbereitet werden.';
894 return $stats;
895 }
896
897 $db = dbx()->get_system_obj('dbxDB');
898 $db->empty(self::PERF_TIMER_DD);
899 $db->empty(self::PERF_REQUEST_DD);
900
901 $optimize = $this->optimize_performance_db();
902 $stats['messages'] = array_merge($stats['messages'], $optimize['messages']);
903 $stats['request_after'] = $this->performance_table_count(self::PERF_REQUEST_DD);
904 $stats['timer_after'] = $this->performance_table_count(self::PERF_TIMER_DD);
905 $stats['ok'] = (bool) ($optimize['ok'] ?? false);
906
907 return $stats;
908 }
909
910 private function render_performance_process(string $title, array $stats): string {
911 $tpl = dbx()->get_system_obj('dbxTPL');
912 $ok = (bool) ($stats['ok'] ?? false);
913 $messages = array();
914 foreach (($stats['messages'] ?? array()) as $message) {
915 if (trim((string) $message) !== '') {
916 $messages[] = dbx()->esc($message);
917 }
918 }
919 $message = ($ok ? 'Prozess abgeschlossen.' : 'Prozess mit Fehler beendet.')
920 . ' Request: ' . $this->fmt($stats['request_before'] ?? 0) . ' -> ' . $this->fmt($stats['request_after'] ?? 0)
921 . ', Timer: ' . $this->fmt($stats['timer_before'] ?? 0) . ' -> ' . $this->fmt($stats['timer_after'] ?? 0)
922 . ', neu geschrieben: ' . $this->fmt($stats['inserted'] ?? 0)
923 . (count($messages) ? ' | ' . implode(' | ', $messages) : '');
924
925 return $tpl->get_tpl('dbxAdmin|schema-process', array(
926 'target_id' => 'dbx_process_performance_' . substr(md5($title), 0, 10),
927 'title' => dbx()->esc($title),
928 'status_key' => $ok ? 'finished' : 'error',
929 'status_label' => $ok ? 'Fertig' : 'Fehler',
930 'status_class' => $ok ? 'bg-success' : 'bg-danger',
931 'status_icon' => $ok ? 'bi bi-check-circle' : 'bi bi-exclamation-triangle',
932 'message' => $message,
933 'percent' => $ok ? 100 : 0,
934 'step_percent' => $ok ? 100 : 0,
935 'bar_class' => $ok ? 'bg-success' : 'bg-danger',
936 'task_label' => 'Performance DB',
937 'step_label' => 'Optimierung',
938 'process_label' => 'Performance Wartung',
939 'updated_at' => dbx()->esc(date('d.m.Y H:i:s')),
940 'next_url' => '',
941 'pause_url' => '',
942 'resume_url' => '',
943 'continue_url' => '',
944 'cancel_url' => '',
945 'restart_url' => '',
946 'autostart' => 0,
947 'interval' => 800,
948 'pause_visible' => '_none',
949 'resume_visible' => '_none',
950 'continue_visible' => '_none',
951 'restart_visible' => '_none',
952 'cancel_visible' => '_none',
953 'back_url' => '?dbx_modul=dbxAdmin',
954 ));
955 }
956
957 private function run_performance_maintenance_process(string $mode): string {
958 dbx()->set_system_var('dbx_performance_timer_skip', 1);
959
960 if ($mode === 'clear') {
961 return $this->render_performance_process('Performance DB leeren und optimieren', $this->clear_performance_db());
962 }
963
964 return $this->render_performance_process('Performance DB komprimieren und optimieren', $this->compress_performance_db());
965 }
966
967 private function performance_openwin_action(string $href, string $label, string $icon, string $class = 'btn-outline-secondary'): string {
968 return '<a class="btn btn-sm ' . dbx()->esc($class) . ' dbx-win" href="' . dbx()->esc($href) . '" data-url="' . dbx()->esc($href) . '" data-title="' . dbx()->esc($label) . '" data-width="760" data-height="520" role="button">'
969 . '<i class="bi ' . dbx()->esc($icon) . '"></i> ' . dbx()->esc($label)
970 . '</a>';
971 }
972
973 private function performance_maintenance_actions(): string {
974 return $this->performance_openwin_action('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=performance_compress', 'Komprimieren', 'bi-file-zip')
975 . $this->performance_openwin_action('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=performance_clear', 'Performance DB leeren', 'bi-trash3', 'btn-outline-danger');
976 }
977
978 private function performance_request_average() {
979 if ($this->performanceRequestAverage !== null) {
980 return $this->performanceRequestAverage;
981 }
982
983 $this->performanceRequestAverage = array();
984
985 if (!$this->ensure_performance_tables()) {
986 return $this->performanceRequestAverage;
987 }
988
989 list($server, $table) = $this->performance_dd_info(self::PERF_REQUEST_DD);
990 if ($server === '' || $table === '') {
991 return $this->performanceRequestAverage;
992 }
993
994 try {
995 $db = dbx()->get_system_obj('dbxDB');
996 $rows = $db->select_query($server, "SELECT COUNT(*) AS row_count, AVG(total_time_ms) AS avg_time_ms, AVG(total_memory_kb) AS avg_memory_kb, AVG(timer_count) AS avg_timer_count FROM $table");
997 $row = is_array($rows) ? ($rows[0] ?? array()) : array();
998
999 if ((int) ($row['row_count'] ?? 0) <= 0) {
1000 return $this->performanceRequestAverage;
1001 }
1002
1003 $this->performanceRequestAverage = array(
1004 'count' => (int) ($row['row_count'] ?? 0),
1005 'avg_time_ms' => (int) round((float) ($row['avg_time_ms'] ?? 0)),
1006 'avg_memory_kb' => (int) round((float) ($row['avg_memory_kb'] ?? 0)),
1007 'avg_timer_count' => (int) round((float) ($row['avg_timer_count'] ?? 0)),
1008 );
1009 } catch (\Throwable $e) {
1010 $this->performanceRequestAverage = array();
1011 }
1012
1013 return $this->performanceRequestAverage;
1014 }
1015
1016 private function performance_timer_averages() {
1017 if ($this->performanceTimerAverages !== null) {
1018 return $this->performanceTimerAverages;
1019 }
1020
1021 $this->performanceTimerAverages = array();
1022
1023 if (!$this->ensure_performance_tables()) {
1024 return $this->performanceTimerAverages;
1025 }
1026
1027 list($server, $table) = $this->performance_dd_info(self::PERF_TIMER_DD);
1028 if ($server === '' || $table === '') {
1029 return $this->performanceTimerAverages;
1030 }
1031
1032 try {
1033 $db = dbx()->get_system_obj('dbxDB');
1034 $sql = "SELECT section, MAX(info) AS info, MIN(sort_order) AS sort_order, COUNT(*) AS row_count, AVG(time_ms) AS avg_time_ms, AVG(memory_kb) AS avg_memory_kb
1035 FROM $table
1036 WHERE section <> 'system'
1037 GROUP BY section
1038 ORDER BY MIN(sort_order) ASC, section ASC";
1039 $rows = $db->select_query($server, $sql);
1040 $this->performanceTimerAverages = is_array($rows) ? $rows : array();
1041 } catch (\Throwable $e) {
1042 $this->performanceTimerAverages = array();
1043 }
1044
1045 return $this->performanceTimerAverages;
1046 }
1047
1048 private function performance_timer_average($section) {
1049 $section = (string) $section;
1050
1051 foreach ($this->performance_timer_averages() as $row) {
1052 if ((string) ($row['section'] ?? '') === $section) {
1053 return array(
1054 'count' => (int) ($row['row_count'] ?? 0),
1055 'avg_time_ms' => (int) round((float) ($row['avg_time_ms'] ?? 0)),
1056 'avg_memory_kb' => (int) round((float) ($row['avg_memory_kb'] ?? 0)),
1057 );
1058 }
1059 }
1060
1061 return array();
1062 }
1063
1064 private function performance_module_averages() {
1065 if ($this->performanceModuleAverages !== null) {
1066 return $this->performanceModuleAverages;
1067 }
1068
1069 $this->performanceModuleAverages = array();
1070
1071 if (!$this->ensure_performance_tables()) {
1072 return $this->performanceModuleAverages;
1073 }
1074
1075 list($requestServer, $requestTable) = $this->performance_dd_info(self::PERF_REQUEST_DD);
1076 list($timerServer, $timerTable) = $this->performance_dd_info(self::PERF_TIMER_DD);
1077 if ($requestServer === '' || $requestTable === '' || $timerServer !== $requestServer || $timerTable === '') {
1078 return $this->performanceModuleAverages;
1079 }
1080
1081 try {
1082 $db = dbx()->get_system_obj('dbxDB');
1083 $sql = "SELECT r.modul AS modul,
1084 COUNT(*) AS row_count,
1085 AVG(r.total_time_ms) AS avg_total_ms,
1086 AVG(COALESCE(d.db_time_ms, 0)) AS avg_db_ms
1087 FROM $requestTable r
1088 LEFT JOIN (
1089 SELECT request_id, SUM(time_ms) AS db_time_ms
1090 FROM $timerTable
1091 WHERE section = 'db-total'
1092 GROUP BY request_id
1093 ) d ON d.request_id = r.id
1094 WHERE TRIM(COALESCE(r.modul, '')) <> ''
1095 GROUP BY r.modul
1096 ORDER BY AVG(r.total_time_ms) DESC, r.modul ASC
1097 LIMIT 16";
1098 $rows = $db->select_query($requestServer, $sql);
1099 $this->performanceModuleAverages = is_array($rows) ? $rows : array();
1100 } catch (\Throwable $e) {
1101 $this->performanceModuleAverages = array();
1102 }
1103
1104 return $this->performanceModuleAverages;
1105 }
1106
1107 private function hero_performance($metrics) {
1108 $request = $this->performance_request_average();
1109 $dbTotal = $this->performance_timer_average('db-total');
1110
1111 $requestMs = $request ? (int) ($request['avg_time_ms'] ?? 0) : (int) ($metrics['request_runtime_ms'] ?? 0);
1112 $dbMs = $dbTotal ? (int) ($dbTotal['avg_time_ms'] ?? 0) : 0;
1113 $phpMs = max(0, $requestMs - $dbMs);
1114 $dbShare = $requestMs > 0 ? min(999, (int) round(($dbMs / $requestMs) * 100)) : 0;
1115 $phpShare = $requestMs > 0 ? min(999, (int) round(($phpMs / $requestMs) * 100)) : 0;
1116
1117 return array(
1118 'request_avg' => $this->fmt_ms($requestMs),
1119 'request_raw' => $requestMs,
1120 'request_count' => $this->fmt($request['count'] ?? 0),
1121 'php_avg' => $this->fmt_ms($phpMs),
1122 'php_raw' => $phpMs,
1123 'php_share' => $this->fmt($phpShare),
1124 'db_avg' => $this->fmt_ms($dbMs),
1125 'db_raw' => $dbMs,
1126 'db_count' => $this->fmt($dbTotal['count'] ?? 0),
1127 'db_share' => $this->fmt($dbShare),
1128 );
1129 }
1130
1131 private function hero_performance_gauge($label, $icon, $tone, $raw, $value, $subtitle) {
1132 $tpl = dbx()->get_system_obj('dbxTPL');
1133
1134 return $tpl->get_tpl('dbxAdmin|admin-dashboard-hero-gauge', array(
1135 'label' => $label,
1136 'icon' => $icon,
1137 'tone' => $tone,
1138 'raw' => max(0, (int) $raw),
1139 'max' => 6000,
1140 'value' => $value,
1141 'subtitle' => $subtitle,
1142 ));
1143 }
1144
1145 private function hero_performance_gauges($heroPerformance) {
1146 $items = '';
1147 $items .= $this->hero_performance_gauge(
1148 'Request gesamt',
1149 'bi-stopwatch',
1150 'request',
1151 $heroPerformance['request_raw'] ?? 0,
1152 $heroPerformance['request_avg'] ?? '0,000 Sec',
1153 'Durchschnitt aus ' . ($heroPerformance['request_count'] ?? '0') . ' Requests'
1154 );
1155 $items .= $this->hero_performance_gauge(
1156 'PHP gesamt',
1157 'bi-cpu',
1158 'php',
1159 $heroPerformance['php_raw'] ?? 0,
1160 $heroPerformance['php_avg'] ?? '0,000 Sec',
1161 'Durchschnitt ohne DB-Anteil'
1162 );
1163 $items .= $this->hero_performance_gauge(
1164 'DB gesamt',
1165 'bi-database-check',
1166 'db',
1167 $heroPerformance['db_raw'] ?? 0,
1168 $heroPerformance['db_avg'] ?? '0,000 Sec',
1169 'Durchschnitt aus DB-Timern'
1170 );
1171
1172 return $items;
1173 }
1174
1175 private function hero_summary_rows(array $items): string {
1176 $html = '';
1177 $max = 1;
1178 foreach ($items as $item) {
1179 $max = max($max, (int) ($item['count'] ?? 0));
1180 }
1181
1182 foreach ($items as $item) {
1183 $count = (int) ($item['count'] ?? 0);
1184 $pct = max(3, min(100, (int) round(($count / $max) * 100)));
1185 $tone = dbx()->esc((string) ($item['tone'] ?? 'blue'));
1186 $html .= '<div class="dbx-admin-dashboard-hero-summary-row dbx-admin-dashboard-hero-summary-row-' . $tone . '">'
1187 . '<span>' . dbx()->esc((string) ($item['label'] ?? '')) . '</span>'
1188 . '<div><em style="width:' . $pct . '%"></em></div>'
1189 . '<strong>' . dbx()->esc($this->fmt($count)) . '</strong>'
1190 . '</div>';
1191 }
1192
1193 return $html;
1194 }
1195
1196 private function hero_summary_card(string $title, string $subtitle, string $icon, string $tone, int $total, array $rows): string {
1197 $tpl = dbx()->get_system_obj('dbxTPL');
1198
1199 return $tpl->get_tpl('dbxAdmin|admin-dashboard-hero-summary', array(
1200 'title' => $title,
1201 'subtitle' => $subtitle,
1202 'icon' => $icon,
1203 'tone' => $tone,
1204 'total' => $this->fmt($total),
1205 'rows' => $this->hero_summary_rows($rows),
1206 ));
1207 }
1208
1209 private function hero_status_summaries(): string {
1210 $contactDd = 'dbxContact|contactRequest';
1211 $contactTotal = $this->safe_count($contactDd);
1212 $contactRows = array(
1213 array('label' => 'Offen', 'count' => $this->safe_count($contactDd, array('status' => 'open')), 'tone' => 'blue'),
1214 array('label' => 'In Arbeit', 'count' => $this->safe_count($contactDd, array('status' => 'in_progress')), 'tone' => 'cyan'),
1215 array('label' => 'Rueckfrage', 'count' => $this->safe_count($contactDd, array('status' => 'waiting_customer')), 'tone' => 'amber'),
1216 array('label' => 'Beantwortet', 'count' => $this->safe_count($contactDd, array('status' => 'answered')), 'tone' => 'green'),
1217 array('label' => 'Geschlossen', 'count' => $this->safe_count($contactDd, array('status' => 'closed')), 'tone' => 'slate'),
1218 );
1219
1220 $sysmsgDd = 'dbxSysMsg';
1221 $sysmsgTotal = $this->safe_count($sysmsgDd);
1222 $sysmsgRows = array(
1223 array('label' => 'Info', 'count' => $this->safe_count($sysmsgDd, "LOWER(status) = 'info'"), 'tone' => 'blue'),
1224 array('label' => 'Warning', 'count' => $this->safe_count($sysmsgDd, "LOWER(status) = 'warning'"), 'tone' => 'amber'),
1225 array('label' => 'Error', 'count' => $this->safe_count($sysmsgDd, "LOWER(status) = 'error'"), 'tone' => 'red'),
1226 array('label' => 'Security', 'count' => $this->safe_count($sysmsgDd, "LOWER(status) = 'security'"), 'tone' => 'purple'),
1227 );
1228
1229 return $this->hero_summary_card('Kontakte', 'Alle Anfragen nach Status', 'bi-life-preserver', 'contact', $contactTotal, $contactRows)
1230 . $this->hero_summary_card('SysMsg', 'Meldungen nach Status', 'bi-bell', 'sysmsg', $sysmsgTotal, $sysmsgRows);
1231 }
1232
1233 private function performance_label($section, $info = '') {
1234 $section = trim((string) $section);
1235 $info = trim((string) $info);
1236
1237 if ($section === 'db-total') {
1238 return 'DB Gesamt';
1239 }
1240
1241 if ($section === 'js-total') {
1242 return 'JS Gesamt';
1243 }
1244
1245 if ($section === 'system') {
1246 return 'PHP/System Gesamt';
1247 }
1248
1249 if ($section === 'db-select') {
1250 return 'DB Select gesamt';
1251 }
1252
1253 if ($section === 'db-save') {
1254 return 'DB Save gesamt';
1255 }
1256
1257 if (substr($section, 0, 10) === 'db-select-') {
1258 return 'DB Select';
1259 }
1260
1261 if (substr($section, 0, 8) === 'db-save-') {
1262 return 'DB Save';
1263 }
1264
1265 $known = array(
1266 'system-load' => 'System Load',
1267 'session-load' => 'Session Load',
1268 'system-check' => 'System Check',
1269 'modul-run' => 'Modul Run',
1270 'page-load' => 'Page Load',
1271 'interpreter' => 'Interpreter',
1272 'db-select' => 'DB Select',
1273 'db-save' => 'DB Save',
1274 );
1275
1276 if (isset($known[$section])) {
1277 return $known[$section];
1278 }
1279
1280 if (substr($section, 0, 4) === 'run-') {
1281 $modul = substr($section, 4);
1282 return $modul !== '' ? $modul : 'Modul';
1283 }
1284
1285 $label = ucwords(str_replace(array('-', '_'), ' ', $section));
1286 return $label !== '' ? $label : ($info !== '' ? $info : 'Timer');
1287 }
1288
1289 private function performance_is_db_section($section) {
1290 $section = (string) $section;
1291 return $section === 'db-total'
1292 || $section === 'db-select'
1293 || $section === 'db-save'
1294 || substr($section, 0, 10) === 'db-select-'
1295 || substr($section, 0, 8) === 'db-save-';
1296 }
1297
1298 private function performance_dd_detail($section, $info = '') {
1299 $section = trim((string) $section);
1300 $info = trim((string) $info);
1301
1302 if (substr($section, 0, 10) === 'db-select-') {
1303 $dd = substr($info, 0, 7) === 'select ' ? trim(substr($info, 7)) : trim(substr($section, 10));
1304 return $dd !== '' ? $dd : 'DD';
1305 }
1306
1307 if (substr($section, 0, 8) === 'db-save-') {
1308 $dd = substr($info, 0, 5) === 'save ' ? trim(substr($info, 5)) : trim(substr($section, 8));
1309 return $dd !== '' ? $dd : 'DD';
1310 }
1311
1312 return null;
1313 }
1314
1315 private function performance_db_sort($row) {
1316 $section = (string) ($row['section'] ?? '');
1317
1318 if ($section === 'db-total') {
1319 return 0;
1320 }
1321
1322 if ($section === 'db-select') {
1323 return 10;
1324 }
1325
1326 if ($section === 'db-save') {
1327 return 20;
1328 }
1329
1330 if (substr($section, 0, 10) === 'db-select-') {
1331 return 100;
1332 }
1333
1334 if (substr($section, 0, 8) === 'db-save-') {
1335 return 200;
1336 }
1337
1338 return 900;
1339 }
1340
1341 private function performance_tone($index) {
1342 $tones = array('teal', 'green', 'navy', 'amber', 'cyan', 'red', 'purple', 'slate');
1343 return $tones[$index % count($tones)];
1344 }
1345
1346 private function performance_rows($metrics, $mode = 'request') {
1347 $rows = array();
1348
1349 if ($mode === 'module') {
1350 $index = 1;
1351 foreach ($this->performance_module_averages() as $module) {
1352 $modul = trim((string) ($module['modul'] ?? ''));
1353 $count = (int) ($module['row_count'] ?? 0);
1354 if ($modul === '' || $count <= 0) {
1355 continue;
1356 }
1357
1358 $totalMs = (int) round((float) ($module['avg_total_ms'] ?? 0));
1359 $dbMs = (int) round((float) ($module['avg_db_ms'] ?? 0));
1360 $phpMs = max(0, $totalMs - $dbMs);
1361 $subtitle = 'Modul ' . $modul . ' · ' . $this->fmt($count) . ' Requests';
1362
1363 $rows[] = array(
1364 'section' => 'module-' . $modul . '-php',
1365 'label' => $modul . ' · PHP gesamt',
1366 'icon' => 'bi-cpu',
1367 'value_ms' => $phpMs,
1368 'detail' => 'PHP gesamt',
1369 'precision' => 3,
1370 'min_display_sec' => 0,
1371 'subtitle' => $subtitle,
1372 'tone' => $this->performance_tone($index),
1373 );
1374
1375 $rows[] = array(
1376 'section' => 'module-' . $modul . '-db',
1377 'label' => $modul . ' · DB gesamt',
1378 'icon' => 'bi-database-check',
1379 'value_ms' => $dbMs,
1380 'detail' => 'DB gesamt',
1381 'precision' => 4,
1382 'min_display_sec' => 0.0001,
1383 'subtitle' => $subtitle,
1384 'tone' => $this->performance_tone($index + 1),
1385 );
1386 $index += 2;
1387 }
1388
1389 return array('rows' => $rows);
1390 }
1391
1392 $segments = $this->performance_timer_averages();
1393 $index = 1;
1394 foreach ($segments as $segment) {
1395 $section = (string) ($segment['section'] ?? '');
1396 $count = (int) ($segment['row_count'] ?? 0);
1397 if ($section === '' || $count <= 0) {
1398 continue;
1399 }
1400
1401 if ($mode === 'db' && $section === 'db-total') {
1402 continue;
1403 }
1404
1405 $isDb = $this->performance_is_db_section($section);
1406 if (($mode === 'db') !== $isDb) {
1407 continue;
1408 }
1409
1410 $rows[] = array(
1411 'section' => $section,
1412 'label' => $this->performance_label($section, $segment['info'] ?? ''),
1413 'icon' => 'bi-stopwatch',
1414 'value_ms' => (int) round((float) ($segment['avg_time_ms'] ?? 0)),
1415 'detail' => $this->performance_dd_detail($section, $segment['info'] ?? ''),
1416 'precision' => $isDb ? 4 : 3,
1417 'min_display_sec' => $isDb ? 0.0001 : 0,
1418 'subtitle' => 'Durchschnitt aus ' . $this->fmt($count) . ' Messungen, Memory ' . $this->fmt_memory_delta_kb($segment['avg_memory_kb'] ?? 0),
1419 'tone' => $this->performance_tone($index),
1420 );
1421 $index++;
1422 }
1423
1424 if ($mode === 'db') {
1425 usort($rows, function ($a, $b) {
1426 $prio = $this->performance_db_sort($a) <=> $this->performance_db_sort($b);
1427 if ($prio !== 0) {
1428 return $prio;
1429 }
1430
1431 return strcmp((string) ($a['detail'] ?? $a['label'] ?? ''), (string) ($b['detail'] ?? $b['label'] ?? ''));
1432 });
1433 }
1434
1435 return array('rows' => $rows);
1436 }
1437
1438 private function performance_module_image_url(string $modul): string {
1439 $modul = trim($modul);
1440 $safeModul = preg_match('/^[A-Za-z0-9_\\-]+$/', $modul) ? $modul : 'dbx';
1441 $baseDir = rtrim((string) dbx()->get_base_dir(), '/\\') . DIRECTORY_SEPARATOR;
1442 $rel = 'dbx/modules/' . $safeModul . '/tpl/img/' . $safeModul . '.png';
1443 $path = $baseDir . str_replace('/', DIRECTORY_SEPARATOR, $rel);
1444
1445 if (!is_file($path)) {
1446 $rel = 'dbx/modules/dbx/tpl/img/dbx.png';
1447 }
1448
1449 return rtrim((string) dbx()->get_base_url(), '/\\') . '/' . $rel;
1450 }
1451
1452 private function performance_metric($row, int $max): string {
1453 $tpl = dbx()->get_system_obj('dbxTPL');
1454 $value = (int) ($row['value_ms'] ?? 0);
1455
1456 return $tpl->get_tpl('dbxAdmin|admin-dashboard-performance-metric', array(
1457 'tone' => (string) ($row['tone'] ?? 'teal'),
1458 'icon' => (string) ($row['icon'] ?? 'bi-speedometer2'),
1459 'label' => (string) ($row['label'] ?? 'Messung'),
1460 'raw' => $value,
1461 'max' => max(1, $max),
1462 'value' => $this->fmt_ms_precision($value, $row['precision'] ?? 3, $row['min_display_sec'] ?? 0),
1463 'detail' => (string) ($row['detail'] ?? ''),
1464 ));
1465 }
1466
1467 private function performance_module_list(): string {
1468 $tpl = dbx()->get_system_obj('dbxTPL');
1469 $max = 6000;
1470 $html = '';
1471 $index = 1;
1472
1473 foreach ($this->performance_module_averages() as $module) {
1474 $modul = trim((string) ($module['modul'] ?? ''));
1475 $count = (int) ($module['row_count'] ?? 0);
1476 if ($modul === '' || $count <= 0) {
1477 continue;
1478 }
1479
1480 $totalMs = (int) round((float) ($module['avg_total_ms'] ?? 0));
1481 $dbMs = (int) round((float) ($module['avg_db_ms'] ?? 0));
1482 $phpMs = max(0, $totalMs - $dbMs);
1483
1484 $metrics = '';
1485 $metrics .= $this->performance_metric(array(
1486 'label' => 'PHP gesamt',
1487 'icon' => 'bi-cpu',
1488 'value_ms' => $phpMs,
1489 'detail' => 'PHP gesamt',
1490 'precision' => 3,
1491 'min_display_sec' => 0,
1492 'tone' => $this->performance_tone($index),
1493 ), $max);
1494 $metrics .= $this->performance_metric(array(
1495 'label' => 'DB gesamt',
1496 'icon' => 'bi-database-check',
1497 'value_ms' => $dbMs,
1498 'detail' => 'DB gesamt',
1499 'precision' => 4,
1500 'min_display_sec' => 0.0001,
1501 'tone' => $this->performance_tone($index + 1),
1502 ), $max);
1503
1504 $html .= $tpl->get_tpl('dbxAdmin|admin-dashboard-performance-module', array(
1505 'module_img' => $this->performance_module_image_url($modul),
1506 'module_name' => $modul,
1507 'request_count' => $this->fmt($count),
1508 'metrics' => $metrics,
1509 ));
1510
1511 $index += 2;
1512 }
1513
1514 return $html;
1515 }
1516
1517 private function speedometer($row, $max) {
1518 $tpl = dbx()->get_system_obj('dbxTPL');
1519 $value = (int) ($row['value_ms'] ?? 0);
1520 $label = (string) ($row['label'] ?? 'Timer');
1521 $icon = (string) ($row['icon'] ?? 'bi-speedometer2');
1522 $tone = (string) ($row['tone'] ?? 'teal');
1523
1524 return $tpl->get_tpl('dbxAdmin|admin-dashboard-speedometer', array(
1525 'bar' => $this->card_bar($label, $icon),
1526 'tone' => $tone,
1527 'raw' => $value,
1528 'max' => max(1, (int) $max),
1529 'value' => $this->fmt_ms_precision($value, $row['precision'] ?? 3, $row['min_display_sec'] ?? 0),
1530 'detail' => $row['detail'] ?? '',
1531 'subtitle' => $row['subtitle'] ?? '',
1532 ));
1533 }
1534
1535 private function speedometer_panel($title, $icon, $subtitle, $rows, $panelClass = '', $panelTarget = '', $controls = '', $barActions = '') {
1536 $tpl = dbx()->get_system_obj('dbxTPL');
1537 $max = 6000;
1538 $items = '';
1539 $panelTarget = trim((string) $panelTarget);
1540 $actions = $barActions . ($panelTarget !== '' ? $this->collapse_action($panelTarget, 'Zuklappen', true) : '');
1541
1542 foreach ($rows as $row) {
1543 $items .= $this->speedometer($row, $max);
1544 }
1545
1546 return $tpl->get_tpl('dbxAdmin|admin-dashboard-performance', array(
1547 'panel_class' => $panelClass,
1548 'panel_target' => $panelTarget,
1549 'performance_bar' => $this->card_bar($title, $icon, $subtitle, $actions),
1550 'performance_controls' => $controls,
1551 'performance_items' => $items,
1552 ));
1553 }
1554
1555 private function performance_panel($metrics) {
1556 $tpl = dbx()->get_system_obj('dbxTPL');
1557 $panelTarget = 'request-performance';
1558 $actions = $this->performance_maintenance_actions()
1559 . $this->collapse_action($panelTarget, 'Zuklappen', true)
1560 . $this->help_action('dashboard');
1561
1562 return $tpl->get_tpl('dbxAdmin|admin-dashboard-performance', array(
1563 'panel_class' => '',
1564 'panel_target' => $panelTarget,
1565 'performance_bar' => $this->card_bar('Performance pro Modul', 'bi-speedometer2', 'Durchschnittswerte je Modul: PHP gesamt und DB gesamt', $actions),
1566 'performance_controls' => $this->performance_level_control(),
1567 'performance_items' => $this->performance_module_list(),
1568 ));
1569 }
1570
1571 private function normalize_sys_msg_level($level): string {
1572 $level = strtolower(trim((string) $level));
1573 if ($level === 'warn') {
1574 $level = 'warning';
1575 }
1576
1577 return in_array($level, array('error', 'warning', 'all'), true) ? $level : 'all';
1578 }
1579
1580 private function sys_msg_level_config(): string {
1581 return $this->normalize_sys_msg_level(dbx()->get_config('dbx', 'sys_msg_level', 'all'));
1582 }
1583
1584 private function sys_msg_level_options(string $current): string {
1585 $options = array(
1586 'error' => 'Nur Error',
1587 'warning' => 'Error + Warning',
1588 'all' => 'Alles',
1589 );
1590 $html = '';
1591
1592 foreach ($options as $value => $label) {
1593 $selected = $value === $current ? ' selected' : '';
1594 $html .= '<option value="' . dbx()->esc($value) . '"' . $selected . '>' . dbx()->esc($label) . '</option>';
1595 }
1596
1597 return $html;
1598 }
1599
1600 private function set_sys_msg_level_config(string $level): bool {
1601 $config = dbx()->get_config('dbx');
1602 if (!is_array($config)) {
1603 $config = array();
1604 }
1605
1606 $config['sys_msg_level'] = $this->normalize_sys_msg_level($level);
1607 return (int) dbx()->set_config('dbx', $config) > 0;
1608 }
1609
1610 private function process_sys_msg_level_action(): bool {
1611 $run2 = dbx()->get_modul_var('dbx_run2', '', 'parameter');
1612 if ($run2 !== 'sysmsg_level_save') {
1613 return false;
1614 }
1615
1616 $level = dbx()->get_request_var('sys_msg_level', null, 'parameter');
1617 if ($level === null || $level === '') {
1618 $level = $_POST['sys_msg_level'] ?? 'all';
1619 }
1620
1621 return $this->set_sys_msg_level_config((string) $level);
1622 }
1623
1624 private function sys_msg_level_control_data(): array {
1625 $level = $this->sys_msg_level_config();
1626 $states = array(
1627 'all' => array(
1628 'tone' => 'on',
1629 'icon' => 'bi-bell-fill',
1630 'label' => 'Systemmeldungen: Alles',
1631 'hint' => 'Alle Systemmeldungen werden gespeichert.',
1632 ),
1633 'warning' => array(
1634 'tone' => 'on',
1635 'icon' => 'bi-exclamation-triangle-fill',
1636 'label' => 'Error + Warning',
1637 'hint' => 'Nur Error und Warning werden gespeichert.',
1638 ),
1639 'error' => array(
1640 'tone' => 'off',
1641 'icon' => 'bi-exclamation-octagon-fill',
1642 'label' => 'Nur Error',
1643 'hint' => 'Nur Fehlermeldungen werden gespeichert.',
1644 ),
1645 );
1646 $state = $states[$level] ?? $states['all'];
1647
1648 return array(
1649 'sys_msg_level_save_base' => dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=sysmsg_level_save'),
1650 'sys_msg_level_options' => $this->sys_msg_level_options($level),
1651 'sysmsg_status_tone' => dbx()->esc($state['tone']),
1652 'sysmsg_status_icon' => dbx()->esc($state['icon']),
1653 'sysmsg_status_label' => dbx()->esc($state['label']),
1654 'sysmsg_status_hint' => dbx()->esc($state['hint']),
1655 );
1656 }
1657
1658 private function sys_msg_level_control(): string {
1659 $oForm = new \dbxForm();
1660 $oForm->init('admin-dashboard-sysmsg-control', 'admin-dashboard-sysmsg-control');
1661 $oForm->set_form_help_enabled(false);
1662 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=sysmsg_level_save';
1663 $oForm->_msg_info = '';
1664
1665 foreach ($this->sys_msg_level_control_data() as $key => $value) {
1666 $oForm->add_rep($key, $value);
1667 }
1668
1669 return $oForm->add_norep($oForm->run());
1670 }
1671
1672 private function sysmsg_panel_body_html(): string {
1673 $oForm = new \dbxForm();
1674 $oForm->init('admin-dashboard-sysmsg-body', 'admin-dashboard-sysmsg-body');
1675 $oForm->set_form_help_enabled(false);
1676 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run';
1677 $oForm->_msg_info = '';
1678 $oForm->add_rep('sysmsg_control', $this->sys_msg_level_control());
1679
1680 return $oForm->add_norep($oForm->run());
1681 }
1682
1683 private function sysmsg_panel() {
1684 $oForm = new \dbxForm();
1685 $oForm->init('admin-dashboard-sysmsg-panel', 'admin-dashboard-sysmsg-panel');
1686 $oForm->set_form_help_enabled(false);
1687 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run';
1688 $oForm->_msg_info = '';
1689 $oForm->add_rep('sysmsg_body', $this->sysmsg_panel_body_html());
1690
1691 return $oForm->add_norep($oForm->run());
1692 }
1693
1694 private function session_db_enabled_config(): bool {
1695 return $this->dbx_config_bool('session_db', 1);
1696 }
1697
1698 private function set_session_db_config(bool $enabled): bool {
1699 $config = dbx()->get_config('dbx');
1700 if (!is_array($config)) {
1701 $config = array();
1702 }
1703
1704 $config['session_db'] = $enabled ? 1 : 0;
1705 return (int) dbx()->set_config('dbx', $config) > 0;
1706 }
1707
1708 private function process_session_db_action(): bool {
1709 $run2 = dbx()->get_modul_var('dbx_run2', '', 'parameter');
1710 if ($run2 !== 'session_db_save') {
1711 return false;
1712 }
1713
1714 return $this->set_session_db_config(isset($_POST['session_db']));
1715 }
1716
1717 private function session_panel_body_data(): array {
1718 $enabled = $this->session_db_enabled_config();
1719
1720 return array(
1721 'session_save_url' => dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=session_db_save'),
1722 'session_enabled_checked' => $enabled ? 'checked' : '',
1723 'session_status_tone' => $enabled ? 'on' : 'off',
1724 'session_status_icon' => $enabled ? 'bi-check-circle-fill' : 'bi-pause-circle-fill',
1725 'session_status_label' => dbx()->esc($enabled ? 'Session-DB aktiv' : 'Session-DB inaktiv'),
1726 'session_status_hint' => dbx()->esc(
1727 $enabled
1728 ? 'Normale HTTP-Requests und HTML-AJAX-Requests schreiben ihre Session am Request-Ende in die DB.'
1729 : 'Sessions laufen nur ueber PHP-Session; die Session-Liste wird nicht fortgeschrieben.'
1730 ),
1731 );
1732 }
1733
1734 private function session_panel_control_html(): string {
1735 $oForm = new \dbxForm();
1736 $oForm->init('admin-dashboard-session-control', 'admin-dashboard-session-control');
1737 $oForm->set_form_help_enabled(false);
1738 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run';
1739 $oForm->_msg_info = '';
1740
1741 foreach ($this->session_panel_body_data() as $key => $value) {
1742 $oForm->add_rep($key, $value);
1743 }
1744
1745 return $oForm->add_norep($oForm->run());
1746 }
1747
1748 private function session_panel_body_html(): string {
1749 $oForm = new \dbxForm();
1750 $oForm->init('admin-dashboard-session-body', 'admin-dashboard-session-body');
1751 $oForm->set_form_help_enabled(false);
1752 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run';
1753 $oForm->_msg_info = '';
1754 $oForm->add_rep('session_control', $this->session_panel_control_html());
1755
1756 return $oForm->add_norep($oForm->run());
1757 }
1758
1759 private function session_panel() {
1760 $oForm = new \dbxForm();
1761 $oForm->init('admin-dashboard-session-panel', 'admin-dashboard-session-panel');
1762 $oForm->set_form_help_enabled(false);
1763 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run';
1764 $oForm->_msg_info = '';
1765 $oForm->add_rep('session_body', $this->session_panel_body_html());
1766
1767 return $oForm->add_norep($oForm->run());
1768 }
1769
1770 private function module_count() {
1771 $count = 0;
1772 $path = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/');
1773
1774 if (!is_dir($path)) {
1775 return 0;
1776 }
1777
1778 $files = scandir($path);
1779 foreach ($files as $file) {
1780 if ($file === '.' || $file === '..') {
1781 continue;
1782 }
1783
1784 if (is_dir($path . $file) && $file !== 'tpl' && substr($file, 0, 1) !== '.') {
1785 $count++;
1786 }
1787 }
1788
1789 return $count;
1790 }
1791
1792 private function dd_inventory() {
1793 $records = array();
1794 $base = dbx()->os_path(dbx()->get_base_dir() . 'dbx/modules/');
1795
1796 if (!is_dir($base)) {
1797 return $records;
1798 }
1799
1800 $modules = scandir($base);
1801 foreach ($modules as $modul) {
1802 if ($modul === '.' || $modul === '..' || substr($modul, 0, 1) === '.') {
1803 continue;
1804 }
1805
1806 $ddPath = dbx()->os_path($base . $modul . '/dd/');
1807 if (!is_dir($ddPath)) {
1808 continue;
1809 }
1810
1811 $files = scandir($ddPath);
1812 foreach ($files as $file) {
1813 if (!str_ends_with($file, '.dd.php')) {
1814 continue;
1815 }
1816
1817 $dd = str_replace('.dd.php', '', $file);
1818 if ($dd === '' || $dd === 'new') {
1819 continue;
1820 }
1821
1822 $key = $modul . '|' . $dd;
1823
1824 try {
1825 $db = dbx()->get_system_obj('dbxDB');
1826 $tableDef = $db->get_dd_table($key, 1);
1827
1828 if (!is_array($tableDef)) {
1829 continue;
1830 }
1831
1832 $server = (string) ($tableDef['server'] ?? '');
1833 $table = (string) ($tableDef['table'] ?? '');
1834
1835 if ($server === '' || $table === '') {
1836 continue;
1837 }
1838
1839 $exist = $db->get_table_exist($server, $table) ? 1 : 0;
1840 $count = $exist ? $this->safe_count($key) : 0;
1841
1842 $records[] = array(
1843 'dd' => $dd,
1844 'key' => $key,
1845 'modul' => $modul,
1846 'server' => $server,
1847 'table' => $table,
1848 'exist' => $exist,
1849 'count' => $count,
1850 'sync' => (int) ($tableDef['autosync'] ?? 0),
1851 'trace' => (int) ($tableDef['trace'] ?? 0),
1852 );
1853 } catch (\Throwable $e) {
1854 continue;
1855 }
1856 }
1857 }
1858
1859 return $records;
1860 }
1861
1862 private function metrics() {
1863 if ($this->metricCache) {
1864 return $this->metricCache;
1865 }
1866
1867 $this->ensure_history_table();
1868
1869 $inventory = $this->dd_inventory();
1870 $uniqueTables = array();
1871 $uniqueServers = array();
1872 $records = 0;
1873 $existing = 0;
1874 $autosync = 0;
1875 $trace = 0;
1876
1877 foreach ($inventory as $row) {
1878 $server = (string) ($row['server'] ?? '');
1879 $table = (string) ($row['table'] ?? '');
1880 $key = $server . '|' . $table;
1881
1882 if ($server !== '') {
1883 $uniqueServers[$server] = 1;
1884 }
1885
1886 if ($key !== '|' && !isset($uniqueTables[$key])) {
1887 $uniqueTables[$key] = 1;
1888 $records += (int) ($row['count'] ?? 0);
1889 }
1890
1891 if (!empty($row['exist'])) {
1892 $existing++;
1893 }
1894
1895 if (!empty($row['sync'])) {
1896 $autosync++;
1897 }
1898
1899 if (!empty($row['trace'])) {
1900 $trace++;
1901 }
1902 }
1903
1904 $onlineCutoff = date('Y-m-d H:i:s', time() - 900);
1905 $users = $this->safe_count('dbxUser');
1906 $activeUsers = $this->safe_count('dbxUser', "status = 'active'");
1907 $sessions = $this->safe_count('dbxSession');
1908 $online = $this->safe_count('dbxSession', "update_date >= '" . $onlineCutoff . "'");
1909 $sysmsg = $this->safe_count('dbxSysMsg');
1910 $sysmsgRisk = $this->safe_count('dbxSysMsg', "LOWER(status) IN ('warning','error')");
1911 $missing = $this->safe_count('dbxMissing');
1912 $traceCount = $this->safe_count('dbxTrace');
1913 $errorLogExists = dbx()->get_include_obj('dbxSysMsg')->error_log_exists();
1914
1915 $inventoryCount = count($inventory);
1916 $healthPercent = $this->percent($existing, max(1, $inventoryCount));
1917 if ($sysmsgRisk > 0) {
1918 $healthPercent = max(0, $healthPercent - min(30, $sysmsgRisk * 3));
1919 }
1920 if ($missing > 0) {
1921 $healthPercent = max(0, $healthPercent - min(20, $missing * 2));
1922 }
1923 $healthReason = $this->health_reason_label($inventoryCount, $existing, $sysmsgRisk, $missing);
1924 if ($errorLogExists) {
1925 $healthPercent = 0;
1926 $healthReason = 'dbxError.log';
1927 }
1928
1929 $this->metricCache = array(
1930 'inventory' => $inventory,
1931 'users' => $users,
1932 'active_users' => $activeUsers,
1933 'sessions' => $sessions,
1934 'online' => $online,
1935 'modules' => $this->module_count(),
1936 'dd_count' => $inventoryCount,
1937 'records' => $records,
1938 'databases' => count($uniqueServers),
1939 'tables' => count($uniqueTables),
1940 'sysmsg' => $sysmsg,
1941 'sysmsg_risk' => $sysmsgRisk,
1942 'missing' => $missing,
1943 'trace_count' => $traceCount,
1944 'existing_dd' => $existing,
1945 'autosync' => $autosync,
1946 'trace_enabled' => $trace,
1947 'health_percent' => $healthPercent,
1948 'health_reason' => $healthReason,
1949 'health_error_log' => $errorLogExists ? 1 : 0,
1950 'request_runtime_ms' => $this->request_runtime_ms(),
1951 'memory_peak_kb' => $this->memory_peak_kb(),
1952 );
1953
1954 return $this->metricCache;
1955 }
1956
1957 private function widget($data) {
1958 $tpl = dbx()->get_system_obj('dbxTPL');
1959
1960 $data['bar'] = $this->card_bar($data['label'] ?? '', $data['icon'] ?? 'bi-circle');
1961 $data['label'] = $data['label'] ?? '';
1962 $data['value'] = $data['value'] ?? '';
1963 $data['raw'] = (int) ($data['raw'] ?? 0);
1964 $data['note'] = $data['note'] ?? '';
1965 $data['icon'] = $data['icon'] ?? 'bi-circle';
1966 $data['tone'] = $data['tone'] ?? 'teal';
1967 $data['spark'] = $data['spark'] ?? '';
1968 $data['trend'] = $data['trend'] ?? '';
1969
1970 return $tpl->get_tpl('dbxAdmin|admin-dashboard-widget', $data);
1971 }
1972
1973 private function widgets($metrics, $history) {
1974 $widgets = array(
1975 array(
1976 'label' => 'Benutzer',
1977 'value' => $this->fmt($metrics['users']),
1978 'raw' => $metrics['users'],
1979 'note' => $this->fmt($metrics['active_users']) . ' aktiv',
1980 'icon' => 'bi-people',
1981 'tone' => 'teal',
1982 'spark' => $this->spark_values($history, 'users', $metrics['users']),
1983 'trend' => $this->trend_text($history, 'users'),
1984 ),
1985 array(
1986 'label' => 'Online',
1987 'value' => $this->fmt($metrics['online']),
1988 'raw' => $metrics['online'],
1989 'note' => $this->fmt($metrics['sessions']) . ' Sessions',
1990 'icon' => 'bi-broadcast-pin',
1991 'tone' => 'green',
1992 'spark' => $this->spark_values($history, 'online', $metrics['online']),
1993 'trend' => $this->trend_text($history, 'online'),
1994 ),
1995 array(
1996 'label' => 'Module',
1997 'value' => $this->fmt($metrics['modules']),
1998 'raw' => $metrics['modules'],
1999 'note' => $this->fmt($metrics['dd_count']) . ' DDs',
2000 'icon' => 'bi-grid-1x2',
2001 'tone' => 'navy',
2002 'spark' => $this->spark_values($history, 'modules', $metrics['modules']),
2003 'trend' => $this->trend_text($history, 'modules'),
2004 ),
2005 array(
2006 'label' => 'Datensaetze',
2007 'value' => $this->fmt($metrics['records']),
2008 'raw' => $metrics['records'],
2009 'note' => $this->fmt($metrics['tables']) . ' Tabellen',
2010 'icon' => 'bi-database-check',
2011 'tone' => 'amber',
2012 'spark' => $this->spark_values($history, 'records', $metrics['records']),
2013 'trend' => $this->trend_text($history, 'records'),
2014 ),
2015 array(
2016 'label' => 'Datenbanken',
2017 'value' => $this->fmt($metrics['databases']),
2018 'raw' => $metrics['databases'],
2019 'note' => $this->fmt($metrics['existing_dd']) . ' Quellen ok',
2020 'icon' => 'bi-hdd-stack',
2021 'tone' => 'cyan',
2022 'spark' => $this->spark_values($history, 'databases', $metrics['databases']),
2023 'trend' => $this->trend_text($history, 'databases'),
2024 ),
2025 array(
2026 'label' => 'Systemzustand',
2027 'value' => (int) $metrics['health_percent'] . '%',
2028 'raw' => $metrics['health_percent'],
2029 'note' => $this->fmt($metrics['sysmsg_risk']) . ' Warnungen/Fehler',
2030 'icon' => 'bi-shield-check',
2031 'tone' => 'red',
2032 'spark' => $this->spark_values($history, 'health_percent', $metrics['health_percent']),
2033 'trend' => $this->trend_text($history, 'health_percent', '%'),
2034 ),
2035 array(
2036 'label' => 'Speed',
2037 'value' => $this->fmt($metrics['request_runtime_ms']) . ' ms',
2038 'raw' => $metrics['request_runtime_ms'],
2039 'note' => 'PHP Request',
2040 'icon' => 'bi-speedometer2',
2041 'tone' => 'purple',
2042 'spark' => $this->spark_values($history, 'request_runtime_ms', $metrics['request_runtime_ms']),
2043 'trend' => $this->trend_text($history, 'request_runtime_ms', ' ms'),
2044 ),
2045 array(
2046 'label' => 'DBX Memory',
2047 'value' => $this->fmt($metrics['memory_peak_kb']) . ' KB',
2048 'raw' => $metrics['memory_peak_kb'],
2049 'note' => 'dbx Verbrauch',
2050 'icon' => 'bi-memory',
2051 'tone' => 'slate',
2052 'spark' => $this->spark_values($history, 'memory_peak_kb', $metrics['memory_peak_kb']),
2053 'trend' => $this->trend_text($history, 'memory_peak_kb', ' KB'),
2054 ),
2055 );
2056
2057 $content = '';
2058 foreach ($widgets as $widget) {
2059 $content .= $this->widget($widget);
2060 }
2061
2062 return $content;
2063 }
2064
2065 private function database_rows($metrics) {
2066 $rows = array();
2067 $inventory = $metrics['inventory'] ?? array();
2068
2069 usort($inventory, function ($a, $b) {
2070 return ((int) ($b['count'] ?? 0)) <=> ((int) ($a['count'] ?? 0));
2071 });
2072
2073 $seen = array();
2074 foreach ($inventory as $row) {
2075 $server = (string) ($row['server'] ?? '');
2076 $table = (string) ($row['table'] ?? '');
2077 $key = $server . '|' . $table;
2078
2079 if ($key === '|' || isset($seen[$key])) {
2080 continue;
2081 }
2082
2083 $seen[$key] = 1;
2084 $count = (int) ($row['count'] ?? 0);
2085 $rows[] = array(
2086 'title' => dbx()->esc($row['dd'] ?? $table),
2087 'meta' => dbx()->esc($server . ' / ' . $table),
2088 'count' => $this->fmt($count),
2089 'percent' => $this->percent($count, max(1, (int) ($metrics['records'] ?? 0))),
2090 'status' => !empty($row['exist']) ? 'ok' : 'fehlt',
2091 'tone' => !empty($row['exist']) ? 'ok' : 'warn',
2092 );
2093
2094 if (count($rows) >= 7) {
2095 break;
2096 }
2097 }
2098
2099 if (!$rows) {
2100 $rows[] = array(
2101 'title' => 'Keine Tabellen',
2102 'meta' => 'Noch keine DD-Tabellen gefunden',
2103 'count' => '0',
2104 'percent' => 0,
2105 'status' => 'leer',
2106 'tone' => 'warn',
2107 );
2108 }
2109
2110 return $rows;
2111 }
2112
2113 private function database_report($metrics) {
2114 $oReport = new \dbxReport();
2115 $panelTarget = 'database-report';
2116 $oReport->init('admin-dashboard-db', 'admin-dashboard-db-report');
2117 $oReport->add_obj('database_bar', 'dbx|component-bar', $this->card_bar_data(
2118 'Datenbanken und Tabellen',
2119 'bi-hdd-stack',
2120 '',
2121 $this->card_action('?dbx_modul=dbxAdmin&dbx_run1=db&dbx_run2=list_db', 'DB Sync')
2122 . $this->collapse_action($panelTarget, 'Zuklappen', true)
2123 ));
2124 $oReport->add_rep('panel_target', dbx()->esc($panelTarget));
2125 $oReport->_mode = 'tpl';
2126 $oReport->_pages = 0;
2127 $oReport->_rdata = $this->database_rows($metrics);
2128 $oReport->_rcount = count($oReport->_rdata);
2129 $oReport->_rrows = 20;
2130 $oReport->_msg_info = '';
2131
2132 return $oReport->run();
2133 }
2134
2135 private function activity_rows($metrics) {
2136 $rows = array();
2137 $traceRows = $this->safe_select('dbxTrace', '', array('id', 'create_date', 'action', 'dd', 'record_id'), 'create_date', 'DESC', '', 5);
2138
2139 foreach ($traceRows as $row) {
2140 $title = trim((string) ($row['action'] ?? 'Trace'));
2141 $dd = trim((string) ($row['dd'] ?? ''));
2142 $record = trim((string) ($row['record_id'] ?? ''));
2143
2144 $rows[] = array(
2145 'icon' => 'bi-clock-history',
2146 'title' => dbx()->esc($title !== '' ? ucfirst($title) : 'Trace'),
2147 'meta' => dbx()->esc(($dd !== '' ? $dd : 'Datensatz') . ($record !== '' ? ' #' . $record : '')),
2148 'time' => dbx()->esc($row['create_date'] ?? ''),
2149 'tone' => 'trace',
2150 );
2151 }
2152
2153 if (count($rows) < 5) {
2154 $msgRows = $this->safe_select('dbxSysMsg', '', array('id', 'create_date', 'status', 'modul', 'message'), 'create_date', 'DESC', '', 5 - count($rows));
2155 foreach ($msgRows as $row) {
2156 $message = trim(strip_tags((string) ($row['message'] ?? 'Systemmeldung')));
2157 if (strlen($message) > 92) {
2158 $message = substr($message, 0, 89) . '...';
2159 }
2160
2161 $rows[] = array(
2162 'icon' => 'bi-info-circle',
2163 'title' => dbx()->esc($row['status'] ?? 'Info'),
2164 'meta' => dbx()->esc($message),
2165 'time' => dbx()->esc($row['create_date'] ?? ''),
2166 'tone' => 'msg',
2167 );
2168 }
2169 }
2170
2171 if (!$rows) {
2172 $rows[] = array(
2173 'icon' => 'bi-check2-circle',
2174 'title' => 'Keine Aktivitaet',
2175 'meta' => 'Trace und Systemmeldungen sind aktuell leer.',
2176 'time' => '',
2177 'tone' => 'empty',
2178 );
2179 }
2180
2181 return $rows;
2182 }
2183
2184 private function activity_report($metrics) {
2185 $oReport = new \dbxReport();
2186 $oReport->init('admin-dashboard-activity', 'admin-dashboard-activity-report');
2187 $oReport->add_obj('activity_bar', 'dbx|component-bar', $this->card_bar_data('Aktivitaet', 'bi-clock-history'));
2188 $oReport->_mode = 'tpl';
2189 $oReport->_pages = 0;
2190 $oReport->_rdata = $this->activity_rows($metrics);
2191 $oReport->_rcount = count($oReport->_rdata);
2192 $oReport->_rrows = 10;
2193 $oReport->_msg_info = '';
2194
2195 return $oReport->run();
2196 }
2197
2198 private function quick_actions() {
2199 $oForm = new \dbxForm();
2200 $oForm->init('admin-dashboard-actions', 'admin-dashboard-actions');
2201 $oForm->_fd = 'dbxAdmin|admin-dashboard-status';
2202 $oForm->load_fd_messages();
2203 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run';
2204 $oForm->_msg_info = '';
2205 $oForm->add_obj('actions_bar', 'dbx|component-bar', $this->card_bar_data('Quick Actions', 'bi-lightning-charge'));
2206
2207 $actions = array(
2208 'users' => array('href' => '?dbx_modul=dbxAdmin&dbx_run1=user&dbx_run2=list_user', 'icon' => 'bi-people', 'label' => 'Benutzer', 'ajax_class' => 'dbxAjax'),
2209 'sessions' => array('href' => '?dbx_modul=dbxAdmin&dbx_run1=session&dbx_run2=list_session', 'icon' => 'bi-broadcast', 'label' => 'Sessions', 'ajax_class' => 'dbxAjax'),
2210 'modules' => array('href' => '?dbx_modul=dbxAdmin&dbx_run1=modules&dbx_run2=modul_list', 'icon' => 'bi-grid', 'label' => 'Module', 'ajax_class' => 'dbxAjax'),
2211 'dd' => array('href' => '?dbx_modul=dbxAdmin&dbx_run1=dd&dbx_run2=list_dd', 'icon' => 'bi-diagram-3', 'label' => 'DD Sync', 'ajax_class' => 'dbxAjax'),
2212 'db' => array('href' => '?dbx_modul=dbxAdmin&dbx_run1=db&dbx_run2=list_db', 'icon' => 'bi-hdd-stack', 'label' => 'DB Sync', 'ajax_class' => 'dbxAjax'),
2213 'sysmsg' => array('href' => '?dbx_modul=dbxAdmin&dbx_run1=sysmsg&dbx_run2=list_sysmsg', 'icon' => 'bi-bell', 'label' => 'SysMsg', 'ajax_class' => 'dbxAjax'),
2214 'update' => array('href' => '?dbx_modul=dbxAdmin&dbx_run1=update', 'icon' => 'bi-arrow-repeat', 'label' => $oForm->get_fd_message('update_title'), 'ajax_class' => ''),
2215 );
2216
2217 foreach ($actions as $key => $data) {
2218 $oForm->add_obj('action_' . $key, 'dbxAdmin|admin-dashboard-action-link', $data);
2219 }
2220
2221 return $oForm->run();
2222 }
2223
2224 private function load_content_cache_classes(): void {
2225 dbx()->load_content_cache_classes();
2226 }
2227
2228 private function content_cache_language_catalog(): array {
2229 return array(
2230 'de' => array('label' => 'Deutsch', 'flag' => '🇩🇪', 'tone' => 'teal'),
2231 'en' => array('label' => 'English', 'flag' => '🇬🇧', 'tone' => 'navy'),
2232 'es' => array('label' => 'Español', 'flag' => '🇪🇸', 'tone' => 'amber'),
2233 );
2234 }
2235
2236 private function content_cache_enabled_config(): bool {
2237 return \dbx\dbxContent\dbxContentPageCache::isConfigEnabled();
2238 }
2239
2240 private function dbx_config_bool(string $key, $default = 0): bool {
2241 $value = dbx()->get_config('dbx', $key);
2242 if ($value === 'undef' || $value === '' || $value === null) {
2243 $value = $default;
2244 }
2245
2246 return (int) $value === 1;
2247 }
2248
2249 private function performance_request_enabled_config(): bool {
2250 return $this->dbx_config_bool('performance_timer_request', dbx()->get_config('dbx', 'performance_timer'));
2251 }
2252
2253 private function performance_db_enabled_config(): bool {
2254 return $this->dbx_config_bool('performance_timer_db', dbx()->get_config('dbx', 'performance_timer_detail'));
2255 }
2256
2257 private function normalize_performance_level($level): string {
2258 $level = strtolower(trim((string) $level));
2259 if ($level === 'details') {
2260 $level = 'detail';
2261 }
2262
2263 return in_array($level, array('off', 'main', 'detail'), true) ? $level : 'off';
2264 }
2265
2266 private function performance_level_config(): string {
2267 $level = dbx()->get_config('dbx', 'performance_timer_level');
2268 if ($level !== 'undef' && $level !== '' && $level !== null) {
2269 return $this->normalize_performance_level($level);
2270 }
2271
2272 return ($this->performance_request_enabled_config() || $this->performance_db_enabled_config()) ? 'detail' : 'off';
2273 }
2274
2275 private function set_performance_level_config(string $level): bool {
2276 $level = $this->normalize_performance_level($level);
2277 $config = dbx()->get_config('dbx');
2278 if (!is_array($config)) {
2279 $config = array();
2280 }
2281
2282 $enabled = $level !== 'off';
2283 $detail = $level === 'detail';
2284 $config['performance_timer_level'] = $level;
2285 $config['performance_timer'] = $enabled ? 1 : 0;
2286 $config['performance_timer_request'] = $enabled ? 1 : 0;
2287 $config['performance_timer_db'] = $enabled ? 1 : 0;
2288 $config['performance_timer_detail'] = $detail ? 1 : 0;
2289
2290 return (int) dbx()->set_config('dbx', $config) > 0;
2291 }
2292
2293 private function set_performance_config(string $target, bool $enabled): bool {
2294 if (!$enabled) {
2295 return $this->set_performance_level_config('off');
2296 }
2297
2298 return $this->set_performance_level_config('detail');
2299 }
2300
2301 private function process_performance_config_action(): bool {
2302 $run2 = dbx()->get_modul_var('dbx_run2', '', 'parameter');
2303 if ($run2 !== 'performance_save') {
2304 return false;
2305 }
2306
2307 if (isset($_POST['performance_level'])) {
2308 return $this->set_performance_level_config((string) $_POST['performance_level']);
2309 }
2310
2311 $target = strtolower(trim((string) ($_POST['performance_target'] ?? 'request')));
2312 $target = $target === 'db' ? 'db' : 'request';
2313 $enabled = isset($_POST['performance_enabled']);
2314 return $this->set_performance_config($target, $enabled);
2315 }
2316
2317 private function is_ajax_request(): bool {
2318 return (int) dbx()->get_system_var('dbx_ajax', 0, 'int') === 1;
2319 }
2320
2321 private function respond_dashboard_ajax_html(string $html): void {
2322 if (!headers_sent()) {
2323 header('Content-Type: text/html; charset=utf-8');
2324 }
2325
2326 echo $html;
2327
2328 $oSession = dbx()->get_system_obj('dbxSession');
2329 if (is_object($oSession) && method_exists($oSession, 'save_session')) {
2330 $oSession->save_session();
2331 }
2332
2333 exit;
2334 }
2335
2336 private function performance_level_options(string $current): string {
2337 $options = array(
2338 'off' => 'Aus',
2339 'main' => 'Nur Hauptkennzahlen',
2340 'detail' => 'Hauptkennzahlen und Details',
2341 );
2342 $html = '';
2343
2344 foreach ($options as $value => $label) {
2345 $selected = $value === $current ? ' selected' : '';
2346 $html .= '<option value="' . dbx()->esc($value) . '"' . $selected . '>' . dbx()->esc($label) . '</option>';
2347 }
2348
2349 return $html;
2350 }
2351
2352 private function performance_level_control(): string {
2353 $level = $this->performance_level_config();
2354 $meta = array(
2355 'off' => array(
2356 'label' => 'Performance aus',
2357 'hint' => 'dbxapp schreibt aktuell keine neuen Performance-Daten.',
2358 'icon' => 'bi-pause-circle-fill',
2359 'tone' => 'off',
2360 ),
2361 'main' => array(
2362 'label' => 'Performance Hauptkennzahlen',
2363 'hint' => 'dbxapp schreibt nur PHP/System, JS und DB Gesamtwerte.',
2364 'icon' => 'bi-check-circle-fill',
2365 'tone' => 'on',
2366 ),
2367 'detail' => array(
2368 'label' => 'Performance Details',
2369 'hint' => 'dbxapp schreibt Hauptkennzahlen und Detail-Timer.',
2370 'icon' => 'bi-check-circle-fill',
2371 'tone' => 'on',
2372 ),
2373 );
2374 $state = $meta[$level] ?? $meta['off'];
2375
2376 $oForm = new \dbxForm();
2377 $oForm->init('admin-dashboard-performance-config', 'admin-dashboard-performance-config');
2378 $oForm->set_form_help_enabled(false);
2379 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=performance_save';
2380 $oForm->_msg_info = '';
2381 $oForm->add_rep('action', dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=performance_save'));
2382 $oForm->add_rep('status_tone', dbx()->esc($state['tone']));
2383 $oForm->add_rep('status_icon', dbx()->esc($state['icon']));
2384 $oForm->add_rep('status_label', dbx()->esc($state['label']));
2385 $oForm->add_rep('status_hint', dbx()->esc($state['hint']));
2386 $oForm->add_rep('performance_level_options', $this->performance_level_options($level));
2387
2388 return $oForm->add_norep($oForm->run());
2389 }
2390
2391 private function performance_toggle_control(string $target): string {
2392 return $this->performance_level_control();
2393 }
2394
2395 private function content_cache_files_for_lng(string $lng): int {
2397 $lng = strtolower(trim($lng));
2398 if ($lng === '') {
2399 return 0;
2400 }
2401
2403 return count(glob($base . 'full-page/*_' . $lng . '_*.htm') ?: array());
2404 }
2405
2406 private function content_cache_language_rows(): string {
2408 $tpl = dbx()->get_system_obj('dbxTPL');
2409 $catalog = $this->content_cache_language_catalog();
2410 $lngs = function_exists('dbx_accessible_lngs') ? dbx_accessible_lngs() : array('de');
2411 $tones = array('teal', 'navy', 'amber', 'cyan', 'purple', 'green');
2412 $rows = '';
2413 $toneIndex = 0;
2414
2415 foreach ($lngs as $lng) {
2416 $lng = strtolower(trim((string) $lng));
2417 if ($lng === '') {
2418 continue;
2419 }
2420
2421 $meta = $catalog[$lng] ?? array(
2422 'label' => strtoupper($lng),
2423 'flag' => '🌐',
2424 'tone' => $tones[$toneIndex % count($tones)],
2425 );
2426 $toneIndex++;
2427
2428 $pagesTotal = $this->safe_count(\dbx\dbxContent\dbxContentLng::ddContent($lng));
2429 $pagesActive = $this->safe_count(\dbx\dbxContent\dbxContentLng::ddContent($lng), 'activ = 1');
2430 $folders = $this->safe_count(\dbx\dbxContent\dbxContentLng::ddFolder($lng));
2431
2432 $rows .= $tpl->get_tpl('dbxAdmin|admin-dashboard-content-cache-lng', array(
2433 'flag' => dbx()->esc($meta['flag'] ?? '🌐'),
2434 'label' => dbx()->esc($meta['label'] ?? strtoupper($lng)),
2435 'code' => dbx()->esc(strtoupper($lng)),
2436 'tone' => dbx()->esc($meta['tone'] ?? 'teal'),
2437 'pages_total' => $this->fmt($pagesTotal),
2438 'pages_active' => $this->fmt($pagesActive),
2439 'folders' => $this->fmt($folders),
2440 'cached' => $this->fmt($this->content_cache_files_for_lng($lng)),
2441 ));
2442 }
2443
2444 if ($rows === '') {
2445 $rows = '<article class="dbx-admin-dashboard-cache-lng-card dbx-admin-dashboard-cache-lng-empty">'
2446 . '<div class="dbx-admin-dashboard-cache-lng-title"><strong>Keine Sprachen konfiguriert</strong></div>'
2447 . '</article>';
2448 }
2449
2450 return $rows;
2451 }
2452
2453 private function process_content_cache_action(): void {
2454 $run2 = dbx()->get_modul_var('dbx_run2', '', 'parameter');
2455 if ($run2 === '') {
2456 return;
2457 }
2458
2460
2461 if ($run2 === 'cache_flush') {
2463 return;
2464 }
2465
2466 if ($run2 === 'sitemap_rebuild') {
2468 return;
2469 }
2470
2471 if ($run2 === 'cache_save') {
2472 $enabled = isset($_POST['cache_content']);
2474 }
2475 }
2476
2477 private function content_cache_panel_body_data(): array {
2481 $enabled = $this->content_cache_enabled_config();
2482
2483 return array(
2484 'cache_save_url' => dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=cache_save'),
2485 'cache_flush_url' => dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=cache_flush'),
2486 'sitemap_rebuild_url' => dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=sitemap_rebuild'),
2487 'sitemap_url' => dbx()->esc(rtrim((string) dbx()->get_base_url(), '/') . '/sitemap.xml'),
2488 'cache_admin_url' => dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=cache'),
2489 'cache_enabled_checked' => $enabled ? 'checked' : '',
2490 'cache_status_tone' => $enabled ? 'on' : 'off',
2491 'cache_status_icon' => $enabled ? 'bi-check-circle-fill' : 'bi-pause-circle-fill',
2492 'cache_status_label' => dbx()->esc($enabled ? 'Cache-Schreiben aktiv' : 'Cache-Schreiben pausiert'),
2493 'cache_status_hint' => $enabled
2494 ? dbx()->esc('Vorhandene Gastseiten werden gelesen; Cache-Misses werden als vollstaendige Endausgabe gespeichert.')
2495 . '<br>'
2496 . dbx()->esc('Head-Metadaten, Design, Menues und Module sind fertig aufgeloest; ein HIT braucht nur den Session-Zugriff, aber keine Content-Datenbank.')
2497 : dbx()->esc('Vorhandene Gastseiten werden weiterhin aus dem Cache ausgegeben. Cache-Misses werden live gerendert, aber nicht neu gespeichert.'),
2498 'cache_content_count' => $this->fmt((int) ($stats['content'] ?? 0)),
2499 'sitemap_count' => $this->fmt((int) ($sitemapStats['urls'] ?? 0)),
2500 'sitemap_generated' => dbx()->esc((string) ($sitemapStats['generated_at'] ?? '')),
2501 'sitemap_state' => dbx()->esc(!empty($sitemapStats['exists']) ? 'vorhanden' : 'nicht erstellt'),
2502 'cache_dir' => dbx()->esc((string) ($stats['base_dir'] ?? '')),
2503 'lng_rows' => $this->content_cache_language_rows(),
2504 );
2505 }
2506
2507 private function content_cache_panel_body_html(): string {
2508 $oForm = new \dbxForm();
2509 $oForm->init('admin-dashboard-content-cache-body', 'admin-dashboard-content-cache-body');
2510 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run';
2511 $oForm->_msg_info = '';
2512
2513 foreach ($this->content_cache_panel_body_data() as $key => $value) {
2514 $oForm->add_rep($key, $value);
2515 }
2516
2517 return $oForm->add_norep($oForm->run());
2518 }
2519
2520 private function content_cache_bar_actions_html(): string {
2522 $enabled = $this->content_cache_enabled_config();
2523 $oForm = new \dbxForm();
2524 $oForm->init('admin-dashboard-content-cache-actions', 'admin-dashboard-content-cache-actions');
2525 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=cache_save';
2526 $oForm->_msg_info = '';
2527 $oForm->add_rep('cache_save_url', dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=cache_save'));
2528 $oForm->add_rep('cache_flush_url', dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=cache_flush'));
2529 $oForm->add_rep('sitemap_rebuild_url', dbx()->esc('?dbx_modul=dbxAdmin&dbx_run1=run&dbx_run2=sitemap_rebuild'));
2530 $oForm->add_rep('cache_enabled_checked', $enabled ? 'checked' : '');
2531 $oForm->add_rep('bar_extra', $this->help_action('cache'));
2532
2533 return $oForm->add_norep($oForm->run());
2534 }
2535
2536 private function content_cache_panel() {
2537 $oForm = new \dbxForm();
2538 $panelTarget = 'content-cache';
2539 $oForm->init('admin-dashboard-content-cache', 'admin-dashboard-content-cache');
2540 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run';
2541 $oForm->_msg_info = '';
2542 $oForm->add_obj('cache_bar', 'dbx|component-bar', $this->card_bar_data(
2543 'Gast-Full-Page-Cache',
2544 'bi-lightning-charge-fill',
2545 'Komplette Endausgabe gueltiger Permalinks, ausschliesslich fuer nicht angemeldete Gaeste',
2546 $this->content_cache_bar_actions_html()
2547 . $this->card_action('?dbx_modul=dbxContent_admin&dbx_run1=content&dbx_run2=edit', 'CMS')
2548 . $this->collapse_action($panelTarget, 'Zuklappen', true)
2549 ));
2550 $oForm->add_rep('panel_target', dbx()->esc($panelTarget));
2551 $oForm->add_rep('cache_body', $this->content_cache_panel_body_html());
2552
2553 return $oForm->run();
2554 }
2555
2556 private function hero_panel($metrics) {
2557 $heroPerformance = $this->hero_performance($metrics);
2558 $oForm = new \dbxForm();
2559 $oForm->init('admin-dashboard-hero', 'admin-dashboard-hero');
2560 $oForm->_fd = 'dbxAdmin|admin-dashboard-status';
2561 $oForm->load_fd_messages();
2562 $oForm->_msg_info = '';
2563 $oForm->add_rep('bar_class', 'dbx-admin-dashboard-hero-bar');
2564 $oForm->add_rep('bar_title', $oForm->get_fd_message('bar_title'));
2565 $oForm->add_rep('bar_subtitle', $oForm->get_fd_message('bar_subtitle'));
2566 $oForm->add_obj(
2567 'bar_actions',
2568 'obj-value',
2569 '<small class="dbx-bar-meta">'
2570 . dbx()->esc($oForm->get_fd_message('status_timestamp'))
2571 . ' '
2572 . dbx()->esc(date('d.m.Y H:i'))
2573 . '</small>'
2574 );
2575 $oForm->add_rep('health_percent', (int) $metrics['health_percent']);
2576 $hasErrorLog = !empty($metrics['health_error_log']);
2577 $healthReason = $hasErrorLog
2578 ? $oForm->get_fd_message('health_error')
2579 : (string)($metrics['health_reason'] ?? $oForm->get_fd_message('health_ok'));
2580 if ($healthReason === 'OK') {
2581 $healthReason = $oForm->get_fd_message('health_ok');
2582 }
2583 $oForm->add_rep('health_reason', dbx()->esc($healthReason));
2584 $oForm->add_rep('health_state_class', $hasErrorLog ? 'is-error' : 'is-ok');
2585 $oForm->add_rep('health_icon', $hasErrorLog ? 'bi-exclamation-octagon-fill' : 'bi-shield-check');
2586 $oForm->add_rep('performance_aria', $oForm->get_fd_message('performance_aria'));
2587 $oForm->add_rep('system_status_label', $oForm->get_fd_message('system_status_label'));
2588 $oForm->add_obj('hero_performance_gauges', 'obj-value', $this->hero_performance_gauges($heroPerformance));
2589 $oForm->add_obj('hero_status_summaries', 'obj-value', $this->hero_status_summaries());
2590 $oForm->add_obj('update_status', 'obj-value', $this->update_status_panel($oForm));
2591 $oForm->add_obj('error_log', 'obj-value', $hasErrorLog ? $this->error_log_panel($oForm) : '');
2592
2593 return $oForm->run();
2594 }
2595
2596 private function widgets_panel($metrics) {
2597 $oForm = new \dbxForm();
2598 $oForm->init('admin-dashboard-widgets', 'admin-dashboard-widgets');
2599 $oForm->_msg_info = '';
2600 $oForm->add_obj('widgets', 'obj-value', $this->widgets($metrics, $this->metric_history($metrics)));
2601
2602 return $oForm->run();
2603 }
2604
2605 private function chart_json($metrics) {
2606 $data = array(
2607 array('label' => 'Benutzer', 'value' => (int) $metrics['users'], 'tone' => 'teal'),
2608 array('label' => 'Online', 'value' => (int) $metrics['online'], 'tone' => 'green'),
2609 array('label' => 'Module', 'value' => (int) $metrics['modules'], 'tone' => 'navy'),
2610 array('label' => 'DDs', 'value' => (int) $metrics['dd_count'], 'tone' => 'cyan'),
2611 array('label' => 'DBs', 'value' => (int) $metrics['databases'], 'tone' => 'amber'),
2612 );
2613
2614 return dbx()->esc(json_encode($data, JSON_UNESCAPED_UNICODE | JSON_HEX_APOS | JSON_HEX_QUOT));
2615 }
2616
2617 private function chart_panel($metrics) {
2618 $oForm = new \dbxForm();
2619 $oForm->init('admin-dashboard-chart-panel', 'admin-dashboard-chart-panel');
2620 $oForm->_msg_info = '';
2621 $oForm->add_rep('chart_json', $this->chart_json($metrics));
2622 $oForm->add_rep('trace_count', $this->fmt($metrics['trace_count']));
2623 $oForm->add_rep('missing_count', $this->fmt($metrics['missing']));
2624 $oForm->add_rep('autosync_count', $this->fmt($metrics['autosync']));
2625 $oForm->add_rep('trace_enabled_count', $this->fmt($metrics['trace_enabled']));
2626 $oForm->add_rep('request_runtime_ms', $this->fmt($metrics['request_runtime_ms']));
2627 $oForm->add_rep('memory_peak_kb', $this->fmt($metrics['memory_peak_kb']));
2628 $oForm->add_obj('chart_bar', 'dbx|component-bar', $this->card_bar_data('Grafische Auswertung', 'bi-bar-chart-line', 'Relative Verteilung'));
2629
2630 return $oForm->run();
2631 }
2632
2633 private function dashboard_area($area) {
2634 $metrics = $this->metrics();
2635
2636 switch ((string) $area) {
2637 case 'hero':
2638 return $this->hero_panel($metrics);
2639 case 'widgets':
2640 return $this->widgets_panel($metrics);
2641 case 'quick_actions':
2642 return $this->quick_actions();
2643 case 'performance_panel':
2644 return $this->performance_panel($metrics);
2645 case 'sysmsg_panel':
2646 return $this->sysmsg_panel();
2647 case 'session_panel':
2648 return $this->session_panel();
2649 case 'content_cache_panel':
2650 return $this->content_cache_panel();
2651 case 'chart_panel':
2652 return $this->chart_panel($metrics);
2653 case 'activity_report':
2654 return $this->activity_report($metrics);
2655 case 'database_report':
2656 return $this->database_report($metrics);
2657 }
2658
2659 return null;
2660 }
2661
2662 public function run() {
2663 $run2 = dbx()->get_modul_var('dbx_run2', '', 'parameter');
2664 if ($run2 === 'delete_error_log') {
2665 $this->process_error_log_action();
2666 $run2 = '';
2667 }
2668 if ($run2 === 'performance_compress') {
2669 return $this->run_performance_maintenance_process('compress');
2670 }
2671 if ($run2 === 'performance_clear') {
2672 return $this->run_performance_maintenance_process('clear');
2673 }
2674
2675 if ($this->is_ajax_request()) {
2676 if ($run2 === 'cache_flush' || $run2 === 'cache_save' || $run2 === 'sitemap_rebuild') {
2677 $this->process_content_cache_action();
2678 $this->respond_dashboard_ajax_html($this->content_cache_panel_body_html());
2679 }
2680
2681 if ($run2 === 'performance_save') {
2682 $this->process_performance_config_action();
2683 $target = strtolower(trim((string) ($_POST['performance_target'] ?? 'request')));
2684 $target = $target === 'db' ? 'db' : 'request';
2685 $this->respond_dashboard_ajax_html($this->performance_toggle_control($target));
2686 }
2687
2688 if ($run2 === 'sysmsg_level_save') {
2689 $this->process_sys_msg_level_action();
2690 $this->respond_dashboard_ajax_html($this->sys_msg_level_control());
2691 }
2692
2693 if ($run2 === 'session_db_save') {
2694 $this->process_session_db_action();
2695 $this->respond_dashboard_ajax_html($this->session_panel_control_html());
2696 }
2697 }
2698
2699 if ($run2 === 'performance_save') {
2700 $this->process_performance_config_action();
2701 }
2702 if ($run2 === 'sysmsg_level_save') {
2703 $this->process_sys_msg_level_action();
2704 }
2705 if ($run2 === 'session_db_save') {
2706 $this->process_session_db_action();
2707 }
2708 if ($run2 === 'cache_flush' || $run2 === 'cache_save' || $run2 === 'sitemap_rebuild') {
2709 $this->process_content_cache_action();
2710 }
2711
2712 if ($run2 !== '') {
2713 $areaContent = $this->dashboard_area($run2);
2714 if ($areaContent !== null) {
2715 return $areaContent;
2716 }
2717 }
2718
2719 $oForm = new \dbxForm();
2720
2721 $oForm->init('admin-dashboard', 'admin-dashboard');
2722 $oForm->_fd = 'dbxAdmin|admin-dashboard-status';
2723 $oForm->load_fd_messages();
2724 $oForm->_action = '?dbx_modul=dbxAdmin&dbx_run1=run';
2725 $oForm->_msg_info = '';
2726
2727 if ($this->dashboardMessageKey !== '') {
2728 $message = $oForm->get_fd_message($this->dashboardMessageKey);
2729 if ($this->dashboardMessageError) {
2730 $oForm->_msg_error = $message;
2731 } else {
2732 $oForm->_msg_success = $message;
2733 }
2734 }
2735
2736 $messageHtml = $this->default_admin_password_warning();
2737 if ($this->dashboardMessageKey !== '') {
2738 $message = dbx()->esc($oForm->get_fd_message($this->dashboardMessageKey));
2739 $tone = $this->dashboardMessageError ? 'danger' : 'success';
2740 $icon = $this->dashboardMessageError
2741 ? 'bi-exclamation-triangle-fill'
2742 : 'bi-check-circle-fill';
2743 $messageHtml .= '<div class="alert alert-' . $tone
2744 . ' d-flex align-items-center gap-2 mb-3" role="alert">'
2745 . '<i class="bi ' . $icon . '" aria-hidden="true"></i>'
2746 . '<span>' . $message . '</span></div>';
2747 }
2748 $oForm->add_obj('dashboard_message', 'obj-value', $messageHtml);
2749
2750 $updateState = $this->update_state($this->update_status());
2751 $oForm->add_rep('update_nav_class', dbx()->esc($updateState['class']));
2752 $oForm->add_rep(
2753 'update_nav_badge',
2754 dbx()->esc($oForm->get_fd_message($updateState['nav_message']))
2755 );
2756 $oForm->add_rep('update_nav_label', dbx()->esc($oForm->get_fd_message('update_nav_title')));
2757
2758 $metrics = $this->metrics();
2759 $this->store_history_snapshot($metrics);
2760
2761 $content = $oForm->run();
2762
2763 return $content;
2764 }
2765}
$table['server']
Definition .dd.php:6
static configured()
Creates the centrally configured updater used by all admin surfaces.
$messages
Definition config.fd.php:2
user($key='id')
Liest einen Wert des aktuellen Benutzers.
Definition dbxApi.php:1305
dbx()
Liefert die zentrale dbXapp-API als Singleton.
Definition dbxApi.php:2805
get_base_dir(int $cutData=0)
Liefert das Basisverzeichnis der Installation.
Definition dbxApi.php:1507
esc($value)
Escaped einen Wert fuer HTML-Text und HTML-Attribute.
Definition dbxApi.php:2310
dbx_accessible_lngs()
Konfigurierte Sprachen aus dbx-Config.
Definition dbxApi.php:3048
load_content_cache_classes()
Laedt die Content-Cache-Klassen einmalig fuer Frontend, CMS und Admin.
Definition dbxApi.php:1898
get_base_url()
Liefert die Basis-URL des aktuellen Requests.
Definition dbxApi.php:1488
if($resolved !==$expectedBase . 'files/test/') $config
if(!empty( $validation[ 'errors'])) if($service->isAllowedDesignFile('../escape.php')|| $service->isAllowedDesignFile('htm/evil.php')) $delta
if($web->content_permalink_redirect_target() !=='') $tpl
if($out !==$expected) $missing
exit
Definition index.php:146
if( $demoId<=0) foreach(array('create_date', 'create_uid', 'update_date', 'update_uid', 'owner',) as $systemField) $items
Class dbxServer extends dbxObj
DBX schema administration.