document.documentElement.classList.add('js'); function readThemePreference(key, fallback) { try { return localStorage.getItem(key) || fallback; } catch (error) { return fallback; } } const moduleName = document.documentElement.dataset.module || ''; const mainThemeMode = readThemePreference('nexus.theme', 'day'); const mainThemeAccent = readThemePreference('nexus.accent', 'logo'); const moduleThemeMode = moduleName ? readThemePreference(`nexus.module.${moduleName}.theme`, 'inherit') : mainThemeMode; const moduleThemeAccent = moduleName ? readThemePreference(`nexus.module.${moduleName}.accent`, 'inherit') : mainThemeAccent; function normalizeThemeValue(value, allowed, fallback) { return allowed.includes(value) ? value : fallback; } function storedTheme() { const rawMode = moduleName ? readThemePreference(`nexus.module.${moduleName}.theme`, 'inherit') : readThemePreference('nexus.theme', 'day'); const rawAccent = moduleName ? readThemePreference(`nexus.module.${moduleName}.accent`, 'inherit') : readThemePreference('nexus.accent', 'logo'); const mode = moduleName ? normalizeThemeValue(rawMode, ['inherit', 'day', 'night'], 'inherit') : normalizeThemeValue(rawMode, ['day', 'night'], 'day'); const accent = moduleName ? normalizeThemeValue(rawAccent, ['inherit', 'logo', 'pink', 'cyan', 'orange', 'green'], 'inherit') : normalizeThemeValue(rawAccent, ['logo', 'pink', 'cyan', 'orange', 'green'], 'logo'); return { mode, accent }; } function applyTheme(mode, accent, persist = true) { const allowedModes = moduleName ? ['inherit', 'day', 'night'] : ['day', 'night']; const allowedAccents = moduleName ? ['inherit', 'logo', 'pink', 'cyan', 'orange', 'green'] : ['logo', 'pink', 'cyan', 'orange', 'green']; const normalizedMode = normalizeThemeValue(mode, allowedModes, moduleName ? 'inherit' : 'day'); const normalizedAccent = normalizeThemeValue(accent, allowedAccents, moduleName ? 'inherit' : 'logo'); const effectiveMode = normalizedMode === 'inherit' ? mainThemeMode : normalizedMode; const effectiveAccent = normalizedAccent === 'inherit' ? mainThemeAccent : normalizedAccent; document.documentElement.dataset.theme = effectiveMode; document.documentElement.dataset.accent = effectiveAccent; try { if (persist) { if (moduleName) { if (normalizedMode === 'inherit') { localStorage.removeItem(`nexus.module.${moduleName}.theme`); } else { localStorage.setItem(`nexus.module.${moduleName}.theme`, normalizedMode); } if (normalizedAccent === 'inherit') { localStorage.removeItem(`nexus.module.${moduleName}.accent`); } else { localStorage.setItem(`nexus.module.${moduleName}.accent`, normalizedAccent); } } else { localStorage.setItem('nexus.theme', normalizedMode); localStorage.setItem('nexus.accent', normalizedAccent); } } } catch (error) { // Ignore blocked storage; the current page still receives the theme. } return { mode: normalizedMode, accent: normalizedAccent }; } const initialTheme = applyTheme(moduleThemeMode, moduleThemeAccent, false); const themeModeSelect = document.querySelector('[data-theme-mode]'); const themeAccentSelect = document.querySelector('[data-theme-accent]'); if (themeModeSelect) { themeModeSelect.value = initialTheme.mode; themeModeSelect.addEventListener('change', () => { const current = storedTheme(); applyTheme(themeModeSelect.value, current.accent); }); } if (themeAccentSelect) { themeAccentSelect.value = initialTheme.accent; themeAccentSelect.addEventListener('change', () => { const current = storedTheme(); applyTheme(current.mode, themeAccentSelect.value); }); } for (const element of document.querySelectorAll('[data-reveal]')) { element.classList.add('reveal'); } const setupTabs = document.querySelectorAll('[data-setup-tab-target]'); if (setupTabs.length > 0) { const setupPanels = document.querySelectorAll('.setup-db-panel'); const setupControls = document.querySelectorAll('.setup-db-panel input, .setup-db-panel select, .setup-db-panel textarea'); for (const control of setupControls) { if (control.required) { control.dataset.setupRequired = 'true'; } } const activateSetupTab = (targetId) => { const dbPanelsRoot = document.querySelector('[data-setup-db-panels]'); const dbPanelsHidden = dbPanelsRoot ? dbPanelsRoot.hidden : false; for (const tab of setupTabs) { const isActive = tab.dataset.setupTabTarget === targetId; tab.classList.toggle('is-active', isActive); tab.setAttribute('aria-selected', isActive ? 'true' : 'false'); } for (const panel of setupPanels) { const isActive = !dbPanelsHidden && panel.id === targetId; panel.hidden = !isActive; for (const control of panel.querySelectorAll('input, select, textarea')) { if (control.dataset.setupRequired === 'true') { control.required = isActive; } } } }; for (const tab of setupTabs) { tab.addEventListener('click', () => { activateSetupTab(tab.dataset.setupTabTarget); }); } const activeSetupTab = document.querySelector('[data-setup-tab-target].is-active') || setupTabs[0]; activateSetupTab(activeSetupTab.dataset.setupTabTarget); } window.NexusModal = (() => { const openModals = new Set(); const syncBodyState = () => { document.body.classList.toggle('has-modal-open', openModals.size > 0); }; const create = (root, options = {}) => { if (!root) { return null; } const modal = { root, isOpen: () => root.classList.contains('is-open'), open() { root.classList.add('is-open'); root.setAttribute('aria-hidden', 'false'); openModals.add(root); syncBodyState(); if (typeof options.onOpen === 'function') { options.onOpen(); } const focusTarget = options.initialFocus ? root.querySelector(options.initialFocus) : null; if (focusTarget instanceof HTMLElement) { window.setTimeout(() => focusTarget.focus(), 20); } }, close() { root.classList.remove('is-open'); root.setAttribute('aria-hidden', 'true'); openModals.delete(root); syncBodyState(); if (typeof options.onClose === 'function') { options.onClose(); } }, }; if (!root.dataset.modalBound) { root.addEventListener('click', (event) => { if (event.target === root) { modal.close(); } }); root.dataset.modalBound = '1'; } return modal; }; document.addEventListener('keydown', (event) => { if (event.key !== 'Escape') { return; } const active = Array.from(openModals).pop(); if (!active) { return; } active.classList.remove('is-open'); active.setAttribute('aria-hidden', 'true'); openModals.delete(active); syncBodyState(); }); 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 = ''; const copySvg = ''; 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 copyButton = root.querySelector('[data-debug-copy]'); 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 copyText = async (text, successLabel, errorLabel) => { try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(text); } else { const textarea = document.createElement('textarea'); textarea.value = text; 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); } appendEntry({ source: 'nexus', type: 'debug.copy.success', label: successLabel, }); } catch (error) { appendEntry({ source: 'nexus', type: 'debug.copy.error', label: errorLabel, message: error && error.message ? error.message : 'Kopieren fehlgeschlagen.', }); } }; 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 copyAllEntries = async () => { const content = entries .slice() .reverse() .map((entry) => JSON.stringify(entry.payload, null, 2)) .join('\n\n'); if (!content) { appendEntry({ source: 'nexus', type: 'debug.copy.empty', label: 'Keine Debug-Einträge zum Kopieren', }); return; } await copyText(content, 'Debug-Einträge kopiert', 'Debug-Einträge konnten nicht kopiert werden'); }; 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); copyButton?.addEventListener('click', copyAllEntries); clearButton?.addEventListener('click', clearEntries); list?.addEventListener('click', (event) => { const button = event.target instanceof HTMLElement ? event.target.closest('[data-entry-copy]') : null; if (!button) { return; } const entryId = button.getAttribute('data-entry-copy') || ''; const entry = entries.find((item) => String(item.id) === entryId); if (!entry) { return; } const content = JSON.stringify(entry.payload, null, 2); copyText(content, `Eintrag ${entry.type || entry.id} kopiert`, `Eintrag ${entry.type || entry.id} konnte nicht kopiert werden`); }); document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { closePopup(); } }); renderEntries(); })();