From c7bfe7036e5b3adfdc6f403598e5d6a232d8ce51 Mon Sep 17 00:00:00 2001 From: jbergner Date: Wed, 26 Aug 2026 22:35:13 +0200 Subject: [PATCH] 1.4.3 --- MANIFEST.sha256 | 13 +- README.md | 2 +- RELEASE-NOTES-v1.4.3.md | 24 +++ VERSION | 2 +- mega-project.json | 2 +- patches/v1.4.2-to-v1.4.3.diff | 191 ++++++++++++++++++ .../neuroforge/internal/httpapi/httpapi.go | 10 +- .../neuroforge/internal/httpapi/index.html | 5 +- .../internal/httpapi/status_dashboard_test.go | 68 +++++++ 9 files changed, 305 insertions(+), 12 deletions(-) create mode 100644 RELEASE-NOTES-v1.4.3.md create mode 100644 patches/v1.4.2-to-v1.4.3.diff create mode 100644 platform/neuroforge/internal/httpapi/status_dashboard_test.go diff --git a/MANIFEST.sha256 b/MANIFEST.sha256 index b834f37..df3f9f8 100644 --- a/MANIFEST.sha256 +++ b/MANIFEST.sha256 @@ -3,14 +3,15 @@ ed22fda7661db8203563611dc144998161cd024e161b0471d615eaf0defeb7db ./.gitea/workflows/release-tag.yml e1ff71187cc3411a85067b964264011db7bd109a585ef7b9ea5b08bda039d813 ./.gitignore 9c18555764b03bdb004098bf6b8ca7f3eaebdccb8c17f15a11d2cebbab28cbd3 ./Makefile -52adec38e0256b594e84ddab42cb6694bd1649bf5e12dd6e94851a8b4cc01d10 ./README.md +4ce9d3f1bd0c4c95a3783d38fb69e1a6f302c88af3c71eea4980c6f722cce163 ./README.md 4858caa52c0fb6cf302a1c581d07d448e5e90e0daa797e5819610fd6223bd348 ./RELEASE-NOTES-v1.1.0.md 01163462f46314f57660677fdef407c6c2884412ea850aab13a4f650e8c29f50 ./RELEASE-NOTES-v1.2.0.md 4da388ce660aa3b7a0d8075ec066a025b5437960973397360fcb9a5d4cb58c96 ./RELEASE-NOTES-v1.3.0.md 257d6d3ea1d22b8ab97adb5d35ca5ac806702d75cea66982dc711754040fc7a2 ./RELEASE-NOTES-v1.4.0.md 0723a0151b72d492899a5c5409e1fef37de1fe4c458c3053f2f7eccd4b6e70c5 ./RELEASE-NOTES-v1.4.1.md 58923d3292f11487526b8dc4d6c0992025c0f946df8435fde5c91ef4581d46fd ./RELEASE-NOTES-v1.4.2.md -78b591400c56b7b67b8cb3b2b8a8e65e9093897f02ce0878e6b5405c68620fa7 ./VERSION +548f3168f8f29add4f510c4c8048a74465f311dbc3ba2436e5b8db5cc898c552 ./RELEASE-NOTES-v1.4.3.md +bd31e8d9cf8d14a1e9e3fdf3b9f6a524819aed0d1b14627523c6765767c7d8f1 ./VERSION e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./backups/.gitkeep 8127e9db5e5e0af1d88770dc8fa60b381de45dbcc843262698cf9501409b4d58 ./deploy/searxng/settings.yml f95897d6e96afbbcc230f56ae1968a046f76cf24ec78d9c18aa2b22bd8335e34 ./docker-compose.yml @@ -139,7 +140,7 @@ cb3cde795233c0d4842dc7451c5855d1c071e459efbde3be0cabac32a837ba1b ./knowledge/14 7f1d67faf4a6cea0c41c84d7b275d8a8979b19420b52db4925d8e2cd71ead3e0 ./knowledge/16_office-aktivierung-und-lizenzierung.json f1eab883370e0a40ef52a6b6d785a510d8ed48a19d25e0a2bc95f4f2cc8e329e ./knowledge/17_serienbriefe-und-dokumentfunktionen.json 5a0b3d5d5bc712e30a67a4de3432f69070f7e43dd99931f675e72c213006b363 ./knowledge/example-vpn.json -e95d55331b2dfe8326ef4af50cee141d2c003a3f8a01eecaba028f1d4e6d3424 ./mega-project.json +e94bbbfe2769b3d30c69bdf4239b030d23423249cb3fa7f5de3171656cd3a908 ./mega-project.json c02d6d702401343e6d7002c3db117935964e2abd18db966b604871e59715ee05 ./patches/SHA256SUMS 47a6fa2c79bbba0c04af86dfa65d58529f492c698060fe586456c22a4eadb877 ./patches/glpi-agent-mega.diff 9b411c90d96a86c80f088ee4637046eeefaf31c62059d11aa1a999d6eb08b5b4 ./patches/glpi-knowledge-mega.diff @@ -149,6 +150,7 @@ f0491de3cb6201f98ca6be8e865237adbba4772471f7fc7165d030e0c045fdb6 ./patches/neur 163a8a990aacd986bacb385494c2b2062ac10a9a8f0a456d19a6d1746c2fc500 ./patches/v1.3.0-to-v1.4.0.diff 654de4aac7cf318680872897c3de192afbafb61c7169acb8e7514116df86efde ./patches/v1.4.0-envfix-to-v1.4.1.diff 1ebc306bd6870389500c86cf07dcd677b5bd0448c762812c7e1c0f8732f36eb1 ./patches/v1.4.1-to-v1.4.2.diff +651c883facddfd0316f89e3cf3c24fd913371fcec2a5f447b8d105fa5dec463e ./patches/v1.4.2-to-v1.4.3.diff 564817f8edabde0c4e4a1a427a3aa5418aae7bf12e9463044a7e6e0f13973657 ./platform/neuroforge/.env.example 39319b6f2058e4c8d6656a9cf01675374f81a04075b956b093a2befb5e05ada4 ./platform/neuroforge/.gitignore 189486a885c7fb78e0eb878d93cda0c70ca6d7ff9bfdfb3f5f32487cf03a9688 ./platform/neuroforge/BENCHMARK-v0.5.0.md @@ -204,8 +206,8 @@ edfedb67b1f55b2fa8a4a4d29b809b53083b36752e8dc087fa8603f4ec26154e ./platform/neu 65a8b8196343e7cfd9444ca314da4a83c81bc046d9478b8b93217d9cc68ba562 ./platform/neuroforge/internal/cost/cost.go b9bb2934e01ed2bfb6b16e4139e10e51f1387112eb8654f7f692779a2fd2d273 ./platform/neuroforge/internal/cost/cost_test.go fb66ce4ab760b979eacd4f7f17dedc41916f93c53ba582be572d250f99654695 ./platform/neuroforge/internal/httpapi/admin_app_auth_test.go -4071eb134f63f19c95bba709c505e3748fcb7613264b6876350f49e6b693d61b ./platform/neuroforge/internal/httpapi/httpapi.go -c5884789a3ae17be5d61367e5dc19f89a87ac83f9516e80310f66fc9e49f5103 ./platform/neuroforge/internal/httpapi/index.html +4b8555feb594fb71aac1c868eca00cc952eb4e998fbea10b442389f7d5e8160b ./platform/neuroforge/internal/httpapi/httpapi.go +b6df45ac405afddcd68d21a7f1cd7a5a2b4e2379d350eafa2589de6b1dc2540c ./platform/neuroforge/internal/httpapi/index.html 20f4a6cc30d5ce84ceed4fbdfc9f5c57ab5f82274e0c54464817d571e0e881b0 ./platform/neuroforge/internal/httpapi/integration.go 7f6e4767dfcb948570e05baadca6917f27a406fe57ef82eeb63e3b3979ec94b6 ./platform/neuroforge/internal/httpapi/integration_api_test.go f8cfcc7a2bc781394231c25e36e75c41563f5250a1387e5687880d2591efd93b ./platform/neuroforge/internal/httpapi/integration_graph.go @@ -220,6 +222,7 @@ c40412c77dd8635bbd4bf9c6d7eb11b0d7902aee896bdd1ee99e5cdfb72c7453 ./platform/neu 093f6c37c1021cb252501d2aa9c0f42a75444ed69748b455847623da122947b1 ./platform/neuroforge/internal/httpapi/outcomes_test.go b3b1f5cfd6b0ec04341978898d72856af03a95b8922daf9864caeb27e37f1925 ./platform/neuroforge/internal/httpapi/research_live.go b37f7ec3be867666999a9ec314139dd1bc4c74efb53edfa224b4fe8119858aac ./platform/neuroforge/internal/httpapi/research_live_test.go +4e65ab0a760369a6d6c4932c180ff6c67f0768d91e08ccf471f53c6ecd5d09be ./platform/neuroforge/internal/httpapi/status_dashboard_test.go 8a2251d4f4ae8114ac3f5e98fe2e1d6ce24e7817adcfbfc1f383d770e6ce139c ./platform/neuroforge/internal/httpapi/v3.go b1587e066b56ef72f17162583698614d31c413e2ac7e75359b731c2e882a50ee ./platform/neuroforge/internal/httpapi/v4.go 1ef5cacb0652f2a6301c57f8987dd848fbe590abb73cab44899d9152ccce45d0 ./platform/neuroforge/internal/httpapi/v5.go diff --git a/README.md b/README.md index d92c1b8..aef1427 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GLPI NeuroForge Mega v1.4.0 -> Release: **v1.4.2** · Bugfix für leeren NeuroForge-Wissensraum-Graph. +> Release: **v1.4.3** · Dashboard-Status-Kompatibilitätsfix für korrekte Memory-/Synapsen-/Revision-Anzeige. Ein kontrolliertes Monorepo aus **GLPI AI Agent**, **GLPI AI Knowledgebase** und **NeuroForge + SQAR**. Ziel ist nicht ein untrennbarer Monolith, sondern eine gemeinsame Plattform mit klaren Zuständigkeiten, getrennten Credentials und nachvollziehbaren Failure-Modi. diff --git a/RELEASE-NOTES-v1.4.3.md b/RELEASE-NOTES-v1.4.3.md new file mode 100644 index 0000000..12ffe94 --- /dev/null +++ b/RELEASE-NOTES-v1.4.3.md @@ -0,0 +1,24 @@ +# GLPI NeuroForge Mega v1.4.3 + +## NeuroForge Dashboard status fix + +Diese Patch-Version behebt eine Inkonsistenz zwischen der erweiterten `/admin/api/status`-Antwort und dem eingebetteten NeuroForge-Dashboard. + +### Behoben + +- `Memories` zeigte `0`, obwohl der Knowledge-Summary bereits semantische Memories meldete. +- `Synapsen` konnte fälschlich `0` anzeigen. +- die Seitenleiste zeigte `rev undefined`. +- die Betriebsansicht konnte HNSW-, PQ-, Job- und Maintenance-Werte als `0`/leer darstellen. + +### Ursache + +Der Status-Endpunkt liefert die schnellen Zähler seit der erweiterten Observability-Antwort unter `stats`, während ältere Dashboard-Teile weiterhin die ursprünglichen flachen Felder erwarteten. + +### Lösung + +- Der Server liefert die Status-Zähler wieder zusätzlich auf Top-Level und behält parallel das strukturierte `stats`-Objekt. +- Das WebUI normalisiert beide Antwortformen defensiv. Dadurch ist sowohl ein neues UI gegen einen älteren Server als auch ein älteres UI gegen den neuen Server robuster. +- Ein Regressionstest stellt sicher, dass die flachen Dashboard-Felder nicht erneut verschwinden. + +Keine Datenmigration und keine `.env`-Änderung erforderlich. diff --git a/VERSION b/VERSION index 88c5fb8..428b770 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.4.0 +1.4.3 diff --git a/mega-project.json b/mega-project.json index 32d52e5..1a449fb 100644 --- a/mega-project.json +++ b/mega-project.json @@ -28,7 +28,7 @@ "schema": "Wiki/Schema.md", "glpi_relations": "KnowbaseItem_Item when exposed by GLPI OpenAPI" }, - "version": "1.4.2", + "version": "1.4.3", "controlled_learning": { "raw_chat_auto_learning": false, "validated_outcomes": [ diff --git a/patches/v1.4.2-to-v1.4.3.diff b/patches/v1.4.2-to-v1.4.3.diff new file mode 100644 index 0000000..365c8cf --- /dev/null +++ b/patches/v1.4.2-to-v1.4.3.diff @@ -0,0 +1,191 @@ +diff --git a/README.md b/README.md +index d92c1b8..aef1427 100644 +--- a/README.md ++++ b/README.md +@@ -1,6 +1,6 @@ + # GLPI NeuroForge Mega v1.4.0 + +-> Release: **v1.4.2** · Bugfix für leeren NeuroForge-Wissensraum-Graph. ++> Release: **v1.4.3** · Dashboard-Status-Kompatibilitätsfix für korrekte Memory-/Synapsen-/Revision-Anzeige. + + Ein kontrolliertes Monorepo aus **GLPI AI Agent**, **GLPI AI Knowledgebase** und **NeuroForge + SQAR**. Ziel ist nicht ein untrennbarer Monolith, sondern eine gemeinsame Plattform mit klaren Zuständigkeiten, getrennten Credentials und nachvollziehbaren Failure-Modi. + +diff --git a/RELEASE-NOTES-v1.4.3.md b/RELEASE-NOTES-v1.4.3.md +new file mode 100644 +index 0000000..12ffe94 +--- /dev/null ++++ b/RELEASE-NOTES-v1.4.3.md +@@ -0,0 +1,24 @@ ++# GLPI NeuroForge Mega v1.4.3 ++ ++## NeuroForge Dashboard status fix ++ ++Diese Patch-Version behebt eine Inkonsistenz zwischen der erweiterten `/admin/api/status`-Antwort und dem eingebetteten NeuroForge-Dashboard. ++ ++### Behoben ++ ++- `Memories` zeigte `0`, obwohl der Knowledge-Summary bereits semantische Memories meldete. ++- `Synapsen` konnte fälschlich `0` anzeigen. ++- die Seitenleiste zeigte `rev undefined`. ++- die Betriebsansicht konnte HNSW-, PQ-, Job- und Maintenance-Werte als `0`/leer darstellen. ++ ++### Ursache ++ ++Der Status-Endpunkt liefert die schnellen Zähler seit der erweiterten Observability-Antwort unter `stats`, während ältere Dashboard-Teile weiterhin die ursprünglichen flachen Felder erwarteten. ++ ++### Lösung ++ ++- Der Server liefert die Status-Zähler wieder zusätzlich auf Top-Level und behält parallel das strukturierte `stats`-Objekt. ++- Das WebUI normalisiert beide Antwortformen defensiv. Dadurch ist sowohl ein neues UI gegen einen älteren Server als auch ein älteres UI gegen den neuen Server robuster. ++- Ein Regressionstest stellt sicher, dass die flachen Dashboard-Felder nicht erneut verschwinden. ++ ++Keine Datenmigration und keine `.env`-Änderung erforderlich. +diff --git a/VERSION b/VERSION +index 88c5fb8..428b770 100644 +--- a/VERSION ++++ b/VERSION +@@ -1 +1 @@ +-1.4.0 ++1.4.3 +diff --git a/mega-project.json b/mega-project.json +index 32d52e5..1a449fb 100644 +--- a/mega-project.json ++++ b/mega-project.json +@@ -28,7 +28,7 @@ + "schema": "Wiki/Schema.md", + "glpi_relations": "KnowbaseItem_Item when exposed by GLPI OpenAPI" + }, +- "version": "1.4.2", ++ "version": "1.4.3", + "controlled_learning": { + "raw_chat_auto_learning": false, + "validated_outcomes": [ +diff --git a/platform/neuroforge/internal/httpapi/httpapi.go b/platform/neuroforge/internal/httpapi/httpapi.go +index e6bf73c..dce944f 100644 +--- a/platform/neuroforge/internal/httpapi/httpapi.go ++++ b/platform/neuroforge/internal/httpapi/httpapi.go +@@ -460,12 +460,18 @@ func (s *Server) adminStatus(w http.ResponseWriter, r *http.Request) { + "last_index": obs.ClusterLastIndex, "commit_index": obs.ClusterCommitIndex, "peers": obs.ClusterPeers, "voters": obs.ClusterVoters, "quorum": obs.ClusterQuorum, + "replicated_log": obs.ClusterLog, + } +- s.json(w, 200, map[string]any{ ++ payload := map[string]any{ + "stats": stats, "wal": s.store.WALStatus(), + "storage": map[string]any{"memory_segments": obs.Segments, "index_snapshot": map[string]any{"revision": obs.IndexSnapshotRevision, "deltas": obs.IndexDeltaCount, "segmented": cfg.Storage.IndexSegments.Enabled}, "tiering": tiering, "disk_ann": s.store.DiskANNStatus()}, + "cluster": cluster, "cost": s.cost.Totals(), "providers": providers, "usage": s.store.RecentUsage(25), + "observability": obs, "runtime": currentRuntimeSnapshot(), "http": s.metrics.dashboardSnapshot(), +- }) ++ } ++ // Keep the original flat status fields for dashboard and external-client ++ // compatibility while retaining the richer nested stats object. ++ for k, v := range stats { ++ payload[k] = v ++ } ++ s.json(w, 200, payload) + } + func (s *Server) adminGetConfig(w http.ResponseWriter, r *http.Request) { + s.json(w, 200, s.store.Config()) +diff --git a/platform/neuroforge/internal/httpapi/index.html b/platform/neuroforge/internal/httpapi/index.html +index 95b9eb6..50e566d 100644 +--- a/platform/neuroforge/internal/httpapi/index.html ++++ b/platform/neuroforge/internal/httpapi/index.html +@@ -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])=>`
${esc(a)}${esc(b)}
`).join('')} + function renderEvents(xs,id){xs=Array.isArray(xs)?xs:[];$(id).innerHTML=xs.length?xs.map(e=>`
${esc(e.type)}${esc(e.actor||'system')}

${esc(e.summary||'')}

${new Date(e.created_at).toLocaleString()}${e.reason?`${esc(e.reason)}`:''}
`).join(''):'

Noch keine Lernereignisse.

'} + function renderDashboardGoals(xs){$('dashGoals').innerHTML=xs.length?xs.map(g=>`
${esc(g.title)}${g.auto_run?'AUTO':'MANUELL'}
${Math.round((g.progress||0)*100)}%${g.research_enabled?'Research an':'Research aus'}${g.next_cycle_at?when(g.next_cycle_at):'nicht geplant'}
`).join(''):'

Keine aktiven Ziele.

'} +@@ -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=>`
${esc(n.name||n.id)}${n.enabled?'aktiv':'aus'}

${esc(n.base_url)}

Chat: ${esc(n.chat_model)}Embed: ${esc(n.embedding_model)}ctx ${n.num_ctx||'default'}think ${esc(n.think||'off')}
`).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=`
HNSW ${fmtN(s.hnsw_nodes)}
Disk-PQ ${fmtN(s.disk_pq_items)}
Mode ${esc(s.index_mode||'–')}
`;$('opsStorage').innerHTML=`
Revision ${fmtN(s.revision)}
Segmente ${fmtN(st.segments?.files||st.memory_segments?.files||0)}
Disk ${fmtBytes(st.segments?.bytes||st.memory_segments?.bytes||0)}
`;$('opsObs').innerHTML=`
Pending jobs ${fmtN(s.pending_jobs)}
Remote shards ${fmtN(s.remote_shards)}
Letzter Fehler ${esc(s.maintenance?.last_error||'keiner')}
`;$('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=`
HNSW ${fmtN(s.hnsw_nodes)}
Disk-PQ ${fmtN(s.disk_pq_items)}
Mode ${esc(s.index_mode||'–')}
`;$('opsStorage').innerHTML=`
Revision ${fmtN(s.revision)}
Segmente ${fmtN(st.segments?.files||st.memory_segments?.files||0)}
Disk ${fmtBytes(st.segments?.bytes||st.memory_segments?.bytes||0)}
`;$('opsObs').innerHTML=`
Pending jobs ${fmtN(s.pending_jobs)}
Remote shards ${fmtN(s.remote_shards)}
Letzter Fehler ${esc(s.maintenance?.last_error||'keiner')}
`;$('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(); + +diff --git a/platform/neuroforge/internal/httpapi/status_dashboard_test.go b/platform/neuroforge/internal/httpapi/status_dashboard_test.go +new file mode 100644 +index 0000000..0e15a3a +--- /dev/null ++++ b/platform/neuroforge/internal/httpapi/status_dashboard_test.go +@@ -0,0 +1,68 @@ ++package httpapi ++ ++import ( ++ "encoding/json" ++ "net/http" ++ "net/http/httptest" ++ "strings" ++ "testing" ++ ++ "neuroforge/internal/core" ++) ++ ++func TestAdminStatusKeepsFlatDashboardFields(t *testing.T) { ++ s, _ := newMetricsTestServer(t) ++ if err := s.store.AddMemory(&core.Memory{Text: "status dashboard regression", MemoryType: "semantic"}); err != nil { ++ t.Fatal(err) ++ } ++ ++ req := httptest.NewRequest(http.MethodGet, "/admin/api/status", nil) ++ req.Header.Set("X-Admin-Token", s.store.Secrets().AdminToken) ++ rr := httptest.NewRecorder() ++ s.Handler().ServeHTTP(rr, req) ++ if rr.Code != http.StatusOK { ++ t.Fatalf("status=%d body=%s", rr.Code, rr.Body.String()) ++ } ++ ++ var body map[string]any ++ if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil { ++ t.Fatal(err) ++ } ++ stats, ok := body["stats"].(map[string]any) ++ if !ok { ++ t.Fatalf("missing nested stats: %#v", body["stats"]) ++ } ++ for _, key := range []string{"revision", "memories", "synapses", "pending_jobs", "hnsw_nodes", "disk_pq_items", "index_mode", "maintenance"} { ++ flat, exists := body[key] ++ if !exists { ++ t.Fatalf("flat status field %q missing", key) ++ } ++ nested, exists := stats[key] ++ if !exists { ++ t.Fatalf("nested status field %q missing", key) ++ } ++ if key != "maintenance" && key != "index_mode" && flat != nested { ++ t.Fatalf("field %q flat=%v nested=%v", key, flat, nested) ++ } ++ } ++ if got := int(body["memories"].(float64)); got != 1 { ++ t.Fatalf("memories=%d want 1", got) ++ } ++} ++ ++func TestAdminDashboardNormalizesNestedStatusResponse(t *testing.T) { ++ b, err := webFS.ReadFile("index.html") ++ if err != nil { ++ t.Fatal(err) ++ } ++ html := string(b) ++ for _, want := range []string{ ++ "function normalizeStatus(s)", ++ "s=normalizeStatus(raw)", ++ "bereit · rev '+fmtN(s.revision)", ++ } { ++ if !strings.Contains(html, want) { ++ t.Fatalf("dashboard missing status compatibility guard %q", want) ++ } ++ } ++} diff --git a/platform/neuroforge/internal/httpapi/httpapi.go b/platform/neuroforge/internal/httpapi/httpapi.go index e6bf73c..dce944f 100644 --- a/platform/neuroforge/internal/httpapi/httpapi.go +++ b/platform/neuroforge/internal/httpapi/httpapi.go @@ -460,12 +460,18 @@ func (s *Server) adminStatus(w http.ResponseWriter, r *http.Request) { "last_index": obs.ClusterLastIndex, "commit_index": obs.ClusterCommitIndex, "peers": obs.ClusterPeers, "voters": obs.ClusterVoters, "quorum": obs.ClusterQuorum, "replicated_log": obs.ClusterLog, } - s.json(w, 200, map[string]any{ + payload := map[string]any{ "stats": stats, "wal": s.store.WALStatus(), "storage": map[string]any{"memory_segments": obs.Segments, "index_snapshot": map[string]any{"revision": obs.IndexSnapshotRevision, "deltas": obs.IndexDeltaCount, "segmented": cfg.Storage.IndexSegments.Enabled}, "tiering": tiering, "disk_ann": s.store.DiskANNStatus()}, "cluster": cluster, "cost": s.cost.Totals(), "providers": providers, "usage": s.store.RecentUsage(25), "observability": obs, "runtime": currentRuntimeSnapshot(), "http": s.metrics.dashboardSnapshot(), - }) + } + // Keep the original flat status fields for dashboard and external-client + // compatibility while retaining the richer nested stats object. + for k, v := range stats { + payload[k] = v + } + s.json(w, 200, payload) } func (s *Server) adminGetConfig(w http.ResponseWriter, r *http.Request) { s.json(w, 200, s.store.Config()) diff --git a/platform/neuroforge/internal/httpapi/index.html b/platform/neuroforge/internal/httpapi/index.html index 95b9eb6..50e566d 100644 --- a/platform/neuroforge/internal/httpapi/index.html +++ b/platform/neuroforge/internal/httpapi/index.html @@ -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])=>`
${esc(a)}${esc(b)}
`).join('')} function renderEvents(xs,id){xs=Array.isArray(xs)?xs:[];$(id).innerHTML=xs.length?xs.map(e=>`
${esc(e.type)}${esc(e.actor||'system')}

${esc(e.summary||'')}

${new Date(e.created_at).toLocaleString()}${e.reason?`${esc(e.reason)}`:''}
`).join(''):'

Noch keine Lernereignisse.

'} function renderDashboardGoals(xs){$('dashGoals').innerHTML=xs.length?xs.map(g=>`
${esc(g.title)}${g.auto_run?'AUTO':'MANUELL'}
${Math.round((g.progress||0)*100)}%${g.research_enabled?'Research an':'Research aus'}${g.next_cycle_at?when(g.next_cycle_at):'nicht geplant'}
`).join(''):'

Keine aktiven Ziele.

'} @@ -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=>`
${esc(n.name||n.id)}${n.enabled?'aktiv':'aus'}

${esc(n.base_url)}

Chat: ${esc(n.chat_model)}Embed: ${esc(n.embedding_model)}ctx ${n.num_ctx||'default'}think ${esc(n.think||'off')}
`).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=`
HNSW ${fmtN(s.hnsw_nodes)}
Disk-PQ ${fmtN(s.disk_pq_items)}
Mode ${esc(s.index_mode||'–')}
`;$('opsStorage').innerHTML=`
Revision ${fmtN(s.revision)}
Segmente ${fmtN(st.segments?.files||st.memory_segments?.files||0)}
Disk ${fmtBytes(st.segments?.bytes||st.memory_segments?.bytes||0)}
`;$('opsObs').innerHTML=`
Pending jobs ${fmtN(s.pending_jobs)}
Remote shards ${fmtN(s.remote_shards)}
Letzter Fehler ${esc(s.maintenance?.last_error||'keiner')}
`;$('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=`
HNSW ${fmtN(s.hnsw_nodes)}
Disk-PQ ${fmtN(s.disk_pq_items)}
Mode ${esc(s.index_mode||'–')}
`;$('opsStorage').innerHTML=`
Revision ${fmtN(s.revision)}
Segmente ${fmtN(st.segments?.files||st.memory_segments?.files||0)}
Disk ${fmtBytes(st.segments?.bytes||st.memory_segments?.bytes||0)}
`;$('opsObs').innerHTML=`
Pending jobs ${fmtN(s.pending_jobs)}
Remote shards ${fmtN(s.remote_shards)}
Letzter Fehler ${esc(s.maintenance?.last_error||'keiner')}
`;$('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(); diff --git a/platform/neuroforge/internal/httpapi/status_dashboard_test.go b/platform/neuroforge/internal/httpapi/status_dashboard_test.go new file mode 100644 index 0000000..0e15a3a --- /dev/null +++ b/platform/neuroforge/internal/httpapi/status_dashboard_test.go @@ -0,0 +1,68 @@ +package httpapi + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "neuroforge/internal/core" +) + +func TestAdminStatusKeepsFlatDashboardFields(t *testing.T) { + s, _ := newMetricsTestServer(t) + if err := s.store.AddMemory(&core.Memory{Text: "status dashboard regression", MemoryType: "semantic"}); err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest(http.MethodGet, "/admin/api/status", nil) + req.Header.Set("X-Admin-Token", s.store.Secrets().AdminToken) + rr := httptest.NewRecorder() + s.Handler().ServeHTTP(rr, req) + if rr.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", rr.Code, rr.Body.String()) + } + + var body map[string]any + if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil { + t.Fatal(err) + } + stats, ok := body["stats"].(map[string]any) + if !ok { + t.Fatalf("missing nested stats: %#v", body["stats"]) + } + for _, key := range []string{"revision", "memories", "synapses", "pending_jobs", "hnsw_nodes", "disk_pq_items", "index_mode", "maintenance"} { + flat, exists := body[key] + if !exists { + t.Fatalf("flat status field %q missing", key) + } + nested, exists := stats[key] + if !exists { + t.Fatalf("nested status field %q missing", key) + } + if key != "maintenance" && key != "index_mode" && flat != nested { + t.Fatalf("field %q flat=%v nested=%v", key, flat, nested) + } + } + if got := int(body["memories"].(float64)); got != 1 { + t.Fatalf("memories=%d want 1", got) + } +} + +func TestAdminDashboardNormalizesNestedStatusResponse(t *testing.T) { + b, err := webFS.ReadFile("index.html") + if err != nil { + t.Fatal(err) + } + html := string(b) + for _, want := range []string{ + "function normalizeStatus(s)", + "s=normalizeStatus(raw)", + "bereit · rev '+fmtN(s.revision)", + } { + if !strings.Contains(html, want) { + t.Fatalf("dashboard missing status compatibility guard %q", want) + } + } +}