yyxx
This commit is contained in:
@@ -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,
|
||||
]),
|
||||
]);
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
<option value="<?= e((string) $timezoneOption['value']) ?>"><?= e((string) $timezoneOption['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</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
|
||||
$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>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
</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>
|
||||
<?php asset_scripts('footer'); ?>
|
||||
</body>
|
||||
|
||||
@@ -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();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
@@ -54,7 +56,7 @@ $authUser = $auth->user();
|
||||
<?php asset_styles(); ?>
|
||||
<?php asset_scripts('header'); ?>
|
||||
</head>
|
||||
<body>
|
||||
<body data-nexus-debug-admin="<?= $isDebugAdmin ? '1' : '0' ?>" data-nexus-debug-enabled="<?= $isNexusDebugEnabled ? '1' : '0' ?>">
|
||||
<main class="main-shell">
|
||||
<section class="home-hero app-header main-header-box" data-module-name="<?= e((string)($currentModuleName ?? '')) ?>">
|
||||
<a class="brand-mark" href="/" aria-label="Nexus">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = '<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('&', '&')
|
||||
.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();
|
||||
})();
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 .= '<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>';
|
||||
return '</div></div></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user