yyxx
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-05-05 23:46:23 +02:00
parent e335a8d5bf
commit 48b7583f19
9 changed files with 528 additions and 277 deletions

View File

@@ -26,20 +26,7 @@
return []; return [];
} }
})(); })();
const initialDebugMode = (() => { const initialDebugMode = document.body && document.body.dataset.nexusDebugEnabled === '1';
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;
}
})();
function getCookie(name) { function getCookie(name) {
const pattern = `; ${document.cookie}`; const pattern = `; ${document.cookie}`;
const parts = pattern.split(`; ${name}=`); const parts = pattern.split(`; ${name}=`);
@@ -52,20 +39,12 @@
function setCookie(name, value, maxAgeSeconds) { function setCookie(name, value, maxAgeSeconds) {
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAgeSeconds}; samesite=lax`; document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAgeSeconds}; samesite=lax`;
} }
const initialDebugView = (() => { const debugBus = window.__nexusDebugBus || {
try {
const value = window.localStorage.getItem('mining-checker-debug-view');
return value === 'text' ? 'text' : 'structured';
} catch (error) {
return 'structured';
}
})();
const debugBus = window.__miningCheckerDebugBus || {
enabled: initialDebugMode, enabled: initialDebugMode,
listener: null, listener: null,
sequence: 0, sequence: 0,
}; };
window.__miningCheckerDebugBus = debugBus; window.__nexusDebugBus = debugBus;
function emitDebug(entry) { function emitDebug(entry) {
debugBus.sequence += 1; 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) { function emitServerTraceEntries(trace, meta) {
if (!Array.isArray(trace) || !trace.length) { if (!Array.isArray(trace) || !trace.length) {
emitDebug({ emitDebug({
@@ -585,10 +559,6 @@
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [message, setMessage] = 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 [schemaStatus, setSchemaStatus] = useState(normalizeSchemaStatus(null));
const [initForm, setInitForm] = useState({ drop_existing: false }); const [initForm, setInitForm] = useState({ drop_existing: false });
const [sqlImportFile, setSqlImportFile] = useState(null); const [sqlImportFile, setSqlImportFile] = useState(null);
@@ -706,43 +676,12 @@
}); });
useEffect(() => { useEffect(() => {
debugBus.enabled = debugEnabled; debugBus.enabled = initialDebugMode;
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);
emitDebug({ emitDebug({
type: 'debug:mode', 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 measurements = Array.isArray(payload?.measurements) ? payload.measurements : [];
const latest = payload?.summary?.latest_measurement || null; 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() { function resetReportCurrencyOverride() {
setReportCurrencyOverride(''); setReportCurrencyOverride('');
setCookie('mining_checker_report_currency', '', 0); 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', { return h('div', {
className: 'mc-grid-bg', className: 'mc-grid-bg',
}, [ }, [
@@ -1813,78 +1716,6 @@
message ? h('div', { key: 'message', className: 'mc-alert mc-alert--success' }, message) : null, 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, loading ? h('div', { key: 'loading', className: 'mc-empty' }, 'Lade Mining-Checker Daten …') : null,
payload ? renderTab() : 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,
]), ]),
]); ]);

View File

@@ -44,14 +44,7 @@ final class Router
$this->config = ModuleConfig::load($this->moduleBasePath); $this->config = ModuleConfig::load($this->moduleBasePath);
$requestUri = (string) ($_SERVER['REQUEST_URI'] ?? ''); $requestUri = (string) ($_SERVER['REQUEST_URI'] ?? '');
$requestPath = (string) (parse_url($requestUri, PHP_URL_PATH) ?: ''); $requestPath = (string) (parse_url($requestUri, PHP_URL_PATH) ?: '');
$debugConfig = $this->config->debug(); $debugEnabled = function_exists('nexus_debug_enabled') ? nexus_debug_enabled() : false;
$debugEnabled = filter_var(
$_GET['debug']
?? $_SERVER['HTTP_X_MINING_DEBUG']
?? $_COOKIE['mining_checker_debug']
?? ($debugConfig['enabled'] ?? false),
FILTER_VALIDATE_BOOL
);
$latestDebugFilePath = rtrim($this->config->debugDir(), '/') . '/latest-server.json'; $latestDebugFilePath = rtrim($this->config->debugDir(), '/') . '/latest-server.json';
$isLatestDebugRequest = str_ends_with($requestPath, '/api/mining-checker/v1/debug/latest') $isLatestDebugRequest = str_ends_with($requestPath, '/api/mining-checker/v1/debug/latest')
|| $requestPath === 'api/mining-checker/v1/debug/latest' || $requestPath === 'api/mining-checker/v1/debug/latest'

View File

@@ -5,6 +5,8 @@ $error = null;
$notice = null; $notice = null;
$testGroup = null; $testGroup = null;
$dbTestMessages = []; $dbTestMessages = [];
$nexusDebugSettings = modules()->settings(nexus_debug_settings_key());
$nexusDebugEnabled = !empty($nexusDebugSettings['enabled']);
require_admin(); require_admin();
@@ -15,21 +17,6 @@ if (!$module) {
} }
$fields = (array)($module['setup']['fields'] ?? []); $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 = []; $fieldTypes = [];
$fieldMeta = []; $fieldMeta = [];
foreach ($fields as $field) { foreach ($fields as $field) {
@@ -416,6 +403,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$isSchedulerAutosave = isset($_POST['scheduler_autosave']) && (string) $_POST['scheduler_autosave'] === '1'; $isSchedulerAutosave = isset($_POST['scheduler_autosave']) && (string) $_POST['scheduler_autosave'] === '1';
$isSchedulerTest = isset($_POST['scheduler_test']) && (string) $_POST['scheduler_test'] === '1'; $isSchedulerTest = isset($_POST['scheduler_test']) && (string) $_POST['scheduler_test'] === '1';
$payload = []; $payload = [];
$nexusDebugEnabled = isset($_POST['nexus_debug_enabled']);
if ($isSchedulerAutosave || $isSchedulerTest) { if ($isSchedulerAutosave || $isSchedulerTest) {
if ($cronTaskDefinitions !== []) { if ($cronTaskDefinitions !== []) {
@@ -576,13 +564,14 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} }
} else { } else {
modules()->saveSettings($moduleName, $current); modules()->saveSettings($moduleName, $current);
modules()->saveSettings(nexus_debug_settings_key(), ['enabled' => $nexusDebugEnabled]);
if ($isFxRatesSetup && modules()->hasFunction($moduleName, 'save_runtime_settings')) { if ($isFxRatesSetup && modules()->hasFunction($moduleName, 'save_runtime_settings')) {
module_fn($moduleName, 'save_runtime_settings', $payload); module_fn($moduleName, 'save_runtime_settings', $payload);
$current = modules()->settings($moduleName); $current = modules()->settings($moduleName);
} }
$refreshSchedulerState(); $refreshSchedulerState();
if (empty($payload['debug_enabled'])) { if (!$nexusDebugEnabled) {
module_debug_clear($moduleName); nexus_debug_clear();
} }
$notice = 'Setup gespeichert.'; $notice = 'Setup gespeichert.';
$module = modules()->get($moduleName) ?: $module; $module = modules()->get($moduleName) ?: $module;
@@ -616,6 +605,22 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
<option value="<?= e((string) $timezoneOption['value']) ?>"><?= e((string) $timezoneOption['label']) ?></option> <option value="<?= e((string) $timezoneOption['value']) ?>"><?= e((string) $timezoneOption['label']) ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</datalist> </datalist>
<section class="setup-panel">
<div class="setup-panel__head">
<div>
<span class="pill">Nexus</span>
<h2>Debug</h2>
<p class="muted">Aktiviert das projektweite Debug-Popup. Sichtbar und nutzbar nur fuer Benutzer aus der Admin-Gruppe `appadmin`.</p>
</div>
</div>
<div class="setup-grid">
<label class="setup-field muted">
<span>Globales Debug aktivieren</span>
<input type="checkbox" name="nexus_debug_enabled" value="1" <?= $nexusDebugEnabled ? 'checked' : '' ?>>
<small class="muted">Wenn aktiv, sammelt Nexus sitzungsbezogene Debug-Eintraege projektweit und zeigt sie ueber den Bug-Button als Popup an.</small>
</label>
</div>
</section>
<?php if ($isFxRatesSetup): ?> <?php if ($isFxRatesSetup): ?>
<?php <?php
$fxCatalog = is_array($current['currency_catalog'] ?? null) ? $current['currency_catalog'] : []; $fxCatalog = is_array($current['currency_catalog'] ?? null) ? $current['currency_catalog'] : [];
@@ -776,11 +781,6 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
<small class="muted">Letzter Sync: <?= e((string) $current['currency_catalog_synced_at']) ?></small> <small class="muted">Letzter Sync: <?= e((string) $current['currency_catalog_synced_at']) ?></small>
<?php endif; ?> <?php endif; ?>
</div> </div>
<label class="setup-field muted">
<span>Modul-Debug aktivieren</span>
<input type="checkbox" name="debug_enabled" value="1" <?= !empty($current['debug_enabled']) ? 'checked' : '' ?>>
<small class="muted">Wenn aktiv, darf das Modul Debug-Daten sammeln und den Debug-Bereich anzeigen.</small>
</label>
</div> </div>
</section> </section>

View File

@@ -1,4 +1,17 @@
</main> </main>
<?php if (auth_is_admin()): ?>
<?php
$nexusDebugPayload = json_encode([
'enabled' => nexus_debug_enabled(),
'entries' => nexus_debug_entries(),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($nexusDebugPayload)) {
$nexusDebugPayload = '{"enabled":false,"entries":[]}';
}
?>
<div id="nexus-debug-root"></div>
<script id="nexus-debug-data" type="application/json"><?= $nexusDebugPayload ?></script>
<?php endif; ?>
<script src="<?= e(app()->assets()->versioned('/assets/js/app.js')) ?>" defer></script> <script src="<?= e(app()->assets()->versioned('/assets/js/app.js')) ?>" defer></script>
<?php asset_scripts('footer'); ?> <?php asset_scripts('footer'); ?>
</body> </body>

View File

@@ -28,6 +28,8 @@ $headerActions = isset($GLOBALS['layout_header_actions']) && is_array($GLOBALS['
: []; : [];
$auth = app()->auth(); $auth = app()->auth();
$authUser = $auth->user(); $authUser = $auth->user();
$isDebugAdmin = auth_is_admin();
$isNexusDebugEnabled = $isDebugAdmin && nexus_debug_enabled();
?> ?>
<!doctype html> <!doctype html>
<html lang="de"> <html lang="de">
@@ -54,7 +56,7 @@ $authUser = $auth->user();
<?php asset_styles(); ?> <?php asset_styles(); ?>
<?php asset_scripts('header'); ?> <?php asset_scripts('header'); ?>
</head> </head>
<body> <body data-nexus-debug-admin="<?= $isDebugAdmin ? '1' : '0' ?>" data-nexus-debug-enabled="<?= $isNexusDebugEnabled ? '1' : '0' ?>">
<main class="main-shell"> <main class="main-shell">
<section class="home-hero app-header main-header-box" data-module-name="<?= e((string)($currentModuleName ?? '')) ?>"> <section class="home-hero app-header main-header-box" data-module-name="<?= e((string)($currentModuleName ?? '')) ?>">
<a class="brand-mark" href="/" aria-label="Nexus"> <a class="brand-mark" href="/" aria-label="Nexus">

View File

@@ -1378,6 +1378,180 @@ body.has-modal-open {
color: var(--muted); 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 { .module-box-title {
margin: 0; margin: 0;
font-size: 1.1rem; font-size: 1.1rem;

View File

@@ -188,3 +188,199 @@ window.NexusModal = (() => {
return { create }; 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 = '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 4.5h6l1.2 2.2 2.5-1.4 1 1.7-2.4 1.4A6.9 6.9 0 0 1 19 13h2.5v2H19a6.9 6.9 0 0 1-.7 2.9l2.4 1.4-1 1.7-2.5-1.4L15 21.5H9l-1.2-2.2-2.5 1.4-1-1.7 2.4-1.4A6.9 6.9 0 0 1 6 15H3.5v-2H6a6.9 6.9 0 0 1 .7-2.9L4.3 8.7l1-1.7 2.5 1.4L9 4.5Zm1.2 2L9 8.8V15a3 3 0 0 0 6 0V8.8l-1.2-2.3h-3.6ZM10.5 11a1 1 0 1 1 0 2 1 1 0 0 1 0-2Zm3 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z" fill="currentColor"/></svg>';
root.innerHTML = `
<button type="button" class="nexus-debug-bug" aria-label="Debug öffnen">
${bugSvg}
<span class="nexus-debug-badge">0</span>
</button>
<section class="nexus-debug-popup" aria-hidden="true">
<div class="nexus-debug-popup__head">
<div>
<div class="pill">Debug</div>
<h2>Nexus Debug</h2>
</div>
<button type="button" class="module-button module-button--ghost module-button--small" data-debug-close>Schließen</button>
</div>
<div class="nexus-debug-popup__toolbar">
<div class="muted">Projektweiter Debug-Stream der aktuellen Admin-Sitzung.</div>
<div class="nexus-debug-popup__actions">
<button type="button" class="module-button module-button--ghost module-button--small" data-debug-reload>Neu laden</button>
<button type="button" class="module-button module-button--ghost module-button--small" data-debug-clear>Leeren</button>
</div>
</div>
<div class="nexus-debug-popup__body" data-debug-list></div>
</section>
`;
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 = '<div class="nexus-debug-empty">Noch keine Debug-Einträge vorhanden.</div>';
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 `
<details class="nexus-debug-entry"${index === 0 ? ' open' : ''}>
<summary>
<strong>${escapeHtml(title)}</strong>
<span class="nexus-debug-entry__meta">${escapeHtml(entry.source)} · ${escapeHtml(entry.at)}</span>
</summary>
<pre>${escapeHtml(payloadText)}</pre>
</details>
`;
}).join('');
};
const escapeHtml = (value) => String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
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();
})();

View File

@@ -121,6 +121,31 @@ if (preg_match('~^api/module-auth/([a-zA-Z0-9_-]+)$~', $uriPath, $moduleAuthMatc
exit; 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)) { if (preg_match('~^api/mining-checker(?:/(.*))?$~', $uriPath, $apiMatches)) {
$moduleMeta = app()->modules()->get('mining-checker') ?? ['auth' => ['required' => false]]; $moduleMeta = app()->modules()->get('mining-checker') ?? ['auth' => ['required' => false]];
if (!$auth->canAccessModule($moduleMeta)) { if (!$auth->canAccessModule($moduleMeta)) {

View File

@@ -276,20 +276,94 @@ function module_design(string $module): array
return $cache[$module]; 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 function module_debug_enabled(string $module): bool
{ {
if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) { if (preg_match('/[^a-zA-Z0-9_\-]/', $module)) {
return false; return false;
} }
try { return nexus_debug_enabled();
$settings = modules()->settings($module);
} catch (\Throwable $e) {
return false;
}
$value = $settings['debug_enabled'] ?? '0';
return $value === true || $value === 1 || $value === '1' || $value === 'true';
} }
function module_debug_entries(string $module): array function module_debug_entries(string $module): array
@@ -301,9 +375,9 @@ function module_debug_entries(string $module): array
return []; return [];
} }
app()->session()->start(); return array_values(array_filter(nexus_debug_entries(), static function ($entry) use ($module): bool {
$entries = $_SESSION['module_debug'][$module] ?? []; return is_array($entry) && (string) ($entry['source'] ?? '') === $module;
return is_array($entries) ? array_values(array_filter($entries, 'is_array')) : []; }));
} }
function module_debug_push(string $module, array $entry): void function module_debug_push(string $module, array $entry): void
@@ -315,17 +389,7 @@ function module_debug_push(string $module, array $entry): void
return; return;
} }
app()->session()->start(); nexus_debug_push($module, $entry);
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);
} }
function module_debug_clear(string $module): void function module_debug_clear(string $module): void
@@ -334,8 +398,7 @@ function module_debug_clear(string $module): void
return; return;
} }
app()->session()->start(); nexus_debug_clear($module);
unset($_SESSION['module_debug'][$module]);
} }
function module_shell_header(string $module, array $options = []): string 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 function module_shell_footer(): string
{ {
$html = ''; return '</div></div></div>';
$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 .= '<section class="module-debug">';
$html .= '<details class="module-debug-details"' . ($entries !== [] ? ' open' : '') . '>';
$html .= '<summary class="module-debug-summary">';
$html .= '<span>Debug</span>';
$html .= '<span class="module-debug-meta">' . e((string) count($entries)) . ' Eintraege</span>';
$html .= '</summary>';
$html .= '<div class="module-debug-body">';
$html .= '<div class="module-debug-toolbar">';
$html .= '<div class="muted">Standard-Debugbereich des Moduls. Zeigt die letzten Request-/Response-Daten der aktuellen Sitzung.</div>';
$html .= '<a class="module-button module-button--ghost module-button--small" href="' . e($clearHref) . '">Debug leeren</a>';
$html .= '</div>';
if ($entries === []) {
$html .= '<div class="module-debug-empty">Noch keine Debug-Daten vorhanden.</div>';
} 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 .= '<details class="module-debug-entry"' . ($index === 0 ? ' open' : '') . '>';
$html .= '<summary><strong>' . e($label) . '</strong>' . ($at !== '' ? '<span class="module-debug-entry-time">' . e($at) . '</span>' : '') . '</summary>';
$html .= '<pre class="module-debug-pre">' . e($payload) . '</pre>';
$html .= '</details>';
}
}
$html .= '</div>';
$html .= '</details>';
$html .= '</section>';
}
return $html . '</div></div></div>';
} }
/** /**