(() => { 'use strict'; const $ = id => document.getElementById(id); const state = {payload: null, loading: false, refreshTimer: 0, live: null, openRun: '', openEvents: new Set()}; const nf = new Intl.NumberFormat('de-DE'); const dtf = new Intl.DateTimeFormat('de-DE', {dateStyle: 'short', timeStyle: 'medium'}); const tf = new Intl.DateTimeFormat('de-DE', {hour: '2-digit', minute: '2-digit', second: '2-digit'}); function esc(value) { return String(value ?? '').replace(/[&<>'"]/g, char => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[char])); } function num(value) { return nf.format(Number(value || 0)); } function pct(value) { return `${Math.round(Number(value || 0) * 100)} %`; } function when(value) { const date = new Date(value); return Number.isNaN(date.getTime()) ? '–' : dtf.format(date); } function clock(value) { const date = new Date(value); return Number.isNaN(date.getTime()) ? '–' : tf.format(date); } function duration(ms) { ms = Number(ms || 0); if (ms < 1000) return `${Math.max(0, Math.round(ms))} ms`; if (ms < 60000) return `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)} s`; const minutes = Math.floor(ms / 60000); const seconds = Math.round((ms % 60000) / 1000); if (minutes < 60) return `${minutes} min ${seconds} s`; return `${Math.floor(minutes / 60)} h ${minutes % 60} min`; } function bytes(value) { let number = Number(value || 0); const units = ['B','KiB','MiB','GiB','TiB']; let index = 0; while (number >= 1024 && index < units.length - 1) { number /= 1024; index++; } return `${number.toFixed(index ? 1 : 0)} ${units[index]}`; } function statusClass(value) { return ['success','warning','error','running'].includes(value) ? value : 'neutral'; } function mutationTotals(m = {}) { return { created: Number(m.nodes_created || 0) + Number(m.edges_created || 0) + Number(m.vectors_created || 0), updated: Number(m.nodes_updated || 0) + Number(m.edges_updated || 0) + Number(m.vectors_updated || 0), deleted: Number(m.nodes_deleted || 0) + Number(m.edges_deleted || 0) + Number(m.vectors_deleted || 0) }; } function hasMutation(m = {}) { const t = mutationTotals(m); return t.created + t.updated + t.deleted > 0; } function deltaTags(m = {}) { const tags = []; if (Number(m.nodes_created)) tags.push(`+${num(m.nodes_created)} Nodes`); if (Number(m.edges_created)) tags.push(`+${num(m.edges_created)} Edges`); if (Number(m.vectors_created)) tags.push(`+${num(m.vectors_created)} Vektoren`); if (Number(m.nodes_updated)) tags.push(`~${num(m.nodes_updated)} Nodes`); if (Number(m.edges_updated)) tags.push(`~${num(m.edges_updated)} Edges`); if (Number(m.vectors_updated)) tags.push(`↻${num(m.vectors_updated)} Vektoren`); if (Number(m.nodes_deleted)) tags.push(`−${num(m.nodes_deleted)} Nodes`); if (Number(m.edges_deleted)) tags.push(`−${num(m.edges_deleted)} Edges`); if (Number(m.vectors_deleted)) tags.push(`−${num(m.vectors_deleted)} Vektoren`); return tags.length ? tags.join('') : 'keine Graphänderung'; } function metadataValue(metadata, ...keys) { for (const key of keys) { const value = metadata?.[key]; if (value !== undefined && value !== null && value !== '') return value; } return undefined; } function eventSeverity(activity = {}) { const type = String(activity.type || '').toLowerCase(); const message = String(activity.message || '').toLowerCase(); if (type.includes('failed') || type.includes('error') || message.includes('fehlgeschlagen')) return 'error'; if (type.includes('skipped') || type.includes('rejected') || type.includes('no_candidate') || type.includes('paused') || type.includes('deferred') || message.includes('übersprungen')) return 'warning'; if (type.includes('completed') || type.includes('created') || type.includes('accepted') || type.includes('ingested') || type.includes('learned') || type.includes('synced') || type.includes('results') || type.includes('flushed')) return 'success'; return 'neutral'; } async function api(path) { const response = await fetch(path, {headers: {'Accept':'application/json'}, cache: 'no-store'}); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`); return data; } async function loadDashboard(reason = 'manual') { if (state.loading) return; state.loading = true; const banner = $('connectionBanner'); banner.className = 'connection-banner loading'; banner.textContent = reason === 'live' ? 'Neue Aktivität erkannt · Analyse wird aktualisiert …' : 'Analysedaten werden geladen …'; const hours = Number($('rangeHours').value || 24); $('exportAnalysis').href = `/api/analysis/export?hours=${hours}`; try { state.payload = await api(`/api/analysis/dashboard?hours=${hours}&limit=400`); renderAll(); banner.className = 'connection-banner connected'; banner.textContent = `Verbunden · Analyse bis ${clock(state.payload.generated_at)} · ${num(state.payload.history?.raw_event_count)} Ereignisse im Zeitraum`; } catch (error) { banner.className = 'connection-banner error'; banner.textContent = `Analyse konnte nicht geladen werden: ${error.message}`; } finally { state.loading = false; } } function renderAll() { renderLastRun(); renderSummary(); renderTimeline(); renderRuns(); renderDistributions(); renderRelations(); renderEmbeddings(); renderResearch(); renderNewest(); renderChanges(); renderEvents(); $('generatedAt').textContent = `Erzeugt am ${when(state.payload.generated_at)} · Zeitraum ${state.payload.range_hours} h`; } function renderLastRun() { const runs = state.payload?.history?.runs || []; const run = runs.find(item => item.status !== 'running') || runs[0]; const status = $('lastRunStatus'); if (!run) { status.className = 'status-pill neutral'; status.textContent = 'noch keine Daten'; $('lastRunContent').className = 'last-run-content empty-state'; $('lastRunContent').textContent = 'Es wurde noch kein auswertbarer Lauf aufgezeichnet.'; return; } status.className = `status-pill ${statusClass(run.status)}`; status.textContent = run.verdict || run.status; const totals = mutationTotals(run.mutations); $('lastRunContent').className = 'last-run-content'; $('lastRunContent').innerHTML = `
${esc(run.kind)} · ${when(run.started_at)}${run.trigger ? ` · Trigger: ${esc(run.trigger)}` : ''}

${esc(run.title)}

${esc(run.explanation || run.outcome || 'Keine zusätzliche Erklärung vorhanden.')}

${deltaTags(run.mutations)}
Neu${num(totals.created)}Nodes, Edges, Vektoren
Aktualisiert${num(totals.updated)}inkl. neu berechneter Vektoren
Dauer${duration(run.duration_ms)}${num(run.event_count)} technische Ereignisse
`; } function renderSummary() { const graph = state.payload.graph || {}; const summary = graph.summary || {}; const history = state.payload.history || {}; const system = state.payload.system || {}; const totals = mutationTotals(history.totals || {}); $('summaryGraph').textContent = `${num(summary.node_count)} / ${num(summary.edge_count)}`; $('summaryGraphSub').textContent = `${num(summary.node_count)} Nodes · ${num(summary.edge_count)} aktive Edges · Version ${num(system.version)}`; $('summaryCoverage').textContent = pct(graph.vector_coverage); $('summaryCoverageSub').textContent = `${num(graph.vector_rows)} Embeddings für ${num(graph.vector_eligible_nodes)} wissenshaltige Nodes`; $('summaryChanges').textContent = `+${num(totals.created)} · ~${num(totals.updated)} · −${num(totals.deleted)}`; $('summaryChangesSub').textContent = `Neu · aktualisiert/neu berechnet · entfernt in ${state.payload.range_hours} h`; $('summaryQuality').textContent = `${num(summary.components)} / ${num(summary.knowledge_orphans)} / ${num(summary.contradictions)}`; $('summaryQualitySub').textContent = 'Komponenten · Wissenswaisen · Widerspruchs-Edges'; const settings = system.runtime_settings || {}; $('summaryThinking').textContent = settings.thinking_enabled ? (system.enrich_running ? 'LÄUFT' : 'AKTIV') : 'PAUSIERT'; $('summaryThinkingSub').textContent = `${num(system.relations_created)} Relationen · ${num(system.articles_created)} Artikel · ${(settings.processing_mode === 'clustered' ? 'CLUSTER/FAST' : 'PRÄZISE')}`; const autonomous = system.autonomous_research || {}; const queue = autonomous.counts || {}; $('summaryAutonomous').textContent = settings.autonomous_research_enabled ? (autonomous.running ? 'LÄUFT' : 'AKTIV') : 'PAUSIERT'; $('summaryAutonomousSub').textContent = `${num(queue.queued)} wartend · ${num(queue.completed)} abgeschlossen · ${num(queue.failed)} fehlgeschlagen`; const pool = system.ollama_pool || {}; const nodes = pool.nodes || []; const healthy = nodes.filter(node => node.healthy && node.compatible).length; $('summaryOllama').textContent = `${healthy}/${nodes.length || 0} gesund`; const shared = pool.shared_queue || {}; const dedupe = system.research_dedupe || {}; const articleModels = system.article_model_status || {}; const author = articleModels.synthesis || {}; const reviewer = articleModels.review || {}; const modelText = author.model || reviewer.model ? ` · Artikel ${esc(author.model || '?')}→${esc(reviewer.model || '?')} (${num(author.healthy_nodes_with_model)}/${num(reviewer.healthy_nodes_with_model)} Nodes)` : ''; const workKinds = shared.kinds || {}; const kindCount = prefix => { let active = 0, waiting = 0; Object.entries(workKinds).forEach(([name, item]) => { if (name === prefix || name.startsWith(prefix + '.')) { active += Number(item?.active || 0); waiting += Number(item?.waiting || 0); } }); return `${num(active)}/${num(waiting)}`; }; const queueKindsText = Object.keys(workKinds).length ? ` · Search ${kindCount('searxng')} · Fetch ${kindCount('web.fetch')} · Chat ${kindCount('ollama.chat')} · Embed ${kindCount('ollama.embedding')}` : ''; $('summaryOllamaSub').innerHTML = `${num(shared.active)} aktiv · ${num(shared.waiting)} gemeinsam wartend${queueKindsText} · Dedupe ${num(dedupe.inflight)} laufend/${num(dedupe.cached)} Cache${modelText}`; const storage = system.graph_storage || {}; const persistence = system.persistence || {}; const audit = state.payload.history?.audit || {}; const persistenceError = persistence.last_error || audit.last_error; $('summaryPersistence').textContent = persistenceError ? 'FEHLER' : (persistence.graph_dirty ? 'OFFEN' : 'SAUBER'); $('summaryPersistenceSub').textContent = persistenceError ? `Audit/SQLite: ${persistenceError}` : `${bytes(storage.database_bytes)} DB · ${bytes(storage.wal_bytes)} WAL · ${num(Number(storage.pending_nodes||0)+Number(storage.pending_edges||0)+Number(storage.pending_vectors||0))} offene Upserts · Audit ${num(audit.queue_depth)}/${num(audit.queue_capacity)}`; } function renderTimeline() { const canvas = $('timelineCanvas'); const data = state.payload?.history?.timeline || []; $('timelineHint').textContent = `${data.length} Zeitsegmente · ${state.payload.range_hours} Stunden`; const rect = canvas.getBoundingClientRect(); const dpr = Math.min(window.devicePixelRatio || 1, 2); canvas.width = Math.max(600, Math.floor(rect.width * dpr)); canvas.height = Math.floor(300 * dpr); const ctx = canvas.getContext('2d'); ctx.setTransform(dpr,0,0,dpr,0,0); const width = canvas.width / dpr, height = canvas.height / dpr; ctx.clearRect(0,0,width,height); ctx.fillStyle = '#07131d'; ctx.fillRect(0,0,width,height); if (!data.length) { ctx.fillStyle='#8ca4b7'; ctx.font='13px Segoe UI'; ctx.fillText('Noch keine historischen Daten für diesen Zeitraum.',20,35); return; } const pad = {l:48,r:22,t:22,b:38}; const chartW = width-pad.l-pad.r, chartH = height-pad.t-pad.b, mid = pad.t+chartH*.63; const values = data.map(bucket => { const m=bucket.mutations||{}; return {create:Number(m.nodes_created||0)+Number(m.edges_created||0)+Number(m.vectors_created||0),update:Number(m.nodes_updated||0)+Number(m.edges_updated||0)+Number(m.vectors_updated||0),del:Number(m.nodes_deleted||0)+Number(m.edges_deleted||0)+Number(m.vectors_deleted||0),events:Number(bucket.events||0)}; }); const maxUp=Math.max(1,...values.map(v=>v.create+v.update)); const maxDown=Math.max(1,...values.map(v=>v.del)); const maxEvents=Math.max(1,...values.map(v=>v.events)); ctx.strokeStyle='#29404f';ctx.lineWidth=1; ctx.beginPath();ctx.moveTo(pad.l,mid+.5);ctx.lineTo(width-pad.r,mid+.5);ctx.stroke(); ctx.font='10px Segoe UI';ctx.fillStyle='#7890a3';ctx.textAlign='right'; ctx.fillText(num(maxUp),pad.l-8,pad.t+8);ctx.fillText('0',pad.l-8,mid+3);ctx.fillText(`−${num(maxDown)}`,pad.l-8,height-pad.b); const slot=chartW/data.length, bar=Math.max(2,Math.min(18,slot*.58)); const eventPoints=[]; data.forEach((bucket,index)=>{ const x=pad.l+slot*index+slot/2; const v=values[index]; const createH=(v.create/maxUp)*(mid-pad.t-8); const updateH=(v.update/maxUp)*(mid-pad.t-8); ctx.fillStyle='#62d39a';ctx.fillRect(x-bar/2,mid-createH,bar,createH); ctx.fillStyle='#5e8cff';ctx.fillRect(x-bar/2,mid-createH-updateH,bar,updateH); const delH=(v.del/maxDown)*(height-pad.b-mid-5);ctx.fillStyle='#ff7d84';ctx.fillRect(x-bar/2,mid+1,bar,delH); const ey=pad.t+(1-v.events/maxEvents)*(mid-pad.t-10);eventPoints.push([x,ey]); if(index===0||index===data.length-1||index%Math.max(1,Math.ceil(data.length/6))===0){ctx.fillStyle='#7890a3';ctx.textAlign='center';ctx.fillText(new Date(bucket.start).toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}),x,height-16)} }); ctx.strokeStyle='#f2c86d';ctx.lineWidth=1.7;ctx.beginPath();eventPoints.forEach(([x,y],i)=>i?ctx.lineTo(x,y):ctx.moveTo(x,y));ctx.stroke(); ctx.fillStyle='#f2c86d';eventPoints.forEach(([x,y])=>{ctx.beginPath();ctx.arc(x,y,2.2,0,Math.PI*2);ctx.fill()}); } function renderRuns() { const runs = state.payload?.history?.runs || []; const kindFilter = $('runKindFilter'); const existing = new Set([...kindFilter.options].map(option => option.value)); [...new Set(runs.map(run => run.kind).filter(Boolean))].sort().forEach(kind => { if (!existing.has(kind)) kindFilter.add(new Option(kind,kind)); }); const kind = kindFilter.value, status = $('runStatusFilter').value, search = $('runSearch').value.trim().toLowerCase(); const filtered = runs.filter(run => (!kind||run.kind===kind)&&(!status||run.status===status)&&(!search||JSON.stringify(run).toLowerCase().includes(search))); $('runCount').textContent = `${num(filtered.length)} von ${num(runs.length)} Läufen`; const body = $('runsBody'); if (!filtered.length) { body.innerHTML='Keine Läufe entsprechen den Filtern.'; return; } body.innerHTML = filtered.map(run => { const metrics = run.metrics || {}; const metricText = [metrics.exact_comparisons ? `${num(metrics.exact_comparisons)} Cosine` : (metrics.comparisons ? `${num(metrics.comparisons)} Vergleiche` : ''), metrics.coarse_comparisons ? `${num(metrics.coarse_comparisons)} Hash` : '', metrics.research_search_results ? `${num(metrics.research_search_results)} Treffer` : '', metrics.research_accepted ? `${num(metrics.research_accepted)} Belege` : '', metrics.articles_created ? `${num(metrics.articles_created)} Artikel` : ''].filter(Boolean).join(' · ') || `${num(run.event_count)} Events`; const open = state.openRun === run.id; return `${clock(run.started_at)}${new Date(run.started_at).toLocaleDateString('de-DE')}${esc(run.title)}${esc(run.kind)}${run.trigger?` · ${esc(run.trigger)}`:''}${esc(run.verdict||run.status)}${esc(run.outcome||'')}
${deltaTags(run.mutations)}
${esc(metricText)}${run.status==='running'?'läuft':duration(run.duration_ms)}${open ? runDetails(run) : ''}`; }).join(''); body.querySelectorAll('.run-row').forEach(row => row.addEventListener('click', () => { state.openRun = state.openRun === row.dataset.runId ? '' : row.dataset.runId; renderRuns(); })); } function runDetails(run) { const events=(run.events||[]).slice().reverse(); return `

Erklärung

${esc(run.explanation||'Keine Erklärung vorhanden.')}

${deltaTags(run.mutations)}

Messwerte

${esc(JSON.stringify(run.metrics||{},null,2))}

Betroffene IDs

${esc(JSON.stringify({node_ids:run.node_ids||[],edge_ids:run.edge_ids||[]},null,2))}

Ereignisse dieses Laufs

${events.map(event=>`
${clock(event.activity.timestamp)} · ${esc(event.activity.type)}${esc(event.activity.message||event.activity.phase||'')}
${deltaTags(event.point?.delta)}
${event.change_count?`${num(event.change_count)} konkrete Änderungen${event.changes_truncated?` · ${num(event.changes_truncated)} gekürzt`:''}`:''}
`).join('')}
`; } function renderBarList(id, values, limit = 12) { const entries = Object.entries(values || {}).sort((a,b)=>b[1]-a[1]).slice(0,limit); const max = Math.max(1,...entries.map(entry=>Number(entry[1]))); $(id).innerHTML = entries.length ? entries.map(([label,value])=>`
${esc(label)}${num(value)}
`).join('') : 'Keine Daten'; } function renderDistributions() { const graph=state.payload.graph||{}; renderBarList('sourceDistribution',graph.node_sources,14); renderBarList('kindDistribution',graph.node_kinds,14); renderBarList('originDistribution',graph.node_origins,14); renderBarList('edgeTypeDistribution',graph.edge_types,14); } function renderRelations() { const graph=state.payload.graph||{}, summary=graph.summary||{}; $('relationAIEdges').textContent=num(summary.ai_edges); $('relationConfidence').textContent=graph.average_ai_confidence ? pct(graph.average_ai_confidence) : '–'; $('relationSimilarity').textContent=graph.average_ai_similarity ? pct(graph.average_ai_similarity) : '–'; $('relationContradictions').textContent=num(summary.contradictions); const observations=(state.payload.history?.events||[]).filter(event=>metadataValue(event.activity?.metadata,'semantic_similarity')!==undefined).slice(0,12); $('similarityList').innerHTML=observations.length?observations.map(event=>{const m=event.activity.metadata||{};return `
${esc(event.activity.message||event.activity.type)}${pct(Number(m.semantic_similarity))}
${when(event.activity.timestamp)} · ${num(m.candidate_comparisons||m.comparisons||0)} Vergleiche${m.confidence!==undefined?` · ${pct(m.confidence)} Konfidenz`:''}
`}).join(''):'Noch keine Näheprüfungen im Zeitraum.'; } function renderEmbeddings() { const graph=state.payload.graph||{}, totals=state.payload.history?.totals||{}; $('embeddingRows').textContent=num(graph.vector_rows); $('embeddingCreated').textContent=num(totals.vectors_created); $('embeddingUpdated').textContent=num(totals.vectors_updated); $('embeddingDeleted').textContent=num(totals.vectors_deleted); $('embeddingCoverageBadge').className=`status-pill ${graph.vector_coverage>=.95?'success':graph.vector_coverage>=.7?'warning':'error'}`; $('embeddingCoverageBadge').textContent=pct(graph.vector_coverage); renderBarList('embeddingDimensions',graph.embedding_dimensions,8); const events=(state.payload.history?.events||[]).filter(event=>event.activity?.type?.includes('embedding')||event.activity?.type?.includes('.learned')).slice(0,12); $('embeddingEvents').innerHTML=events.length?events.map(event=>`
${esc(event.activity.message||event.activity.type)}${clock(event.activity.timestamp)}
${esc(event.activity.type)} · ${esc(metadataValue(event.activity.metadata,'model')||'')} ${metadataValue(event.activity.metadata,'dimensions')?`· ${num(metadataValue(event.activity.metadata,'dimensions'))} Dimensionen`:''}
${deltaTags(event.point?.delta)}
${event.change_count?`${num(event.change_count)} konkrete Änderungen${event.changes_truncated?` · ${num(event.changes_truncated)} gekürzt`:''}`:''}
`).join(''):'Keine Embedding-Ereignisse im Zeitraum.'; } function researchMetrics() { const events=state.payload.history?.events||[]; const metrics={results:0,fetched:0,accepted:0,rejected:0,articles:0,failures:0}; events.forEach(event=>{const a=event.activity||{},m=a.metadata||{},type=a.type||'';if(type.includes('research.results'))metrics.results+=Number(m.result_count||m.research_search_results||0);if(type.includes('fetch.completed'))metrics.fetched++;if(type.includes('evidence.accepted'))metrics.accepted++;if(type.includes('evidence.rejected'))metrics.rejected++;if(type==='article.created'||(type==='autonomous.research.task.completed'&&m.article_created))metrics.articles++;if(type.includes('research')&&type.includes('failed'))metrics.failures++;}); return metrics; } function renderResearch() { const metrics=researchMetrics(); $('researchResults').textContent=num(metrics.results);$('researchFetched').textContent=num(metrics.fetched);$('researchAccepted').textContent=num(metrics.accepted);$('researchArticles').textContent=num(metrics.articles); $('researchBadge').className=`status-pill ${metrics.failures?'warning':metrics.accepted||metrics.results?'success':'neutral'}`; $('researchBadge').textContent=metrics.failures?`${num(metrics.failures)} Fehler`:metrics.accepted?`${num(metrics.accepted)} Belege`:'keine Aktivität'; const runs=(state.payload.history?.runs||[]).filter(run=>['research','autonomous-research','searxng-test','opportunity-scan'].includes(run.kind)).slice(0,12); $('researchRuns').innerHTML=runs.length?runs.map(run=>`
${esc(run.title)}${esc(run.verdict||run.status)}
${when(run.started_at)} · ${duration(run.duration_ms)} · ${num(run.event_count)} Events

${esc(run.explanation||run.outcome||'')}

${deltaTags(run.mutations)}
`).join(''):'Keine Rechercheläufe im Zeitraum.'; } function renderNewest() { const graph=state.payload.graph||{}; $('newestNodes').innerHTML=(graph.newest_nodes||[]).map(node=>`
${esc(node.label)}${when(node.updated_at)}

${esc(node.summary||'Keine Zusammenfassung')}

${esc(node.kind)}${esc(node.status||'kein Status')}${esc(node.origin)}${node.metadata?.source?`source: ${esc(node.metadata.source)}`:''}
${esc(node.id)}
`).join('')||'Keine Nodes vorhanden.'; $('newestEdges').innerHTML=(graph.newest_edges||[]).map(edge=>`
${esc(edge.type)}${when(edge.updated_at)}

${esc(edge.explanation||'Keine Erklärung')} ${edge.confidence?`· Konfidenz ${pct(edge.confidence)}`:''}

${esc(edge.status||'kein Status')}${esc(edge.origin)}
${esc(edge.source)} → ${esc(edge.target)}
`).join('')||'Keine Edges vorhanden.'; } function changeActionLabel(action) { return ({created:'erzeugt',updated:'aktualisiert',recalculated:'neu berechnet',deleted:'entfernt'})[action] || action || 'unbekannt'; } function renderChanges() { const history = state.payload?.history || {}; const changes = history.changes || []; const kind = $('changeKindFilter').value; const action = $('changeActionFilter').value; const search = $('changeSearch').value.trim().toLowerCase(); const filtered = changes.filter(change => (!kind || change.entity_kind === kind) && (!action || change.action === action) && (!search || JSON.stringify(change).toLowerCase().includes(search))); $('changeCount').textContent = `${num(filtered.length)} angezeigt · ${num(history.change_count)} insgesamt im Zeitraum`; const dropped = Number(history.dropped_detailed_changes || 0); const truncatedEvents = (history.events || []).reduce((sum, event) => sum + Number(event.changes_truncated || 0), 0); const notice = $('changeTruncationNotice'); if (dropped || truncatedEvents || Number(history.change_count || 0) > changes.length) { notice.hidden = false; notice.textContent = `Es werden die neuesten ${num(changes.length)} Detailänderungen geladen. ${num(dropped || truncatedEvents)} weitere Detailzeilen wurden wegen des Schutzlimits nicht gespeichert; die Summen für Nodes, Edges und Vektoren bleiben vollständig.`; } else { notice.hidden = true; } const body = $('changesBody'); if (!filtered.length) { body.innerHTML = 'Keine konkreten Änderungen entsprechen den Filtern.'; return; } body.innerHTML = filtered.map(change => { const details = change.details || {}; const relation = change.relation_type || details.kind || ''; const endpoints = details.source && details.target ? `${details.source} → ${details.target}` : ''; const subtitle = change.label || endpoints || details.source || ''; const sourceText = details.source && change.entity_kind === 'node' ? `source: ${details.source}` : ''; const deltas = []; if (details.semantic_similarity !== undefined || details.previous_semantic_similarity !== undefined) deltas.push(`Nähe ${details.previous_semantic_similarity!==undefined?pct(details.previous_semantic_similarity):'–'} → ${details.semantic_similarity!==undefined?pct(details.semantic_similarity):'–'}`); if (details.confidence !== undefined && details.previous_confidence !== undefined) deltas.push(`Konfidenz ${pct(details.previous_confidence)} → ${pct(details.confidence)}`); if (details.dimensions !== undefined) deltas.push(details.previous_dimensions!==undefined?`Dimensionen ${num(details.previous_dimensions)} → ${num(details.dimensions)}`:`${num(details.dimensions)} Dimensionen`); const technical = [change.origin, sourceText, ...deltas].filter(Boolean).join(' · '); return `${when(change.timestamp)}${esc(changeActionLabel(change.action))}${esc(change.entity_kind)}${details.status?`${esc(details.status)}`:''}${esc(change.entity_id)}${subtitle?`${esc(subtitle)}`:''}${esc(relation||'–')}${esc(technical)}${num(change.graph_version)}`; }).join(''); body.querySelectorAll('.event-link').forEach(button => button.addEventListener('click', event => { event.stopPropagation(); $('eventSearch').value = button.dataset.eventId; renderEvents(); document.querySelector('.raw-events-section')?.scrollIntoView({behavior:'smooth',block:'start'}); })); } function renderEvents() { const events=state.payload.history?.events||[]; const typeFilter=$('eventTypeFilter'); const known=new Set([...typeFilter.options].map(option=>option.value)); [...new Set(events.map(event=>event.activity?.type).filter(Boolean))].sort().forEach(type=>{if(!known.has(type))typeFilter.add(new Option(type,type))}); const type=typeFilter.value,search=$('eventSearch').value.trim().toLowerCase(),changesOnly=$('changesOnly').checked; const filtered=events.filter(event=>(!type||event.activity.type===type)&&(!changesOnly||hasMutation(event.point?.delta))&&(!search||JSON.stringify(event).toLowerCase().includes(search))); $('eventCount').textContent=`${num(filtered.length)} von ${num(events.length)} Ereignissen`; $('eventsList').innerHTML=filtered.length?filtered.map(event=>{const a=event.activity||{},open=state.openEvents.has(a.id);return `
${when(a.timestamp)} ${esc(a.type)}${esc(a.message||a.query||a.phase||'')}${deltaTags(event.point?.delta)}
Aktivität${a.query?`
${esc(a.query)}
`:''}
${esc(JSON.stringify(a,null,2))}
Graph-Checkpoint unmittelbar nach dem Event
${esc(JSON.stringify({...event.point,change_count:event.change_count,changes_truncated:event.changes_truncated||0},null,2))}
`}).join(''):'
Keine Ereignisse entsprechen den Filtern.
'; $('eventsList').querySelectorAll('.event-card').forEach(card=>card.querySelector('.event-summary').addEventListener('click',()=>{const id=card.dataset.eventId;state.openEvents.has(id)?state.openEvents.delete(id):state.openEvents.add(id);renderEvents()})); } function scheduleLiveRefresh() { if (!$('autoRefresh').checked) return; clearTimeout(state.refreshTimer); state.refreshTimer=setTimeout(()=>loadDashboard('live'),900); } function connectLive() { if (state.live) state.live.close(); const source=new EventSource('/api/stream'); state.live=source; source.addEventListener('activity',scheduleLiveRefresh); source.onopen=()=>{if(!state.loading){$('connectionBanner').className='connection-banner live';$('connectionBanner').textContent='Live-Eventstream verbunden · wartet auf neue Hirnaktivität';}}; source.onerror=()=>{if(!state.loading){$('connectionBanner').className='connection-banner error';$('connectionBanner').textContent='Live-Eventstream unterbrochen · Dashboard kann weiterhin manuell aktualisiert werden';}}; } $('refreshNow').addEventListener('click',()=>loadDashboard('manual')); $('rangeHours').addEventListener('change',()=>loadDashboard('range')); $('runKindFilter').addEventListener('change',renderRuns);$('runStatusFilter').addEventListener('change',renderRuns);$('runSearch').addEventListener('input',renderRuns); $('changeKindFilter').addEventListener('change',renderChanges);$('changeActionFilter').addEventListener('change',renderChanges);$('changeSearch').addEventListener('input',renderChanges); $('eventTypeFilter').addEventListener('change',renderEvents);$('eventSearch').addEventListener('input',renderEvents);$('changesOnly').addEventListener('change',renderEvents); $('autoRefresh').addEventListener('change',event=>{event.target.checked?connectLive():state.live?.close()}); window.addEventListener('resize',()=>{if(state.payload)renderTimeline()}); loadDashboard('startup'); connectLive(); })();