@@ -148,6 +148,7 @@ function toast(msg,bad=false){let t=$('toast');t.textContent=msg;t.className='to
function esc ( s ) { return String ( s ? ? '' ) . replace ( /[&<>"']/g , m => ( { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' } [ m ] ) ) }
function safeHref ( raw ) { try { let u = new URL ( String ( raw || '' ) , location . origin ) ; return ( u . protocol === 'http:' || u . protocol === 'https:' ) ? u . href : '#' } catch { return '#' } }
function fmtN ( n ) { return Number ( n || 0 ) . toLocaleString ( 'de-DE' ) } function fmtBytes ( n ) { n = Number ( n || 0 ) ; if ( n < 1024 ) return n + ' B' ; if ( n < 1048576 ) return ( n / 1024 ) . toFixed ( 1 ) + ' KiB' ; if ( n < 1073741824 ) return ( n / 1048576 ) . toFixed ( 1 ) + ' MiB' ; return ( n / 1073741824 ) . toFixed ( 2 ) + ' GiB' }
function normalizeStatus ( s ) { s = s && typeof s === 'object' ? s : { } ; let st = s . stats && typeof s . stats === 'object' ? s . stats : { } ; return { ... s , ... st } }
function when ( d ) { if ( ! d ) return '– ' ; let x = new Date ( d ) , ms = x - Date . now ( ) , a = Math . abs ( ms ) , m = Math . round ( a / 60000 ) ; if ( a < 60000 ) return ms > 0 ? 'gleich' : 'gerade' ; if ( m < 60 ) return ( ms > 0 ? 'in ' : 'vor ' ) + m + ' min' ; let h = Math . round ( m / 60 ) ; return ( ms > 0 ? 'in ' : 'vor ' ) + h + ' h' }
function validHeaderToken ( value ) { return typeof value === 'string' && /^[\x21-\x7E]+$/ . test ( value ) }
function tokenHeaders ( json = true ) { if ( ! admin ) throw new Error ( 'Admin-Token fehlt.' ) ; if ( ! validHeaderToken ( admin ) ) throw new Error ( 'Admin-Token enthält Leerzeichen/Unicode. Bitte echten ASCII-Token neu eingeben.' ) ; let h = { 'X-Admin-Token' : admin } ; if ( json ) h [ 'Content-Type' ] = 'application/json' ; return h }
@@ -156,7 +157,7 @@ async function setToken(){let v=prompt('Admin-Token',admin||'');if(v===null)retu
function switchView ( v ) { currentView = v ; $$ ( '.view' ) . forEach ( x => x . classList . toggle ( 'active' , x . id === 'view-' + v ) ) ; $$ ( '[data-view]' ) . forEach ( x => x . classList . toggle ( 'active' , x . dataset . view === v ) ) ; $ ( 'pageTitle' ) . textContent = viewMeta [ v ] [ 0 ] ; $ ( 'pageSub' ) . textContent = viewMeta [ v ] [ 1 ] ; if ( v !== 'goals' ) stopResearchPolling ( ) ; if ( v === 'knowledge' ) loadKnowledgeGraph ( ) ; if ( v === 'sources' ) loadSources ( ) ; if ( v === 'research' ) loadResearchConfig ( ) ; if ( v === 'goals' ) { loadGoals ( ) ; if ( researchWatch . goalId ) startResearchPolling ( ) } if ( v === 'models' ) loadModels ( ) ; if ( v === 'ops' ) loadOps ( ) }
$$ ( '[data-view]' ) . forEach ( b => b . addEventListener ( 'click' , ( ) => switchView ( b . dataset . view ) ) ) ; $ ( 'tokenBtn' ) . addEventListener ( 'click' , setToken ) ; $ ( 'refreshBtn' ) . addEventListener ( 'click' , refreshAll ) ;
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 ) } }
async function refreshAll ( ) { if ( ! admin ) { $ ( 'sideStatus' ) . textContent = 'Admin-Token setzen' ; return } try { let [ raw , 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' ) ] ) , s = normalizeStatus ( raw ) ; src = Array . isArray ( src ) ? src : [ ] ; g = Array . isArray ( g ) ? g : [ ] ; $ ( 'sideDot' ) . className = 'dot ok' ; $ ( 'sideStatus' ) . textContent = 'bereit · rev ' + fmtN ( 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 ) { 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>' }
@@ -222,7 +223,7 @@ $('chatBtn').addEventListener('click',async()=>{let input=document.getElementByI
async function loadModels ( ) { try { let d = await req ( '/admin/api/model-routing' ) ; $ ( 'modelJSON' ) . value = JSON . stringify ( d , null , 2 ) ; let nodes = d . ollama || [ ] ; $ ( 'modelCards' ) . innerHTML = nodes . map ( n => ` <div class="item"><div class="itemTitle"><b> ${ esc ( n . name || n . id ) } </b><span class="pill ${ n . enabled ? 'good' : '' } "> ${ n . enabled ? 'aktiv' : 'aus' } </span></div><p class="mono small"> ${ esc ( n . base _url ) } </p><div class="meta"><span>Chat: ${ esc ( n . chat _model ) } </span><span>Embed: ${ esc ( n . embedding _model ) } </span><span>ctx ${ n . num _ctx || 'default' } </span><span>think ${ esc ( n . think || 'off' ) } </span></div></div> ` ) . join ( '' ) } catch ( e ) { toast ( e . message , true ) } } $ ( 'modelSaveBtn' ) . addEventListener ( 'click' , async ( ) => { try { let d = JSON . parse ( $ ( 'modelJSON' ) . value ) ; await req ( '/admin/api/model-routing' , { method : 'PUT' , body : JSON . stringify ( d ) } ) ; toast ( 'Modelle & Routing gespeichert.' ) ; loadModels ( ) } catch ( e ) { toast ( e . message , true ) } } ) ; $ ( 'modelHealthBtn' ) . addEventListener ( 'click' , async ( ) => { try { $ ( 'modelHealth' ) . textContent = 'prüfe …' ; let d = await req ( '/admin/api/provider-health' , { method : 'POST' , body : '{}' } ) ; $ ( 'modelHealth' ) . textContent = JSON . stringify ( d , null , 2 ) } catch ( e ) { $ ( 'modelHealth' ) . textContent = e . message } } ) ;
// ---- Operations ----
async function loadOps ( ) { try { let [ s , st , cfg ] = await Promise . all ( [ req ( '/admin/api/status' ) , req ( '/admin/api/storage' ) , req ( '/admin/api/config' ) ] ) ; $ ( 'opsIndex' ) . innerHTML = ` <div class="item">HNSW <b> ${ fmtN ( s . hnsw _nodes ) } </b></div><div class="item">Disk-PQ <b> ${ fmtN ( s . disk _pq _items ) } </b></div><div class="item">Mode <b> ${ esc ( s . index _mode || '– ' ) } </b></div> ` ; $ ( 'opsStorage' ) . innerHTML = ` <div class="item">Revision <b> ${ fmtN ( s . revision ) } </b></div><div class="item">Segmente <b> ${ fmtN ( st . segments ? . files || st . memory _segments ? . files || 0 ) } </b></div><div class="item">Disk <b> ${ fmtBytes ( st . segments ? . bytes || st . memory _segments ? . bytes || 0 ) } </b></div> ` ; $ ( 'opsObs' ) . innerHTML = ` <div class="item">Pending jobs <b> ${ fmtN ( s . pending _jobs ) } </b></div><div class="item">Remote shards <b> ${ fmtN ( s . remote _shards ) } </b></div><div class="item">Letzter Fehler <span class="small"> ${ esc ( s . maintenance ? . last _error || 'keiner' ) } </span></div> ` ; $ ( 'configJSON' ) . value = JSON . stringify ( cfg , null , 2 ) } catch ( e ) { toast ( e . message , true ) } } $ ( 'configSaveBtn' ) . addEventListener ( 'click' , async ( ) => { try { let c = JSON . parse ( $ ( 'configJSON' ) . value ) ; await req ( '/admin/api/config' , { method : 'PUT' , body : JSON . stringify ( c ) } ) ; toast ( 'Konfiguration gespeichert.' ) } catch ( e ) { toast ( e . message , true ) } } ) ; $ ( 'checkpointBtn' ) . addEventListener ( 'click' , ( ) => opPost ( '/admin/api/checkpoint' , 'Checkpoint erstellt' ) ) ; $ ( 'compactBtn' ) . addEventListener ( 'click' , ( ) => opPost ( '/admin/api/storage/compact' , 'Segmente kompaktiert' ) ) ; $ ( 'pqBuildBtn' ) . addEventListener ( 'click' , ( ) => opPost ( '/admin/api/index/disk/rebuild' , 'PQ-Index neu gebaut' ) ) ; $ ( 'indexMergeBtn' ) . addEventListener ( 'click' , ( ) => opPost ( '/admin/api/index/merge' , 'Index gemerged' ) ) ; async function opPost ( path , msg ) { try { await req ( path , { method : 'POST' , body : '{}' } ) ; toast ( msg ) ; loadOps ( ) } catch ( e ) { toast ( e . message , true ) } }
async function loadOps ( ) { try { let [ raw , st , cfg ] = await Promise . all ( [ req ( '/admin/api/status' ) , req ( '/admin/api/storage' ) , req ( '/admin/api/config' ) ] ) , s = normalizeStatus ( raw ) ; $ ( 'opsIndex' ) . innerHTML = ` <div class="item">HNSW <b> ${ fmtN ( s . hnsw _nodes ) } </b></div><div class="item">Disk-PQ <b> ${ fmtN ( s . disk _pq _items ) } </b></div><div class="item">Mode <b> ${ esc ( s . index _mode || '– ' ) } </b></div> ` ; $ ( 'opsStorage' ) . innerHTML = ` <div class="item">Revision <b> ${ fmtN ( s . revision ) } </b></div><div class="item">Segmente <b> ${ fmtN ( st . segments ? . files || st . memory _segments ? . files || 0 ) } </b></div><div class="item">Disk <b> ${ fmtBytes ( st . segments ? . bytes || st . memory _segments ? . bytes || 0 ) } </b></div> ` ; $ ( 'opsObs' ) . innerHTML = ` <div class="item">Pending jobs <b> ${ fmtN ( s . pending _jobs ) } </b></div><div class="item">Remote shards <b> ${ fmtN ( s . remote _shards ) } </b></div><div class="item">Letzter Fehler <span class="small"> ${ esc ( s . maintenance ? . last _error || 'keiner' ) } </span></div> ` ; $ ( 'configJSON' ) . value = JSON . stringify ( cfg , null , 2 ) } catch ( e ) { toast ( e . message , true ) } } $ ( 'configSaveBtn' ) . addEventListener ( 'click' , async ( ) => { try { let c = JSON . parse ( $ ( 'configJSON' ) . value ) ; await req ( '/admin/api/config' , { method : 'PUT' , body : JSON . stringify ( c ) } ) ; toast ( 'Konfiguration gespeichert.' ) } catch ( e ) { toast ( e . message , true ) } } ) ; $ ( 'checkpointBtn' ) . addEventListener ( 'click' , ( ) => opPost ( '/admin/api/checkpoint' , 'Checkpoint erstellt' ) ) ; $ ( 'compactBtn' ) . addEventListener ( 'click' , ( ) => opPost ( '/admin/api/storage/compact' , 'Segmente kompaktiert' ) ) ; $ ( 'pqBuildBtn' ) . addEventListener ( 'click' , ( ) => opPost ( '/admin/api/index/disk/rebuild' , 'PQ-Index neu gebaut' ) ) ; $ ( 'indexMergeBtn' ) . addEventListener ( 'click' , ( ) => opPost ( '/admin/api/index/merge' , 'Index gemerged' ) ) ; async function opPost ( path , msg ) { try { await req ( path , { method : 'POST' , body : '{}' } ) ; toast ( msg ) ; loadOps ( ) } catch ( e ) { toast ( e . message , true ) } }
async function boot ( ) { if ( ! admin ) { await setToken ( ) ; if ( ! admin ) return } await refreshAll ( ) ; setInterval ( ( ) => { if ( admin && [ 'dashboard' , 'goals' ] . includes ( currentView ) ) refreshAll ( ) } , 15000 ) } boot ( ) ;
< / script >