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

@@ -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;

View File

@@ -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('&', '&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();
})();