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])=>`
`).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])=>``).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)
+ }
+ }
+}