Files
emailtemplate.it/public/assets/js/ui-user.js
2026-01-12 23:57:46 +01:00

858 lines
29 KiB
JavaScript

import { apiAction, toast } from './api.js';
const state = {
settings: {},
rotate: { bridge: false, sender: false, external: false },
users: [],
userMap: new Map(),
senders: [],
senderMap: new Map(),
currentTab: 'profile',
loading: false,
};
const pageType = document.body?.dataset?.page || 'account';
const DEBUG_EMAIL = 'madmin@papa-kind-treff.info';
const MAX_CONSOLE_LINES = 200;
let avatarBtn;
let userMenuPanel;
let profileForm;
let passwordForm;
let settingsForm;
let teamTable;
let userForm;
let senderTable;
let senderForm;
let menuInitialized = false;
let menuOpen = false;
let debugButton;
let debugDialog;
let debugPhpLoaded = false;
let debugPhpLoading = false;
let debugActiveTab = 'php';
let phpInfoContainer;
let consoleContainer;
let debugStylesInjected = false;
let consolePatched = false;
const consoleBuffer = [];
let adminTablesAllSelect;
let adminTablesSelectedSelect;
let adminTablesAddBtn;
let adminTablesRemoveBtn;
ensureConsoleCapture();
export function initUserPanel() {
avatarBtn = document.getElementById('btn-user');
userMenuPanel = document.getElementById('userMenuPanel');
ensureConsoleCapture();
handleUserContextChange();
if (!menuInitialized && avatarBtn && userMenuPanel) {
avatarBtn.addEventListener('click', toggleUserMenu);
document.addEventListener('click', handleDocumentClick, true);
document.addEventListener('keydown', handleMenuKeydown);
userMenuPanel.addEventListener('click', handleMenuItemClick);
menuInitialized = true;
}
}
export function initAccountPage() {
profileForm = document.getElementById('profileForm');
passwordForm = document.getElementById('passwordForm');
settingsForm = document.getElementById('settingsForm');
teamTable = document.getElementById('teamTable');
userForm = document.getElementById('userForm');
senderTable = document.getElementById('senderTable');
senderForm = document.getElementById('senderForm');
adminTablesAllSelect = document.getElementById('adminBridgeTablesAll');
adminTablesSelectedSelect = document.getElementById('adminBridgeTablesSelected');
adminTablesAddBtn = document.getElementById('adminBridgeTablesAdd');
adminTablesRemoveBtn = document.getElementById('adminBridgeTablesRemove');
document.getElementById('btn-user-add')?.addEventListener('click', () => openUserForm());
document.getElementById('userFormCancel')?.addEventListener('click', () => closeUserForm());
userForm?.addEventListener('submit', submitUserForm);
document.getElementById('btn-sender-add')?.addEventListener('click', () => openSenderForm());
document.getElementById('senderFormCancel')?.addEventListener('click', () => closeSenderForm());
senderForm?.addEventListener('submit', submitSenderForm);
profileForm?.addEventListener('submit', submitProfileForm);
passwordForm?.addEventListener('submit', submitPasswordForm);
settingsForm?.addEventListener('submit', submitSettingsForm);
teamTable?.addEventListener('click', handleTeamTableClick);
senderTable?.addEventListener('click', handleSenderTableClick);
document.querySelectorAll('[data-user-tab]').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.getAttribute('data-user-tab')));
});
if (settingsForm) {
settingsForm.querySelectorAll('button[data-rotate]').forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.getAttribute('data-rotate');
if (type && state.rotate[type] !== undefined) {
state.rotate[type] = true;
toast('Token wird nach dem Speichern erneuert.', true, { duration: 2000 });
}
});
});
settingsForm.querySelectorAll('button[data-download]').forEach(btn => {
btn.addEventListener('click', () => {
const type = btn.getAttribute('data-download');
if (type) downloadFile(type);
});
});
}
adminTablesAddBtn?.addEventListener('click', () => {
addAdminTables(getSelectedOptions(adminTablesAllSelect));
});
adminTablesRemoveBtn?.addEventListener('click', () => {
removeAdminTables(getSelectedOptions(adminTablesSelectedSelect));
});
window.addEventListener('bridge-setup-updated', (ev) => {
const setup = ev?.detail || {};
refreshAdminTables(setup.tables || [], state.settings.bridge_tables || []);
});
switchTab(state.currentTab);
loadAccountData();
updateRoleVisibility();
}
function isOwner() {
return (window.__currentUser?.role || '').toLowerCase() === 'owner';
}
function isAdmin() {
const role = (window.__currentUser?.role || '').toLowerCase();
return role === 'owner' || role === 'admin';
}
function enforcePageAccess() {
if (pageType !== 'admin') return;
if (isAdmin()) return;
toast('Kein Zugriff auf diesen Bereich', false, { duration: 2500 });
window.location.href = '/admin/profile.php';
}
function handleUserContextChange() {
updateAvatar();
updateRoleVisibility();
enforcePageAccess();
refreshDebugAccess();
}
function updateAvatar() {
const target = document.getElementById('userAvatar');
if (!target) return;
const name = window.__currentUser?.name || window.__currentUser?.email || '';
target.textContent = name ? name.trim().charAt(0).toUpperCase() : 'U';
}
function toggleUserMenu(ev) {
ev?.preventDefault();
if (!userMenuPanel || !avatarBtn) return;
menuOpen = !menuOpen;
userMenuPanel.classList.toggle('hidden', !menuOpen);
avatarBtn.setAttribute('aria-expanded', menuOpen ? 'true' : 'false');
}
function closeUserMenu() {
if (!menuOpen) return;
menuOpen = false;
if (userMenuPanel) userMenuPanel.classList.add('hidden');
if (avatarBtn) avatarBtn.setAttribute('aria-expanded', 'false');
}
function handleDocumentClick(ev) {
if (!userMenuPanel || !avatarBtn || !menuOpen) return;
const target = ev.target;
if (avatarBtn.contains(target) || userMenuPanel.contains(target)) return;
closeUserMenu();
}
function handleMenuKeydown(ev) {
if (ev.key === 'Escape') {
closeUserMenu();
}
}
function handleMenuItemClick(ev) {
const item = ev.target.closest('.user-menu-item');
if (!item) return;
closeUserMenu();
}
function updateRoleVisibility() {
const role = (window.__currentUser?.role || '').toLowerCase();
document.querySelectorAll('[data-role]').forEach(el => {
const allowed = (el.getAttribute('data-role') || '').split(/[\s,]+/).filter(Boolean).map(r => r.toLowerCase());
if (!allowed.length) return;
const visible = allowed.some(targetRole => {
if (targetRole === 'owner') return role === 'owner';
if (targetRole === 'admin') return role === 'owner' || role === 'admin';
if (targetRole === 'editor') return role === 'owner' || role === 'admin' || role === 'editor';
return true;
});
el.classList.toggle('hidden', !visible);
});
document.querySelectorAll('.owner-only').forEach(el => {
el.classList.toggle('hidden', !isOwner());
});
}
function switchTab(tab) {
if (!tab) return;
state.currentTab = tab;
document.querySelectorAll('[data-user-panel]').forEach(panel => {
panel.classList.toggle('hidden', panel.getAttribute('data-user-panel') !== tab);
});
document.querySelectorAll('[data-user-tab]').forEach(btn => {
const isActive = btn.getAttribute('data-user-tab') === tab;
btn.classList.toggle('bg-sky-50', isActive);
btn.classList.toggle('text-sky-700', isActive);
});
}
async function loadAccountData() {
try {
state.loading = true;
const res = await apiAction('account.profile.get', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Profil konnte nicht geladen werden');
if (res.user) {
window.__currentUser = res.user;
handleUserContextChange();
}
fillProfileForm(res.user);
fillSettingsForm(res.settings || {});
if (teamTable && isOwner()) {
await loadUsers();
}
if (senderTable) {
if (isAdmin()) {
await loadSenders();
} else {
state.senders = [];
state.senderMap = new Map();
renderSenderList();
}
}
reportViewDebugInfo(res);
} catch (err) {
console.error(err);
toast(err.message || 'Fehler beim Laden', false);
} finally {
state.loading = false;
}
}
function fillProfileForm(user) {
if (!profileForm) return;
profileForm.name.value = user?.name || '';
profileForm.email.value = user?.email || '';
}
function fillSettingsForm(settings) {
state.settings = settings;
if (!settingsForm) return;
settingsForm.bridge_url.value = settings.bridge_url || '';
settingsForm.bridge_token.value = settings.bridge_token || '';
settingsForm.sender_token.value = settings.sender_token || '';
settingsForm.external_api_token.value = settings.external_api_token || '';
state.rotate = { bridge: false, sender: false, external: false };
refreshAdminTables(settings.bridge_setup?.tables || [], settings.bridge_tables || []);
}
async function submitProfileForm(ev) {
ev.preventDefault();
const data = {
name: profileForm.name.value.trim(),
email: profileForm.email.value.trim(),
};
try {
const res = await apiAction('account.profile.update', { method: 'POST', data });
if (!res?.ok) throw new Error(res?.error || 'Profil konnte nicht gespeichert werden');
window.__currentUser = res.user;
handleUserContextChange();
toast('Profil aktualisiert', true);
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function submitPasswordForm(ev) {
ev.preventDefault();
const data = {
current_password: passwordForm.current_password.value,
new_password: passwordForm.new_password.value,
};
try {
const res = await apiAction('account.password.update', { method: 'POST', data });
if (!res?.ok) throw new Error(res?.error || 'Passwort konnte nicht geändert werden');
passwordForm.reset();
toast('Passwort aktualisiert', true);
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function submitSettingsForm(ev) {
ev.preventDefault();
const bridgeTables = normalizeTableList(state.settings.bridge_tables || []);
const data = {
bridge_url: settingsForm.bridge_url.value.trim(),
bridge_token: settingsForm.bridge_token.value.trim(),
sender_token: settingsForm.sender_token.value.trim(),
external_api_token: settingsForm.external_api_token.value.trim(),
bridge_tables: bridgeTables,
rotate_bridge_token: state.rotate.bridge ? 1 : 0,
rotate_sender_token: state.rotate.sender ? 1 : 0,
rotate_external_token: state.rotate.external ? 1 : 0,
};
try {
const res = await apiAction('account.settings.update', { method: 'POST', data });
if (!res?.ok) throw new Error(res?.error || 'Einstellungen konnten nicht gespeichert werden');
fillSettingsForm(res.settings || {});
toast('Integrationen gespeichert', true);
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function downloadFile(type) {
try {
const action = type === 'sender' ? 'downloads.sender' : 'downloads.bridge';
const res = await apiAction(action, { method: 'POST', data: {} });
if (!res?.ok || !res.content) throw new Error(res?.error || 'Download fehlgeschlagen');
const bytes = atob(res.content);
const buffer = new Uint8Array(bytes.length);
for (let i = 0; i < bytes.length; i++) buffer[i] = bytes.charCodeAt(i);
const blob = new Blob([buffer], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = res.file_name || `${type}.php`;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (err) {
toast(err.message || 'Download fehlgeschlagen', false);
}
}
function normalizeTableList(input) {
const items = Array.isArray(input) ? input : (typeof input === 'string' ? input.split(/[\s,]+/) : []);
const result = [];
const seen = new Set();
items.forEach(entry => {
const name = String(entry || '').trim();
if (name && !seen.has(name)) {
seen.add(name);
result.push(name);
}
});
return result;
}
function refreshAdminTables(availableTables, selectedTables) {
const whitelist = normalizeTableList(availableTables);
let selected = normalizeTableList(selectedTables);
if (!selected.length) {
selected = whitelist.slice();
}
if (whitelist.length) {
selected = selected.filter(name => whitelist.includes(name));
}
state.settings.bridge_tables = selected;
state.settings.bridge_setup = state.settings.bridge_setup || {};
state.settings.bridge_setup.tables = whitelist;
updateAdminTableSelects(whitelist, selected);
}
function updateAdminTableSelects(availableTables, selectedTables) {
const selectedSet = new Set(selectedTables);
const available = availableTables.filter(name => !selectedSet.has(name));
renderSelect(adminTablesAllSelect, available, 'Keine Tabellen freigegeben.');
renderSelect(adminTablesSelectedSelect, selectedTables, 'Noch keine Tabellen ausgewaehlt.');
}
function renderSelect(selectEl, list, emptyLabel) {
if (!selectEl) return;
selectEl.innerHTML = '';
if (!list.length) {
const opt = document.createElement('option');
opt.textContent = emptyLabel;
opt.disabled = true;
selectEl.appendChild(opt);
return;
}
list.forEach(name => {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
selectEl.appendChild(opt);
});
}
function getSelectedOptions(selectEl) {
if (!selectEl) return [];
return Array.from(selectEl.selectedOptions || []).map(opt => opt.value);
}
function addAdminTables(list) {
const whitelist = normalizeTableList(state.settings.bridge_setup?.tables || []);
if (!whitelist.length) return;
const selected = normalizeTableList(state.settings.bridge_tables || []);
const merged = normalizeTableList([...selected, ...list]).filter(name => whitelist.includes(name));
state.settings.bridge_tables = merged;
updateAdminTableSelects(whitelist, merged);
}
function removeAdminTables(list) {
const whitelist = normalizeTableList(state.settings.bridge_setup?.tables || []);
if (!whitelist.length) return;
const removeSet = new Set(list);
const next = normalizeTableList(state.settings.bridge_tables || []).filter(name => !removeSet.has(name));
state.settings.bridge_tables = next;
updateAdminTableSelects(whitelist, next);
}
async function loadUsers() {
try {
const res = await apiAction('account.users.list', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Team konnte nicht geladen werden');
state.users = res.items || [];
state.userMap = new Map(state.users.map(u => [u.id, u]));
renderUserList();
} catch (err) {
toast(err.message || 'Fehler beim Laden der Nutzer', false);
}
}
function renderUserList() {
if (!teamTable) return;
const tbody = teamTable.querySelector('tbody');
if (!tbody) return;
if (!state.users.length) {
tbody.innerHTML = '<tr><td colspan="5" class="text-sm text-slate-500">Keine Nutzer vorhanden.</td></tr>';
return;
}
tbody.innerHTML = state.users.map(user => {
const badge = user.is_active ? '<span class="badge bg-green-100 text-green-800">Aktiv</span>' : '<span class="badge bg-slate-200 text-slate-700">Inaktiv</span>';
return `<tr>
<td>${escapeHtml(user.name)}</td>
<td>${escapeHtml(user.email)}</td>
<td>${escapeHtml(user.role)}</td>
<td>${badge}</td>
<td class="text-right flex gap-2 justify-end">
<button class="btn" data-user-action="edit" data-user-id="${user.id}">Bearbeiten</button>
<button class="btn" data-user-action="reset" data-user-id="${user.id}">Passwort</button>
<button class="btn btn-danger" data-user-action="delete" data-user-id="${user.id}">Löschen</button>
</td>
</tr>`;
}).join('');
}
function handleTeamTableClick(ev) {
const btn = ev.target.closest('button[data-user-action]');
if (!btn) return;
const id = Number(btn.getAttribute('data-user-id'));
const action = btn.getAttribute('data-user-action');
const user = state.userMap.get(id);
if (!user) return;
if (action === 'edit') {
openUserForm(user);
} else if (action === 'delete') {
if (confirm(`Soll ${user.name} wirklich entfernt werden?`)) deleteUser(id);
} else if (action === 'reset') {
openUserForm(user, true);
}
}
function openUserForm(user = null, resetOnly = false) {
if (!userForm) return;
userForm.classList.remove('hidden');
userForm.user_id.value = user?.id || '';
userForm.name.value = user?.name || '';
userForm.email.value = user?.email || '';
userForm.role.value = user?.role || 'user';
userForm.is_active.checked = user ? !!user.is_active : true;
const resetRow = userForm.querySelector('.reset-only');
if (resetRow) resetRow.classList.toggle('hidden', !user);
if (resetRow) resetRow.querySelector('input').checked = resetOnly;
}
function closeUserForm() {
if (!userForm) return;
userForm.classList.add('hidden');
userForm.reset();
userForm.user_id.value = '';
const resetInput = userForm.querySelector('.reset-only input');
if (resetInput) resetInput.checked = false;
}
async function submitUserForm(ev) {
ev.preventDefault();
const formData = new FormData(userForm);
const payload = {
name: formData.get('name')?.toString().trim() || '',
email: formData.get('email')?.toString().trim() || '',
role: formData.get('role')?.toString() || 'user',
is_active: userForm.is_active.checked ? 1 : 0,
};
const userId = formData.get('user_id')?.toString();
let action = 'account.users.create';
if (userId) {
action = 'account.users.update';
payload.user_id = Number(userId);
payload.reset_password = userForm.querySelector('input[name="reset_password"]').checked ? 1 : 0;
}
try {
const res = await apiAction(action, { method: 'POST', data: payload });
if (!res?.ok) throw new Error(res?.error || 'Speichern fehlgeschlagen');
closeUserForm();
await loadUsers();
toast('Nutzer gespeichert', true);
if (res.temp_password) {
copyToClipboard(res.temp_password);
alert(`Neues Passwort: ${res.temp_password}`);
}
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function deleteUser(userId) {
try {
const res = await apiAction('account.users.delete', { method: 'POST', data: { user_id: userId } });
if (!res?.ok) throw new Error(res?.error || 'Löschen fehlgeschlagen');
await loadUsers();
toast('Nutzer gelöscht', true);
} catch (err) {
toast(err.message || 'Fehler beim Löschen', false);
}
}
function copyToClipboard(value) {
if (navigator.clipboard?.writeText) {
navigator.clipboard.writeText(value).catch(() => {});
}
}
async function loadSenders() {
if (!senderTable) return;
try {
const res = await apiAction('account.senders.list', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Absender konnten nicht geladen werden');
state.senders = res.items || [];
state.senderMap = new Map(state.senders.map(item => [item.id, item]));
renderSenderList();
} catch (err) {
toast(err.message || 'Fehler beim Laden der Absender', false);
}
}
function renderSenderList() {
if (!senderTable) return;
const tbody = senderTable.querySelector('tbody');
if (!tbody) return;
if (!state.senders.length) {
tbody.innerHTML = '<tr><td colspan="5" class="text-sm text-slate-500">Keine Absender vorhanden.</td></tr>';
return;
}
tbody.innerHTML = state.senders.map(sender => `
<tr>
<td>${escapeHtml(sender.label || sender.from_name || sender.from_email)}</td>
<td>${escapeHtml(sender.from_name || '—')}</td>
<td>${escapeHtml(sender.from_email)}</td>
<td>${escapeHtml(sender.reply_to || '')}</td>
<td class="text-right flex gap-2 justify-end">
<button class="btn" data-sender-action="edit" data-sender-id="${sender.id}">Bearbeiten</button>
<button class="btn btn-danger" data-sender-action="delete" data-sender-id="${sender.id}">Löschen</button>
</td>
</tr>
`).join('');
}
function handleSenderTableClick(ev) {
const btn = ev.target.closest('button[data-sender-action]');
if (!btn) return;
const id = Number(btn.getAttribute('data-sender-id'));
const action = btn.getAttribute('data-sender-action');
const sender = state.senderMap.get(id);
if (!sender) return;
if (action === 'edit') {
openSenderForm(sender);
} else if (action === 'delete') {
if (confirm(`Absender "${sender.label || sender.from_email}" wirklich löschen?`)) {
deleteSender(id);
}
}
}
function openSenderForm(sender = null) {
if (!senderForm) return;
senderForm.classList.remove('hidden');
senderForm.sender_id.value = sender?.id || '';
senderForm.label.value = sender?.label || '';
senderForm.from_name.value = sender?.from_name || '';
senderForm.from_email.value = sender?.from_email || '';
senderForm.reply_to.value = sender?.reply_to || '';
}
function closeSenderForm() {
if (!senderForm) return;
senderForm.classList.add('hidden');
senderForm.reset();
senderForm.sender_id.value = '';
}
async function submitSenderForm(ev) {
ev.preventDefault();
if (!senderForm) return;
const payload = {
sender_id: senderForm.sender_id.value ? Number(senderForm.sender_id.value) : undefined,
label: senderForm.label.value.trim(),
from_name: senderForm.from_name.value.trim(),
from_email: senderForm.from_email.value.trim(),
reply_to: senderForm.reply_to.value.trim(),
};
if (!payload.from_email) {
toast('Bitte eine Absenderadresse angeben', false);
return;
}
try {
const res = await apiAction('account.senders.save', { method: 'POST', data: payload });
if (!res?.ok) throw new Error(res?.error || 'Speichern fehlgeschlagen');
closeSenderForm();
await loadSenders();
toast('Absender gespeichert', true);
} catch (err) {
toast(err.message || 'Fehler beim Speichern', false);
}
}
async function deleteSender(senderId) {
try {
const res = await apiAction('account.senders.delete', { method: 'POST', data: { sender_id: senderId } });
if (!res?.ok) throw new Error(res?.error || 'Löschen fehlgeschlagen');
await loadSenders();
toast('Absender gelöscht', true);
} catch (err) {
toast(err.message || 'Fehler beim Löschen', false);
}
}
function escapeHtml(str) {
return String(str || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function refreshDebugAccess() {
const allowed = (window.__currentUser?.email || '').toLowerCase() === DEBUG_EMAIL;
if (!allowed) {
debugButton?.remove();
debugButton = null;
if (debugDialog?.open) {
debugDialog.close();
}
return;
}
ensureDebugStyles();
ensureConsoleCapture();
ensureDebugDialog();
if (!debugButton) {
debugButton = document.createElement('button');
debugButton.id = 'debugToggleButton';
debugButton.className = 'debug-floating-btn';
debugButton.type = 'button';
debugButton.textContent = 'Debug';
debugButton.addEventListener('click', () => openDebugDialog('php'));
document.body.appendChild(debugButton);
}
}
function ensureDebugStyles() {
if (debugStylesInjected) return;
const style = document.createElement('style');
style.id = 'debugStyles';
style.textContent = `
.debug-floating-btn{position:fixed;left:16px;bottom:16px;padding:.5rem 1rem;border-radius:999px;border:none;background:#0ea5e9;color:#fff;font-weight:600;box-shadow:0 10px 25px rgba(15,23,42,.25);z-index:60;cursor:pointer}
.debug-floating-btn:hover{background:#0284c7}
dialog#debugDialog::backdrop{background:rgba(15,23,42,.45)}
#debugDialog{border:none;border-radius:1rem;padding:0;width:80vw;max-width:960px}
.debug-shell{display:flex;flex-direction:column;height:80vh}
.debug-header{display:flex;align-items:center;justify-content:space-between;padding:1rem;border-bottom:1px solid #e2e8f0}
.debug-tabs{display:flex;border-bottom:1px solid #e2e8f0}
.debug-tabs button{flex:1;padding:.75rem 1rem;border:none;background:transparent;cursor:pointer;font-weight:600}
.debug-tabs button.active{background:#e0f2fe;color:#0c4a6e}
.debug-panel{flex:1;overflow:auto;padding:1rem;background:#f8fafc}
#debugConsoleContent{font-family:monospace;font-size:.85rem;white-space:pre-wrap}
.debug-console-entry{margin-bottom:.35rem}
.debug-console-entry.log{color:#15803d}
.debug-console-entry.warn{color:#b45309}
.debug-console-entry.error{color:#b91c1c}
`;
document.head.appendChild(style);
debugStylesInjected = true;
}
function ensureDebugDialog() {
if (debugDialog) return;
debugDialog = document.createElement('dialog');
debugDialog.id = 'debugDialog';
debugDialog.innerHTML = `
<div class="debug-shell bg-white rounded-2xl shadow-2xl">
<div class="debug-header">
<strong>Debug Tools</strong>
<button type="button" class="btn" data-debug-close>Schließen</button>
</div>
<div class="debug-tabs">
<button type="button" data-debug-tab="php" class="active">PHP Debug</button>
<button type="button" data-debug-tab="console">Console</button>
</div>
<div class="debug-panel" data-debug-panel="php">
<div id="debugPhpContent" class="text-sm text-slate-700">Lade Daten…</div>
</div>
<div class="debug-panel hidden" data-debug-panel="console">
<pre id="debugConsoleContent"></pre>
</div>
</div>`;
document.body.appendChild(debugDialog);
phpInfoContainer = debugDialog.querySelector('#debugPhpContent');
consoleContainer = debugDialog.querySelector('#debugConsoleContent');
debugDialog.querySelector('[data-debug-close]')?.addEventListener('click', () => closeDebugDialog());
debugDialog.addEventListener('close', () => setDebugTab('php'));
debugDialog.querySelectorAll('[data-debug-tab]').forEach(btn => {
btn.addEventListener('click', () => setDebugTab(btn.getAttribute('data-debug-tab')));
});
}
function openDebugDialog(tab = 'php') {
ensureDebugDialog();
setDebugTab(tab);
if (!debugDialog.open) debugDialog.showModal();
if (tab === 'php') {
loadPhpInfo();
} else {
renderConsolePanel();
}
}
function closeDebugDialog() {
if (debugDialog?.open) debugDialog.close();
}
function setDebugTab(tab) {
if (!debugDialog) return;
debugActiveTab = tab || 'php';
debugDialog.querySelectorAll('[data-debug-tab]').forEach(btn => {
const isActive = btn.getAttribute('data-debug-tab') === debugActiveTab;
btn.classList.toggle('active', isActive);
});
debugDialog.querySelectorAll('[data-debug-panel]').forEach(panel => {
panel.classList.toggle('hidden', panel.getAttribute('data-debug-panel') !== debugActiveTab);
});
if (debugActiveTab === 'php') {
loadPhpInfo();
} else {
renderConsolePanel();
}
}
async function loadPhpInfo() {
if (debugPhpLoaded || debugPhpLoading || !phpInfoContainer) return;
debugPhpLoading = true;
phpInfoContainer.textContent = 'Lade phpinfo…';
try {
const res = await apiAction('debug.phpinfo', { method: 'GET' });
if (!res?.ok) throw new Error(res?.error || 'Fehler beim Laden');
const frame = document.createElement('iframe');
frame.style.width = '100%';
frame.style.height = '100%';
frame.style.minHeight = '400px';
frame.style.border = 'none';
frame.srcdoc = res.html || '<p>Keine Daten</p>';
phpInfoContainer.innerHTML = '';
phpInfoContainer.appendChild(frame);
debugPhpLoaded = true;
} catch (err) {
phpInfoContainer.textContent = err.message || 'Fehler beim Laden';
} finally {
debugPhpLoading = false;
}
}
function renderConsolePanel() {
if (!consoleContainer) return;
if (!consoleBuffer.length) {
consoleContainer.textContent = 'Noch keine Konsolenmeldungen in dieser Sitzung.';
return;
}
const lines = consoleBuffer.map(entry => {
const time = entry.time.toLocaleTimeString();
return `<div class="debug-console-entry ${entry.type}">[${time}] ${escapeHtml(entry.text)}</div>`;
});
consoleContainer.innerHTML = lines.join('');
}
function ensureConsoleCapture() {
if (consolePatched) return;
['log', 'warn', 'error'].forEach(type => {
const original = console[type];
console[type] = function (...args) {
appendConsoleMessage(type, args);
if (typeof original === 'function') {
original.apply(console, args);
}
};
});
consolePatched = true;
}
function appendConsoleMessage(type, args) {
const text = args.map(formatConsoleArg).join(' ');
consoleBuffer.push({ type, text, time: new Date() });
if (consoleBuffer.length > MAX_CONSOLE_LINES) consoleBuffer.shift();
if (debugActiveTab === 'console' && debugDialog?.open) {
renderConsolePanel();
}
}
function formatConsoleArg(arg) {
if (typeof arg === 'string') return arg;
try {
return JSON.stringify(arg);
} catch (err) {
return String(arg);
}
}
function reportViewDebugInfo(apiResponse) {
const role = (window.__currentUser?.role || '').toLowerCase() || 'unknown';
const expected = {
profile: true,
dashboard: role === 'owner' || role === 'admin',
administration: role === 'owner' || role === 'admin',
downloads: role === 'owner',
};
const summary = {
hasUser: !!apiResponse?.user,
userRole: apiResponse?.user?.role ?? null,
settingsKeys: apiResponse?.settings ? Object.keys(apiResponse.settings) : [],
usersCount: Array.isArray(state.users) ? state.users.length : null,
sendersCount: Array.isArray(state.senders) ? state.senders.length : null,
requestedTab: state.currentTab,
};
console.log('[view-debug]', { expectedViews: expected, role, apiSummary: summary });
}