diff --git a/modules/mining-checker/assets/js/app.js b/modules/mining-checker/assets/js/app.js index 06a61e9..365ab77 100644 --- a/modules/mining-checker/assets/js/app.js +++ b/modules/mining-checker/assets/js/app.js @@ -26,20 +26,7 @@ return []; } })(); - const initialDebugMode = (() => { - try { - return window.localStorage.getItem('mining-checker-debug-enabled') === '1'; - } catch (error) { - return false; - } - })(); - const initialDebugConsoleOpen = (() => { - try { - return window.localStorage.getItem('mining-checker-debug-console-open') === '1'; - } catch (error) { - return false; - } - })(); + const initialDebugMode = document.body && document.body.dataset.nexusDebugEnabled === '1'; function getCookie(name) { const pattern = `; ${document.cookie}`; const parts = pattern.split(`; ${name}=`); @@ -52,20 +39,12 @@ function setCookie(name, value, maxAgeSeconds) { document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAgeSeconds}; samesite=lax`; } - const initialDebugView = (() => { - try { - const value = window.localStorage.getItem('mining-checker-debug-view'); - return value === 'text' ? 'text' : 'structured'; - } catch (error) { - return 'structured'; - } - })(); - const debugBus = window.__miningCheckerDebugBus || { + const debugBus = window.__nexusDebugBus || { enabled: initialDebugMode, listener: null, sequence: 0, }; - window.__miningCheckerDebugBus = debugBus; + window.__nexusDebugBus = debugBus; function emitDebug(entry) { debugBus.sequence += 1; @@ -80,11 +59,6 @@ } } - function persistDebugCookie(enabled) { - const value = enabled ? '1' : '0'; - document.cookie = `mining_checker_debug=${value}; path=/; max-age=31536000; samesite=lax`; - } - function emitServerTraceEntries(trace, meta) { if (!Array.isArray(trace) || !trace.length) { emitDebug({ @@ -585,10 +559,6 @@ const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [message, setMessage] = useState(''); - const [debugEnabled, setDebugEnabled] = useState(initialDebugMode); - const [debugConsoleOpen, setDebugConsoleOpen] = useState(initialDebugConsoleOpen); - const [debugView, setDebugView] = useState(initialDebugView); - const [debugEntries, setDebugEntries] = useState([]); const [schemaStatus, setSchemaStatus] = useState(normalizeSchemaStatus(null)); const [initForm, setInitForm] = useState({ drop_existing: false }); const [sqlImportFile, setSqlImportFile] = useState(null); @@ -706,43 +676,12 @@ }); useEffect(() => { - debugBus.enabled = debugEnabled; - debugBus.listener = (entry) => { - setDebugEntries((current) => [entry].concat(current).slice(0, 250)); - }; - - try { - window.localStorage.setItem('mining-checker-debug-enabled', debugEnabled ? '1' : '0'); - } catch (error) { - // Ignore localStorage write failures. - } - persistDebugCookie(debugEnabled); - + debugBus.enabled = initialDebugMode; emitDebug({ type: 'debug:mode', - enabled: debugEnabled, + enabled: initialDebugMode, }); - - return () => { - debugBus.listener = null; - }; - }, [debugEnabled]); - - useEffect(() => { - try { - window.localStorage.setItem('mining-checker-debug-console-open', debugConsoleOpen ? '1' : '0'); - } catch (error) { - // Ignore localStorage write failures. - } - }, [debugConsoleOpen]); - - useEffect(() => { - try { - window.localStorage.setItem('mining-checker-debug-view', debugView); - } catch (error) { - // Ignore localStorage write failures. - } - }, [debugView]); + }, []); const measurements = Array.isArray(payload?.measurements) ? payload.measurements : []; const latest = payload?.summary?.latest_measurement || null; @@ -1765,46 +1704,10 @@ } } - async function copyDebugConsole() { - const content = debugEntries - .slice() - .reverse() - .map((entry) => `${entry.time} · ${entry.type}\n${JSON.stringify(entry, null, 2)}`) - .join('\n\n'); - - if (!content) { - setMessage('Keine Debug-Ausgaben zum Kopieren vorhanden.'); - return; - } - - try { - if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(content); - } else { - const textarea = document.createElement('textarea'); - textarea.value = content; - textarea.setAttribute('readonly', 'readonly'); - textarea.style.position = 'absolute'; - textarea.style.left = '-9999px'; - document.body.appendChild(textarea); - textarea.select(); - document.execCommand('copy'); - document.body.removeChild(textarea); - } - - setMessage('Debug-Inhalt wurde in die Zwischenablage kopiert.'); - } catch (err) { - setError('Debug-Inhalt konnte nicht kopiert werden.'); - } - } - function resetReportCurrencyOverride() { setReportCurrencyOverride(''); setCookie('mining_checker_report_currency', '', 0); } - const debugConsoleText = debugEntries - .map((entry) => `${entry.time} · ${entry.type}\n${JSON.stringify(entry, null, 2)}`) - .join('\n\n'); return h('div', { className: 'mc-grid-bg', }, [ @@ -1813,78 +1716,6 @@ message ? h('div', { key: 'message', className: 'mc-alert mc-alert--success' }, message) : null, loading ? h('div', { key: 'loading', className: 'mc-empty' }, 'Lade Mining-Checker Daten …') : null, payload ? renderTab() : null, - h('div', { key: 'debug-tools', className: 'mc-debug-tools' }, [ - h('button', { - key: 'debug-toggle', - type: 'button', - className: cx('mc-button', debugEnabled ? 'mc-button--secondary' : 'mc-button--ghost'), - onClick: () => { - const next = !debugEnabled; - setDebugEnabled(next); - if (next) { - setDebugConsoleOpen(true); - } - }, - }, debugEnabled ? 'Debug aktiv' : 'Debug einschalten'), - h('button', { - key: 'console-toggle', - type: 'button', - className: 'mc-button mc-button--ghost', - onClick: () => setDebugConsoleOpen((current) => { - const next = !current; - if (next) { - setDebugEnabled(true); - } - return next; - }), - }, debugConsoleOpen ? 'Online-Konsole ausblenden' : 'Online-Konsole einblenden'), - debugEntries.length ? h('button', { - key: 'debug-clear', - type: 'button', - className: 'mc-button mc-button--ghost', - onClick: () => setDebugEntries([]), - }, 'Konsole leeren') : null, - debugEntries.length ? h('button', { - key: 'debug-copy', - type: 'button', - className: 'mc-button mc-button--ghost', - onClick: copyDebugConsole, - }, 'Copy content') : null, - ]), - debugConsoleOpen ? h('section', { key: 'debug-console', className: 'mc-panel mc-debug-console' }, [ - h(SectionTitle, { - key: 'debug-title', - title: 'Online-Konsole', - subtitle: 'Zeigt API-, Provider- und DB-Debugspuren direkt aus dem Modul.', - action: h(Badge, { tone: debugEnabled ? 'success' : 'warn' }, debugEnabled ? 'Debug aktiv' : 'Debug aus'), - }), - h('div', { key: 'debug-body', className: 'mc-panel-body' }, [ - h('div', { key: 'debug-view-switch', className: 'mc-debug-view-switch' }, [ - h('button', { - key: 'view-structured', - type: 'button', - className: cx('mc-button', debugView === 'structured' ? 'mc-button--tab-active' : 'mc-button--tab'), - onClick: () => setDebugView('structured'), - }, 'Strukturiert'), - h('button', { - key: 'view-text', - type: 'button', - className: cx('mc-button', debugView === 'text' ? 'mc-button--tab-active' : 'mc-button--tab'), - onClick: () => setDebugView('text'), - }, 'Text-Konsole'), - ]), - debugView === 'text' - ? h('pre', { className: 'mc-code-block mc-debug-text-console' }, debugConsoleText || 'Noch keine Debug-Ausgaben vorhanden.') - : h('div', { className: 'mc-debug-log' }, - debugEntries.length - ? debugEntries.map((entry) => h('div', { key: entry.id, className: 'mc-debug-entry' }, [ - h('div', { key: 'meta', className: 'mc-kicker' }, `${entry.time} · ${entry.type}`), - h('pre', { key: 'payload', className: 'mc-code-block' }, JSON.stringify(entry, null, 2)), - ])) - : h('div', { className: 'mc-empty' }, 'Noch keine Debug-Ausgaben vorhanden.') - ), - ]), - ]) : null, ]), ]); diff --git a/modules/mining-checker/src/Api/Router.php b/modules/mining-checker/src/Api/Router.php index 6988913..5e7ee28 100644 --- a/modules/mining-checker/src/Api/Router.php +++ b/modules/mining-checker/src/Api/Router.php @@ -44,14 +44,7 @@ final class Router $this->config = ModuleConfig::load($this->moduleBasePath); $requestUri = (string) ($_SERVER['REQUEST_URI'] ?? ''); $requestPath = (string) (parse_url($requestUri, PHP_URL_PATH) ?: ''); - $debugConfig = $this->config->debug(); - $debugEnabled = filter_var( - $_GET['debug'] - ?? $_SERVER['HTTP_X_MINING_DEBUG'] - ?? $_COOKIE['mining_checker_debug'] - ?? ($debugConfig['enabled'] ?? false), - FILTER_VALIDATE_BOOL - ); + $debugEnabled = function_exists('nexus_debug_enabled') ? nexus_debug_enabled() : false; $latestDebugFilePath = rtrim($this->config->debugDir(), '/') . '/latest-server.json'; $isLatestDebugRequest = str_ends_with($requestPath, '/api/mining-checker/v1/debug/latest') || $requestPath === 'api/mining-checker/v1/debug/latest' diff --git a/partials/landingpages/modules/setup.php b/partials/landingpages/modules/setup.php index 90df052..42bc341 100644 --- a/partials/landingpages/modules/setup.php +++ b/partials/landingpages/modules/setup.php @@ -5,6 +5,8 @@ $error = null; $notice = null; $testGroup = null; $dbTestMessages = []; +$nexusDebugSettings = modules()->settings(nexus_debug_settings_key()); +$nexusDebugEnabled = !empty($nexusDebugSettings['enabled']); require_admin(); @@ -15,21 +17,6 @@ if (!$module) { } $fields = (array)($module['setup']['fields'] ?? []); -$hasGlobalDebugField = false; -foreach ($fields as $field) { - if ((string)($field['name'] ?? '') === 'debug_enabled') { - $hasGlobalDebugField = true; - break; - } -} -if (!$hasGlobalDebugField) { - $fields[] = [ - 'name' => 'debug_enabled', - 'label' => 'Modul-Debug aktivieren', - 'type' => 'checkbox', - 'help' => 'Wenn aktiv, darf das Modul Debug-Daten sammeln und den Debug-Bereich anzeigen.', - ]; -} $fieldTypes = []; $fieldMeta = []; foreach ($fields as $field) { @@ -416,6 +403,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $isSchedulerAutosave = isset($_POST['scheduler_autosave']) && (string) $_POST['scheduler_autosave'] === '1'; $isSchedulerTest = isset($_POST['scheduler_test']) && (string) $_POST['scheduler_test'] === '1'; $payload = []; + $nexusDebugEnabled = isset($_POST['nexus_debug_enabled']); if ($isSchedulerAutosave || $isSchedulerTest) { if ($cronTaskDefinitions !== []) { @@ -576,13 +564,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } } else { modules()->saveSettings($moduleName, $current); + modules()->saveSettings(nexus_debug_settings_key(), ['enabled' => $nexusDebugEnabled]); if ($isFxRatesSetup && modules()->hasFunction($moduleName, 'save_runtime_settings')) { module_fn($moduleName, 'save_runtime_settings', $payload); $current = modules()->settings($moduleName); } $refreshSchedulerState(); - if (empty($payload['debug_enabled'])) { - module_debug_clear($moduleName); + if (!$nexusDebugEnabled) { + nexus_debug_clear(); } $notice = 'Setup gespeichert.'; $module = modules()->get($moduleName) ?: $module; @@ -616,6 +605,22 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups) +
+
+
+ Nexus +

Debug

+

Aktiviert das projektweite Debug-Popup. Sichtbar und nutzbar nur fuer Benutzer aus der Admin-Gruppe `appadmin`.

+
+
+
+ +
+
Letzter Sync: - diff --git a/partials/structure/layout_end.php b/partials/structure/layout_end.php index 6f5b07c..0119fb0 100755 --- a/partials/structure/layout_end.php +++ b/partials/structure/layout_end.php @@ -1,4 +1,17 @@ + + nexus_debug_enabled(), + 'entries' => nexus_debug_entries(), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + if (!is_string($nexusDebugPayload)) { + $nexusDebugPayload = '{"enabled":false,"entries":[]}'; + } + ?> +
+ + diff --git a/partials/structure/layout_start.php b/partials/structure/layout_start.php index 6dd9ae2..aa8f549 100755 --- a/partials/structure/layout_start.php +++ b/partials/structure/layout_start.php @@ -28,6 +28,8 @@ $headerActions = isset($GLOBALS['layout_header_actions']) && is_array($GLOBALS[' : []; $auth = app()->auth(); $authUser = $auth->user(); +$isDebugAdmin = auth_is_admin(); +$isNexusDebugEnabled = $isDebugAdmin && nexus_debug_enabled(); ?> @@ -54,7 +56,7 @@ $authUser = $auth->user(); - +
diff --git a/public/assets/css/app.css b/public/assets/css/app.css index a8e98dd..47690d9 100644 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -1378,6 +1378,180 @@ body.has-modal-open { color: var(--muted); } +.nexus-debug-bug { + position: fixed; + right: 20px; + bottom: 20px; + z-index: 1200; + width: 58px; + height: 58px; + border: 1px solid var(--line); + border-radius: 999px; + background: color-mix(in srgb, var(--surface) 88%, var(--brand-accent-2) 12%); + color: var(--text); + box-shadow: 0 18px 40px rgba(1, 22, 32, 0.18); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; +} + +.nexus-debug-bug svg { + width: 26px; + height: 26px; +} + +.nexus-debug-badge { + position: absolute; + top: -4px; + right: -2px; + min-width: 22px; + height: 22px; + padding: 0 6px; + border-radius: 999px; + background: var(--brand-accent-2); + color: #07131a; + font-size: 0.72rem; + font-weight: 700; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.nexus-debug-popup { + position: fixed; + right: 20px; + bottom: 88px; + z-index: 1199; + width: min(560px, calc(100vw - 32px)); + max-height: min(70vh, 760px); + display: none; + overflow: hidden; + border: 1px solid var(--line); + border-radius: 24px; + background: var(--surface); + box-shadow: 0 28px 80px rgba(1, 22, 32, 0.24); +} + +.nexus-debug-popup.is-open { + display: grid; + grid-template-rows: auto auto minmax(0, 1fr); +} + +.nexus-debug-popup__head, +.nexus-debug-popup__toolbar { + padding: 18px 20px; +} + +.nexus-debug-popup__head { + display: flex; + align-items: start; + justify-content: space-between; + gap: 16px; + border-bottom: 1px solid var(--line); +} + +.nexus-debug-popup__head h2 { + margin: 8px 0 0; + font-size: 1.2rem; +} + +.nexus-debug-popup__toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + border-bottom: 1px solid var(--line); +} + +.nexus-debug-popup__actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.nexus-debug-popup__body { + overflow: auto; + padding: 16px 20px 20px; + display: grid; + gap: 12px; +} + +.nexus-debug-empty { + padding: 14px 16px; + border: 1px dashed var(--line); + border-radius: 16px; + color: var(--muted); +} + +.nexus-debug-entry { + border: 1px solid var(--line); + border-radius: 16px; + background: color-mix(in srgb, var(--surface) 92%, var(--brand-accent-2) 8%); +} + +.nexus-debug-entry summary { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 14px; + cursor: pointer; + list-style: none; +} + +.nexus-debug-entry summary::-webkit-details-marker { + display: none; +} + +.nexus-debug-entry__meta { + color: var(--muted); + font-size: 0.82rem; + text-align: right; +} + +.nexus-debug-entry pre { + margin: 0; + padding: 0 14px 14px; + white-space: pre-wrap; + word-break: break-word; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.84rem; + line-height: 1.5; +} + +@media (max-width: 720px) { + .nexus-debug-bug { + right: 14px; + bottom: 14px; + } + + .nexus-debug-popup { + right: 12px; + left: 12px; + bottom: 82px; + width: auto; + max-height: 72vh; + } + + .nexus-debug-popup__head, + .nexus-debug-popup__toolbar, + .nexus-debug-popup__body { + padding-left: 16px; + padding-right: 16px; + } + + .nexus-debug-entry summary { + align-items: start; + flex-direction: column; + } + + .nexus-debug-entry__meta { + text-align: left; + } +} + .module-box-title { margin: 0; font-size: 1.1rem; diff --git a/public/assets/js/app.js b/public/assets/js/app.js index adb21c5..c6aa17a 100755 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -188,3 +188,199 @@ window.NexusModal = (() => { return { create }; })(); + +(() => { + const root = document.getElementById('nexus-debug-root'); + const dataNode = document.getElementById('nexus-debug-data'); + if (!root || !dataNode) { + return; + } + + let payload = { enabled: false, entries: [] }; + try { + const parsed = JSON.parse(dataNode.textContent || '{}'); + if (parsed && typeof parsed === 'object') { + payload = parsed; + } + } catch (error) { + payload = { enabled: false, entries: [] }; + } + + const enabled = !!payload.enabled && document.body.dataset.nexusDebugEnabled === '1'; + const isAdmin = document.body.dataset.nexusDebugAdmin === '1'; + const entries = Array.isArray(payload.entries) ? payload.entries.slice(0, 250) : []; + let sequence = entries.length; + + const debugBus = window.__nexusDebugBus || { enabled: false, listener: null }; + debugBus.enabled = enabled; + window.__nexusDebugBus = debugBus; + + if (!isAdmin || !enabled) { + return; + } + + const bugSvg = ''; + + root.innerHTML = ` + + + `; + + const bugButton = root.querySelector('.nexus-debug-bug'); + const badge = root.querySelector('.nexus-debug-badge'); + const popup = root.querySelector('.nexus-debug-popup'); + const closeButton = root.querySelector('[data-debug-close]'); + const reloadButton = root.querySelector('[data-debug-reload]'); + const clearButton = root.querySelector('[data-debug-clear]'); + const list = root.querySelector('[data-debug-list]'); + + const updateBadge = () => { + if (!badge) { + return; + } + badge.textContent = String(entries.length); + badge.hidden = entries.length === 0; + }; + + const normalizeEntry = (entry) => { + sequence += 1; + return { + id: entry && entry.id ? entry.id : `nexus-debug-${sequence}`, + source: entry && entry.source ? String(entry.source) : 'nexus', + type: entry && entry.type ? String(entry.type) : 'debug', + label: entry && entry.label ? String(entry.label) : '', + at: entry && (entry.at || entry.time) ? String(entry.at || entry.time) : new Date().toISOString(), + payload: entry && typeof entry === 'object' ? entry : { value: entry }, + }; + }; + + const renderEntries = () => { + if (!list) { + return; + } + updateBadge(); + if (!entries.length) { + list.innerHTML = '
Noch keine Debug-Einträge vorhanden.
'; + return; + } + + list.innerHTML = entries.map((entry, index) => { + const title = entry.label || entry.type || `Eintrag ${index + 1}`; + const payloadText = JSON.stringify(entry.payload, null, 2); + return ` +
+ + ${escapeHtml(title)} + + +
${escapeHtml(payloadText)}
+
+ `; + }).join(''); + }; + + const escapeHtml = (value) => String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + + const openPopup = () => { + popup?.classList.add('is-open'); + popup?.setAttribute('aria-hidden', 'false'); + }; + + const closePopup = () => { + popup?.classList.remove('is-open'); + popup?.setAttribute('aria-hidden', 'true'); + }; + + const appendEntry = (entry) => { + entries.unshift(normalizeEntry(entry)); + if (entries.length > 250) { + entries.length = 250; + } + renderEntries(); + }; + + const reloadEntries = async () => { + try { + const response = await fetch('/api/debug/entries', { + credentials: 'same-origin', + headers: { Accept: 'application/json' }, + }); + const data = await response.json().catch(() => ({})); + const nextEntries = Array.isArray(data?.data?.entries) ? data.data.entries.map(normalizeEntry) : []; + entries.splice(0, entries.length, ...nextEntries); + renderEntries(); + } catch (error) { + appendEntry({ + source: 'nexus', + type: 'debug.reload.error', + label: 'Reload fehlgeschlagen', + message: error && error.message ? error.message : 'Debug-Einträge konnten nicht geladen werden.', + }); + } + }; + + const clearEntries = async () => { + try { + await fetch('/api/debug/clear', { + method: 'POST', + credentials: 'same-origin', + headers: { Accept: 'application/json' }, + }); + } catch (error) { + // Ignore clear failures; local UI is still reset below. + } + entries.splice(0, entries.length); + renderEntries(); + }; + + const previousListener = typeof debugBus.listener === 'function' ? debugBus.listener : null; + debugBus.listener = (entry) => { + if (previousListener) { + previousListener(entry); + } + appendEntry(entry); + }; + + bugButton?.addEventListener('click', () => { + if (popup?.classList.contains('is-open')) { + closePopup(); + } else { + openPopup(); + } + }); + closeButton?.addEventListener('click', closePopup); + reloadButton?.addEventListener('click', reloadEntries); + clearButton?.addEventListener('click', clearEntries); + + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + closePopup(); + } + }); + + renderEntries(); +})(); diff --git a/public/index.php b/public/index.php index a25a930..19804ab 100755 --- a/public/index.php +++ b/public/index.php @@ -121,6 +121,31 @@ if (preg_match('~^api/module-auth/([a-zA-Z0-9_-]+)$~', $uriPath, $moduleAuthMatc exit; } +if ($uriPath === 'api/debug/entries') { + require_admin(); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'data' => [ + 'enabled' => nexus_debug_enabled(), + 'entries' => nexus_debug_entries(), + ], + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +if ($uriPath === 'api/debug/clear' && strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') { + require_admin(); + nexus_debug_clear(); + header('Content-Type: application/json; charset=utf-8'); + echo json_encode([ + 'data' => [ + 'cleared' => true, + 'entries' => [], + ], + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + if (preg_match('~^api/mining-checker(?:/(.*))?$~', $uriPath, $apiMatches)) { $moduleMeta = app()->modules()->get('mining-checker') ?? ['auth' => ['required' => false]]; if (!$auth->canAccessModule($moduleMeta)) { diff --git a/src/App/functions.php b/src/App/functions.php index 0a9dddd..4e2da4c 100644 --- a/src/App/functions.php +++ b/src/App/functions.php @@ -276,20 +276,94 @@ function module_design(string $module): array return $cache[$module]; } +function nexus_debug_settings_key(): string +{ + return '__nexus_debug__'; +} + +function nexus_debug_configured(): bool +{ + try { + $settings = modules()->settings(nexus_debug_settings_key()); + } catch (\Throwable $e) { + return false; + } + + $value = $settings['enabled'] ?? '0'; + return $value === true || $value === 1 || $value === '1' || $value === 'true'; +} + +function nexus_debug_enabled(): bool +{ + if (!nexus_debug_configured()) { + return false; + } + + if (function_exists('auth_enabled') && auth_enabled()) { + return auth_is_admin(); + } + + return true; +} + +function nexus_debug_entries(): array +{ + if (!nexus_debug_enabled()) { + return []; + } + + app()->session()->start(); + $entries = $_SESSION['nexus_debug_entries'] ?? []; + return is_array($entries) ? array_values(array_filter($entries, 'is_array')) : []; +} + +function nexus_debug_push(string $source, array $entry): void +{ + if (!nexus_debug_enabled()) { + return; + } + + $source = trim($source); + if ($source === '') { + $source = 'nexus'; + } + + app()->session()->start(); + if (!isset($_SESSION['nexus_debug_entries']) || !is_array($_SESSION['nexus_debug_entries'])) { + $_SESSION['nexus_debug_entries'] = []; + } + + $entry['source'] = $entry['source'] ?? $source; + $entry['at'] = $entry['at'] ?? date('Y-m-d H:i:s'); + array_unshift($_SESSION['nexus_debug_entries'], $entry); + $_SESSION['nexus_debug_entries'] = array_slice($_SESSION['nexus_debug_entries'], 0, 250); +} + +function nexus_debug_clear(?string $source = null): void +{ + app()->session()->start(); + if ($source === null || trim($source) === '') { + unset($_SESSION['nexus_debug_entries']); + return; + } + + $entries = $_SESSION['nexus_debug_entries'] ?? []; + if (!is_array($entries)) { + return; + } + + $_SESSION['nexus_debug_entries'] = array_values(array_filter($entries, static function ($entry) use ($source): bool { + return !is_array($entry) || (string) ($entry['source'] ?? '') !== $source; + })); +} + function module_debug_enabled(string $module): bool { if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { return false; } - try { - $settings = modules()->settings($module); - } catch (\Throwable $e) { - return false; - } - - $value = $settings['debug_enabled'] ?? '0'; - return $value === true || $value === 1 || $value === '1' || $value === 'true'; + return nexus_debug_enabled(); } function module_debug_entries(string $module): array @@ -301,9 +375,9 @@ function module_debug_entries(string $module): array return []; } - app()->session()->start(); - $entries = $_SESSION['module_debug'][$module] ?? []; - return is_array($entries) ? array_values(array_filter($entries, 'is_array')) : []; + return array_values(array_filter(nexus_debug_entries(), static function ($entry) use ($module): bool { + return is_array($entry) && (string) ($entry['source'] ?? '') === $module; + })); } function module_debug_push(string $module, array $entry): void @@ -315,17 +389,7 @@ function module_debug_push(string $module, array $entry): void return; } - app()->session()->start(); - if (!isset($_SESSION['module_debug']) || !is_array($_SESSION['module_debug'])) { - $_SESSION['module_debug'] = []; - } - if (!isset($_SESSION['module_debug'][$module]) || !is_array($_SESSION['module_debug'][$module])) { - $_SESSION['module_debug'][$module] = []; - } - - $entry['at'] = $entry['at'] ?? date('Y-m-d H:i:s'); - array_unshift($_SESSION['module_debug'][$module], $entry); - $_SESSION['module_debug'][$module] = array_slice($_SESSION['module_debug'][$module], 0, 25); + nexus_debug_push($module, $entry); } function module_debug_clear(string $module): void @@ -334,8 +398,7 @@ function module_debug_clear(string $module): void return; } - app()->session()->start(); - unset($_SESSION['module_debug'][$module]); + nexus_debug_clear($module); } function module_shell_header(string $module, array $options = []): string @@ -442,53 +505,7 @@ function module_shell_header(string $module, array $options = []): string function module_shell_footer(): string { - $html = ''; - $module = current_module_name(); - if (is_string($module) && $module !== '' && module_debug_enabled($module)) { - if ((string) ($_GET['module_debug_clear'] ?? '') === '1') { - module_debug_clear($module); - } - - $entries = module_debug_entries($module); - $currentPath = app()->request()->path(); - $clearHref = $currentPath . '?module_debug_clear=1'; - - $html .= '
'; - $html .= '
'; - $html .= ''; - $html .= 'Debug'; - $html .= '' . e((string) count($entries)) . ' Eintraege'; - $html .= ''; - $html .= '
'; - $html .= ''; - - if ($entries === []) { - $html .= '
Noch keine Debug-Daten vorhanden.
'; - } else { - foreach ($entries as $index => $entry) { - $label = trim((string) ($entry['label'] ?? ('Eintrag ' . ($index + 1)))); - $at = trim((string) ($entry['at'] ?? '')); - $payload = json_encode($entry, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); - if (!is_string($payload)) { - $payload = '{}'; - } - - $html .= '
'; - $html .= '' . e($label) . '' . ($at !== '' ? '' . e($at) . '' : '') . ''; - $html .= '
' . e($payload) . '
'; - $html .= '
'; - } - } - - $html .= '
'; - $html .= '
'; - $html .= '
'; - } - - return $html . ''; + return ''; } /**