1129 lines
41 KiB
JavaScript
1129 lines
41 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 DEBUG_ENV = (window.APP_ENV || '').toLowerCase();
|
|
const MAX_CONSOLE_LINES = 200;
|
|
|
|
let avatarBtn;
|
|
let userMenuPanel;
|
|
let profileForm;
|
|
let passwordForm;
|
|
let settingsForm;
|
|
let teamTable;
|
|
let userForm;
|
|
let senderTable;
|
|
let senderForm;
|
|
let sectionsList;
|
|
let sectionsCreateForm;
|
|
let sectionNameInput;
|
|
let sectionsDeleteDialog;
|
|
let sectionsDeleteForm;
|
|
let sectionsDeleteTarget;
|
|
let sectionsDeleteText;
|
|
let sectionsDeleteCancel;
|
|
let sectionDragId = null;
|
|
let menuInitialized = false;
|
|
let menuOpen = false;
|
|
let debugButton;
|
|
let debugDialog;
|
|
let debugPhpLoaded = false;
|
|
let debugPhpLoading = false;
|
|
let debugLogsLoaded = false;
|
|
let debugActiveTab = 'php';
|
|
let phpInfoContainer;
|
|
let consoleContainer;
|
|
let logsListContainer;
|
|
let logDetailContainer;
|
|
let debugStylesInjected = false;
|
|
let consolePatched = false;
|
|
const consoleBuffer = [];
|
|
let adminTablesAllSelect;
|
|
let adminTablesSelectedSelect;
|
|
let adminTablesAddBtn;
|
|
let adminTablesRemoveBtn;
|
|
let adminLoadBridgeBtn;
|
|
|
|
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');
|
|
adminLoadBridgeBtn = document.getElementById('btn-admin-load-bridge');
|
|
sectionsList = document.getElementById('sectionsList');
|
|
sectionsCreateForm = document.getElementById('sectionsCreateForm');
|
|
sectionNameInput = document.getElementById('sectionNameInput');
|
|
sectionsDeleteDialog = document.getElementById('sectionsDeleteDialog');
|
|
sectionsDeleteForm = document.getElementById('sectionsDeleteForm');
|
|
sectionsDeleteTarget = document.getElementById('sectionsDeleteTarget');
|
|
sectionsDeleteText = document.getElementById('sectionsDeleteText');
|
|
sectionsDeleteCancel = document.getElementById('sectionsDeleteCancel');
|
|
|
|
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));
|
|
});
|
|
adminLoadBridgeBtn?.addEventListener('click', () => {
|
|
refreshBridgeTablesFromEndpoint();
|
|
});
|
|
|
|
initSectionsManager();
|
|
|
|
window.addEventListener('bridge-setup-updated', (ev) => {
|
|
const setup = ev?.detail || {};
|
|
refreshAdminTables(setup.tables || [], state.settings.bridge_tables || []);
|
|
});
|
|
|
|
switchTab(state.currentTab);
|
|
loadAccountData();
|
|
updateRoleVisibility();
|
|
}
|
|
|
|
function initSectionsManager() {
|
|
if (!sectionsList || !sectionsCreateForm || !sectionNameInput) return;
|
|
|
|
sectionsCreateForm.addEventListener('submit', async (ev) => {
|
|
ev.preventDefault();
|
|
const name = sectionNameInput.value.trim();
|
|
if (!name) return;
|
|
try {
|
|
const res = await apiAction('sections_config.create', { method: 'POST', data: { name } });
|
|
if (!res?.ok) throw new Error(res?.error || 'Erstellen fehlgeschlagen');
|
|
sectionNameInput.value = '';
|
|
await loadSectionsConfig();
|
|
toast('Section erstellt', true);
|
|
} catch (err) {
|
|
toast(err.message || 'Erstellen fehlgeschlagen', false);
|
|
}
|
|
});
|
|
|
|
sectionsDeleteCancel && (sectionsDeleteCancel.onclick = () => {
|
|
sectionsDeleteDialog?.close();
|
|
});
|
|
|
|
sectionsDeleteForm?.addEventListener('submit', async (ev) => {
|
|
ev.preventDefault();
|
|
const id = Number(sectionsDeleteForm?.dataset?.sectionId || 0);
|
|
const target = Number(sectionsDeleteTarget?.value || 0);
|
|
if (!id || !target) return;
|
|
try {
|
|
const res = await apiAction('sections_config.delete', { method: 'POST', data: { id, move_to: target } });
|
|
if (!res?.ok) throw new Error(res?.error || 'Löschen fehlgeschlagen');
|
|
sectionsDeleteDialog?.close();
|
|
await loadSectionsConfig();
|
|
toast('Section gelöscht', true);
|
|
} catch (err) {
|
|
toast(err.message || 'Löschen fehlgeschlagen', false);
|
|
}
|
|
});
|
|
|
|
loadSectionsConfig();
|
|
}
|
|
|
|
async function loadSectionsConfig() {
|
|
try {
|
|
const res = await apiAction('sections_config.list', { method: 'GET' });
|
|
const items = Array.isArray(res?.items) ? res.items : [];
|
|
renderSectionsList(items);
|
|
} catch (err) {
|
|
toast(err.message || 'Sections konnten nicht geladen werden', false);
|
|
}
|
|
}
|
|
|
|
function renderSectionsList(items) {
|
|
if (!sectionsList) return;
|
|
const rows = items || [];
|
|
sectionsList.innerHTML = rows.map((item) => {
|
|
const isTemplate = Number(item.is_template) === 1;
|
|
const dragAttr = isTemplate ? '' : 'draggable="true"';
|
|
const badge = isTemplate ? '<span class="text-xs text-sky-700 bg-sky-100 px-2 py-0.5 rounded-full">Fix</span>' : '';
|
|
const editBtn = isTemplate ? '' : `<button type="button" class="btn text-xs" data-edit="${item.id}">Umbenennen</button>`;
|
|
const delBtn = isTemplate ? '' : `<button type="button" class="btn btn-danger text-xs" data-del="${item.id}">Löschen</button>`;
|
|
return `<li class="section-item flex items-center gap-3 border rounded-lg px-3 py-2 bg-white" data-id="${item.id}" ${dragAttr}>
|
|
<span class="cursor-grab text-slate-400 select-none">☰</span>
|
|
<div class="flex-1">
|
|
<div class="font-medium">${escapeHtml(item.name || '')}</div>
|
|
<div class="text-xs text-slate-500">${escapeHtml(item.slug || '')}</div>
|
|
</div>
|
|
${badge}
|
|
<div class="flex gap-2">${editBtn}${delBtn}</div>
|
|
</li>`;
|
|
}).join('');
|
|
|
|
sectionsList.querySelectorAll('[data-edit]').forEach(btn => btn.addEventListener('click', async () => {
|
|
const id = Number(btn.dataset.edit || 0);
|
|
const current = rows.find(r => Number(r.id) === id);
|
|
if (!current) return;
|
|
const next = prompt('Neuer Name', current.name || '');
|
|
if (!next || next.trim() === current.name) return;
|
|
try {
|
|
const res = await apiAction('sections_config.update', { method: 'POST', data: { id, name: next.trim() } });
|
|
if (!res?.ok) throw new Error(res?.error || 'Speichern fehlgeschlagen');
|
|
await loadSectionsConfig();
|
|
toast('Section gespeichert', true);
|
|
} catch (err) {
|
|
toast(err.message || 'Speichern fehlgeschlagen', false);
|
|
}
|
|
}));
|
|
|
|
sectionsList.querySelectorAll('[data-del]').forEach(btn => btn.addEventListener('click', () => {
|
|
const id = Number(btn.dataset.del || 0);
|
|
const current = rows.find(r => Number(r.id) === id);
|
|
if (!current) return;
|
|
const targets = rows.filter(r => Number(r.id) !== id);
|
|
if (!targets.length) {
|
|
toast('Keine Ziel-Section verfügbar', false);
|
|
return;
|
|
}
|
|
if (sectionsDeleteText) {
|
|
sectionsDeleteText.textContent = `Section "${current.name}" löschen?`;
|
|
}
|
|
if (sectionsDeleteTarget) {
|
|
sectionsDeleteTarget.innerHTML = targets
|
|
.map(r => `<option value="${r.id}">${escapeHtml(r.name || '')}</option>`)
|
|
.join('');
|
|
}
|
|
if (sectionsDeleteForm) {
|
|
sectionsDeleteForm.dataset.sectionId = String(id);
|
|
}
|
|
sectionsDeleteDialog?.showModal?.();
|
|
}));
|
|
|
|
sectionsList.querySelectorAll('[draggable="true"]').forEach(item => {
|
|
item.addEventListener('dragstart', (ev) => {
|
|
sectionDragId = item.dataset.id || null;
|
|
ev.dataTransfer?.setData('text/plain', sectionDragId || '');
|
|
});
|
|
item.addEventListener('dragend', () => {
|
|
sectionDragId = null;
|
|
});
|
|
item.addEventListener('dragover', (ev) => {
|
|
ev.preventDefault();
|
|
});
|
|
item.addEventListener('drop', async (ev) => {
|
|
ev.preventDefault();
|
|
const targetId = item.dataset.id || null;
|
|
if (!sectionDragId || !targetId || sectionDragId === targetId) return;
|
|
const ids = Array.from(sectionsList.querySelectorAll('[data-id]')).map(el => el.getAttribute('data-id'));
|
|
const fromIndex = ids.indexOf(sectionDragId);
|
|
const toIndex = ids.indexOf(targetId);
|
|
if (fromIndex === -1 || toIndex === -1) return;
|
|
ids.splice(fromIndex, 1);
|
|
ids.splice(toIndex, 0, sectionDragId);
|
|
try {
|
|
const res = await apiAction('sections_config.reorder', { method: 'POST', data: { order: ids } });
|
|
if (!res?.ok) throw new Error(res?.error || 'Sortierung fehlgeschlagen');
|
|
await loadSectionsConfig();
|
|
toast('Sortierung gespeichert', true);
|
|
} catch (err) {
|
|
toast(err.message || 'Sortierung fehlgeschlagen', false);
|
|
} finally {
|
|
sectionDragId = null;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
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 || '';
|
|
if (settingsForm.editor_default) {
|
|
settingsForm.editor_default.value = settings.editor_default || 'grapesjs';
|
|
}
|
|
window.__editorDefault = settings.editor_default || 'grapesjs';
|
|
window.__listSortDefault = settings.list_sort || 'created_asc';
|
|
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(),
|
|
editor_default: settingsForm.editor_default ? settingsForm.editor_default.value : undefined,
|
|
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 => {
|
|
let name = '';
|
|
if (typeof entry === 'string') {
|
|
name = entry;
|
|
} else if (entry && typeof entry === 'object') {
|
|
name = entry.name || entry.table || entry.label || '';
|
|
}
|
|
name = String(name || '').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);
|
|
}
|
|
|
|
async function refreshBridgeTablesFromEndpoint() {
|
|
if (state.loading) return;
|
|
state.loading = true;
|
|
try {
|
|
const res = await apiAction('account.bridge.test', { method: 'POST', data: {} });
|
|
if (!res?.ok) throw new Error(res?.error || 'Bridge konnte nicht abgefragt werden');
|
|
const fetched = normalizeTableList(res.tables || []);
|
|
if (!fetched.length) {
|
|
toast('Keine Tabellen vom Bridge-Endpunkt erhalten', false);
|
|
return;
|
|
}
|
|
const selected = normalizeTableList(state.settings.bridge_tables || []);
|
|
const selectedSet = new Set(selected);
|
|
const nextSelected = fetched.filter(name => selectedSet.has(name));
|
|
state.settings.bridge_setup = state.settings.bridge_setup || {};
|
|
state.settings.bridge_setup.tables = fetched;
|
|
state.settings.bridge_tables = nextSelected;
|
|
updateAdminTableSelects(fetched, nextSelected);
|
|
toast('Tabellen aktualisiert', true);
|
|
} catch (err) {
|
|
toast(err.message || 'Bridge konnte nicht geprüft werden', false);
|
|
} finally {
|
|
state.loading = false;
|
|
}
|
|
}
|
|
|
|
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function refreshDebugAccess() {
|
|
const isStaging = DEBUG_ENV === 'staging';
|
|
const allowed = isStaging && (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}
|
|
.debug-logs-grid{display:grid;grid-template-columns:220px 1fr;gap:12px;height:100%}
|
|
.debug-logs-list{background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;padding:.5rem;overflow:auto}
|
|
.debug-logs-list button{width:100%;text-align:left;border:none;background:transparent;padding:.35rem .5rem;border-radius:.5rem;cursor:pointer}
|
|
.debug-logs-list button.active{background:#e0f2fe;color:#0c4a6e}
|
|
.debug-logs-detail{background:#fff;border:1px solid #e2e8f0;border-radius:.75rem;padding:.75rem;overflow:auto;font-family:monospace;font-size:.85rem;white-space:pre-wrap}
|
|
`;
|
|
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>
|
|
<button type="button" data-debug-tab="logs">Logs</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 class="debug-panel hidden" data-debug-panel="logs">
|
|
<div class="debug-logs-grid">
|
|
<div class="debug-logs-list" id="debugLogsList">Keine Logs geladen.</div>
|
|
<div class="debug-logs-detail" id="debugLogDetail">Bitte Log auswaehlen.</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
document.body.appendChild(debugDialog);
|
|
phpInfoContainer = debugDialog.querySelector('#debugPhpContent');
|
|
consoleContainer = debugDialog.querySelector('#debugConsoleContent');
|
|
logsListContainer = debugDialog.querySelector('#debugLogsList');
|
|
logDetailContainer = debugDialog.querySelector('#debugLogDetail');
|
|
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 if (debugActiveTab === 'console') {
|
|
renderConsolePanel();
|
|
} else if (debugActiveTab === 'logs') {
|
|
loadDebugLogs();
|
|
}
|
|
}
|
|
|
|
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('');
|
|
}
|
|
|
|
async function loadDebugLogs() {
|
|
if (debugLogsLoaded || !logsListContainer || !logDetailContainer) return;
|
|
logsListContainer.textContent = 'Lade Logs…';
|
|
logDetailContainer.textContent = 'Bitte Log auswaehlen.';
|
|
try {
|
|
const res = await apiAction('debug.logs.list', { method: 'GET' });
|
|
if (!res?.ok) throw new Error(res?.error || 'Logs konnten nicht geladen werden');
|
|
const items = Array.isArray(res.items) ? res.items : [];
|
|
if (!items.length) {
|
|
logsListContainer.textContent = 'Keine Logs vorhanden.';
|
|
return;
|
|
}
|
|
logsListContainer.innerHTML = '';
|
|
items.forEach((item, idx) => {
|
|
const btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.textContent = item.name || item.file || `Log ${idx + 1}`;
|
|
btn.addEventListener('click', () => {
|
|
logsListContainer.querySelectorAll('button').forEach(b => b.classList.remove('active'));
|
|
btn.classList.add('active');
|
|
loadDebugLogFile(item.name || item.file || '');
|
|
});
|
|
logsListContainer.appendChild(btn);
|
|
if (idx === 0) btn.click();
|
|
});
|
|
debugLogsLoaded = true;
|
|
} catch (err) {
|
|
logsListContainer.textContent = err.message || 'Logs konnten nicht geladen werden';
|
|
}
|
|
}
|
|
|
|
async function loadDebugLogFile(name) {
|
|
if (!name || !logDetailContainer) return;
|
|
logDetailContainer.textContent = 'Lade Log…';
|
|
try {
|
|
const res = await apiAction('debug.logs.read', { method: 'GET', data: { name } });
|
|
if (!res?.ok) throw new Error(res?.error || 'Log konnte nicht geladen werden');
|
|
logDetailContainer.textContent = res.content || '(leer)';
|
|
} catch (err) {
|
|
logDetailContainer.textContent = err.message || 'Log konnte nicht geladen werden';
|
|
}
|
|
}
|
|
|
|
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 });
|
|
}
|