@@ -158,7 +158,7 @@ $$('[data-view]').forEach(b=>b.addEventListener('click',()=>switchView(b.dataset
async function refreshAll ( ) { if ( ! admin ) { $ ( 'sideStatus' ) . textContent = 'Admin-Token setzen' ; return } try { let [ s , k , g , src ] = await Promise . all ( [ req ( '/admin/api/status' ) , req ( '/admin/api/knowledge/summary' ) , req ( '/api/v1/goals' ) , req ( '/api/v1/sources?limit=8' ) ] ) ; $ ( 'sideDot' ) . className = 'dot ok' ; $ ( 'sideStatus' ) . textContent = 'bereit · rev ' + s . revision ; $ ( 'mMem' ) . textContent = fmtN ( s . memories ) ; $ ( 'mSyn' ) . textContent = fmtN ( s . synapses ) ; $ ( 'mSources' ) . textContent = fmtN ( k . sources || src . length ) ; let active = ( g || [ ] ) . filter ( x => x . status === 'active' ) ; $ ( 'mGoals' ) . textContent = active . length ; $ ( 'mNextGoal' ) . textContent = active . length ? ( 'nächster ' + when ( active . map ( x => x . next _cycle _at ) . filter ( Boolean ) . sort ( ) [ 0 ] ) ) : 'keine aktiven Ziele' ; $ ( 'mTypes' ) . textContent = Object . entries ( k . by _type || { } ) . map ( ( [ a , b ] ) => a + ' ' + b ) . join ( ' · ' ) ; renderSystemState ( s ) ; renderEvents ( k . recent _events || [ ] , 'dashEvents' ) ; renderDashboardGoals ( active . slice ( 0 , 5 ) ) ; if ( currentView === 'ops' ) loadOps ( ) } catch ( e ) { $ ( 'sideDot' ) . className = 'dot bad' ; $ ( 'sideStatus' ) . textContent = 'Fehler' ; toast ( e . message , true ) } }
function renderSystemState ( s ) { let o = s . observability || { } , m = s . maintenance || { } ; $ ( 'systemState' ) . innerHTML = [ [ 'Index' , ` ${ s . index _mode || o . index _mode || '– ' } · HNSW ${ fmtN ( s . hnsw _nodes ) } · PQ ${ fmtN ( s . disk _pq _items ) } ` ] , [ 'Jobs' , fmtN ( s . pending _jobs ) + ' ausstehend' ] , [ 'Cluster' , s . cluster ? . enabled ? ( s . cluster . role + ' · term ' + s . cluster . term ) : 'deaktiviert' ] , [ 'Letzter Autonomie-Lauf' , m . last _autonomy _run ? new Date ( m . last _autonomy _run ) . toLocaleString ( ) : '– ' ] , [ 'Letzter Fehler' , m . last _error || 'keiner' ] ] . map ( ( [ a , b ] ) => ` <div class="item"><div class="itemTitle"><b> ${ esc ( a ) } </b><span class="muted small"> ${ esc ( b ) } </span></div></div> ` ) . join ( '' ) }
function renderEvents ( xs , id ) { $ ( id ) . innerHTML = xs . length ? xs . map ( e => ` <div class="item"><div class="itemTitle"><b> ${ esc ( e . type ) } </b><span class="pill"> ${ esc ( e . actor || 'system' ) } </span></div><p> ${ esc ( e . summary || '' ) } </p><div class="meta"><span> ${ new Date ( e . created _at ) . toLocaleString ( ) } </span> ${ e . reason ? ` <span> ${ esc ( e . reason ) } </span> ` : '' } </div></div> ` ) . join ( '' ) : '<p class="muted">Noch keine Lernereignisse.</p>' }
function renderEvents ( xs , id ) { xs = Array . isArray ( xs ) ? xs : [ ] ; $ ( id ) . innerHTML = xs . length ? xs . map ( e => ` <div class="item"><div class="itemTitle"><b> ${ esc ( e . type ) } </b><span class="pill"> ${ esc ( e . actor || 'system' ) } </span></div><p> ${ esc ( e . summary || '' ) } </p><div class="meta"><span> ${ new Date ( e . created _at ) . toLocaleString ( ) } </span> ${ e . reason ? ` <span> ${ esc ( e . reason ) } </span> ` : '' } </div></div> ` ) . join ( '' ) : '<p class="muted">Noch keine Lernereignisse.</p>' }
function renderDashboardGoals ( xs ) { $ ( 'dashGoals' ) . innerHTML = xs . length ? xs . map ( g => ` <div class="item"><div class="itemTitle"><b> ${ esc ( g . title ) } </b><span class="pill ${ g . auto _run ? 'good' : '' } "> ${ g . auto _run ? 'AUTO' : 'MANUELL' } </span></div><div class="progress" style="margin:9px 0"><span style="width: ${ Math . round ( ( g . progress || 0 ) * 100 ) } %"></span></div><div class="meta"><span> ${ Math . round ( ( g . progress || 0 ) * 100 ) } %</span><span> ${ g . research _enabled ? 'Research an' : 'Research aus' } </span><span> ${ g . next _cycle _at ? when ( g . next _cycle _at ) : 'nicht geplant' } </span></div></div> ` ) . join ( '' ) : '<p class="muted">Keine aktiven Ziele.</p>' }
// ---- Knowledge graph with client-side LOD ----
@@ -171,12 +171,12 @@ function lod(){return cam.z<.62?0:cam.z<1.45?1:2}function drawGraph(){let r=canv
function drawClusters ( ) { let groups = { } ; for ( let p of graphPos . values ( ) ) { let t = p . n . memory _type || 'other' ; ( groups [ t ] ? ? = [ ] ) . push ( p ) } for ( let [ t , xs ] of Object . entries ( groups ) ) { let x = xs . reduce ( ( a , p ) => a + p . x , 0 ) / xs . length , y = xs . reduce ( ( a , p ) => a + p . y , 0 ) / xs . length , s = worldToScreen ( { x , y } ) , rad = Math . min ( 58 , 20 + Math . sqrt ( xs . length ) * 6 ) ; ctx . fillStyle = ( typeColors [ t ] || '#bd8cff' ) + '44' ; ctx . strokeStyle = typeColors [ t ] || '#bd8cff' ; ctx . lineWidth = 2 ; ctx . beginPath ( ) ; ctx . arc ( s . x , s . y , rad , 0 , Math . PI * 2 ) ; ctx . fill ( ) ; ctx . stroke ( ) ; ctx . fillStyle = '#eef3fb' ; ctx . font = '600 13px system-ui' ; ctx . textAlign = 'center' ; ctx . fillText ( t , s . x , s . y - 2 ) ; ctx . fillStyle = '#a9b7cb' ; ctx . font = '11px system-ui' ; ctx . fillText ( xs . length + ' nodes' , s . x , s . y + 15 ) } ctx . textAlign = 'left' }
function fitGraph ( ) { if ( ! graphPos . size ) return ; let xs = [ ... graphPos . values ( ) ] . map ( p => p . x ) , ys = [ ... graphPos . values ( ) ] . map ( p => p . y ) , w = Math . max ( ... xs ) - Math . min ( ... xs ) + 140 , h = Math . max ( ... ys ) - Math . min ( ... ys ) + 140 , r = canvas . getBoundingClientRect ( ) ; cam . z = Math . max ( . 25 , Math . min ( 1.15 , Math . min ( r . width / w , r . height / h ) ) ) ; cam . x = cam . y = 0 ; drawGraph ( ) }
canvas . addEventListener ( 'wheel' , e => { e . preventDefault ( ) ; let before = screenToWorld ( e . offsetX , e . offsetY ) , factor = Math . exp ( - e . deltaY * . 0012 ) ; cam . z = Math . max ( . 2 , Math . min ( 5 , cam . z * factor ) ) ; let after = worldToScreen ( before ) ; cam . x += e . offsetX - after . x ; cam . y += e . offsetY - after . y ; drawGraph ( ) } , { passive : false } ) ; canvas . addEventListener ( 'pointerdown' , e => { pointer . down = true ; pointer . x = e . clientX ; pointer . y = e . clientY ; pointer . moved = false ; canvas . setPointerCapture ( e . pointerId ) } ) ; canvas . addEventListener ( 'pointermove' , e => { if ( ! pointer . down ) return ; let dx = e . clientX - pointer . x , dy = e . clientY - pointer . y ; if ( Math . hypot ( dx , dy ) > 2 ) pointer . moved = true ; cam . x += dx ; cam . y += dy ; pointer . x = e . clientX ; pointer . y = e . clientY ; drawGraph ( ) } ) ; canvas . addEventListener ( 'pointerup' , e => { pointer . down = false ; if ( pointer . moved ) return ; let rect = canvas . getBoundingClientRect ( ) , x = e . clientX - rect . left , y = e . clientY - rect . top ; if ( lod ( ) === 0 ) { cam . z = . 8 ; drawGraph ( ) ; return } let best = null , bd = 15 ; for ( let p of graphPos . values ( ) ) { let s = worldToScreen ( p ) , d = Math . hypot ( s . x - x , s . y - y ) ; if ( d < bd ) { best = p ; bd = d } } if ( best ) openMemory ( best . n . id ) } ) ; $ ( 'zoomInBtn' ) . addEventListener ( 'click' , ( ) => { cam . z = Math . min ( 5 , cam . z * 1.3 ) ; drawGraph ( ) } ) ; $ ( 'zoomOutBtn' ) . addEventListener ( 'click' , ( ) => { cam . z = Math . max ( . 2 , cam . z / 1.3 ) ; drawGraph ( ) } ) ; $ ( 'fitBtn' ) . addEventListener ( 'click' , fitGraph ) ;
async function loadKnowledgeGraph ( center = '' ) { try { let d = await req ( '/admin/api/knowledge/graph?depth=3&max_nodes=600' + ( center ? '¢er=' + encodeURIComponent ( center ) : '' ) ) ; graphData = d ; $ ( 'graphCount' ) . textContent = fmtN ( d . nodes . length ) + ' Knoten · ' + fmtN ( d . edges . length ) + ' Kanten' ; layoutGraph ( ) } catch ( e ) { toast ( e . message , true ) } } $ ( 'graphReloadBtn' ) . addEventListener ( 'click' , ( ) => loadKnowledgeGraph ( selectedMemory ) ) ;
async function loadKnowledgeGraph ( center = '' ) { try { let d = await req ( '/admin/api/knowledge/graph?depth=3&max_nodes=600' + ( center ? '¢er=' + encodeURIComponent ( center ) : '' ) ) ; graphData = { ... ( d || { } ) , nodes : Array . isArray ( d ? . nodes ) ? d . nodes : [ ] , edges : Array . isArray ( d ? . edges ) ? d . edges : [ ] } ; $ ( 'graphCount' ) . textContent = fmtN ( graphData . nodes . length ) + ' Knoten · ' + fmtN ( graphData . edges . length ) + ' Kanten' ; layoutGraph ( ) } catch ( e ) { graphData = { nodes : [ ] , edges : [ ] } ; graphLayoutReady = false ; $ ( 'graphCount' ) . textContent = '0 Knoten · 0 Kanten' ; drawGraph ( ) ; toast ( e . message , true ) } } $ ( 'graphReloadBtn' ) . addEventListener ( 'click' , ( ) => loadKnowledgeGraph ( selectedMemory ) ) ;
$ ( 'knowledgeSearchBtn' ) . addEventListener ( 'click' , searchKnowledge ) ; $ ( 'knowledgeQuery' ) . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' ) searchKnowledge ( ) } ) ; async function searchKnowledge ( ) { let q = $ ( 'knowledgeQuery' ) . value . trim ( ) ; if ( ! q ) return ; try { let d = await req ( '/admin/api/knowledge/search' , { method : 'POST' , body : JSON . stringify ( { text : q , k : 16 } ) } ) ; $ ( 'knowledgeHits' ) . innerHTML = ( d . hits || [ ] ) . map ( h => ` <div class="item click" data-mid=" ${ esc ( h . memory . id ) } "><div class="itemTitle"><b> ${ esc ( h . memory . text . slice ( 0 , 120 ) ) } </b><span class="pill"> ${ Number ( h . score || 0 ) . toFixed ( 3 ) } </span></div><div class="meta"><span> ${ esc ( h . candidate _source || '' ) } </span><span>sim ${ Number ( h . similarity || 0 ) . toFixed ( 3 ) } </span><span>base ${ Number ( h . base _score || 0 ) . toFixed ( 3 ) } </span><span>graph + ${ Number ( h . graph _boost || 0 ) . toFixed ( 3 ) } </span></div></div> ` ) . join ( '' ) || '<p class="muted">Keine Treffer.</p>' ; $$ ( '#knowledgeHits [data-mid]' ) . forEach ( x => x . addEventListener ( 'click' , ( ) => openMemory ( x . dataset . mid ) ) ) ; if ( d . hits ? . [ 0 ] ) { selectedMemory = d . hits [ 0 ] . memory . id ; await loadKnowledgeGraph ( selectedMemory ) ; await openMemory ( selectedMemory , false ) } } catch ( e ) { toast ( e . message , true ) } }
async function openMemory ( id , recenter = true ) { selectedMemory = id ; try { let d = await req ( '/admin/api/knowledge/memory/' + encodeURIComponent ( id ) ) , m = d . memory || { } , p = m . provenance || { } ; $ ( 'knowledgeInspector' ) . innerHTML = ` <div class="meta"><span class="pill"> ${ esc ( m . memory _type ) } </span><span class="pill"> ${ esc ( m . kind ) } </span><span class="pill ${ m . status === 'active' ? 'good' : m . status === 'conflicted' ? 'warn' : '' } "> ${ esc ( m . status || 'active' ) } </span></div><h3> ${ esc ( m . truth _key || p . source _title || m . id ) } </h3><div style="white-space:pre-wrap;line-height:1.5;font-size:13px"> ${ esc ( m . text ) } </div><h3>Qualität</h3><div class="kv"><div>Confidence</div><div> ${ Number ( m . confidence || 0 ) . toFixed ( 3 ) } </div><div>Salience</div><div> ${ Number ( m . salience || 0 ) . toFixed ( 3 ) } </div><div>Reward</div><div> ${ Number ( m . reward || 0 ) . toFixed ( 3 ) } </div><div>Evidenzquellen</div><div> ${ fmtN ( m . evidence _count || m . evidence _source _ids ? . length || 0 ) } </div><div>Aufrufe</div><div> ${ fmtN ( m . access _count ) } </div></div><h3>Provenance</h3><div class="kv"><div>Quelle</div><div> ${ esc ( p . source || 'legacy/unknown' ) } </div><div>Source-ID</div><div class="mono"> ${ esc ( p . source _id || '– ' ) } </div><div>Titel</div><div> ${ esc ( p . source _title || '– ' ) } </div><div>URI</div><div> ${ p . source _uri ? ` <a href=" ${ esc ( safeHref ( p . source _uri ) ) } " target="_blank" rel="noopener" style="color:var(--accent)"> ${ esc ( p . source _uri ) } </a> ` : '– ' } </div><div>Chunk</div><div> ${ p . chunk _index || '– ' } / ${ p . chunk _count || '– ' } </div><div>Embedding</div><div> ${ esc ( [ p . embedding _model , p . embedding _node _id ] . filter ( Boolean ) . join ( ' · ' ) || '– ' ) } </div><div>Generierung</div><div> ${ esc ( [ p . generation _model , p . generation _node _id ] . filter ( Boolean ) . join ( ' · ' ) || '– ' ) } </div></div><h3>Stärkste Synapsen</h3> ${ ( d . edges || [ ] ) . slice ( 0 , 12 ) . map ( e => ` <div class="item click" data-mid=" ${ esc ( e . a === id ? e . b : e . a ) } "><div class="itemTitle"><span class="mono small"> ${ esc ( e . a === id ? e . b : e . a ) } </span><span class="pill">w ${ Number ( e . weight ) . toFixed ( 2 ) } </span></div></div> ` ) . join ( '' ) || '<p class="muted">Keine Synapsen.</p>' } <h3>Timeline</h3> ${ ( d . events || [ ] ) . map ( e => ` <div class="item"><b> ${ esc ( e . type ) } </b><p> ${ esc ( e . summary ) } </p><div class="meta"> ${ new Date ( e . created _at ) . toLocaleString ( ) } </div></div> ` ) . join ( '' ) || '<p class="muted">Keine Events.</p>' } ` ; $$ ( '#knowledgeInspector [data-mid]' ) . forEach ( x => x . addEventListener ( 'click' , ( ) => openMemory ( x . dataset . mid ) ) ) ; drawGraph ( ) ; if ( recenter ) loadKnowledgeGraph ( id ) } catch ( e ) { toast ( e . message , true ) } }
// ---- Sources & ingestion ----
async function loadSources ( ) { try { let xs = await req ( '/api/v1/sources?limit=100' ) ; $ ( 'sourcesList' ) . innerHTML = xs . length ? xs . map ( s => ` <div class="item"><div class="itemTitle"><div class="row"><span class="sourceIcon"> ${ s . type === 'document' ? '▤' : s . type === 'web' ? '⌁' : 'T' } </span><div><b> ${ esc ( s . title ) } </b><div class="meta"><span> ${ esc ( s . type ) } </span><span> ${ s . chunk _count || 0 } chunks</span><span> ${ fmtBytes ( s . bytes ) } </span></div></div></div><span class="pill ${ s . status === 'ready' ? 'good' : s . status === 'error' ? 'bad' : 'warn' } "> ${ esc ( s . status ) } </span></div> ${ s . uri ? ` <p class="mono small"> ${ esc ( s . uri ) } </p> ` : '' } ${ s . error ? ` <p class="error small"> ${ esc ( s . error ) } </p> ` : '' } </div> ` ) . join ( '' ) : '<p class="muted">Noch keine externen Quellen.</p>' ; $ ( 'mSources' ) . textContent = fmtN ( xs . length ) } catch ( e ) { toast ( e . message , true ) } } $ ( 'sourcesReloadBtn' ) . addEventListener ( 'click' , loadSources ) ;
async function loadSources ( ) { try { let xs = await req ( '/api/v1/sources?limit=100' ) ; xs = Array . isArray ( xs ) ? xs : [ ] ; $ ( 'sourcesList' ) . innerHTML = xs . length ? xs . map ( s => ` <div class="item"><div class="itemTitle"><div class="row"><span class="sourceIcon"> ${ s . type === 'document' ? '▤' : s . type === 'web' ? '⌁' : 'T' } </span><div><b> ${ esc ( s . title ) } </b><div class="meta"><span> ${ esc ( s . type ) } </span><span> ${ s . chunk _count || 0 } chunks</span><span> ${ fmtBytes ( s . bytes ) } </span></div></div></div><span class="pill ${ s . status === 'ready' ? 'good' : s . status === 'error' ? 'bad' : 'warn' } "> ${ esc ( s . status ) } </span></div> ${ s . uri ? ` <p class="mono small"> ${ esc ( s . uri ) } </p> ` : '' } ${ s . error ? ` <p class="error small"> ${ esc ( s . error ) } </p> ` : '' } </div> ` ) . join ( '' ) : '<p class="muted">Noch keine externen Quellen.</p>' ; $ ( 'mSources' ) . textContent = fmtN ( xs . length ) } catch ( e ) { toast ( e . message , true ) } } $ ( 'sourcesReloadBtn' ) . addEventListener ( 'click' , loadSources ) ;
const dz = $ ( 'dropZone' ) , fi = $ ( 'docFile' ) ; [ 'dragenter' , 'dragover' ] . forEach ( n => dz . addEventListener ( n , e => { e . preventDefault ( ) ; dz . classList . add ( 'drag' ) } ) ) ; [ 'dragleave' , 'drop' ] . forEach ( n => dz . addEventListener ( n , e => { e . preventDefault ( ) ; dz . classList . remove ( 'drag' ) } ) ) ; dz . addEventListener ( 'drop' , e => { if ( e . dataTransfer . files . length ) fi . files = e . dataTransfer . files } ) ; $ ( 'uploadBtn' ) . addEventListener ( 'click' , async ( ) => { let f = fi . files [ 0 ] ; if ( ! f ) return toast ( 'Bitte eine Datei auswählen.' , true ) ; let fd = new FormData ( ) ; fd . append ( 'file' , f ) ; fd . append ( 'title' , $ ( 'docTitle' ) . value ) ; fd . append ( 'tags' , $ ( 'docTags' ) . value ) ; fd . append ( 'trust' , $ ( 'docTrust' ) . value ) ; try { let r = await fetch ( '/api/v1/ingest/document' , { method : 'POST' , headers : tokenHeaders ( false ) , body : fd } ) , txt = await r . text ( ) , d ; try { d = JSON . parse ( txt ) } catch { d = txt } if ( ! r . ok ) throw new Error ( d . error || d ) ; toast ( ` Dokument gelernt: ${ d . chunks } Chunks, ${ d . duplicates } Duplikate ` ) ; fi . value = '' ; loadSources ( ) ; refreshAll ( ) } catch ( e ) { toast ( e . message , true ) } } ) ;
$ ( 'textLearnBtn' ) . addEventListener ( 'click' , async ( ) => { try { let d = await req ( '/api/v1/ingest/text' , { method : 'POST' , body : JSON . stringify ( { title : $ ( 'textTitle' ) . value || 'Manual text' , text : $ ( 'textInput' ) . value , source _uri : $ ( 'textURI' ) . value , trust : Number ( $ ( 'textTrust' ) . value || 1 ) , source _type : 'text' , memory _type : 'semantic' } ) } ) ; toast ( ` Text gelernt: ${ d . chunks } Chunks ` ) ; $ ( 'textInput' ) . value = '' ; loadSources ( ) ; refreshAll ( ) } catch ( e ) { toast ( e . message , true ) } } ) ;
@@ -211,7 +211,7 @@ async function loadResearchHistory(){if(!researchWatch.goalId)return;try{let xs=
async function pollGoalResearch ( ) { if ( ! researchWatch . goalId || currentView !== 'goals' ) return ; try { let q = ` ?after= ${ researchWatch . after } ` + ( researchWatch . runId ? ` &run_id= ${ encodeURIComponent ( researchWatch . runId ) } ` : '' ) ; let d = await req ( '/api/v1/goals/' + encodeURIComponent ( researchWatch . goalId ) + '/research/live' + q ) ; if ( ! d . run ) { researchWatch . run = null ; researchWatch . events = [ ] ; researchWatch . after = 0 ; renderResearchLive ( ) ; return } if ( d . reset || researchWatch . runId !== d . run . id ) { researchWatch . runId = d . run . id ; researchWatch . after = 0 ; researchWatch . events = [ ] } researchWatch . run = d . run ; if ( d . events ? . length ) { researchWatch . events . push ( ... d . events ) ; if ( researchWatch . events . length > 500 ) researchWatch . events = researchWatch . events . slice ( - 500 ) ; researchWatch . after = Math . max ( researchWatch . after , ... d . events . map ( e => Number ( e . seq || 0 ) ) ) } renderResearchLive ( ) ; if ( d . run . status !== 'running' ) loadResearchHistory ( ) } catch ( e ) { $ ( 'liveRunMeta' ) . textContent = 'Live-Research konnte nicht geladen werden: ' + e . message ; $ ( 'livePulse' ) . className = 'livePulse error' } }
function openGoalResearch ( id , title ) { researchWatch . goalId = id ; researchWatch . goalTitle = title || id ; researchWatch . runId = '' ; researchWatch . after = 0 ; researchWatch . events = [ ] ; researchWatch . run = null ; $ ( 'goalResearchLive' ) . classList . add ( 'show' ) ; $ ( 'liveGoalTitle' ) . textContent = 'Live Research · ' + researchWatch . goalTitle ; renderResearchLive ( ) ; loadResearchHistory ( ) ; startResearchPolling ( ) ; setTimeout ( ( ) => $ ( 'goalResearchLive' ) . scrollIntoView ( { behavior : 'smooth' , block : 'start' } ) , 20 ) }
$ ( 'liveCloseBtn' ) . addEventListener ( 'click' , ( ) => { $ ( 'goalResearchLive' ) . classList . remove ( 'show' ) ; stopResearchPolling ( ) } ) ; $ ( 'liveRefreshBtn' ) . addEventListener ( 'click' , ( ) => { researchWatch . after = 0 ; researchWatch . events = [ ] ; researchWatch . runId = '' ; pollGoalResearch ( ) ; loadResearchHistory ( ) } ) ;
async function loadGoals ( ) { try { let xs = await req ( '/api/v1/goals' ) ; $ ( 'goalsList' ) . innerHTML = xs . length ? xs . map ( g => { let active = g . status === 'active' , paused = g . status === 'paused' ; return ` <div class="goal ${ paused ? 'goalPaused' : '' } "><div class="goalHead"><div><h3> ${ esc ( g . title ) } </h3><div class="meta"><span>Priorität ${ g . priority } </span><span> ${ g . research _enabled ? '⌕ Research' : 'ohne Research' } </span><span> ${ g . auto _run ? 'AUTO' : 'MANUELL' } </span></div></div><span class="pill ${ active ? 'good' : paused ? 'warn' : '' } "> ${ paused ? 'PAUSIERT' : esc ( g . status ) } </span></div><p class="muted small"> ${ esc ( g . description || '' ) } </p><div class="progress"><span style="width: ${ Math . round ( ( g . progress || 0 ) * 100 ) } %"></span></div><div class="meta"><span> ${ Math . round ( ( g . progress || 0 ) * 100 ) } %</span><span>Intervall ${ g . interval _minutes || '– ' } min</span><span> ${ paused ? 'Scheduler angehalten' : ( 'Nächster: ' + ( g . next _cycle _at ? when ( g . next _cycle _at ) : '– ' ) ) } </span></div> ${ g . next _action ? ` <p><b>Nächste Aktion:</b> ${ esc ( g . next _action ) } </p> ` : '' } ${ g . last _error ? ` <p class="error small"> ${ esc ( g . last _error ) } </p> ` : '' } <div class="goalActions"> ${ active ? ` <button class="btn secondary" data-goal-action="cycle" data-goal-id=" ${ esc ( g . id ) } " data-goal-title=" ${ esc ( g . title ) } ">Jetzt lernen</button><button class="btn ghost" data-goal-action="pause" data-goal-id=" ${ esc ( g . id ) } ">Pausieren</button> ` : '' } ${ paused ? ` <button class="btn secondary" data-goal-action="resume" data-goal-id=" ${ esc ( g . id ) } ">Fortsetzen</button> ` : '' } ${ g . research _enabled ? ` <button class="btn ghost" data-research-live=" ${ esc ( g . id ) } " data-goal-title=" ${ esc ( g . title ) } ">Live Research</button> ` : '' } <button class="btn ghost" data-focus=" ${ esc ( g . id ) } ">Graph</button><button class="btn danger" data-goal-action="delete" data-goal-id=" ${ esc ( g . id ) } " data-goal-title=" ${ esc ( g . title ) } ">Löschen</button></div></div> ` } ) . join ( '' ) : '<p class="muted">Noch keine Ziele.</p>' ; $$ ( '[data-goal-action]' ) . forEach ( b => b . addEventListener ( 'click' , ( ) => goalAction ( b ) ) ) ; $$ ( '[data-research-live]' ) . forEach ( b => b . addEventListener ( 'click' , ( ) => openGoalResearch ( b . dataset . researchLive , b . dataset . goalTitle ) ) ) ; $$ ( '[data-focus]' ) . forEach ( b => b . addEventListener ( 'click' , ( ) => { switchView ( 'knowledge' ) ; $ ( 'knowledgeQuery' ) . value = 'goal:' + b . dataset . focus ; searchKnowledge ( ) } ) ) } catch ( e ) { toast ( e . message , true ) } }
async function loadGoals ( ) { try { let xs = await req ( '/api/v1/goals' ) ; xs = Array . isArray ( xs ) ? xs : [ ] ; $ ( 'goalsList' ) . innerHTML = xs . length ? xs . map ( g => { let active = g . status === 'active' , paused = g . status === 'paused' ; return ` <div class="goal ${ paused ? 'goalPaused' : '' } "><div class="goalHead"><div><h3> ${ esc ( g . title ) } </h3><div class="meta"><span>Priorität ${ g . priority } </span><span> ${ g . research _enabled ? '⌕ Research' : 'ohne Research' } </span><span> ${ g . auto _run ? 'AUTO' : 'MANUELL' } </span></div></div><span class="pill ${ active ? 'good' : paused ? 'warn' : '' } "> ${ paused ? 'PAUSIERT' : esc ( g . status ) } </span></div><p class="muted small"> ${ esc ( g . description || '' ) } </p><div class="progress"><span style="width: ${ Math . round ( ( g . progress || 0 ) * 100 ) } %"></span></div><div class="meta"><span> ${ Math . round ( ( g . progress || 0 ) * 100 ) } %</span><span>Intervall ${ g . interval _minutes || '– ' } min</span><span> ${ paused ? 'Scheduler angehalten' : ( 'Nächster: ' + ( g . next _cycle _at ? when ( g . next _cycle _at ) : '– ' ) ) } </span></div> ${ g . next _action ? ` <p><b>Nächste Aktion:</b> ${ esc ( g . next _action ) } </p> ` : '' } ${ g . last _error ? ` <p class="error small"> ${ esc ( g . last _error ) } </p> ` : '' } <div class="goalActions"> ${ active ? ` <button class="btn secondary" data-goal-action="cycle" data-goal-id=" ${ esc ( g . id ) } " data-goal-title=" ${ esc ( g . title ) } ">Jetzt lernen</button><button class="btn ghost" data-goal-action="pause" data-goal-id=" ${ esc ( g . id ) } ">Pausieren</button> ` : '' } ${ paused ? ` <button class="btn secondary" data-goal-action="resume" data-goal-id=" ${ esc ( g . id ) } ">Fortsetzen</button> ` : '' } ${ g . research _enabled ? ` <button class="btn ghost" data-research-live=" ${ esc ( g . id ) } " data-goal-title=" ${ esc ( g . title ) } ">Live Research</button> ` : '' } <button class="btn ghost" data-focus=" ${ esc ( g . id ) } ">Graph</button><button class="btn danger" data-goal-action="delete" data-goal-id=" ${ esc ( g . id ) } " data-goal-title=" ${ esc ( g . title ) } ">Löschen</button></div></div> ` } ) . join ( '' ) : '<p class="muted">Noch keine Ziele.</p>' ; $$ ( '[data-goal-action]' ) . forEach ( b => b . addEventListener ( 'click' , ( ) => goalAction ( b ) ) ) ; $$ ( '[data-research-live]' ) . forEach ( b => b . addEventListener ( 'click' , ( ) => openGoalResearch ( b . dataset . researchLive , b . dataset . goalTitle ) ) ) ; $$ ( '[data-focus]' ) . forEach ( b => b . addEventListener ( 'click' , ( ) => { switchView ( 'knowledge' ) ; $ ( 'knowledgeQuery' ) . value = 'goal:' + b . dataset . focus ; searchKnowledge ( ) } ) ) } catch ( e ) { toast ( e . message , true ) } }
async function goalAction ( b ) { let id = b . dataset . goalId , action = b . dataset . goalAction ; if ( action === 'delete' && ! confirm ( ` Ziel „ ${ b . dataset . goalTitle || id } “ wirklich löschen? Bereits gelerntes Wissen bleibt erhalten. ` ) ) return ; if ( action === 'cycle' ) openGoalResearch ( id , b . dataset . goalTitle || id ) ; let old = b . textContent ; try { b . disabled = true ; b . textContent = action === 'cycle' ? 'läuft …' : action === 'delete' ? 'lösche …' : 'speichere …' ; let path = '/api/v1/goals/' + encodeURIComponent ( id ) + ( action === 'cycle' ? '/cycle' : action === 'pause' ? '/pause' : action === 'resume' ? '/resume' : '' ) ; let d = await req ( path , { method : action === 'delete' ? 'DELETE' : 'POST' , body : action === 'delete' ? undefined : '{}' } ) ; if ( action === 'cycle' ) toast ( ` Zyklus fertig · Evaluation ${ Number ( d . evaluation ) . toFixed ( 2 ) } · ${ d . sources _ingested || 0 } Quellen ` ) ; else if ( action === 'pause' ) toast ( 'Ziel pausiert. Der Scheduler verarbeitet es nicht weiter.' ) ; else if ( action === 'resume' ) toast ( 'Ziel wieder aktiv. Nächster Lauf wurde eingeplant.' ) ; else { toast ( 'Ziel gelöscht. Bereits gelerntes Wissen bleibt erhalten.' ) ; if ( researchWatch . goalId === id ) { $ ( 'goalResearchLive' ) . classList . remove ( 'show' ) ; stopResearchPolling ( ) ; researchWatch . goalId = '' } } await loadGoals ( ) ; refreshAll ( ) } catch ( e ) { toast ( e . message , true ) } finally { b . disabled = false ; b . textContent = old } }
$ ( 'goalCreateBtn' ) . addEventListener ( 'click' , async ( ) => { let title = $ ( 'goalTitle' ) . value . trim ( ) ; if ( ! title ) return toast ( 'Titel fehlt.' , true ) ; try { let g = await req ( '/api/v1/goals' , { method : 'POST' , body : JSON . stringify ( { title , description : $ ( 'goalDesc' ) . value , target : $ ( 'goalTarget' ) . value , priority : Number ( $ ( 'goalPriority' ) . value || 50 ) , interval _minutes : Number ( $ ( 'goalInterval' ) . value || 10 ) , status : 'active' } ) } ) ; toast ( ` Ziel ${ g . title } angelegt – erster Lauf ist eingeplant. ` ) ; $ ( 'goalTitle' ) . value = '' ; $ ( 'goalDesc' ) . value = '' ; loadGoals ( ) ; refreshAll ( ) } catch ( e ) { toast ( e . message , true ) } } ) ; $ ( 'runAutonomyBtn' ) . addEventListener ( 'click' , async ( ) => { try { let d = await req ( '/admin/api/autonomy' , { method : 'POST' , body : '{}' } ) ; toast ( ` ${ d . cycles ? . length || 0 } fällige Zyklen ausgeführt. ` ) ; loadGoals ( ) } catch ( e ) { toast ( e . message , true ) } } ) ;