(function () { let active = false; let initialized = false; const API_URL = (() => { if (window.API_URL) return window.API_URL; try { return `${window.location.origin}/api`; } catch (error) { return 'https://fb.srv.medeba-media.de/api'; } })(); const LOGIN_PAGE = 'login.html'; const state = { traces: [], selectedTraceId: null, loading: false }; const tableBody = document.getElementById('aiDebugTableBody'); const statusEl = document.getElementById('aiDebugStatus'); const detailMeta = document.getElementById('aiDebugDetailMeta'); const detailViz = document.getElementById('aiDebugDetailViz'); const detailJson = document.getElementById('aiDebugDetailJson'); const refreshBtn = document.getElementById('aiDebugRefreshBtn'); const statusFilter = document.getElementById('aiDebugStatusFilter'); const limitFilter = document.getElementById('aiDebugLimitFilter'); function handleUnauthorized(response) { if (response && response.status === 401) { if (typeof redirectToLogin === 'function') { redirectToLogin(); } else { window.location.href = LOGIN_PAGE; } return true; } return false; } async function apiFetchJSON(path, options = {}) { const response = await fetch(`${API_URL}${path}`, { credentials: 'include', ...options }); if (handleUnauthorized(response)) { throw new Error('Nicht angemeldet'); } if (!response.ok) { const payload = await response.json().catch(() => ({})); throw new Error(payload.error || 'Unbekannter Fehler'); } return response.json(); } function setStatus(message, isError = false) { if (!statusEl) return; statusEl.textContent = message || ''; statusEl.classList.toggle('ai-debug-status--error', !!isError); } function formatDate(value) { if (!value) return '—'; const date = new Date(value); if (Number.isNaN(date.getTime())) return '—'; return date.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } function formatMs(value) { const numeric = Number(value); if (!Number.isFinite(numeric) || numeric < 0) { return '—'; } return `${Math.round(numeric)} ms`; } function compactId(value) { if (!value || typeof value !== 'string') return '—'; if (value.length <= 16) return value; return `${value.slice(0, 8)}…${value.slice(-6)}`; } function toPositiveNumber(value) { const numeric = Number(value); if (!Number.isFinite(numeric) || numeric < 0) { return null; } return numeric; } function getBackendTotalMs(item) { return item && item.backend_timings && typeof item.backend_timings === 'object' ? item.backend_timings.totalMs : null; } function getAiRequestMs(item) { return item && item.frontend_timings && typeof item.frontend_timings === 'object' ? item.frontend_timings.aiRequestMs : null; } function renderTable() { if (!tableBody) return; tableBody.innerHTML = ''; if (!state.traces.length) { const tr = document.createElement('tr'); tr.innerHTML = 'Keine Debug-Läufe gefunden.'; tableBody.appendChild(tr); return; } const totals = state.traces .map((item) => toPositiveNumber(item.total_duration_ms)) .filter((value) => value !== null); const maxTotal = totals.length ? Math.max(...totals) : 0; state.traces.forEach((item) => { const tr = document.createElement('tr'); tr.dataset.traceId = item.trace_id; if (item.trace_id === state.selectedTraceId) { tr.classList.add('is-selected'); } const totalMs = toPositiveNumber(item.total_duration_ms); const totalBarPercent = maxTotal > 0 && totalMs !== null ? Math.max(4, Math.round((totalMs / maxTotal) * 100)) : 0; tr.innerHTML = ` ${formatDate(item.created_at)} ${item.status || '—'}
${formatMs(item.total_duration_ms)}
${formatMs(getBackendTotalMs(item))} ${formatMs(getAiRequestMs(item))} ${compactId(item.flow_id)} / ${compactId(item.trace_id)} `; tableBody.appendChild(tr); }); } function collectFrontendBars(trace) { const front = trace && trace.frontend_timings && typeof trace.frontend_timings === 'object' ? trace.frontend_timings : {}; const mapping = [ ['extractPostTextMs', 'Text-Extraktion'], ['profileLookupMs', 'Profil-Lookup'], ['aiRequestMs', 'AI-Request'], ['waitForCommentInputMs', 'Wartezeit Kommentarfeld'], ['setCommentTextMs', 'Kommentar einfügen'], ['confirmParticipationMs', 'Auto-Bestätigung'], ['clipboardWriteMs', 'Clipboard-Fallback'], ['totalMs', 'Frontend Total'] ]; return mapping .map(([key, label]) => ({ key, label, value: toPositiveNumber(front[key]) })) .filter((entry) => entry.value !== null); } function collectBackendBars(trace) { const back = trace && trace.backend_timings && typeof trace.backend_timings === 'object' ? trace.backend_timings : {}; const mapping = [ ['loadSettingsMs', 'Settings laden'], ['reactivateCredentialsMs', 'Credentials reaktivieren'], ['loadCredentialsMs', 'Credentials laden'], ['buildPromptMs', 'Prompt bauen'], ['credentialLoopMs', 'Provider-/Credential-Loop'], ['totalMs', 'Backend Total'] ]; return mapping .map(([key, label]) => ({ key, label, value: toPositiveNumber(back[key]) })) .filter((entry) => entry.value !== null); } function collectCredentialBars(trace) { const attempts = trace && Array.isArray(trace.backend_attempts) ? trace.backend_attempts : []; return attempts .map((attempt, index) => { const credential = attempt && attempt.credentialName ? attempt.credentialName : `Credential ${index + 1}`; const status = attempt && attempt.status ? attempt.status : 'unknown'; return { key: `credential_${index + 1}`, label: `${credential} (${status})`, value: toPositiveNumber(attempt && attempt.duration_ms) }; }) .filter((entry) => entry.value !== null); } function renderBarGroup(title, rows) { if (!Array.isArray(rows) || !rows.length) { return ''; } const max = Math.max(...rows.map((entry) => entry.value)); const sorted = [...rows].sort((a, b) => b.value - a.value); const peakKey = sorted[0] ? sorted[0].key : null; const items = rows.map((entry) => { const width = max > 0 ? Math.max(4, Math.round((entry.value / max) * 100)) : 0; const peakClass = entry.key === peakKey ? ' ai-debug-bar-row--peak' : ''; return `
  • ${entry.label} ${formatMs(entry.value)}
  • `; }).join(''); return `

    ${title}

    `; } function renderDetailVisualization(trace) { if (!detailViz) { return; } if (!trace) { detailViz.innerHTML = ''; return; } const frontendBars = collectFrontendBars(trace); const backendBars = collectBackendBars(trace); const credentialBars = collectCredentialBars(trace); const sections = [ renderBarGroup('Frontend-Phasen', frontendBars), renderBarGroup('Backend-Phasen', backendBars), renderBarGroup('Credential-Attempts', credentialBars) ].filter(Boolean); if (!sections.length) { detailViz.innerHTML = '
    Keine Timing-Daten für diesen Lauf verfügbar.
    '; return; } detailViz.innerHTML = sections.join(''); } function renderDetail(trace) { if (!detailMeta || !detailJson) { return; } if (!trace) { detailMeta.textContent = 'Bitte einen Eintrag auswählen.'; renderDetailVisualization(null); detailJson.textContent = ''; return; } detailMeta.textContent = `${trace.status || '—'} · ${formatDate(trace.created_at)} · Trace ${trace.trace_id || '—'}`; renderDetailVisualization(trace); detailJson.textContent = JSON.stringify(trace, null, 2); } async function loadTraceDetail(traceId) { if (!traceId) { renderDetail(null); return; } try { const trace = await apiFetchJSON(`/ai/debug-traces/${encodeURIComponent(traceId)}`); if (!active) return; renderDetail(trace); } catch (error) { if (!active) return; setStatus(`Details konnten nicht geladen werden: ${error.message}`, true); } } async function loadTraces() { if (!active || state.loading) { return; } state.loading = true; setStatus('Lade Debug-Läufe...'); try { const params = new URLSearchParams(); const status = statusFilter ? statusFilter.value.trim() : ''; const limit = limitFilter ? parseInt(limitFilter.value, 10) : 50; if (status) { params.set('status', status); } if (Number.isFinite(limit) && limit > 0) { params.set('limit', String(limit)); } const data = await apiFetchJSON(`/ai/debug-traces?${params.toString()}`); if (!active) return; state.traces = Array.isArray(data.items) ? data.items : []; if (!state.selectedTraceId || !state.traces.some((item) => item.trace_id === state.selectedTraceId)) { state.selectedTraceId = state.traces.length ? state.traces[0].trace_id : null; } renderTable(); await loadTraceDetail(state.selectedTraceId); setStatus(`${state.traces.length} Lauf/Läufe geladen.`); } catch (error) { if (!active) return; state.traces = []; renderTable(); renderDetail(null); setStatus(`Debug-Läufe konnten nicht geladen werden: ${error.message}`, true); } finally { state.loading = false; } } function setupEvents() { if (refreshBtn) { refreshBtn.addEventListener('click', () => loadTraces()); } if (statusFilter) { statusFilter.addEventListener('change', () => loadTraces()); } if (limitFilter) { limitFilter.addEventListener('change', () => loadTraces()); } if (tableBody) { tableBody.addEventListener('click', (event) => { const row = event.target.closest('tr[data-trace-id]'); if (!row) return; const traceId = row.dataset.traceId; if (!traceId) return; state.selectedTraceId = traceId; renderTable(); loadTraceDetail(traceId); }); } } function init() { if (initialized) return; setupEvents(); initialized = true; } function activate() { init(); active = true; loadTraces(); } function deactivate() { active = false; } window.AIDebugPage = { activate, deactivate }; const section = document.querySelector('[data-view="ai-debug"]'); if (section && section.classList.contains('app-view--active')) { activate(); } })();