This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
# BRAIN ANALYSIS CENTER
|
||||
|
||||
Das technische Analyse-Dashboard ist bewusst von der animierten Gehirnansicht getrennt.
|
||||
|
||||
- Visualisierung: `http://localhost:8090/`
|
||||
- Analyse: `http://localhost:8090/analysis.html`
|
||||
- Kurzpfad: `http://localhost:8090/analysis`
|
||||
|
||||
In der Gehirnansicht führt der neue Button **ANALYSE** direkt zum Dashboard.
|
||||
|
||||
## Zweck
|
||||
|
||||
Das Dashboard beantwortet insbesondere:
|
||||
|
||||
- Hatte der letzte Lauf ein materiell positives Ergebnis?
|
||||
- Wurden Nodes, Edges oder Embeddings erzeugt, verändert oder gelöscht?
|
||||
- Welche konkreten Objekt-IDs waren betroffen?
|
||||
- Wurde lediglich geprüft und anschließend wegen Qualitätsregeln übersprungen?
|
||||
- Wie viele semantische Vergleiche, SearXNG-Treffer, Volltextabrufe und akzeptierte Belege gab es?
|
||||
- Wurde ein Artikel erzeugt oder blieb nur neue Research-Evidence zurück?
|
||||
- Ist der Graph strukturell gesund und wie hoch ist die Vektorabdeckung?
|
||||
- Sind Ollama, Autonomous Research, Persistenz und SQLite aktuell arbeitsfähig?
|
||||
|
||||
## Persistentes Auditprotokoll
|
||||
|
||||
Die Historie liegt in `BRAIN_DATA_DIR/graph.db`. Die Datenbank wird automatisch von Schemaversion 2 auf 3 erweitert. Ein Löschen oder Neuaufbau des Graphen ist nicht erforderlich.
|
||||
|
||||
Neue Tabellen:
|
||||
|
||||
```text
|
||||
analysis_events
|
||||
analysis_points
|
||||
analysis_changes
|
||||
```
|
||||
|
||||
`analysis_events` enthält die ursprünglichen Engine-Aktivitäten mit Query, Meldung, IDs und Metadaten. `analysis_points` speichert den unmittelbar dazu erfassten Graphstand sowie monotone Änderungszähler. `analysis_changes` enthält die konkreten betroffenen Nodes, Edges und Vektoren.
|
||||
|
||||
Die Historie wird 90 Tage aufbewahrt. Die Aufzeichnung beginnt erst mit dieser Version; frühere Läufe lassen sich nicht rückwirkend rekonstruieren.
|
||||
|
||||
## Änderungssemantik
|
||||
|
||||
### Nodes
|
||||
|
||||
- `created`: neue Node-ID wurde aufgenommen.
|
||||
- `updated`: vorhandene Node wurde mit geänderten Daten ersetzt.
|
||||
- `deleted`: Node ist aus einer verwalteten Quelle verschwunden.
|
||||
|
||||
### Edges
|
||||
|
||||
- `created`: neue Relation wurde aufgenommen.
|
||||
- `updated`: Typ, Status, Confidence, Evidenz oder Metadaten einer vorhandenen Relation wurden geändert.
|
||||
- `deleted`: Relation wurde entfernt, etwa weil die Quelle oder ein Endpunkt nicht mehr existiert.
|
||||
|
||||
### Vektoren
|
||||
|
||||
- `created`: ein Node erhielt erstmals ein Embedding.
|
||||
- `recalculated`: ein vorhandenes Embedding wurde ersetzt.
|
||||
- `deleted`: ein Vektor wurde invalidiert oder sein Node entfernt.
|
||||
|
||||
Eine veränderte semantische Nähe wird sichtbar, wenn eine AI-Edge aktualisiert wird. Reine Kandidatenprüfungen ohne Edge-Übernahme bleiben über die Activity-Metadaten wie `semantic_similarity`, `confidence` und `candidate_comparisons` nachvollziehbar.
|
||||
|
||||
## Laufbewertung
|
||||
|
||||
Das Dashboard fasst zusammengehörige Events zu Läufen zusammen:
|
||||
|
||||
- KB-Lernlauf
|
||||
- AI-THINK-Zyklus
|
||||
- SearXNG-Recherche
|
||||
- autonome Rechercheaufgabe
|
||||
- Wissensabfrage
|
||||
- Artikelsynthese
|
||||
- GLPI-Synchronisierung
|
||||
- Persistenz- und Embedding-Aktionen
|
||||
|
||||
Bewertungen:
|
||||
|
||||
| Bewertung | Bedeutung |
|
||||
|---|---|
|
||||
| `positives Ergebnis` | mindestens ein Node, eine Edge oder ein Embedding wurde erzeugt |
|
||||
| `aktualisiert` | bestehende Objekte wurden geändert oder Vektoren neu berechnet |
|
||||
| `Bereinigung` | veraltete Objekte wurden entfernt |
|
||||
| `ohne Übernahme` | Prüfung lief, aber Qualitäts-, Relevanz- oder Quellenregeln verhinderten eine Übernahme |
|
||||
| `ohne Graphänderung` | Lauf endete regulär, ohne den Graphzustand zu verändern |
|
||||
| `fehlgeschlagen` | mindestens ein Fehlerereignis gehört zum Lauf |
|
||||
|
||||
## Detailgrenze
|
||||
|
||||
Bei sehr großen Erstimporten können zehntausende Nodes und Vektoren in einem einzigen Event entstehen. Deshalb werden pro Event höchstens 2.000 konkrete Detailzeilen persistiert. Die aggregierten Änderungszähler bleiben vollständig und exakt. Das Dashboard weist sichtbar auf gekürzte Detaildaten hin.
|
||||
|
||||
Die API liefert standardmäßig die neuesten konkreten Änderungen im gewählten Zeitraum. Dadurch bleibt das Dashboard auch bei großen Wissensbasen bedienbar.
|
||||
|
||||
## Performance
|
||||
|
||||
Die Aufzeichnung selbst ist absichtlich leichtgewichtig:
|
||||
|
||||
1. Graphmutationen erhöhen im Speicher konstante Zähler und sammeln begrenzte Detailinformationen.
|
||||
2. Ein Activity-Event übernimmt den aktuellen Checkpoint in eine asynchrone Queue.
|
||||
3. Bursts werden bis zu 50 ms beziehungsweise 64 Events gesammelt und gemeinsam in einer SQLite-Transaktion geschrieben.
|
||||
4. SQLite wird außerhalb des Render- und Engine-Hotpaths beschrieben.
|
||||
5. Die aufwendige strukturelle Graphanalyse wird erst beim Öffnen des Dashboards berechnet und pro Graphversion gecacht.
|
||||
6. Die animierte Gehirnansicht lädt die Analysehistorie nicht.
|
||||
|
||||
Das Analyse-Dashboard aktualisiert sich über den bestehenden SSE-Stream, aber zusammengefasst und verzögert, damit Event-Bursts nicht für jedes einzelne Ereignis eine neue Vollanalyse auslösen.
|
||||
|
||||
## HTTP-API
|
||||
|
||||
```text
|
||||
GET /api/analysis/dashboard?hours=24&limit=400
|
||||
GET /api/analysis/export?hours=24
|
||||
```
|
||||
|
||||
`hours` ist auf 1 bis 2.160 Stunden begrenzt. `limit` steuert die maximale Zahl zurückgegebener Läufe und Events. Der Export liefert dieselbe Struktur als formatiertes JSON und benötigt bei gesetztem `BRAIN_API_KEY` eine Autorisierung.
|
||||
|
||||
Wesentliche Antwortbereiche:
|
||||
|
||||
```json
|
||||
{
|
||||
"graph": {
|
||||
"summary": {},
|
||||
"node_sources": {},
|
||||
"edge_types": {},
|
||||
"vector_coverage": 0.98,
|
||||
"average_ai_confidence": 0.86,
|
||||
"average_ai_similarity": 0.91
|
||||
},
|
||||
"history": {
|
||||
"runs": [],
|
||||
"events": [],
|
||||
"changes": [],
|
||||
"totals": {},
|
||||
"timeline": [],
|
||||
"change_count": 0
|
||||
},
|
||||
"system": {}
|
||||
}
|
||||
```
|
||||
|
||||
## Dashboardbereiche
|
||||
|
||||
- Ergebnisbewertung des letzten abgeschlossenen Laufs
|
||||
- aktuelle Graph-, Vektor-, Thinking-, Research-, Ollama- und Persistenzkennzahlen
|
||||
- Zeitverlauf von Erzeugungen, Aktualisierungen, Löschungen und Events
|
||||
- filterbares Laufprotokoll mit vollständigen Unterevents
|
||||
- Source-, Kind-, Origin- und Edge-Verteilungen
|
||||
- Confidence- und Similarity-Auswertung
|
||||
- Embedding- und Research-Auswertung
|
||||
- neueste Nodes und Edges
|
||||
- exaktes Änderungsjournal
|
||||
- technischer Roh-Eventstream mit ursprünglichen Metadaten und Graphcheckpoint
|
||||
@@ -0,0 +1,30 @@
|
||||
# Changelog: Analysis Dashboard
|
||||
|
||||
## Neu
|
||||
|
||||
- separate Analyseoberfläche unter `/analysis.html`
|
||||
- direkter **ANALYSE**-Link in der Gehirnansicht
|
||||
- persistente Activity- und Graphcheckpoint-Historie in SQLite
|
||||
- konkrete Änderungsjournal-Einträge für Nodes, Edges und Vektoren
|
||||
- automatische Laufgruppierung und verständliche Ergebnisbewertung
|
||||
- Zeitreihen für Erzeugungen, Aktualisierungen, Löschungen und Eventvolumen
|
||||
- Source-, Origin-, Kind-, Edge-, Similarity-, Confidence- und Embedding-Auswertung
|
||||
- SearXNG-, Volltext-, Evidenz- und Artikelergebnisübersicht
|
||||
- Ollama-, Autonomous-Research- und Persistenzstatus
|
||||
- JSON-Export der gesamten Analyse
|
||||
- Schemaerweiterung `graph.db` von Version 2 auf Version 3
|
||||
|
||||
## Geändert
|
||||
|
||||
- KB-Scans publizieren nun explizite Start-, Fehler- und Abschlussereignisse mit Vorher-/Nachher-Zahlen.
|
||||
- Der Activity-Broker kann einen nicht blockierenden Audit-Sink bedienen.
|
||||
- Graphmutationen führen getrennte monotone Zähler für Created, Updated/Recalculated und Deleted.
|
||||
- Die aufwendige aktuelle Graphanalyse wird pro Graphversion gecacht.
|
||||
|
||||
## Schutzregeln
|
||||
|
||||
- asynchroner SQLite-Auditwriter mit begrenzter Queue und gebündelten Transaktionen
|
||||
- höchstens 2.000 konkrete Objektänderungen pro Event
|
||||
- vollständige Aggregatzähler auch bei gekürztem Detailjournal
|
||||
- 90 Tage Aufbewahrung
|
||||
- keine Analysehistorie im Hotpath der animierten Gehirnansicht
|
||||
@@ -17,6 +17,7 @@ Eigenständiger Go-Dienst für Agent, lokale Knowledgebase, GLPI-Knowledgebase u
|
||||
- Mehrstufiger AI-THINK-Worker: Relation Thinking, quellengebundene Wissenskonsolidierung, Recherche offener Punkte und reine Knowledge-Synthesis für vollständige KB-Artikel.
|
||||
- Persistente **Autonomous-Research-Queue**: selbstständige Wissenslückensuche, externe Trigger, Leerlauf-/Budgetsteuerung und niedrig priorisierte Übergabe an den vorhandenen Ollama-Pool.
|
||||
- Inkrementelle SQLite/WAL-Persistenz über `modernc.org/sqlite`: binäre Float32-Embeddings und standardmäßig alle fünf Minuten gebündelte Row-Updates.
|
||||
- Separates **BRAIN ANALYSIS CENTER** mit persistenter Laufhistorie, exakten Node-/Edge-/Vektoränderungen, Similarity-/Confidence-Auswertung, Research-Ergebnissen und technischem Roh-Eventstream.
|
||||
|
||||
## Vertrauens- und Schreibgrenzen
|
||||
|
||||
@@ -47,6 +48,10 @@ go run ./cmd/brain
|
||||
|
||||
Oberfläche: `http://localhost:8090`
|
||||
|
||||
Technisches Analyse-Dashboard: `http://localhost:8090/analysis.html`
|
||||
|
||||
Das Analyse-Dashboard ist von der animierten Gehirnansicht getrennt. Es bewertet Läufe als positives Ergebnis, Aktualisierung, Bereinigung, Prüfung ohne Übernahme oder Fehler und zeigt die konkret betroffenen Node-, Edge- und Vektor-IDs. Die Historie wird in `graph.db` aufgezeichnet und beginnt ab Installation dieser Version. Details: [`ANALYSIS-DASHBOARD.md`](ANALYSIS-DASHBOARD.md).
|
||||
|
||||
Docker:
|
||||
|
||||
```bash
|
||||
@@ -264,6 +269,8 @@ curl -X POST 'http://localhost:8090/api/enrich?async=1'
|
||||
| `GET` | `/api/status` | Gesamtstatus inklusive Ollama-Pool, GLPI-KB und Persistenzqueue |
|
||||
| `GET` | `/api/graph` | vollständiger aktueller In-Memory-Graph |
|
||||
| `GET` | `/api/analysis` | strukturelle Graphanalyse |
|
||||
| `GET` | `/api/analysis/dashboard` | detaillierte aktuelle Analyse, Laufhistorie, Zeitreihe und konkretes Änderungsjournal |
|
||||
| `GET` | `/api/analysis/export` | vollständige Analyse als formatiertes JSON exportieren |
|
||||
| `GET` | `/api/runtime-settings` | aktuelle Learning-, Thinking-, exakte Source- und View-Einstellungen |
|
||||
| `PUT` | `/api/runtime-settings` | Laufzeiteinstellungen ändern und gebündelt persistieren |
|
||||
| `GET` | `/api/sources` | exakte `source`-Werte mit Anzahl; enthält immer `GLPI_KB_SOURCE` |
|
||||
@@ -305,6 +312,7 @@ Wichtige Bereiche:
|
||||
go test ./...
|
||||
go vet ./...
|
||||
node --check internal/web/static/app.js
|
||||
node --check internal/web/static/analysis.js
|
||||
```
|
||||
|
||||
## Sichtbare SearXNG-Recherche
|
||||
|
||||
+102
-95
@@ -1,95 +1,102 @@
|
||||
e888a548fa32246f01bf7d7545359fa856d6bc32651cafdc776d595d9eada34e ./.env.example
|
||||
8955cfbeff229e73f0cad664863ff21711c270a4233c3225680c89aa6268a901 ./ARCHITECTURE.md
|
||||
cf3e5275f1eb623da3b6f734d233197e8b47879b8e7cdfd4d373a7ecdd921651 ./AUTONOMOUS-RESEARCH.md
|
||||
0ed6ff0d82b3b6776970200a021937611d4f3273f6727d296aec16cfac6153b8 ./CHANGELOG-AUTONOMOUS-RESEARCH.md
|
||||
2bc149241c2d25f755e4a0470dc517f646527ee3e98d3a6c2e7798639bd70866 ./CHANGELOG-CONSTELLATION-ECO-TRANSITIONS.md
|
||||
933cdaeae7895e31d2281d591a75f23f07e7c41d356638654bac4f5e7a20a069 ./CHANGELOG-FILTER-PANEL-SCROLL.md
|
||||
213ac897cd415bbeb9764a847843b9eff366983cf25bb3cbce4efa4c04b9e5f5 ./CHANGELOG-GLPI-POOL-PERSISTENCE.md
|
||||
e2f1f0400999cb59b8be09bc2743e06e0a0b2ecdc44a583af7c9a08b70d8509e ./CHANGELOG-GPU-NODE-LIMIT.md
|
||||
a317376127be1e47b9ec9f0781fcfebdcbf7fccfaeb7840d9241f9586048fadb ./CHANGELOG-GROUNDED-KNOWLEDGE-SYNTHESIS.md
|
||||
02a8d3e541967e2d2ef9dd5451f470679e262ad851aca9f03563b322914c3181 ./CHANGELOG-ITERATIVE-GROUNDED-RESEARCH.md
|
||||
e167a9d64f63c3933ad40c5078684bc019db303043c34ea3965f3b089f3d32c7 ./CHANGELOG-KNOWLEDGE-SYNTHESIS.md
|
||||
87f89a81e1124b18e092a4ea946cb037295cb9e884a46b392286272dc8134dd4 ./CHANGELOG-RUNTIME-HONEYCOMB.md
|
||||
2d04e6d385f4b902080a0bcaab510a846a8ae6a76cb757423c50425c8433e7f9 ./CHANGELOG-SEARXNG-DIAGNOSTICS.md
|
||||
9c760e167a9af2d3d8ca32c5aa4ef6bb4a3153047343a71c4b19ca9aef96ca32 ./CHANGELOG-SEARXNG-VISUALIZATION.md
|
||||
be9f133ae933bdc0e0a8aa5d176dd3e39488a191337043533379e23d179f3ad2 ./CHANGELOG-SOURCE-ONLY-FILTERS.md
|
||||
5433a7c2e67ab35fb320bc872e9024fa5f3e765736184e9f878340b8b45407aa ./CHANGELOG-SQLITE-STARTUP-FIX.md
|
||||
5b9deeab0cd59b3c649fd73f129361a1e773ed3955cded0048b8cb280bb32e88 ./CHANGELOG-SQLITE-STORAGE.md
|
||||
42c1cacdc1f792773622017c3528dc626674456f478c41aaf44ff0a32e5f87bd ./Dockerfile
|
||||
5534536965bf0479455f97324c242160202650ca1256f1ba0420b4ad67125e49 ./GLPI-KB.md
|
||||
c546524a3add0b1beb8d1ca675fd602e2ea7c7bddfc04759519c21a02667eb61 ./ITERATIVE-GROUNDED-RESEARCH.md
|
||||
e3ae87108607ca494668a9974c467a5c529b9599bf88d3d2a79ddc15b64e4a38 ./KNOWLEDGE-SYNTHESIS.md
|
||||
696d2da2338cd8190b9614707e4059d78ce291e7334f273633aad815c3b6a6df ./Makefile
|
||||
381d7d6ac9e3c2e63c9ecdaa42ed4c73058f5d78e57c7532bb75a9663c919530 ./OLLAMA-POOL.md
|
||||
2c0062941ef3edbd40d46b823934a7d0a3a9da7581b83d0b9360e8aaa7694b1b ./PERSISTENCE.md
|
||||
4242cf830d14ecca0e2d069c3a54105e66d24ddc9b7631ef80bb14009d9fcfd9 ./README.md
|
||||
2838cd19ac2bfa35bebbef2541f631b99221b5997bbb6dbc27146a66a3a1ad34 ./RUNTIME-CONTROLS-HONEYCOMB.md
|
||||
c3da43b33e550901d55789f2ee526c2e50f61ee028a3f59e0a40e77e1057fde7 ./SEARXNG-VISUALIZATION.md
|
||||
c69419c0327425186cfb84f25746feff226ce813cf467b3f252e729517455047 ./SOURCE-ONLY-FILTERS.md
|
||||
ae7bc1f1959071f79b76ca8a4ba103064ec0d5c5752af346175731ef4f636d9d ./SQLITE-STORAGE.md
|
||||
116a87c5e7c4fcfc333bbdf84979d5bab619b8a0936cd9f8838df04c9981bff7 ./VALIDATION-AUTONOMOUS-RESEARCH.md
|
||||
e05449bab6250e585a6c0ac0735008cd533baf07dbf7149ddae609ae252d4425 ./VALIDATION-CONSTELLATION-ECO-TRANSITIONS.md
|
||||
e7ab2cddec372db906c7883a6e0861b71999043cfb9b94e527c3b2aee4532035 ./VALIDATION-ITERATIVE-GROUNDED-RESEARCH.md
|
||||
e08b0eaab827fe03d5a72327e8fdfe9ce4025097e28208714246969e7d059561 ./VALIDATION-SEARXNG-DIAGNOSTICS.md
|
||||
f46938b5de7e139e1b21868b1bedce6e312d69cd61602b4f8f43512a327dd422 ./VALIDATION-SOURCE-ONLY-FILTERS.md
|
||||
4cd120496664388717fe422a8c54380708df723fea26b35f81799665dfaf2c1c ./VALIDATION-SQLITE.md
|
||||
f6699e4cdbaadc720e4b8a22c557d02319325283775a2b8a87ff78ca202e3386 ./VISUALIZATION-PERFORMANCE.md
|
||||
8970f2abf17bddcd0d84387e8a4975588df2776f03a7a54c5a283470769ca0c8 ./cmd/brain/main.go
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 ./data/.gitkeep
|
||||
21b51d0e1b7ed07c20f7f3a5da76dedab8df44a94a51724b67b0c3411599fe15 ./deployment/README.md
|
||||
ec106bc91cb70f7f41d3ff0369373ef2a9cf3c4da9cf5af5a13af028c828cf37 ./deployment/docker-compose.full.yml
|
||||
d990e01adebf65f74e40508c99add46e09c9e51f1aa251ed430814a38c2c635e ./docker-compose.yml
|
||||
1542b064707bef0c4488f558f77c02db5f4cfc8d4da228f615ebdc3ee9ace8fd ./go.mod
|
||||
864c3376212497b070feca13d26cfc28e96876078ce7a0b5b0c0470e2dd4fbf8 ./go.sum
|
||||
ed7fa0e09e94aa9e89c93b00303d4ce6f0f20dac626ecb91181c3aabc76bc8ff ./integrations/agent/README.md
|
||||
9e15349702f876b1fa74e6caa1e97367d8a11b266fa5a1e40152b10c859b23c2 ./integrations/agent/glpi-ai-agent-neural-brain.patch
|
||||
3c0fc6913501976100521526e1ee8e7988d33fbce3f7b4bab26387d42b0966f5 ./integrations/knowledgebase/README.md
|
||||
12f8424f863b13f19aab9c2e6c2828c0ad824a55a62fe336e062db534102bc3a ./integrations/knowledgebase/glpi-ai-knowledgebase-neural-brain.patch
|
||||
50d05fa2a183f5f3eaab0545cb48d3abb64be62eb5d84dc2c6d99c7125f7344b ./internal/activity/broker.go
|
||||
3cda19c60313fd29c8d5335a79589ce5a454a5621d5d5cfcbf07381ef3db61a9 ./internal/config/config.go
|
||||
7d05b3e067d453e3dfaffe5cb3335badec1fe54a6db4ef62024abd9f51936c43 ./internal/config/config_test.go
|
||||
2d948168b80e8d0d59f2890396bb9f3000555d9d0ded89ec424b86fe495856d1 ./internal/engine/article.go
|
||||
d5d18a26010fe5f308c05b79e3a3d7501c8490bb33098357cde4373affd7e60d ./internal/engine/article_format_test.go
|
||||
a7945db4987e50c1d09504335a560fc50ef5277a22bda78e8527768b6bbafe78 ./internal/engine/article_research.go
|
||||
501597b1b9340726e13c200099fdc10dc2a661410c76dce16f250b4dc6233871 ./internal/engine/article_research_cache.go
|
||||
3ca7037231935329d49b6b80553fbfef206c079a6aed4c2379dadd46d39ebc0d ./internal/engine/article_research_cache_test.go
|
||||
289d7dca4f0294072521daa229ff2dcae7f159af4631d6567853f54b920506ce ./internal/engine/article_research_test.go
|
||||
ee0197234e4b6b01c06dfe33c1f22cd73212a8b08aeb81fec98665ff21b5349c ./internal/engine/autonomous_research.go
|
||||
62a69f1af6fc3842e34f3847a5f868f81202feaaa6429e4e84db8115f5d14ad8 ./internal/engine/autonomous_research_test.go
|
||||
c4cb62e184adf7561f64f002312ff7ee417d48b0c4f9bed2b6df26a163d50c1b ./internal/engine/engine.go
|
||||
24e022c1e572b42752fed780ff57f2c857d0600460b7806f7664f3c8c7dbb811 ./internal/engine/engine_test.go
|
||||
6209e4e52d8e822d0f72ca5ec04612b0a9ba8a9fe55d2a855d703f2e26e534a5 ./internal/engine/research_diagnostics.go
|
||||
0eb3f00e2ab73d2dc6a4cbc1a4038533a4bb20fbbbb6190abd39feff6b34b8e1 ./internal/engine/research_diagnostics_test.go
|
||||
716d42138db9eb64480c1bbaf4cb3b70bce1edf72c17c3d85a8d84f866fd8700 ./internal/engine/research_events.go
|
||||
5654eadda4cfbc6f160cac9d680ad6b2f4d8c12b65b89bf99df037c3348221b2 ./internal/engine/runtime.go
|
||||
87416a32118e86c42e14a959f3f807463a694d4cb9218640e0664a39bab43991 ./internal/engine/runtime_filter_test.go
|
||||
b82980a646a92751bdd27a866ba1ffc6d34a3ba81d537f7b6e5a78e1432ee6fa ./internal/glpi/client.go
|
||||
525102be56bc51ce8a08655b1b2bb53b67f4a1828903585fd664ed6a5133f617 ./internal/glpi/client_test.go
|
||||
5e1b064bda200cf6f14278093f7a48132b175c73973706322623b049b8083737 ./internal/graph/filter.go
|
||||
c14883e3db65a61672d0171c98a9c2d5ccff3fa6ac74e7236c16819d27ddf503 ./internal/graph/filter_test.go
|
||||
682ba4f391580bde1956d8cd01b8752e67d83bbd671f94f8391eaee60f829852 ./internal/graph/research_tasks.go
|
||||
39fe73bff6287572c66b719e400623426222ec609a535f7fce5240b6da42f7ce ./internal/graph/research_tasks_test.go
|
||||
097de854f393d45d836abc77b7bb9cdc3853c601a7c368465ac5e732016bfa6e ./internal/graph/sqlite_backend.go
|
||||
744d306b9c7151543e91421575775ff6eb1b7e0af187ba7d92d8a298c51704de ./internal/graph/sqlite_backend_test.go
|
||||
3c14438985528cc829e69d7040e5b7d7301991508f66ab1aca67050974d3eddd ./internal/graph/store.go
|
||||
941e6dda2f2b21cb4836e671d46da0a469eddccc8ed78865663b18b280f6dde5 ./internal/graph/store_test.go
|
||||
4476351d388d11c6becd78b4c918fc8d47dfd70500d8b3e2ddf81f7ff61a2cea ./internal/ingest/agent.go
|
||||
05bdcd8f82756af028807a9ba37e64232e2d93b6a803468d0a29665bbdb3067f ./internal/ingest/glpikb.go
|
||||
ff44d56d56b9e301fbcf0f028d1bae6f0b851665f4fee65222097f1a2450f24a ./internal/ingest/glpikb_test.go
|
||||
257a4beba480dab7d9b79c1f32496f6b4f648c4c05170f51a77220dbfd21fe96 ./internal/ingest/knowledge.go
|
||||
a13910fb417484d56e78ae856b71fb66c63987d9abf3b513190bc52697321a18 ./internal/ingest/knowledge_test.go
|
||||
6185da62c3f526bacf2b4d6bd5617a10d72af2318852679030251ccb0f1a3882 ./internal/model/model.go
|
||||
5f6969313f1ca42a498d48787bbb56c173f7c272b54014e03af9bd3ac385c3fe ./internal/ollama/client.go
|
||||
28854e001cfecdd06d94ee5166cfd1af3b2a0281fb5a1a7fb52280f3edf71edc ./internal/ollama/client_test.go
|
||||
0bc8bb4c698c2c5dbef5c980d3e3fb88f10d35a8d7a230cbc81d218798bc1fe6 ./internal/persist/coordinator.go
|
||||
46144aa719ff5c3ab787c214d8e32e4b3c6883bed068410e5403a26f6352cd6a ./internal/persist/coordinator_test.go
|
||||
e7138877303bcc07c429323c4792fed112d8c16d35fd44222fe144ab0b69344f ./internal/research/fetch.go
|
||||
6a9f269783a7c41d5b63f9bd5022f415ed1b5572041ca5ccae1512d6dc2a0b80 ./internal/research/fetch_test.go
|
||||
a46b953c22fb2aef112b11ba65c61bda1e1582903e39489c2d081d17519ebd9d ./internal/research/searxng.go
|
||||
2f82e80ad91590f419b1bbb19647b5191596e97c4d765e8736c1867ad899d457 ./internal/research/searxng_test.go
|
||||
5271e30b793e2fdc4fa23b92ab8b569776ce35ddda8551fffa407eac44b16535 ./internal/web/server.go
|
||||
12c30cf7d223e413abc69579eda36971a2d1f69a7e5973005ee31783fba9bf4b ./internal/web/server_test.go
|
||||
10940493686434eab93e7d12eb686955b367d233fb5d1898b1659e0d7b9f9668 ./internal/web/static/app.css
|
||||
af82ad4661b453f3caa9a3c705d7b2f6fd86ebc929cd784c10e5f7e0f9d46bce ./internal/web/static/app.js
|
||||
5ad03db990f0a78342c49cadc7c402f1018a97a9644df73cf8292b043534ed5b ./internal/web/static/index.html
|
||||
83aded814b6225395935e61fe957963c3c470f368fc9089f505b6de23e959115 ./preview.png
|
||||
e888a548fa32246f01bf7d7545359fa856d6bc32651cafdc776d595d9eada34e .env.example
|
||||
8ddf797373e5cb397a6a8ed35b868393846752339ec7010d0148509794500d3c ANALYSIS-DASHBOARD.md
|
||||
8955cfbeff229e73f0cad664863ff21711c270a4233c3225680c89aa6268a901 ARCHITECTURE.md
|
||||
cf3e5275f1eb623da3b6f734d233197e8b47879b8e7cdfd4d373a7ecdd921651 AUTONOMOUS-RESEARCH.md
|
||||
7aed2194baab0fb66c561446e5f1cd7724c1166bfa3a08c9e59157d1878bf994 CHANGELOG-ANALYSIS-DASHBOARD.md
|
||||
0ed6ff0d82b3b6776970200a021937611d4f3273f6727d296aec16cfac6153b8 CHANGELOG-AUTONOMOUS-RESEARCH.md
|
||||
2bc149241c2d25f755e4a0470dc517f646527ee3e98d3a6c2e7798639bd70866 CHANGELOG-CONSTELLATION-ECO-TRANSITIONS.md
|
||||
933cdaeae7895e31d2281d591a75f23f07e7c41d356638654bac4f5e7a20a069 CHANGELOG-FILTER-PANEL-SCROLL.md
|
||||
213ac897cd415bbeb9764a847843b9eff366983cf25bb3cbce4efa4c04b9e5f5 CHANGELOG-GLPI-POOL-PERSISTENCE.md
|
||||
e2f1f0400999cb59b8be09bc2743e06e0a0b2ecdc44a583af7c9a08b70d8509e CHANGELOG-GPU-NODE-LIMIT.md
|
||||
a317376127be1e47b9ec9f0781fcfebdcbf7fccfaeb7840d9241f9586048fadb CHANGELOG-GROUNDED-KNOWLEDGE-SYNTHESIS.md
|
||||
02a8d3e541967e2d2ef9dd5451f470679e262ad851aca9f03563b322914c3181 CHANGELOG-ITERATIVE-GROUNDED-RESEARCH.md
|
||||
e167a9d64f63c3933ad40c5078684bc019db303043c34ea3965f3b089f3d32c7 CHANGELOG-KNOWLEDGE-SYNTHESIS.md
|
||||
87f89a81e1124b18e092a4ea946cb037295cb9e884a46b392286272dc8134dd4 CHANGELOG-RUNTIME-HONEYCOMB.md
|
||||
2d04e6d385f4b902080a0bcaab510a846a8ae6a76cb757423c50425c8433e7f9 CHANGELOG-SEARXNG-DIAGNOSTICS.md
|
||||
9c760e167a9af2d3d8ca32c5aa4ef6bb4a3153047343a71c4b19ca9aef96ca32 CHANGELOG-SEARXNG-VISUALIZATION.md
|
||||
be9f133ae933bdc0e0a8aa5d176dd3e39488a191337043533379e23d179f3ad2 CHANGELOG-SOURCE-ONLY-FILTERS.md
|
||||
5433a7c2e67ab35fb320bc872e9024fa5f3e765736184e9f878340b8b45407aa CHANGELOG-SQLITE-STARTUP-FIX.md
|
||||
5b9deeab0cd59b3c649fd73f129361a1e773ed3955cded0048b8cb280bb32e88 CHANGELOG-SQLITE-STORAGE.md
|
||||
42c1cacdc1f792773622017c3528dc626674456f478c41aaf44ff0a32e5f87bd Dockerfile
|
||||
5534536965bf0479455f97324c242160202650ca1256f1ba0420b4ad67125e49 GLPI-KB.md
|
||||
c546524a3add0b1beb8d1ca675fd602e2ea7c7bddfc04759519c21a02667eb61 ITERATIVE-GROUNDED-RESEARCH.md
|
||||
e3ae87108607ca494668a9974c467a5c529b9599bf88d3d2a79ddc15b64e4a38 KNOWLEDGE-SYNTHESIS.md
|
||||
696d2da2338cd8190b9614707e4059d78ce291e7334f273633aad815c3b6a6df Makefile
|
||||
381d7d6ac9e3c2e63c9ecdaa42ed4c73058f5d78e57c7532bb75a9663c919530 OLLAMA-POOL.md
|
||||
2c0062941ef3edbd40d46b823934a7d0a3a9da7581b83d0b9360e8aaa7694b1b PERSISTENCE.md
|
||||
5e1965cd2d5f428651ff2fac9874afa38ab6281585d4db330acac1f69c321608 README.md
|
||||
2838cd19ac2bfa35bebbef2541f631b99221b5997bbb6dbc27146a66a3a1ad34 RUNTIME-CONTROLS-HONEYCOMB.md
|
||||
c3da43b33e550901d55789f2ee526c2e50f61ee028a3f59e0a40e77e1057fde7 SEARXNG-VISUALIZATION.md
|
||||
c69419c0327425186cfb84f25746feff226ce813cf467b3f252e729517455047 SOURCE-ONLY-FILTERS.md
|
||||
ae7bc1f1959071f79b76ca8a4ba103064ec0d5c5752af346175731ef4f636d9d SQLITE-STORAGE.md
|
||||
b0123b8425993dea7dd3864f930527b6a1a0e965ff29c0e4899620a64cd9451d VALIDATION-ANALYSIS-DASHBOARD.md
|
||||
116a87c5e7c4fcfc333bbdf84979d5bab619b8a0936cd9f8838df04c9981bff7 VALIDATION-AUTONOMOUS-RESEARCH.md
|
||||
e05449bab6250e585a6c0ac0735008cd533baf07dbf7149ddae609ae252d4425 VALIDATION-CONSTELLATION-ECO-TRANSITIONS.md
|
||||
e7ab2cddec372db906c7883a6e0861b71999043cfb9b94e527c3b2aee4532035 VALIDATION-ITERATIVE-GROUNDED-RESEARCH.md
|
||||
e08b0eaab827fe03d5a72327e8fdfe9ce4025097e28208714246969e7d059561 VALIDATION-SEARXNG-DIAGNOSTICS.md
|
||||
f46938b5de7e139e1b21868b1bedce6e312d69cd61602b4f8f43512a327dd422 VALIDATION-SOURCE-ONLY-FILTERS.md
|
||||
4cd120496664388717fe422a8c54380708df723fea26b35f81799665dfaf2c1c VALIDATION-SQLITE.md
|
||||
f6699e4cdbaadc720e4b8a22c557d02319325283775a2b8a87ff78ca202e3386 VISUALIZATION-PERFORMANCE.md
|
||||
8970f2abf17bddcd0d84387e8a4975588df2776f03a7a54c5a283470769ca0c8 cmd/brain/main.go
|
||||
21b51d0e1b7ed07c20f7f3a5da76dedab8df44a94a51724b67b0c3411599fe15 deployment/README.md
|
||||
ec106bc91cb70f7f41d3ff0369373ef2a9cf3c4da9cf5af5a13af028c828cf37 deployment/docker-compose.full.yml
|
||||
d990e01adebf65f74e40508c99add46e09c9e51f1aa251ed430814a38c2c635e docker-compose.yml
|
||||
1542b064707bef0c4488f558f77c02db5f4cfc8d4da228f615ebdc3ee9ace8fd go.mod
|
||||
864c3376212497b070feca13d26cfc28e96876078ce7a0b5b0c0470e2dd4fbf8 go.sum
|
||||
ed7fa0e09e94aa9e89c93b00303d4ce6f0f20dac626ecb91181c3aabc76bc8ff integrations/agent/README.md
|
||||
9e15349702f876b1fa74e6caa1e97367d8a11b266fa5a1e40152b10c859b23c2 integrations/agent/glpi-ai-agent-neural-brain.patch
|
||||
3c0fc6913501976100521526e1ee8e7988d33fbce3f7b4bab26387d42b0966f5 integrations/knowledgebase/README.md
|
||||
12f8424f863b13f19aab9c2e6c2828c0ad824a55a62fe336e062db534102bc3a integrations/knowledgebase/glpi-ai-knowledgebase-neural-brain.patch
|
||||
93d8993e09473559a191c4e01252d6d1fdb214646d65e1271cde018b47d939ed internal/activity/broker.go
|
||||
3cda19c60313fd29c8d5335a79589ce5a454a5621d5d5cfcbf07381ef3db61a9 internal/config/config.go
|
||||
7d05b3e067d453e3dfaffe5cb3335badec1fe54a6db4ef62024abd9f51936c43 internal/config/config_test.go
|
||||
2d948168b80e8d0d59f2890396bb9f3000555d9d0ded89ec424b86fe495856d1 internal/engine/article.go
|
||||
d5d18a26010fe5f308c05b79e3a3d7501c8490bb33098357cde4373affd7e60d internal/engine/article_format_test.go
|
||||
a7945db4987e50c1d09504335a560fc50ef5277a22bda78e8527768b6bbafe78 internal/engine/article_research.go
|
||||
501597b1b9340726e13c200099fdc10dc2a661410c76dce16f250b4dc6233871 internal/engine/article_research_cache.go
|
||||
3ca7037231935329d49b6b80553fbfef206c079a6aed4c2379dadd46d39ebc0d internal/engine/article_research_cache_test.go
|
||||
289d7dca4f0294072521daa229ff2dcae7f159af4631d6567853f54b920506ce internal/engine/article_research_test.go
|
||||
ee0197234e4b6b01c06dfe33c1f22cd73212a8b08aeb81fec98665ff21b5349c internal/engine/autonomous_research.go
|
||||
62a69f1af6fc3842e34f3847a5f868f81202feaaa6429e4e84db8115f5d14ad8 internal/engine/autonomous_research_test.go
|
||||
1beab82f85be8ba29a429df1cf0fd9ce6d754031796f99a6c8e1638e20193fb1 internal/engine/engine.go
|
||||
24e022c1e572b42752fed780ff57f2c857d0600460b7806f7664f3c8c7dbb811 internal/engine/engine_test.go
|
||||
6209e4e52d8e822d0f72ca5ec04612b0a9ba8a9fe55d2a855d703f2e26e534a5 internal/engine/research_diagnostics.go
|
||||
0eb3f00e2ab73d2dc6a4cbc1a4038533a4bb20fbbbb6190abd39feff6b34b8e1 internal/engine/research_diagnostics_test.go
|
||||
716d42138db9eb64480c1bbaf4cb3b70bce1edf72c17c3d85a8d84f866fd8700 internal/engine/research_events.go
|
||||
5654eadda4cfbc6f160cac9d680ad6b2f4d8c12b65b89bf99df037c3348221b2 internal/engine/runtime.go
|
||||
87416a32118e86c42e14a959f3f807463a694d4cb9218640e0664a39bab43991 internal/engine/runtime_filter_test.go
|
||||
b82980a646a92751bdd27a866ba1ffc6d34a3ba81d537f7b6e5a78e1432ee6fa internal/glpi/client.go
|
||||
525102be56bc51ce8a08655b1b2bb53b67f4a1828903585fd664ed6a5133f617 internal/glpi/client_test.go
|
||||
2c7c2b531a908b77189640b55cfdc7308726c5a7344d946ec3ea5b8d5439e38d internal/graph/analysis_dashboard.go
|
||||
577a7704e5018f5a0487f6095509e81b062847a381ed5b00035a36362373aed8 internal/graph/analysis_dashboard_test.go
|
||||
5e1b064bda200cf6f14278093f7a48132b175c73973706322623b049b8083737 internal/graph/filter.go
|
||||
c14883e3db65a61672d0171c98a9c2d5ccff3fa6ac74e7236c16819d27ddf503 internal/graph/filter_test.go
|
||||
682ba4f391580bde1956d8cd01b8752e67d83bbd671f94f8391eaee60f829852 internal/graph/research_tasks.go
|
||||
39fe73bff6287572c66b719e400623426222ec609a535f7fce5240b6da42f7ce internal/graph/research_tasks_test.go
|
||||
195c734d1b0608ae3eee14cfa8004299f1e88084d660feb9ddec5c8e6fbfc7f6 internal/graph/sqlite_backend.go
|
||||
744d306b9c7151543e91421575775ff6eb1b7e0af187ba7d92d8a298c51704de internal/graph/sqlite_backend_test.go
|
||||
40d3f3fdc40ba3237fb452c087f8071468a97028ce6bd880452af660742c867c internal/graph/store.go
|
||||
941e6dda2f2b21cb4836e671d46da0a469eddccc8ed78865663b18b280f6dde5 internal/graph/store_test.go
|
||||
4476351d388d11c6becd78b4c918fc8d47dfd70500d8b3e2ddf81f7ff61a2cea internal/ingest/agent.go
|
||||
05bdcd8f82756af028807a9ba37e64232e2d93b6a803468d0a29665bbdb3067f internal/ingest/glpikb.go
|
||||
ff44d56d56b9e301fbcf0f028d1bae6f0b851665f4fee65222097f1a2450f24a internal/ingest/glpikb_test.go
|
||||
257a4beba480dab7d9b79c1f32496f6b4f648c4c05170f51a77220dbfd21fe96 internal/ingest/knowledge.go
|
||||
a13910fb417484d56e78ae856b71fb66c63987d9abf3b513190bc52697321a18 internal/ingest/knowledge_test.go
|
||||
6185da62c3f526bacf2b4d6bd5617a10d72af2318852679030251ccb0f1a3882 internal/model/model.go
|
||||
5f6969313f1ca42a498d48787bbb56c173f7c272b54014e03af9bd3ac385c3fe internal/ollama/client.go
|
||||
28854e001cfecdd06d94ee5166cfd1af3b2a0281fb5a1a7fb52280f3edf71edc internal/ollama/client_test.go
|
||||
0bc8bb4c698c2c5dbef5c980d3e3fb88f10d35a8d7a230cbc81d218798bc1fe6 internal/persist/coordinator.go
|
||||
46144aa719ff5c3ab787c214d8e32e4b3c6883bed068410e5403a26f6352cd6a internal/persist/coordinator_test.go
|
||||
e7138877303bcc07c429323c4792fed112d8c16d35fd44222fe144ab0b69344f internal/research/fetch.go
|
||||
6a9f269783a7c41d5b63f9bd5022f415ed1b5572041ca5ccae1512d6dc2a0b80 internal/research/fetch_test.go
|
||||
a46b953c22fb2aef112b11ba65c61bda1e1582903e39489c2d081d17519ebd9d internal/research/searxng.go
|
||||
2f82e80ad91590f419b1bbb19647b5191596e97c4d765e8736c1867ad899d457 internal/research/searxng_test.go
|
||||
984966a437944530008f4888944a91130604aad8891da3f67a89656bc991d9e4 internal/web/server.go
|
||||
b5abd1c3591242a7e8835eb38866410558d0e7901c75f5b11b94039ee3747716 internal/web/server_test.go
|
||||
3e27efdbeaf8aba34864f6d1dd47d101d04c87993d02e35ee08df34affaefe29 internal/web/static/analysis.css
|
||||
db27a3c62848dbb0f383886c1075d2c0c779363cea3e847793104ca708c1d6f0 internal/web/static/analysis.html
|
||||
5b6e8d6952c2ffd66b79d86bf32db139f73e37b0d7d02d154804ef111cce19b6 internal/web/static/analysis.js
|
||||
6a92c00a768cbef68b2565f6e021351833373bacea7c537c8fba61746e917edc internal/web/static/app.css
|
||||
af82ad4661b453f3caa9a3c705d7b2f6fd86ebc929cd784c10e5f7e0f9d46bce internal/web/static/app.js
|
||||
a9327a08da143c45cfb8708a8bff5c4bf780644dddc891d0d2aceffb0bb183b1 internal/web/static/index.html
|
||||
83aded814b6225395935e61fe957963c3c470f368fc9089f505b6de23e959115 preview.png
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Validierung: Analysis Dashboard
|
||||
|
||||
Durchgeführt:
|
||||
|
||||
- Go-Typkompilierung aller Pakete mit lokalem Compile-Stub für `modernc.org/sqlite`
|
||||
- `go vet ./...`
|
||||
- JavaScript-Syntaxprüfung für `app.js` und `analysis.js`
|
||||
- HTML-/DOM-Test der Analyseoberfläche in Headless Chromium
|
||||
- Rendering von Laufbewertung, Kennzahlen, Timeline, Laufprotokoll, Änderungsjournal und Rohereignissen
|
||||
- Browserprüfung ohne JavaScript- oder Konsolenfehler
|
||||
- SQLite-Schemaerzeugung mit Python `sqlite3`
|
||||
- Insert/Select für `analysis_events`, `analysis_points` und `analysis_changes`
|
||||
- Foreign-Key-Cascade vom Event auf Checkpoint und Detailänderungen
|
||||
- Platzhalter- und Spaltenprüfung des Analyse-Inserts
|
||||
- Unit-Test der Laufgruppierung und positiven Ergebnisbewertung
|
||||
- statische Prüfung der neuen Webrouten und eingebetteten Assets
|
||||
- Compose-YAML-Prüfung
|
||||
- ZIP- und Patch-Integritätsprüfung
|
||||
|
||||
Nicht in dieser isolierten Umgebung ausführbar:
|
||||
|
||||
- echter Go-Runtime-Test mit `modernc.org/sqlite`, weil die Go-Modulauflösung nach `proxy.golang.org` blockiert ist
|
||||
- produktiver Langzeittest mit realer Knowledgebase, Ollama-Pool und SearXNG
|
||||
|
||||
Das ausgelieferte Projekt enthält unverändert den echten Treiber `modernc.org/sqlite v1.37.1`; der Compile-Stub ist nicht Bestandteil des Pakets.
|
||||
@@ -16,6 +16,8 @@ type Broker struct {
|
||||
subs map[int]chan model.Activity
|
||||
recent []model.Activity
|
||||
maxRecent int
|
||||
sinkMu sync.RWMutex
|
||||
sink func(model.Activity)
|
||||
}
|
||||
|
||||
func New(maxRecent int) *Broker {
|
||||
@@ -24,6 +26,16 @@ func New(maxRecent int) *Broker {
|
||||
}
|
||||
return &Broker{subs: map[int]chan model.Activity{}, maxRecent: maxRecent}
|
||||
}
|
||||
|
||||
// SetSink attaches a non-UI activity recorder. The sink is called after the
|
||||
// in-memory broker lock is released, so recording cannot block SSE delivery or
|
||||
// deadlock the broker.
|
||||
func (b *Broker) SetSink(sink func(model.Activity)) {
|
||||
b.sinkMu.Lock()
|
||||
b.sink = sink
|
||||
b.sinkMu.Unlock()
|
||||
}
|
||||
|
||||
func (b *Broker) Publish(a model.Activity) {
|
||||
if a.Timestamp.IsZero() {
|
||||
a.Timestamp = time.Now().UTC()
|
||||
@@ -43,6 +55,12 @@ func (b *Broker) Publish(a model.Activity) {
|
||||
}
|
||||
}
|
||||
b.mu.Unlock()
|
||||
b.sinkMu.RLock()
|
||||
sink := b.sink
|
||||
b.sinkMu.RUnlock()
|
||||
if sink != nil {
|
||||
sink(a)
|
||||
}
|
||||
}
|
||||
func (b *Broker) Recent() []model.Activity {
|
||||
b.mu.RLock()
|
||||
|
||||
@@ -92,6 +92,9 @@ type Engine struct {
|
||||
}
|
||||
|
||||
func New(cfg config.Config, g *graph.Store, b *activity.Broker) *Engine {
|
||||
if b != nil && g != nil {
|
||||
b.SetSink(g.RecordActivity)
|
||||
}
|
||||
if strings.TrimSpace(cfg.GLPIKBSource) == "" {
|
||||
cfg.GLPIKBSource = "GLPI Knowledge Base"
|
||||
}
|
||||
@@ -227,6 +230,9 @@ func New(cfg config.Config, g *graph.Store, b *activity.Broker) *Engine {
|
||||
client := glpi.New(cfg.GLPIURL, cfg.GLPIAPIVersion, cfg.GLPIClientID, cfg.GLPIClientSecret, cfg.GLPIUsername, cfg.GLPIPassword, cfg.GLPITimeout)
|
||||
e.GLPIKB = ingest.NewGLPIKBSyncer(ingest.GLPIKBConfig{Enabled: true, Path: cfg.GLPIKBPath, Filter: cfg.GLPIKBFilter, Limit: cfg.GLPIKBLimit, SyncInterval: cfg.GLPIKBSyncInterval, Source: cfg.GLPIKBSource, CachePath: filepath.Join(cfg.DataDir, "glpi-kb-cache.json"), ShouldSync: e.LearningEnabled}, client, g, b, persistence)
|
||||
}
|
||||
if b != nil {
|
||||
b.Publish(model.Activity{Type: "system.started", Source: "brain", Phase: "startup", Message: "Neural Brain wurde gestartet; das Analyseprotokoll zeichnet Läufe und Graphänderungen auf", Strength: .3, Metadata: map[string]any{"chat_model": cfg.ChatModel, "embedding_model": cfg.EmbeddingModel, "graph_version": g.Version()}})
|
||||
}
|
||||
return e
|
||||
}
|
||||
func (e *Engine) Start(ctx context.Context) {
|
||||
@@ -471,9 +477,15 @@ func (e *Engine) Scan(ctx context.Context) error {
|
||||
}
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
started := time.Now().UTC()
|
||||
runID := fmt.Sprintf("learning-scan-%d", started.UnixNano())
|
||||
beforeVersion := e.Graph.Version()
|
||||
beforeMutations := e.Graph.MutationStats()
|
||||
beforeNodes, beforeEdges, _ := e.Graph.Counts()
|
||||
e.Broker.Publish(model.Activity{Type: "learning.scan.started", Source: "brain", Phase: "ingest", Message: "KB-Lernlauf gestartet: Quellen werden verglichen, Änderungen übernommen und Embeddings geprüft", Strength: .52, Metadata: map[string]any{"run_id": runID, "nodes_before": beforeNodes, "edges_before": beforeEdges, "graph_version_before": beforeVersion}})
|
||||
count, err := e.Scanner.Scan()
|
||||
if err != nil {
|
||||
e.Broker.Publish(model.Activity{Type: "learning.scan.failed", Source: "brain", Phase: "ingest", Message: "KB-Lernlauf ist beim Einlesen der Wissensquellen fehlgeschlagen", Strength: .35, Metadata: map[string]any{"run_id": runID, "error": err.Error(), "duration_ms": time.Since(started).Milliseconds()}})
|
||||
return err
|
||||
}
|
||||
pendingEmbeddings := len(e.Graph.NodesForEmbeddingScoped(e.effectiveLearningFilter()))
|
||||
@@ -505,10 +517,16 @@ func (e *Engine) Scan(ctx context.Context) error {
|
||||
e.stateMu.Lock()
|
||||
e.lastScan = time.Now().UTC()
|
||||
e.stateMu.Unlock()
|
||||
nodes, edges, version := e.Graph.Counts()
|
||||
if e.Graph.Version() != beforeVersion {
|
||||
nodes, edges, _ := e.Graph.Counts()
|
||||
e.Broker.Publish(model.Activity{Type: "graph.updated", Source: "brain", Phase: "indexed", Message: fmt.Sprintf("%d Wissenselemente · %d Nodes · %d Edges", count, nodes, edges), Strength: .55, Metadata: map[string]any{"nodes": nodes, "edges": edges, "knowledge_elements": count}})
|
||||
e.Broker.Publish(model.Activity{Type: "graph.updated", Source: "brain", Phase: "indexed", Message: fmt.Sprintf("%d Wissenselemente · %d Nodes · %d Edges", count, nodes, edges), Strength: .55, Metadata: map[string]any{"run_id": runID, "nodes": nodes, "edges": edges, "knowledge_elements": count}})
|
||||
}
|
||||
delta := e.Graph.MutationStats().Delta(beforeMutations)
|
||||
result := "unchanged"
|
||||
if !delta.Empty() {
|
||||
result = "updated"
|
||||
}
|
||||
e.Broker.Publish(model.Activity{Type: "learning.scan.completed", Source: "brain", Phase: "indexed", Message: fmt.Sprintf("KB-Lernlauf abgeschlossen · %d Wissenselemente · %d neue Nodes · %d neue Edges · %d neue/neu berechnete Embeddings", count, delta.NodesCreated, delta.EdgesCreated, delta.VectorsCreated+delta.VectorsUpdated), Strength: .64, Metadata: map[string]any{"run_id": runID, "result": result, "duration_ms": time.Since(started).Milliseconds(), "knowledge_elements": count, "nodes_before": beforeNodes, "nodes_after": nodes, "edges_before": beforeEdges, "edges_after": edges, "graph_version_before": beforeVersion, "graph_version_after": version, "nodes_created": delta.NodesCreated, "nodes_updated": delta.NodesUpdated, "nodes_deleted": delta.NodesDeleted, "edges_created": delta.EdgesCreated, "edges_updated": delta.EdgesUpdated, "edges_deleted": delta.EdgesDeleted, "vectors_created": delta.VectorsCreated, "vectors_updated": delta.VectorsUpdated, "vectors_deleted": delta.VectorsDeleted, "pending_embeddings": len(e.Graph.NodesForEmbeddingScoped(e.effectiveLearningFilter())), "ollama_ok": e.isOllamaOK()}})
|
||||
return nil
|
||||
}
|
||||
func (e *Engine) ensureEmbeddings(ctx context.Context) error {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/local/glpi-neural-brain/internal/activity"
|
||||
"github.com/local/glpi-neural-brain/internal/model"
|
||||
)
|
||||
|
||||
func TestMutationStatsAndAnalysisHistory(t *testing.T) {
|
||||
store, err := Open(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = store.Close() })
|
||||
broker := activity.New(20)
|
||||
broker.SetSink(store.RecordActivity)
|
||||
|
||||
store.UpsertNode(model.Node{ID: "a", Kind: "knowledge", Label: "A", Origin: "test", Metadata: map[string]any{"source": "internal"}})
|
||||
store.UpsertNode(model.Node{ID: "b", Kind: "knowledge", Label: "B", Origin: "test", Metadata: map[string]any{"source": "internal"}})
|
||||
store.SetVector("a", []float64{1, 0})
|
||||
store.SetVector("b", []float64{.9, .1})
|
||||
broker.Publish(model.Activity{Type: "learning.scan.completed", Source: "brain", Message: "done", Metadata: map[string]any{"run_id": "scan-1", "result": "updated"}})
|
||||
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for {
|
||||
history, err := store.AnalysisHistory(context.Background(), time.Now().Add(-time.Hour), 50)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(history.Events) > 0 {
|
||||
if history.Totals.NodesCreated != 2 || history.Totals.VectorsCreated != 2 {
|
||||
t.Fatalf("unexpected mutation totals: %+v", history.Totals)
|
||||
}
|
||||
if history.ChangeCount != 4 || len(history.Changes) != 4 {
|
||||
t.Fatalf("expected four detailed changes, count=%d changes=%+v", history.ChangeCount, history.Changes)
|
||||
}
|
||||
if history.Events[0].ChangeCount != 4 {
|
||||
t.Fatalf("expected event to own four changes: %+v", history.Events[0])
|
||||
}
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("analysis writer did not persist event")
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildAnalysisRunsExplainsPositiveThinkingResult(t *testing.T) {
|
||||
start := time.Now().UTC().Add(-2 * time.Second)
|
||||
events := []AnalysisEventRecord{
|
||||
{Activity: model.Activity{ID: "s", Type: "think.cycle.started", Timestamp: start, Metadata: map[string]any{"trigger": "manual"}}},
|
||||
{Activity: model.Activity{ID: "r", Type: "think.relation.created", Timestamp: start.Add(time.Second), Message: "Relation erstellt"}, Point: AnalysisPoint{Delta: MutationStats{EdgesCreated: 1}}},
|
||||
{Activity: model.Activity{ID: "e", Type: "think.cycle.completed", Timestamp: start.Add(1500 * time.Millisecond), Metadata: map[string]any{"duration_ms": 1500, "relations_created": 1}}},
|
||||
}
|
||||
runs := buildAnalysisRuns(events)
|
||||
if len(runs) != 1 {
|
||||
t.Fatalf("expected one run, got %d: %+v", len(runs), runs)
|
||||
}
|
||||
if runs[0].Status != "success" || runs[0].Mutations.EdgesCreated != 1 || runs[0].Verdict != "positives Ergebnis" {
|
||||
t.Fatalf("unexpected run: %+v", runs[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEdgeUpdateChangeExplainsSimilarityRecalculation(t *testing.T) {
|
||||
previous := model.Edge{ID: "e", Type: "related", Origin: "ai-inference", Status: "active", Confidence: .72, Metadata: map[string]any{"semantic_similarity": .81}}
|
||||
current := model.Edge{ID: "e", Type: "related", Origin: "ai-inference", Status: "active", Confidence: .88, Metadata: map[string]any{"semantic_similarity": .93}}
|
||||
change := edgeUpdateChange(previous, current)
|
||||
if change.Action != "updated" || change.Details["previous_semantic_similarity"] != .81 || change.Details["semantic_similarity"] != .93 {
|
||||
t.Fatalf("similarity delta missing: %+v", change)
|
||||
}
|
||||
if change.Details["previous_confidence"] != .72 || change.Details["confidence"] != .88 {
|
||||
t.Fatalf("confidence delta missing: %+v", change)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const schemaVersion = 2
|
||||
const schemaVersion = 3
|
||||
|
||||
const nodeUpsertSQL = `INSERT INTO nodes(id,kind,label,summary,status,origin,external_id,uri,categories_json,keywords_json,metadata_json,weight,x,y,z,updated_at_ns)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
@@ -128,6 +128,7 @@ func Open(dir string) (*Store, error) {
|
||||
db.Close()
|
||||
return nil, fmt.Errorf("load sqlite graph state: %w", err)
|
||||
}
|
||||
s.initAnalysisWriter()
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -280,6 +281,69 @@ func (s *Store) initSchema(ctx context.Context) error {
|
||||
created_at_ns INTEGER NOT NULL
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_research_task_attempts_task ON research_task_attempts(task_id, attempt, created_at_ns)`,
|
||||
`CREATE TABLE IF NOT EXISTS analysis_events (
|
||||
id TEXT PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
source TEXT NOT NULL DEFAULT '',
|
||||
phase TEXT NOT NULL DEFAULT '',
|
||||
query TEXT NOT NULL DEFAULT '',
|
||||
message TEXT NOT NULL DEFAULT '',
|
||||
node_ids_json TEXT NOT NULL DEFAULT '[]',
|
||||
edge_ids_json TEXT NOT NULL DEFAULT '[]',
|
||||
strength REAL NOT NULL DEFAULT 0,
|
||||
metadata_json TEXT NOT NULL DEFAULT '{}',
|
||||
timestamp_ns INTEGER NOT NULL,
|
||||
process_id TEXT NOT NULL DEFAULT ''
|
||||
) WITHOUT ROWID`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_analysis_events_timestamp ON analysis_events(timestamp_ns DESC)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_analysis_events_type_timestamp ON analysis_events(type, timestamp_ns DESC)`,
|
||||
`CREATE TABLE IF NOT EXISTS analysis_points (
|
||||
event_id TEXT PRIMARY KEY REFERENCES analysis_events(id) ON DELETE CASCADE,
|
||||
timestamp_ns INTEGER NOT NULL,
|
||||
process_id TEXT NOT NULL DEFAULT '',
|
||||
graph_version INTEGER NOT NULL DEFAULT 0,
|
||||
node_count INTEGER NOT NULL DEFAULT 0,
|
||||
edge_count INTEGER NOT NULL DEFAULT 0,
|
||||
vector_count INTEGER NOT NULL DEFAULT 0,
|
||||
node_created INTEGER NOT NULL DEFAULT 0,
|
||||
node_updated INTEGER NOT NULL DEFAULT 0,
|
||||
node_deleted INTEGER NOT NULL DEFAULT 0,
|
||||
edge_created INTEGER NOT NULL DEFAULT 0,
|
||||
edge_updated INTEGER NOT NULL DEFAULT 0,
|
||||
edge_deleted INTEGER NOT NULL DEFAULT 0,
|
||||
vector_created INTEGER NOT NULL DEFAULT 0,
|
||||
vector_updated INTEGER NOT NULL DEFAULT 0,
|
||||
vector_deleted INTEGER NOT NULL DEFAULT 0,
|
||||
delta_node_created INTEGER NOT NULL DEFAULT 0,
|
||||
delta_node_updated INTEGER NOT NULL DEFAULT 0,
|
||||
delta_node_deleted INTEGER NOT NULL DEFAULT 0,
|
||||
delta_edge_created INTEGER NOT NULL DEFAULT 0,
|
||||
delta_edge_updated INTEGER NOT NULL DEFAULT 0,
|
||||
delta_edge_deleted INTEGER NOT NULL DEFAULT 0,
|
||||
delta_vector_created INTEGER NOT NULL DEFAULT 0,
|
||||
delta_vector_updated INTEGER NOT NULL DEFAULT 0,
|
||||
delta_vector_deleted INTEGER NOT NULL DEFAULT 0,
|
||||
change_count INTEGER NOT NULL DEFAULT 0,
|
||||
changes_truncated INTEGER NOT NULL DEFAULT 0
|
||||
) WITHOUT ROWID`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_analysis_points_timestamp ON analysis_points(timestamp_ns DESC)`,
|
||||
`CREATE TABLE IF NOT EXISTS analysis_changes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
event_id TEXT NOT NULL REFERENCES analysis_events(id) ON DELETE CASCADE,
|
||||
timestamp_ns INTEGER NOT NULL,
|
||||
process_id TEXT NOT NULL DEFAULT '',
|
||||
graph_version INTEGER NOT NULL DEFAULT 0,
|
||||
entity_kind TEXT NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
entity_id TEXT NOT NULL,
|
||||
label TEXT NOT NULL DEFAULT '',
|
||||
relation_type TEXT NOT NULL DEFAULT '',
|
||||
origin TEXT NOT NULL DEFAULT '',
|
||||
details_json TEXT NOT NULL DEFAULT '{}'
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_analysis_changes_timestamp ON analysis_changes(timestamp_ns DESC)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_analysis_changes_event ON analysis_changes(event_id, id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_analysis_changes_entity ON analysis_changes(entity_kind, action, timestamp_ns DESC)`,
|
||||
}
|
||||
for _, statement := range statements {
|
||||
if _, err := s.db.ExecContext(ctx, statement); err != nil {
|
||||
@@ -299,7 +363,7 @@ func (s *Store) initSchema(ctx context.Context) error {
|
||||
return fmt.Errorf("unsupported graph database schema %d (expected at most %d)", current, schemaVersion)
|
||||
}
|
||||
if current < schemaVersion {
|
||||
if current != 1 {
|
||||
if current < 1 || current > 2 {
|
||||
return fmt.Errorf("unsupported graph database schema migration %d -> %d", current, schemaVersion)
|
||||
}
|
||||
if _, err := s.db.ExecContext(ctx, `UPDATE graph_meta SET value=? WHERE key='schema_version'`, schemaVersion); err != nil {
|
||||
@@ -692,6 +756,7 @@ func (s *Store) Close() error {
|
||||
if s.db == nil {
|
||||
return nil
|
||||
}
|
||||
s.closeAnalysisWriter()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
_, persistErr := s.PersistVersionContext(ctx)
|
||||
|
||||
+100
-3
@@ -25,6 +25,7 @@ type Store struct {
|
||||
embeddingModel string
|
||||
embeddingDigest string
|
||||
embeddingMetaGeneration uint64
|
||||
mutations MutationStats
|
||||
|
||||
db *sql.DB
|
||||
dbPath string
|
||||
@@ -36,6 +37,21 @@ type Store struct {
|
||||
deletedNodes map[string]uint64
|
||||
deletedEdges map[string]uint64
|
||||
deletedVectors map[string]uint64
|
||||
|
||||
analysisMu sync.Mutex
|
||||
analysisQueue chan analysisRecord
|
||||
analysisWG sync.WaitGroup
|
||||
analysisLastMutations MutationStats
|
||||
analysisPendingChanges []GraphChange
|
||||
analysisPendingTruncated int
|
||||
analysisChangesDropped uint64
|
||||
analysisDropped uint64
|
||||
analysisLastError string
|
||||
analysisLastPersisted time.Time
|
||||
processID string
|
||||
analysisDetailMu sync.Mutex
|
||||
analysisDetailVersion uint64
|
||||
analysisDetailCache DetailedGraphAnalysis
|
||||
}
|
||||
|
||||
func ID(parts ...string) string {
|
||||
@@ -106,8 +122,14 @@ func (s *Store) ConfigureEmbeddingIdentity(modelName, digest string) int {
|
||||
s.version++
|
||||
removed := 0
|
||||
if modelChanged || digestChanged {
|
||||
for id := range s.vectors {
|
||||
for id, vector := range s.vectors {
|
||||
label := ""
|
||||
if node, ok := s.nodes[id]; ok {
|
||||
label = node.Label
|
||||
}
|
||||
delete(s.vectors, id)
|
||||
s.countVectorDeletedLocked()
|
||||
s.recordChangeLocked(vectorChange(id, "deleted", len(vector), label))
|
||||
s.deletedVectors[id] = s.version
|
||||
delete(s.dirtyVectors, id)
|
||||
removed++
|
||||
@@ -126,6 +148,7 @@ func (s *Store) ConfigureEmbeddingIdentity(modelName, digest string) int {
|
||||
func (s *Store) UpsertNode(n model.Node) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
old, existed := s.nodes[n.ID]
|
||||
if n.UpdatedAt.IsZero() {
|
||||
n.UpdatedAt = time.Now().UTC()
|
||||
}
|
||||
@@ -136,7 +159,17 @@ func (s *Store) UpsertNode(n model.Node) {
|
||||
n.X, n.Y, n.Z = position(n.ID, n.Categories)
|
||||
}
|
||||
s.nodes[n.ID] = n
|
||||
if existed {
|
||||
s.countNodeUpdatedLocked()
|
||||
} else {
|
||||
s.countNodeCreatedLocked()
|
||||
}
|
||||
s.version++
|
||||
if existed {
|
||||
s.recordChangeLocked(nodeUpdateChange(old, n))
|
||||
} else {
|
||||
s.recordChangeLocked(nodeChange(n, "created"))
|
||||
}
|
||||
s.markNodeDirtyLocked(n.ID)
|
||||
}
|
||||
func (s *Store) UpsertEdge(e model.Edge) {
|
||||
@@ -146,8 +179,9 @@ func (s *Store) UpsertEdge(e model.Edge) {
|
||||
if e.ID == "" {
|
||||
e.ID = EdgeID(e.Source, e.Target, e.Type, e.Origin)
|
||||
}
|
||||
old, existed := s.edges[e.ID]
|
||||
if e.CreatedAt.IsZero() {
|
||||
if old, ok := s.edges[e.ID]; ok {
|
||||
if existed {
|
||||
e.CreatedAt = old.CreatedAt
|
||||
} else {
|
||||
e.CreatedAt = now
|
||||
@@ -158,7 +192,17 @@ func (s *Store) UpsertEdge(e model.Edge) {
|
||||
e.Weight = 1
|
||||
}
|
||||
s.edges[e.ID] = e
|
||||
if existed {
|
||||
s.countEdgeUpdatedLocked()
|
||||
} else {
|
||||
s.countEdgeCreatedLocked()
|
||||
}
|
||||
s.version++
|
||||
if existed {
|
||||
s.recordChangeLocked(edgeUpdateChange(old, e))
|
||||
} else {
|
||||
s.recordChangeLocked(edgeChange(e, "created"))
|
||||
}
|
||||
s.markEdgeDirtyLocked(e.ID)
|
||||
}
|
||||
func (s *Store) HasEdgeBetween(a, b string) bool {
|
||||
@@ -199,7 +243,21 @@ func (s *Store) SetVector(id string, v []float64) {
|
||||
return
|
||||
}
|
||||
s.vectors[id] = converted
|
||||
if ok {
|
||||
s.countVectorUpdatedLocked()
|
||||
} else {
|
||||
s.countVectorCreatedLocked()
|
||||
}
|
||||
s.version++
|
||||
label := ""
|
||||
if node, exists := s.nodes[id]; exists {
|
||||
label = node.Label
|
||||
}
|
||||
if ok {
|
||||
s.recordChangeLocked(vectorRecalculatedChange(id, len(old), len(converted), label))
|
||||
} else {
|
||||
s.recordChangeLocked(vectorChange(id, "created", len(converted), label))
|
||||
}
|
||||
s.markVectorDirtyLocked(id)
|
||||
}
|
||||
func (s *Store) Vector(id string) ([]float64, bool) {
|
||||
@@ -222,9 +280,15 @@ func (s *Store) ClearVectorsByDimension(dim int) int {
|
||||
removed := 0
|
||||
for id, v := range s.vectors {
|
||||
if len(v) == dim {
|
||||
label := ""
|
||||
if node, ok := s.nodes[id]; ok {
|
||||
label = node.Label
|
||||
}
|
||||
delete(s.vectors, id)
|
||||
s.countVectorDeletedLocked()
|
||||
removed++
|
||||
s.version++
|
||||
s.recordChangeLocked(vectorChange(id, "deleted", len(v), label))
|
||||
s.markVectorDeletedLocked(id)
|
||||
}
|
||||
}
|
||||
@@ -307,12 +371,17 @@ func (s *Store) ReplaceOrigins(origins []string, nodes []model.Node, edges []mod
|
||||
continue
|
||||
}
|
||||
delete(s.nodes, id)
|
||||
s.countNodeDeletedLocked()
|
||||
if _, hadVector := s.vectors[id]; hadVector {
|
||||
vector := s.vectors[id]
|
||||
delete(s.vectors, id)
|
||||
s.countVectorDeletedLocked()
|
||||
s.version++
|
||||
s.recordChangeLocked(vectorChange(id, "deleted", len(vector), old.Label))
|
||||
s.markVectorDeletedLocked(id)
|
||||
}
|
||||
s.version++
|
||||
s.recordChangeLocked(nodeChange(old, "deleted"))
|
||||
s.markNodeDeletedLocked(id)
|
||||
}
|
||||
for id, old := range s.edges {
|
||||
@@ -323,7 +392,9 @@ func (s *Store) ReplaceOrigins(origins []string, nodes []model.Node, edges []mod
|
||||
continue
|
||||
}
|
||||
delete(s.edges, id)
|
||||
s.countEdgeDeletedLocked()
|
||||
s.version++
|
||||
s.recordChangeLocked(edgeChange(old, "deleted"))
|
||||
s.markEdgeDeletedLocked(id)
|
||||
}
|
||||
|
||||
@@ -356,16 +427,28 @@ func (s *Store) ReplaceOrigins(origins []string, nodes []model.Node, edges []mod
|
||||
}
|
||||
|
||||
s.nodes[id] = incoming
|
||||
if existed {
|
||||
s.countNodeUpdatedLocked()
|
||||
} else {
|
||||
s.countNodeCreatedLocked()
|
||||
}
|
||||
s.version++
|
||||
if existed {
|
||||
s.recordChangeLocked(nodeUpdateChange(old, incoming))
|
||||
} else {
|
||||
s.recordChangeLocked(nodeChange(incoming, "created"))
|
||||
}
|
||||
s.markNodeDirtyLocked(id)
|
||||
|
||||
// Embeddings depend on text/categories/keywords, not on display
|
||||
// coordinates or unrelated metadata. Only invalidate a vector when its
|
||||
// actual embedding input changed.
|
||||
if existed && oldEmbeddingFingerprint != embeddingFingerprint(incoming) {
|
||||
if _, hadVector := s.vectors[id]; hadVector {
|
||||
if vector, hadVector := s.vectors[id]; hadVector {
|
||||
delete(s.vectors, id)
|
||||
s.countVectorDeletedLocked()
|
||||
s.version++
|
||||
s.recordChangeLocked(vectorChange(id, "deleted", len(vector), incoming.Label))
|
||||
s.markVectorDeletedLocked(id)
|
||||
}
|
||||
}
|
||||
@@ -387,7 +470,17 @@ func (s *Store) ReplaceOrigins(origins []string, nodes []model.Node, edges []mod
|
||||
}
|
||||
incoming.UpdatedAt = now
|
||||
s.edges[id] = incoming
|
||||
if existed {
|
||||
s.countEdgeUpdatedLocked()
|
||||
} else {
|
||||
s.countEdgeCreatedLocked()
|
||||
}
|
||||
s.version++
|
||||
if existed {
|
||||
s.recordChangeLocked(edgeUpdateChange(old, incoming))
|
||||
} else {
|
||||
s.recordChangeLocked(edgeChange(incoming, "created"))
|
||||
}
|
||||
s.markEdgeDirtyLocked(id)
|
||||
}
|
||||
|
||||
@@ -396,13 +489,17 @@ func (s *Store) ReplaceOrigins(origins []string, nodes []model.Node, edges []mod
|
||||
for id, edge := range s.edges {
|
||||
if _, ok := s.nodes[edge.Source]; !ok {
|
||||
delete(s.edges, id)
|
||||
s.countEdgeDeletedLocked()
|
||||
s.version++
|
||||
s.recordChangeLocked(edgeChange(edge, "deleted"))
|
||||
s.markEdgeDeletedLocked(id)
|
||||
continue
|
||||
}
|
||||
if _, ok := s.nodes[edge.Target]; !ok {
|
||||
delete(s.edges, id)
|
||||
s.countEdgeDeletedLocked()
|
||||
s.version++
|
||||
s.recordChangeLocked(edgeChange(edge, "deleted"))
|
||||
s.markEdgeDeletedLocked(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,8 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /api/status", s.handleStatus)
|
||||
mux.HandleFunc("GET /api/graph", s.handleGraph)
|
||||
mux.HandleFunc("GET /api/analysis", s.handleAnalysis)
|
||||
mux.HandleFunc("GET /api/analysis/dashboard", s.handleAnalysisDashboard)
|
||||
mux.HandleFunc("GET /api/analysis/export", s.handleAnalysisExport)
|
||||
mux.HandleFunc("GET /api/runtime-settings", s.handleGetRuntimeSettings)
|
||||
mux.HandleFunc("PUT /api/runtime-settings", s.handleSetRuntimeSettings)
|
||||
mux.HandleFunc("GET /api/sources", s.handleSources)
|
||||
@@ -54,6 +56,9 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("POST /api/flush", s.handleFlush)
|
||||
mux.HandleFunc("GET /api/state/export", s.handleStateExport)
|
||||
sub, _ := fs.Sub(assets, "static")
|
||||
mux.HandleFunc("GET /analysis", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/analysis.html", http.StatusTemporaryRedirect)
|
||||
})
|
||||
mux.Handle("GET /", http.FileServer(http.FS(sub)))
|
||||
return s.headers(mux)
|
||||
}
|
||||
@@ -76,6 +81,61 @@ func (s *Server) handleAnalysis(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 200, s.Graph.Analyze())
|
||||
}
|
||||
|
||||
func (s *Server) analysisDashboardPayload(r *http.Request) (map[string]any, error) {
|
||||
hours := 24
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("hours")); raw != "" {
|
||||
if value, err := strconv.Atoi(raw); err == nil && value >= 1 && value <= 24*90 {
|
||||
hours = value
|
||||
}
|
||||
}
|
||||
limit := 250
|
||||
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
|
||||
if value, err := strconv.Atoi(raw); err == nil && value >= 20 && value <= 1000 {
|
||||
limit = value
|
||||
}
|
||||
}
|
||||
ctx, cancel := contextTimeout(r, 60*time.Second)
|
||||
defer cancel()
|
||||
history, err := s.Graph.AnalysisHistory(ctx, time.Now().UTC().Add(-time.Duration(hours)*time.Hour), limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"generated_at": history.GeneratedAt,
|
||||
"range_hours": hours,
|
||||
"graph": s.Graph.DetailedAnalysis(),
|
||||
"history": history,
|
||||
"system": s.Engine.Status(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) handleAnalysisDashboard(w http.ResponseWriter, r *http.Request) {
|
||||
payload, err := s.analysisDashboardPayload(r)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, payload)
|
||||
}
|
||||
|
||||
func (s *Server) handleAnalysisExport(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.authorized(r) {
|
||||
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
||||
return
|
||||
}
|
||||
payload, err := s.analysisDashboardPayload(r)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Content-Disposition", `attachment; filename="brain-analysis.json"`)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
encoder := json.NewEncoder(w)
|
||||
encoder.SetIndent("", " ")
|
||||
_ = encoder.Encode(payload)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetRuntimeSettings(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, s.Engine.RuntimeSettingsView())
|
||||
}
|
||||
|
||||
@@ -163,3 +163,44 @@ func TestSettingsPanelContentIsScrollable(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalysisDashboardPageAndAPI(t *testing.T) {
|
||||
data := t.TempDir()
|
||||
g, err := graph.Open(data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = g.Close() })
|
||||
broker := activity.New(20)
|
||||
eng := engine.New(config.Config{DataDir: data, RuntimeDefaultsConfigured: true, LearningEnabled: true, ThinkingEnabled: true, PersistInterval: time.Minute}, g, broker)
|
||||
g.UpsertNode(model.Node{ID: "n1", Kind: "knowledge", Label: "Analyse", Origin: "test", Metadata: map[string]any{"source": "internal"}})
|
||||
broker.Publish(model.Activity{Type: "graph.updated", Source: "brain", Message: "Graph aktualisiert", Metadata: map[string]any{"nodes": 1, "edges": 0}})
|
||||
h := (&Server{Engine: eng, Graph: g, Broker: broker}).Handler()
|
||||
|
||||
page := httptest.NewRecorder()
|
||||
h.ServeHTTP(page, httptest.NewRequest(http.MethodGet, "/analysis.html", nil))
|
||||
if page.Code != http.StatusOK || !strings.Contains(page.Body.String(), "BRAIN ANALYSIS CENTER") || !strings.Contains(page.Body.String(), "ÄNDERUNGSJOURNAL") {
|
||||
t.Fatalf("analysis page missing: %d %s", page.Code, page.Body.String())
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
for {
|
||||
res := httptest.NewRecorder()
|
||||
h.ServeHTTP(res, httptest.NewRequest(http.MethodGet, "/api/analysis/dashboard?hours=24&limit=50", nil))
|
||||
if res.Code != http.StatusOK {
|
||||
t.Fatalf("analysis API returned %d: %s", res.Code, res.Body.String())
|
||||
}
|
||||
var payload map[string]any
|
||||
if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
history, _ := payload["history"].(map[string]any)
|
||||
if count, _ := history["raw_event_count"].(float64); count > 0 {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatal("analysis event was not visible through API")
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
:root{
|
||||
color-scheme:dark;
|
||||
--bg:#071019;
|
||||
--panel:#0d1824;
|
||||
--panel-2:#101f2d;
|
||||
--border:#24384a;
|
||||
--text:#e8f3fb;
|
||||
--muted:#8ca4b7;
|
||||
--cyan:#54dff5;
|
||||
--blue:#5e8cff;
|
||||
--green:#62d39a;
|
||||
--yellow:#f2c86d;
|
||||
--red:#ff7d84;
|
||||
--violet:#b28cff;
|
||||
--shadow:0 18px 50px rgba(0,0,0,.28);
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
html{background:var(--bg);scroll-behavior:smooth}
|
||||
body{margin:0;min-height:100vh;background:radial-gradient(circle at 15% -10%,rgba(55,164,208,.16),transparent 34%),radial-gradient(circle at 90% 5%,rgba(112,78,196,.13),transparent 30%),var(--bg);color:var(--text);font:14px/1.55 Inter,Segoe UI,Arial,sans-serif}
|
||||
button,input,select{font:inherit}
|
||||
a{color:inherit;text-decoration:none}
|
||||
code{font-family:Consolas,Monaco,monospace;color:#c6eaff}
|
||||
.analysis-topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;justify-content:space-between;gap:24px;padding:16px 24px;border-bottom:1px solid rgba(103,155,185,.25);background:rgba(6,15,24,.92);backdrop-filter:blur(18px);box-shadow:0 8px 30px rgba(0,0,0,.22)}
|
||||
.analysis-brand{display:flex;align-items:center;gap:14px;min-width:260px}.analysis-brand strong{display:block;letter-spacing:.12em;font-size:15px}.analysis-brand small{display:block;color:var(--muted);margin-top:2px}.analysis-mark{display:grid;grid-template-columns:repeat(3,6px);gap:3px;align-items:center;height:28px}.analysis-mark i{display:block;width:6px;border-radius:4px;background:linear-gradient(180deg,var(--cyan),var(--blue));box-shadow:0 0 12px rgba(84,223,245,.5)}.analysis-mark i:nth-child(1){height:15px}.analysis-mark i:nth-child(2){height:27px}.analysis-mark i:nth-child(3){height:20px}
|
||||
.analysis-actions{display:flex;align-items:center;justify-content:flex-end;gap:10px;flex-wrap:wrap}.action,.analysis-actions select{border:1px solid var(--border);background:#102131;color:var(--text);border-radius:9px;padding:8px 12px;cursor:pointer}.action:hover,.analysis-actions select:hover{border-color:#4e7188;background:#142b3e}.action.secondary{background:transparent}.range-control,.auto-control{display:flex;align-items:center;gap:8px;color:var(--muted);font-size:12px}.auto-control{padding:8px 10px;border:1px solid var(--border);border-radius:9px;background:#0e1c29}.auto-control input{accent-color:var(--cyan)}
|
||||
.connection-banner{position:sticky;top:76px;z-index:19;text-align:center;padding:8px 16px;font-size:12px;border-bottom:1px solid transparent}.connection-banner.loading{background:#172338;color:var(--yellow)}.connection-banner.connected{background:rgba(34,107,76,.72);color:#c6ffe2}.connection-banner.error{background:rgba(133,43,51,.8);color:#ffd8da}.connection-banner.live{background:rgba(35,79,113,.75);color:#d6f4ff}
|
||||
.analysis-main{max-width:1680px;margin:0 auto;padding:26px 24px 70px}.section{background:linear-gradient(180deg,rgba(16,31,45,.96),rgba(11,24,35,.96));border:1px solid var(--border);border-radius:15px;padding:20px;box-shadow:var(--shadow);margin-bottom:18px}.intro-section{display:flex;align-items:flex-end;justify-content:space-between;gap:30px;background:linear-gradient(135deg,rgba(15,38,53,.98),rgba(17,26,47,.98))}.intro-section h1{margin:3px 0 8px;font-size:30px;line-height:1.15}.intro-section p{margin:0;color:#b7c7d4;max-width:980px}.eyebrow{display:block;color:var(--cyan);font-size:11px;letter-spacing:.16em;font-weight:700}.legend-box{display:grid;grid-template-columns:repeat(2,max-content);gap:8px 16px;padding:12px 15px;border:1px solid var(--border);border-radius:11px;background:rgba(3,12,20,.36);white-space:nowrap}.legend-box span{display:flex;align-items:center;gap:7px;color:#b9cbd8;font-size:12px}.status-dot{width:8px;height:8px;border-radius:50%;display:inline-block}.status-dot.success{background:var(--green);box-shadow:0 0 9px var(--green)}.status-dot.warning{background:var(--yellow)}.status-dot.error{background:var(--red)}.status-dot.neutral{background:#7890a3}
|
||||
.section-heading{display:flex;align-items:center;justify-content:space-between;gap:18px;margin-bottom:14px}.section-heading h2{margin:2px 0 0;font-size:20px}.section-note{color:var(--muted);font-size:12px}.status-pill{display:inline-flex;align-items:center;border-radius:999px;padding:5px 10px;font-size:11px;font-weight:700;letter-spacing:.04em;border:1px solid}.status-pill.success{color:#bbf5d4;border-color:#34845c;background:rgba(37,120,78,.18)}.status-pill.warning{color:#ffe6a6;border-color:#8d7136;background:rgba(135,101,35,.18)}.status-pill.error{color:#ffd0d3;border-color:#97464d;background:rgba(143,49,59,.2)}.status-pill.neutral,.status-pill.running{color:#c6d7e3;border-color:#4b6374;background:rgba(65,91,109,.18)}
|
||||
.last-run-section{border-color:#31566d}.last-run-content{display:grid;grid-template-columns:minmax(260px,1.4fr) repeat(3,minmax(145px,.6fr));gap:14px}.last-run-main{padding:16px;border-radius:11px;background:#0a1722;border:1px solid #294052}.last-run-main h3{margin:0 0 4px;font-size:19px}.last-run-main p{color:#afc1ce;margin:7px 0 0}.last-run-main small{color:var(--muted)}.last-run-stat{padding:15px;border:1px solid #263b4b;border-radius:11px;background:rgba(7,17,26,.55)}.last-run-stat span{display:block;color:var(--muted);font-size:11px;text-transform:uppercase;letter-spacing:.08em}.last-run-stat strong{display:block;margin-top:7px;font-size:22px}.last-run-stat small{color:#9cb0bf}.empty-state{color:var(--muted);padding:18px;text-align:center}
|
||||
.summary-grid{display:grid;grid-template-columns:repeat(4,minmax(190px,1fr));gap:14px;margin-bottom:18px}.summary-card{min-height:126px;padding:17px;border-radius:14px;border:1px solid var(--border);background:linear-gradient(160deg,#122638,#0b1722);box-shadow:var(--shadow)}.summary-card>span{display:block;color:var(--muted);font-size:12px}.summary-card strong{display:block;font-size:24px;margin:12px 0 4px;letter-spacing:-.02em}.summary-card small{display:block;color:#92a8b8;line-height:1.35}
|
||||
.explain-box{padding:10px 13px;margin:0 0 14px;border-left:3px solid #4d9ec0;background:rgba(30,77,99,.16);color:#abc0ce;font-size:12px;border-radius:0 7px 7px 0}.chart-wrap{height:310px;border:1px solid #223848;border-radius:11px;background:#07131d;padding:8px;overflow:hidden}.chart-wrap canvas{width:100%;height:100%}.chart-legend{display:flex;gap:18px;flex-wrap:wrap;margin-top:10px;color:var(--muted);font-size:11px}.chart-legend span{display:flex;align-items:center;gap:6px}.legend-swatch{display:block;width:18px;height:5px;border-radius:3px}.legend-swatch.create{background:var(--green)}.legend-swatch.update{background:var(--blue)}.legend-swatch.delete{background:var(--red)}.legend-swatch.events{background:var(--yellow)}
|
||||
.filters-row{display:flex;align-items:flex-end;gap:12px;flex-wrap:wrap;margin-bottom:14px}.filters-row label{display:flex;flex-direction:column;gap:5px;color:var(--muted);font-size:11px}.filters-row label:has(input[type=checkbox]){flex-direction:row;align-items:center;padding:9px 0}.filters-row .grow{flex:1;min-width:240px}.filters-row select,.filters-row input[type=search]{width:100%;min-height:36px;border:1px solid var(--border);border-radius:8px;background:#091722;color:var(--text);padding:7px 10px}.filters-row input[type=checkbox]{accent-color:var(--cyan)}
|
||||
.table-wrap{overflow:auto;border:1px solid #233949;border-radius:10px}.data-table{width:100%;border-collapse:collapse;min-width:940px}.data-table th{position:sticky;top:0;background:#132536;text-align:left;color:#9db2c1;font-size:10px;text-transform:uppercase;letter-spacing:.08em;padding:10px 12px;border-bottom:1px solid #2c4455}.data-table td{padding:11px 12px;border-bottom:1px solid rgba(57,81,96,.38);vertical-align:top}.data-table tr:last-child td{border-bottom:0}.data-table tbody tr.run-row{cursor:pointer}.data-table tbody tr.run-row:hover{background:rgba(67,133,164,.09)}.run-title{font-weight:650}.run-meta{display:block;color:var(--muted);font-size:11px;margin-top:2px}.delta-tags{display:flex;gap:5px;flex-wrap:wrap}.delta-tag{font-size:10px;padding:2px 6px;border-radius:999px;background:#172a38;color:#c5d5df}.delta-tag.create{color:#bff1d5;background:rgba(52,132,92,.2)}.delta-tag.update{color:#cbd8ff;background:rgba(75,102,179,.23)}.delta-tag.delete{color:#ffd1d4;background:rgba(151,70,77,.22)}.run-detail-row td{padding:0;background:#07131d}.run-detail{padding:15px 18px;display:grid;grid-template-columns:minmax(300px,1fr) minmax(300px,1fr);gap:16px}.run-detail pre,.event-details pre{max-height:320px;overflow:auto;background:#061019;border:1px solid #243a49;border-radius:8px;padding:11px;color:#b9d0df;font:11px/1.5 Consolas,monospace;white-space:pre-wrap;word-break:break-word}.event-mini-list{display:flex;flex-direction:column;gap:7px}.event-mini{padding:8px;border:1px solid #223744;border-radius:7px;background:#0d1b27}.event-mini b{font-size:11px}.event-mini small{display:block;color:var(--muted)}
|
||||
.two-column{display:grid;grid-template-columns:1fr 1fr;gap:18px}.two-column>.section{min-width:0}.distribution-grid{display:grid;grid-template-columns:1fr;gap:17px}.section h3{font-size:13px;margin:17px 0 8px;color:#bdd0dd}.bar-list{display:flex;flex-direction:column;gap:7px}.bar-row{display:grid;grid-template-columns:minmax(110px,1fr) minmax(110px,2fr) 58px;gap:9px;align-items:center}.bar-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#b8cad6;font-size:11px}.bar-track{height:8px;border-radius:999px;background:#0a141d;overflow:hidden}.bar-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--blue),var(--cyan))}.bar-value{text-align:right;color:#d6e6ef;font-variant-numeric:tabular-nums;font-size:11px}.metric-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}.metric-grid.compact>div{padding:12px;border:1px solid #263b49;border-radius:9px;background:#0a1721}.metric-grid span{display:block;color:var(--muted);font-size:10px}.metric-grid strong{display:block;font-size:18px;margin-top:5px}.compact-list,.entity-list{display:flex;flex-direction:column;gap:8px}.compact-item,.entity-item{padding:10px 12px;border:1px solid #243a49;border-radius:9px;background:#0a1721}.compact-item header,.entity-item header{display:flex;justify-content:space-between;gap:12px}.compact-item b,.entity-item b{font-size:12px}.compact-item small,.entity-item small{color:var(--muted)}.compact-item p,.entity-item p{margin:6px 0 0;color:#a9bdca;font-size:11px}.entity-item code{font-size:10px}.entity-tags{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}.entity-tags span{font-size:9px;border:1px solid #314b5c;border-radius:999px;padding:2px 6px;color:#a9c1d0}
|
||||
.events-list{display:flex;flex-direction:column;gap:8px}.event-card{border:1px solid #243a49;border-radius:10px;background:#0a1721;overflow:hidden}.event-summary{display:grid;grid-template-columns:105px 190px minmax(260px,1fr) 260px;gap:10px;align-items:center;padding:10px 12px;cursor:pointer}.event-summary:hover{background:rgba(56,116,144,.09)}.event-time{color:var(--muted);font-size:11px}.event-type{font-family:Consolas,monospace;font-size:10px;color:#b9dded}.event-message{font-size:12px}.event-delta{text-align:right}.event-details{display:none;padding:12px;border-top:1px solid #233745;background:#06111a}.event-card.open .event-details{display:grid;grid-template-columns:1fr 1fr;gap:12px}.query-box{padding:8px 10px;border:1px solid #294253;border-radius:7px;background:#0d1c28;color:#c4d9e5;font-size:11px;margin-top:7px}
|
||||
.analysis-footer{display:flex;justify-content:space-between;gap:20px;padding:18px 24px;border-top:1px solid var(--border);color:var(--muted);font-size:11px;background:#071019}
|
||||
@media(max-width:1200px){.summary-grid{grid-template-columns:repeat(2,1fr)}.two-column{grid-template-columns:1fr}.last-run-content{grid-template-columns:1fr repeat(3,1fr)}.event-summary{grid-template-columns:90px 170px 1fr}.event-delta{grid-column:2/4;text-align:left}}
|
||||
@media(max-width:820px){.analysis-topbar{position:relative;flex-direction:column;align-items:flex-start}.connection-banner{top:0}.analysis-actions{justify-content:flex-start}.analysis-main{padding:18px 12px 50px}.intro-section{flex-direction:column;align-items:flex-start}.legend-box{grid-template-columns:1fr}.summary-grid{grid-template-columns:1fr}.last-run-content{grid-template-columns:1fr}.metric-grid{grid-template-columns:repeat(2,1fr)}.run-detail{grid-template-columns:1fr}.event-summary{display:block}.event-summary>*{margin-bottom:5px}.event-delta{text-align:left}.event-card.open .event-details{grid-template-columns:1fr}.analysis-footer{flex-direction:column}.intro-section h1{font-size:24px}}
|
||||
.notice{margin:0 0 14px;padding:10px 12px;border-radius:8px;font-size:12px;line-height:1.45}.warning-notice{border:1px solid #7b6737;background:rgba(137,104,38,.18);color:#f4dda1}.changes-table{min-width:1180px}.changes-table code{display:block;max-width:390px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#c6e6f4;font:10px/1.45 Consolas,monospace}.change-action{display:inline-flex;border:1px solid;border-radius:999px;padding:3px 7px;font-size:10px;font-weight:700;white-space:nowrap}.change-action.created{color:#bff1d5;border-color:#34845c;background:rgba(52,132,92,.2)}.change-action.updated,.change-action.recalculated{color:#cbd8ff;border-color:#4d67a7;background:rgba(75,102,179,.23)}.change-action.deleted{color:#ffd1d4;border-color:#97464d;background:rgba(151,70,77,.22)}.event-link{max-width:180px;border:0;background:none;color:var(--cyan);font:10px Consolas,monospace;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;padding:2px}.event-link:hover{text-decoration:underline}.change-row:hover{background:rgba(67,133,164,.07)}
|
||||
@@ -0,0 +1,202 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Neural Brain · Analyse</title>
|
||||
<link rel="stylesheet" href="/analysis.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="analysis-topbar">
|
||||
<div class="analysis-brand">
|
||||
<span class="analysis-mark"><i></i><i></i><i></i></span>
|
||||
<div>
|
||||
<strong>BRAIN ANALYSIS CENTER</strong>
|
||||
<small>Nachvollziehbare Läufe, Graphänderungen, Embeddings, Recherche und Persistenz</small>
|
||||
</div>
|
||||
</div>
|
||||
<nav class="analysis-actions" aria-label="Dashboard-Steuerung">
|
||||
<a class="action secondary" href="/">← Gehirnansicht</a>
|
||||
<label class="range-control">Zeitraum
|
||||
<select id="rangeHours">
|
||||
<option value="1">1 Stunde</option>
|
||||
<option value="6">6 Stunden</option>
|
||||
<option value="24" selected>24 Stunden</option>
|
||||
<option value="72">3 Tage</option>
|
||||
<option value="168">7 Tage</option>
|
||||
<option value="720">30 Tage</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="auto-control"><input id="autoRefresh" type="checkbox" checked> Live</label>
|
||||
<button id="refreshNow" class="action">Aktualisieren</button>
|
||||
<a id="exportAnalysis" class="action secondary" href="/api/analysis/export?hours=24">JSON-Export</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div id="connectionBanner" class="connection-banner loading">Analysedaten werden geladen …</div>
|
||||
|
||||
<main class="analysis-main">
|
||||
<section class="section intro-section">
|
||||
<div>
|
||||
<span class="eyebrow">ERKLÄRBARE HIRNAKTIVITÄT</span>
|
||||
<h1>Was hat das System tatsächlich verändert?</h1>
|
||||
<p>Dieses Dashboard trennt die visuelle Gehirnansicht von der technischen Auswertung. Es zeigt nicht nur Endstände, sondern auch, ob ein Lauf neue Nodes, Edges oder Embeddings erzeugt, bestehende Daten verändert, etwas entfernt oder lediglich geprüft und verworfen hat.</p>
|
||||
</div>
|
||||
<div class="legend-box">
|
||||
<span><i class="status-dot success"></i>positiv übernommen</span>
|
||||
<span><i class="status-dot warning"></i>geprüft, aber nicht übernommen</span>
|
||||
<span><i class="status-dot error"></i>fehlgeschlagen</span>
|
||||
<span><i class="status-dot neutral"></i>ohne Graphänderung</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="lastRunSection" class="section last-run-section" aria-live="polite">
|
||||
<div class="section-heading">
|
||||
<div><span class="eyebrow">LETZTER ABGESCHLOSSENER LAUF</span><h2>Ergebnisbewertung</h2></div>
|
||||
<span id="lastRunStatus" class="status-pill neutral">noch keine Daten</span>
|
||||
</div>
|
||||
<div id="lastRunContent" class="last-run-content empty-state">Es wurde noch kein auswertbarer Lauf aufgezeichnet.</div>
|
||||
</section>
|
||||
|
||||
<section class="summary-grid" aria-label="Systemzusammenfassung">
|
||||
<article class="summary-card"><span>Graph aktuell</span><strong id="summaryGraph">–</strong><small id="summaryGraphSub">Nodes · Edges</small></article>
|
||||
<article class="summary-card"><span>Vektorabdeckung</span><strong id="summaryCoverage">–</strong><small id="summaryCoverageSub">Embeddings / Wissens-Nodes</small></article>
|
||||
<article class="summary-card"><span>Änderungen im Zeitraum</span><strong id="summaryChanges">–</strong><small id="summaryChangesSub">Neu · Aktualisiert · Entfernt</small></article>
|
||||
<article class="summary-card"><span>Graphqualität</span><strong id="summaryQuality">–</strong><small id="summaryQualitySub">Komponenten · Waisen · Widersprüche</small></article>
|
||||
<article class="summary-card"><span>AI-THINK</span><strong id="summaryThinking">–</strong><small id="summaryThinkingSub">Status und letzte Ausführung</small></article>
|
||||
<article class="summary-card"><span>Autonome Recherche</span><strong id="summaryAutonomous">–</strong><small id="summaryAutonomousSub">Queue und Ergebnisse</small></article>
|
||||
<article class="summary-card"><span>Ollama-Pool</span><strong id="summaryOllama">–</strong><small id="summaryOllamaSub">Gesundheit und Warteschlangen</small></article>
|
||||
<article class="summary-card"><span>Persistenz</span><strong id="summaryPersistence">–</strong><small id="summaryPersistenceSub">SQLite / WAL / offene Änderungen</small></article>
|
||||
</section>
|
||||
|
||||
<section class="section timeline-section">
|
||||
<div class="section-heading">
|
||||
<div><span class="eyebrow">ZEITVERLAUF</span><h2>Graphänderungen und Laufereignisse</h2></div>
|
||||
<span id="timelineHint" class="section-note">Zeitfenster wird geladen</span>
|
||||
</div>
|
||||
<div class="explain-box"><b>Lesart:</b> Balken oberhalb der Mittellinie sind neu erzeugte oder aktualisierte Graphobjekte. Balken unterhalb zeigen Löschungen. Die Linie zeigt die Anzahl technischer Ereignisse. So bleibt sichtbar, wenn ein Lauf viele Prüfungen durchgeführt, aber den Graphen nicht verändert hat.</div>
|
||||
<div class="chart-wrap"><canvas id="timelineCanvas" height="300" aria-label="Zeitverlauf der Graphänderungen"></canvas></div>
|
||||
<div class="chart-legend"><span><i class="legend-swatch create"></i>Neu</span><span><i class="legend-swatch update"></i>Aktualisiert / neu berechnet</span><span><i class="legend-swatch delete"></i>Entfernt</span><span><i class="legend-swatch events"></i>Ereignisse</span></div>
|
||||
</section>
|
||||
|
||||
<section class="section runs-section">
|
||||
<div class="section-heading">
|
||||
<div><span class="eyebrow">LAUFPROTOKOLL</span><h2>Thinking, Lernen, Recherche und Systemaktionen</h2></div>
|
||||
<span id="runCount" class="section-note">0 Läufe</span>
|
||||
</div>
|
||||
<div class="filters-row">
|
||||
<label>Typ <select id="runKindFilter"><option value="">Alle</option></select></label>
|
||||
<label>Status <select id="runStatusFilter"><option value="">Alle</option><option value="success">Erfolgreich</option><option value="warning">Ohne Übernahme</option><option value="error">Fehler</option><option value="neutral">Neutral</option><option value="running">Läuft</option></select></label>
|
||||
<label class="grow">Suche <input id="runSearch" type="search" placeholder="Titel, Ergebnis, Trigger oder Ereignistyp"></label>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table runs-table">
|
||||
<thead><tr><th>Zeit</th><th>Lauf</th><th>Bewertung</th><th>Graphänderung</th><th>Messwerte</th><th>Dauer</th></tr></thead>
|
||||
<tbody id="runsBody"><tr><td colspan="6" class="empty-state">Läufe werden geladen …</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="two-column">
|
||||
<article class="section">
|
||||
<div class="section-heading"><div><span class="eyebrow">GRAPHBESTAND</span><h2>Nodes nach Herkunft und Typ</h2></div></div>
|
||||
<div class="explain-box"><b>Source</b> ist der exakte Wert aus dem KB-Feld <code>source</code>. <b>Origin</b> beschreibt dagegen den technischen Erzeuger des Graphobjekts, etwa KB-Ingest, Recherche oder AI-Inferenz.</div>
|
||||
<div class="distribution-grid">
|
||||
<div><h3>Source</h3><div id="sourceDistribution" class="bar-list"></div></div>
|
||||
<div><h3>Node-Typ</h3><div id="kindDistribution" class="bar-list"></div></div>
|
||||
<div><h3>Origin</h3><div id="originDistribution" class="bar-list"></div></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="section">
|
||||
<div class="section-heading"><div><span class="eyebrow">BEZIEHUNGEN</span><h2>Edges, Nähe und Konfidenz</h2></div></div>
|
||||
<div class="metric-grid compact">
|
||||
<div><span>AI-Edges</span><strong id="relationAIEdges">–</strong></div>
|
||||
<div><span>Ø Relation-Konfidenz</span><strong id="relationConfidence">–</strong></div>
|
||||
<div><span>Ø gespeicherte Nähe</span><strong id="relationSimilarity">–</strong></div>
|
||||
<div><span>Widersprüche</span><strong id="relationContradictions">–</strong></div>
|
||||
</div>
|
||||
<h3>Edge-Typen</h3><div id="edgeTypeDistribution" class="bar-list"></div>
|
||||
<h3>Letzte Näheprüfungen</h3><div id="similarityList" class="compact-list"></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="two-column">
|
||||
<article class="section">
|
||||
<div class="section-heading"><div><span class="eyebrow">LEARNING</span><h2>Embedding-Aktivität</h2></div><span id="embeddingCoverageBadge" class="status-pill neutral">–</span></div>
|
||||
<div class="metric-grid compact">
|
||||
<div><span>Vektoren aktuell</span><strong id="embeddingRows">–</strong></div>
|
||||
<div><span>Neu im Zeitraum</span><strong id="embeddingCreated">–</strong></div>
|
||||
<div><span>Neu berechnet</span><strong id="embeddingUpdated">–</strong></div>
|
||||
<div><span>Entfernt</span><strong id="embeddingDeleted">–</strong></div>
|
||||
</div>
|
||||
<h3>Dimensionen</h3><div id="embeddingDimensions" class="bar-list"></div>
|
||||
<h3>Letzte Embedding-Ereignisse</h3><div id="embeddingEvents" class="compact-list"></div>
|
||||
</article>
|
||||
|
||||
<article class="section">
|
||||
<div class="section-heading"><div><span class="eyebrow">RESEARCH</span><h2>SearXNG und Wissensanreicherung</h2></div><span id="researchBadge" class="status-pill neutral">–</span></div>
|
||||
<div class="metric-grid compact">
|
||||
<div><span>Suchtreffer</span><strong id="researchResults">–</strong></div>
|
||||
<div><span>Volltexte geladen</span><strong id="researchFetched">–</strong></div>
|
||||
<div><span>Belege akzeptiert</span><strong id="researchAccepted">–</strong></div>
|
||||
<div><span>Artikel erstellt</span><strong id="researchArticles">–</strong></div>
|
||||
</div>
|
||||
<h3>Letzte Recherchevorgänge</h3><div id="researchRuns" class="compact-list"></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="two-column">
|
||||
<article class="section">
|
||||
<div class="section-heading"><div><span class="eyebrow">NEUESTE OBJEKTE</span><h2>Zuletzt veränderte Nodes</h2></div></div>
|
||||
<div id="newestNodes" class="entity-list"></div>
|
||||
</article>
|
||||
<article class="section">
|
||||
<div class="section-heading"><div><span class="eyebrow">NEUESTE OBJEKTE</span><h2>Zuletzt veränderte Edges</h2></div></div>
|
||||
<div id="newestEdges" class="entity-list"></div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="section changes-section">
|
||||
<div class="section-heading">
|
||||
<div><span class="eyebrow">ÄNDERUNGSJOURNAL</span><h2>Exakt betroffene Nodes, Edges und Vektoren</h2></div>
|
||||
<span id="changeCount" class="section-note">0 Änderungen</span>
|
||||
</div>
|
||||
<div class="explain-box"><b>Unterschied zu den Summen:</b> Hier stehen die konkreten Objekt-IDs, die zwischen zwei technischen Events erzeugt, aktualisiert, neu berechnet oder gelöscht wurden. Zum Schutz vor extrem großen Erstimporten werden pro Event höchstens 2.000 Detailzeilen gespeichert; die exakten Gesamtzähler bleiben trotzdem erhalten.</div>
|
||||
<div id="changeTruncationNotice" class="notice warning-notice" hidden></div>
|
||||
<div class="filters-row">
|
||||
<label>Objekt <select id="changeKindFilter"><option value="">Alle</option><option value="node">Node</option><option value="edge">Edge</option><option value="vector">Vektor</option></select></label>
|
||||
<label>Aktion <select id="changeActionFilter"><option value="">Alle</option><option value="created">Erzeugt</option><option value="updated">Aktualisiert</option><option value="recalculated">Neu berechnet</option><option value="deleted">Entfernt</option></select></label>
|
||||
<label class="grow">Suche <input id="changeSearch" type="search" placeholder="ID, Label, Relation, Origin, Source oder Ziel"></label>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table changes-table">
|
||||
<thead><tr><th>Zeit</th><th>Aktion</th><th>Objekt</th><th>ID / Bezeichnung</th><th>Relation / Origin</th><th>Graph-Version</th><th>Zugeordnetes Event</th></tr></thead>
|
||||
<tbody id="changesBody"><tr><td colspan="7" class="empty-state">Änderungen werden geladen …</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section raw-events-section">
|
||||
<div class="section-heading">
|
||||
<div><span class="eyebrow">ROHDATEN</span><h2>Technischer Eventstream</h2></div>
|
||||
<span id="eventCount" class="section-note">0 Ereignisse</span>
|
||||
</div>
|
||||
<div class="explain-box"><b>Warum Rohdaten?</b> Jeder Eintrag enthält die ursprünglichen Engine-Metadaten sowie den unmittelbar erfassten Graphstand. Damit lässt sich auch bei einem übersprungenen Artikel erkennen, wie viele Vergleiche, Suchtreffer, Downloads oder Qualitätsentscheidungen stattgefunden haben.</div>
|
||||
<div class="filters-row">
|
||||
<label>Typ <select id="eventTypeFilter"><option value="">Alle</option></select></label>
|
||||
<label class="grow">Suche <input id="eventSearch" type="search" placeholder="Eventtyp, Meldung, Query oder Metadaten"></label>
|
||||
<label><input id="changesOnly" type="checkbox"> Nur mit Graphänderung</label>
|
||||
</div>
|
||||
<div id="eventsList" class="events-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="analysis-footer">
|
||||
<span id="generatedAt">Noch nicht aktualisiert</span>
|
||||
<span>Analysehistorie wird in <code>graph.db</code> gespeichert; die coole Gehirnansicht bleibt davon getrennt.</span>
|
||||
</footer>
|
||||
|
||||
<script src="/analysis.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,384 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const state = {payload: null, loading: false, refreshTimer: 0, live: null, openRun: '', openEvents: new Set()};
|
||||
const nf = new Intl.NumberFormat('de-DE');
|
||||
const dtf = new Intl.DateTimeFormat('de-DE', {dateStyle: 'short', timeStyle: 'medium'});
|
||||
const tf = new Intl.DateTimeFormat('de-DE', {hour: '2-digit', minute: '2-digit', second: '2-digit'});
|
||||
|
||||
function esc(value) {
|
||||
return String(value ?? '').replace(/[&<>'"]/g, char => ({'&':'&','<':'<','>':'>',"'":''','"':'"'}[char]));
|
||||
}
|
||||
function num(value) { return nf.format(Number(value || 0)); }
|
||||
function pct(value) { return `${Math.round(Number(value || 0) * 100)} %`; }
|
||||
function when(value) {
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? '–' : dtf.format(date);
|
||||
}
|
||||
function clock(value) {
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? '–' : tf.format(date);
|
||||
}
|
||||
function duration(ms) {
|
||||
ms = Number(ms || 0);
|
||||
if (ms < 1000) return `${Math.max(0, Math.round(ms))} ms`;
|
||||
if (ms < 60000) return `${(ms / 1000).toFixed(ms < 10000 ? 1 : 0)} s`;
|
||||
const minutes = Math.floor(ms / 60000);
|
||||
const seconds = Math.round((ms % 60000) / 1000);
|
||||
if (minutes < 60) return `${minutes} min ${seconds} s`;
|
||||
return `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
|
||||
}
|
||||
function bytes(value) {
|
||||
let number = Number(value || 0);
|
||||
const units = ['B','KiB','MiB','GiB','TiB'];
|
||||
let index = 0;
|
||||
while (number >= 1024 && index < units.length - 1) { number /= 1024; index++; }
|
||||
return `${number.toFixed(index ? 1 : 0)} ${units[index]}`;
|
||||
}
|
||||
function statusClass(value) {
|
||||
return ['success','warning','error','running'].includes(value) ? value : 'neutral';
|
||||
}
|
||||
function mutationTotals(m = {}) {
|
||||
return {
|
||||
created: Number(m.nodes_created || 0) + Number(m.edges_created || 0) + Number(m.vectors_created || 0),
|
||||
updated: Number(m.nodes_updated || 0) + Number(m.edges_updated || 0) + Number(m.vectors_updated || 0),
|
||||
deleted: Number(m.nodes_deleted || 0) + Number(m.edges_deleted || 0) + Number(m.vectors_deleted || 0)
|
||||
};
|
||||
}
|
||||
function hasMutation(m = {}) {
|
||||
const t = mutationTotals(m);
|
||||
return t.created + t.updated + t.deleted > 0;
|
||||
}
|
||||
function deltaTags(m = {}) {
|
||||
const tags = [];
|
||||
if (Number(m.nodes_created)) tags.push(`<span class="delta-tag create">+${num(m.nodes_created)} Nodes</span>`);
|
||||
if (Number(m.edges_created)) tags.push(`<span class="delta-tag create">+${num(m.edges_created)} Edges</span>`);
|
||||
if (Number(m.vectors_created)) tags.push(`<span class="delta-tag create">+${num(m.vectors_created)} Vektoren</span>`);
|
||||
if (Number(m.nodes_updated)) tags.push(`<span class="delta-tag update">~${num(m.nodes_updated)} Nodes</span>`);
|
||||
if (Number(m.edges_updated)) tags.push(`<span class="delta-tag update">~${num(m.edges_updated)} Edges</span>`);
|
||||
if (Number(m.vectors_updated)) tags.push(`<span class="delta-tag update">↻${num(m.vectors_updated)} Vektoren</span>`);
|
||||
if (Number(m.nodes_deleted)) tags.push(`<span class="delta-tag delete">−${num(m.nodes_deleted)} Nodes</span>`);
|
||||
if (Number(m.edges_deleted)) tags.push(`<span class="delta-tag delete">−${num(m.edges_deleted)} Edges</span>`);
|
||||
if (Number(m.vectors_deleted)) tags.push(`<span class="delta-tag delete">−${num(m.vectors_deleted)} Vektoren</span>`);
|
||||
return tags.length ? tags.join('') : '<span class="delta-tag">keine Graphänderung</span>';
|
||||
}
|
||||
function metadataValue(metadata, ...keys) {
|
||||
for (const key of keys) {
|
||||
const value = metadata?.[key];
|
||||
if (value !== undefined && value !== null && value !== '') return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function eventSeverity(activity = {}) {
|
||||
const type = String(activity.type || '').toLowerCase();
|
||||
const message = String(activity.message || '').toLowerCase();
|
||||
if (type.includes('failed') || type.includes('error') || message.includes('fehlgeschlagen')) return 'error';
|
||||
if (type.includes('skipped') || type.includes('rejected') || type.includes('no_candidate') || type.includes('paused') || type.includes('deferred') || message.includes('übersprungen')) return 'warning';
|
||||
if (type.includes('completed') || type.includes('created') || type.includes('accepted') || type.includes('ingested') || type.includes('learned') || type.includes('synced') || type.includes('results') || type.includes('flushed')) return 'success';
|
||||
return 'neutral';
|
||||
}
|
||||
|
||||
async function api(path) {
|
||||
const response = await fetch(path, {headers: {'Accept':'application/json'}, cache: 'no-store'});
|
||||
const data = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadDashboard(reason = 'manual') {
|
||||
if (state.loading) return;
|
||||
state.loading = true;
|
||||
const banner = $('connectionBanner');
|
||||
banner.className = 'connection-banner loading';
|
||||
banner.textContent = reason === 'live' ? 'Neue Aktivität erkannt · Analyse wird aktualisiert …' : 'Analysedaten werden geladen …';
|
||||
const hours = Number($('rangeHours').value || 24);
|
||||
$('exportAnalysis').href = `/api/analysis/export?hours=${hours}`;
|
||||
try {
|
||||
state.payload = await api(`/api/analysis/dashboard?hours=${hours}&limit=400`);
|
||||
renderAll();
|
||||
banner.className = 'connection-banner connected';
|
||||
banner.textContent = `Verbunden · Analyse bis ${clock(state.payload.generated_at)} · ${num(state.payload.history?.raw_event_count)} Ereignisse im Zeitraum`;
|
||||
} catch (error) {
|
||||
banner.className = 'connection-banner error';
|
||||
banner.textContent = `Analyse konnte nicht geladen werden: ${error.message}`;
|
||||
} finally {
|
||||
state.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderLastRun();
|
||||
renderSummary();
|
||||
renderTimeline();
|
||||
renderRuns();
|
||||
renderDistributions();
|
||||
renderRelations();
|
||||
renderEmbeddings();
|
||||
renderResearch();
|
||||
renderNewest();
|
||||
renderChanges();
|
||||
renderEvents();
|
||||
$('generatedAt').textContent = `Erzeugt am ${when(state.payload.generated_at)} · Zeitraum ${state.payload.range_hours} h`;
|
||||
}
|
||||
|
||||
function renderLastRun() {
|
||||
const runs = state.payload?.history?.runs || [];
|
||||
const run = runs.find(item => item.status !== 'running') || runs[0];
|
||||
const status = $('lastRunStatus');
|
||||
if (!run) {
|
||||
status.className = 'status-pill neutral'; status.textContent = 'noch keine Daten';
|
||||
$('lastRunContent').className = 'last-run-content empty-state';
|
||||
$('lastRunContent').textContent = 'Es wurde noch kein auswertbarer Lauf aufgezeichnet.';
|
||||
return;
|
||||
}
|
||||
status.className = `status-pill ${statusClass(run.status)}`;
|
||||
status.textContent = run.verdict || run.status;
|
||||
const totals = mutationTotals(run.mutations);
|
||||
$('lastRunContent').className = 'last-run-content';
|
||||
$('lastRunContent').innerHTML = `
|
||||
<div class="last-run-main"><small>${esc(run.kind)} · ${when(run.started_at)}${run.trigger ? ` · Trigger: ${esc(run.trigger)}` : ''}</small><h3>${esc(run.title)}</h3><p>${esc(run.explanation || run.outcome || 'Keine zusätzliche Erklärung vorhanden.')}</p><div class="delta-tags" style="margin-top:10px">${deltaTags(run.mutations)}</div></div>
|
||||
<div class="last-run-stat"><span>Neu</span><strong>${num(totals.created)}</strong><small>Nodes, Edges, Vektoren</small></div>
|
||||
<div class="last-run-stat"><span>Aktualisiert</span><strong>${num(totals.updated)}</strong><small>inkl. neu berechneter Vektoren</small></div>
|
||||
<div class="last-run-stat"><span>Dauer</span><strong>${duration(run.duration_ms)}</strong><small>${num(run.event_count)} technische Ereignisse</small></div>`;
|
||||
}
|
||||
|
||||
function renderSummary() {
|
||||
const graph = state.payload.graph || {};
|
||||
const summary = graph.summary || {};
|
||||
const history = state.payload.history || {};
|
||||
const system = state.payload.system || {};
|
||||
const totals = mutationTotals(history.totals || {});
|
||||
$('summaryGraph').textContent = `${num(summary.node_count)} / ${num(summary.edge_count)}`;
|
||||
$('summaryGraphSub').textContent = `${num(summary.node_count)} Nodes · ${num(summary.edge_count)} aktive Edges · Version ${num(system.version)}`;
|
||||
$('summaryCoverage').textContent = pct(graph.vector_coverage);
|
||||
$('summaryCoverageSub').textContent = `${num(graph.vector_rows)} Embeddings für ${num(graph.vector_eligible_nodes)} wissenshaltige Nodes`;
|
||||
$('summaryChanges').textContent = `+${num(totals.created)} · ~${num(totals.updated)} · −${num(totals.deleted)}`;
|
||||
$('summaryChangesSub').textContent = `Neu · aktualisiert/neu berechnet · entfernt in ${state.payload.range_hours} h`;
|
||||
$('summaryQuality').textContent = `${num(summary.components)} / ${num(summary.knowledge_orphans)} / ${num(summary.contradictions)}`;
|
||||
$('summaryQualitySub').textContent = 'Komponenten · Wissenswaisen · Widerspruchs-Edges';
|
||||
const settings = system.runtime_settings || {};
|
||||
$('summaryThinking').textContent = settings.thinking_enabled ? (system.enrich_running ? 'LÄUFT' : 'AKTIV') : 'PAUSIERT';
|
||||
$('summaryThinkingSub').textContent = `${num(system.relations_created)} Relationen · ${num(system.articles_created)} Artikel seit Prozessstart`;
|
||||
const autonomous = system.autonomous_research || {};
|
||||
const queue = autonomous.counts || {};
|
||||
$('summaryAutonomous').textContent = settings.autonomous_research_enabled ? (autonomous.running ? 'LÄUFT' : 'AKTIV') : 'PAUSIERT';
|
||||
$('summaryAutonomousSub').textContent = `${num(queue.queued)} wartend · ${num(queue.completed)} abgeschlossen · ${num(queue.failed)} fehlgeschlagen`;
|
||||
const pool = system.ollama_pool || {};
|
||||
const nodes = pool.nodes || [];
|
||||
const healthy = nodes.filter(node => node.healthy && node.compatible).length;
|
||||
$('summaryOllama').textContent = `${healthy}/${nodes.length || 0} gesund`;
|
||||
$('summaryOllamaSub').textContent = `${num(pool.normal_waiters)} normale · ${num(pool.low_priority_waiters)} niedrige Priorität in Warteschlange`;
|
||||
const storage = system.graph_storage || {};
|
||||
const persistence = system.persistence || {};
|
||||
const audit = state.payload.history?.audit || {};
|
||||
const persistenceError = persistence.last_error || audit.last_error;
|
||||
$('summaryPersistence').textContent = persistenceError ? 'FEHLER' : (persistence.graph_dirty ? 'OFFEN' : 'SAUBER');
|
||||
$('summaryPersistenceSub').textContent = persistenceError ? `Audit/SQLite: ${persistenceError}` : `${bytes(storage.database_bytes)} DB · ${bytes(storage.wal_bytes)} WAL · ${num(Number(storage.pending_nodes||0)+Number(storage.pending_edges||0)+Number(storage.pending_vectors||0))} offene Upserts · Audit ${num(audit.queue_depth)}/${num(audit.queue_capacity)}`;
|
||||
}
|
||||
|
||||
function renderTimeline() {
|
||||
const canvas = $('timelineCanvas');
|
||||
const data = state.payload?.history?.timeline || [];
|
||||
$('timelineHint').textContent = `${data.length} Zeitsegmente · ${state.payload.range_hours} Stunden`;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
canvas.width = Math.max(600, Math.floor(rect.width * dpr));
|
||||
canvas.height = Math.floor(300 * dpr);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.setTransform(dpr,0,0,dpr,0,0);
|
||||
const width = canvas.width / dpr, height = canvas.height / dpr;
|
||||
ctx.clearRect(0,0,width,height);
|
||||
ctx.fillStyle = '#07131d'; ctx.fillRect(0,0,width,height);
|
||||
if (!data.length) { ctx.fillStyle='#8ca4b7'; ctx.font='13px Segoe UI'; ctx.fillText('Noch keine historischen Daten für diesen Zeitraum.',20,35); return; }
|
||||
const pad = {l:48,r:22,t:22,b:38};
|
||||
const chartW = width-pad.l-pad.r, chartH = height-pad.t-pad.b, mid = pad.t+chartH*.63;
|
||||
const values = data.map(bucket => {
|
||||
const m=bucket.mutations||{};
|
||||
return {create:Number(m.nodes_created||0)+Number(m.edges_created||0)+Number(m.vectors_created||0),update:Number(m.nodes_updated||0)+Number(m.edges_updated||0)+Number(m.vectors_updated||0),del:Number(m.nodes_deleted||0)+Number(m.edges_deleted||0)+Number(m.vectors_deleted||0),events:Number(bucket.events||0)};
|
||||
});
|
||||
const maxUp=Math.max(1,...values.map(v=>v.create+v.update));
|
||||
const maxDown=Math.max(1,...values.map(v=>v.del));
|
||||
const maxEvents=Math.max(1,...values.map(v=>v.events));
|
||||
ctx.strokeStyle='#29404f';ctx.lineWidth=1;
|
||||
ctx.beginPath();ctx.moveTo(pad.l,mid+.5);ctx.lineTo(width-pad.r,mid+.5);ctx.stroke();
|
||||
ctx.font='10px Segoe UI';ctx.fillStyle='#7890a3';ctx.textAlign='right';
|
||||
ctx.fillText(num(maxUp),pad.l-8,pad.t+8);ctx.fillText('0',pad.l-8,mid+3);ctx.fillText(`−${num(maxDown)}`,pad.l-8,height-pad.b);
|
||||
const slot=chartW/data.length, bar=Math.max(2,Math.min(18,slot*.58));
|
||||
const eventPoints=[];
|
||||
data.forEach((bucket,index)=>{
|
||||
const x=pad.l+slot*index+slot/2; const v=values[index];
|
||||
const createH=(v.create/maxUp)*(mid-pad.t-8); const updateH=(v.update/maxUp)*(mid-pad.t-8);
|
||||
ctx.fillStyle='#62d39a';ctx.fillRect(x-bar/2,mid-createH,bar,createH);
|
||||
ctx.fillStyle='#5e8cff';ctx.fillRect(x-bar/2,mid-createH-updateH,bar,updateH);
|
||||
const delH=(v.del/maxDown)*(height-pad.b-mid-5);ctx.fillStyle='#ff7d84';ctx.fillRect(x-bar/2,mid+1,bar,delH);
|
||||
const ey=pad.t+(1-v.events/maxEvents)*(mid-pad.t-10);eventPoints.push([x,ey]);
|
||||
if(index===0||index===data.length-1||index%Math.max(1,Math.ceil(data.length/6))===0){ctx.fillStyle='#7890a3';ctx.textAlign='center';ctx.fillText(new Date(bucket.start).toLocaleString('de-DE',{day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}),x,height-16)}
|
||||
});
|
||||
ctx.strokeStyle='#f2c86d';ctx.lineWidth=1.7;ctx.beginPath();eventPoints.forEach(([x,y],i)=>i?ctx.lineTo(x,y):ctx.moveTo(x,y));ctx.stroke();
|
||||
ctx.fillStyle='#f2c86d';eventPoints.forEach(([x,y])=>{ctx.beginPath();ctx.arc(x,y,2.2,0,Math.PI*2);ctx.fill()});
|
||||
}
|
||||
|
||||
function renderRuns() {
|
||||
const runs = state.payload?.history?.runs || [];
|
||||
const kindFilter = $('runKindFilter');
|
||||
const existing = new Set([...kindFilter.options].map(option => option.value));
|
||||
[...new Set(runs.map(run => run.kind).filter(Boolean))].sort().forEach(kind => { if (!existing.has(kind)) kindFilter.add(new Option(kind,kind)); });
|
||||
const kind = kindFilter.value, status = $('runStatusFilter').value, search = $('runSearch').value.trim().toLowerCase();
|
||||
const filtered = runs.filter(run => (!kind||run.kind===kind)&&(!status||run.status===status)&&(!search||JSON.stringify(run).toLowerCase().includes(search)));
|
||||
$('runCount').textContent = `${num(filtered.length)} von ${num(runs.length)} Läufen`;
|
||||
const body = $('runsBody');
|
||||
if (!filtered.length) { body.innerHTML='<tr><td colspan="6" class="empty-state">Keine Läufe entsprechen den Filtern.</td></tr>'; return; }
|
||||
body.innerHTML = filtered.map(run => {
|
||||
const metrics = run.metrics || {};
|
||||
const metricText = [metrics.comparisons ? `${num(metrics.comparisons)} Vergleiche` : '', metrics.research_search_results ? `${num(metrics.research_search_results)} Treffer` : '', metrics.research_accepted ? `${num(metrics.research_accepted)} Belege` : '', metrics.articles_created ? `${num(metrics.articles_created)} Artikel` : ''].filter(Boolean).join(' · ') || `${num(run.event_count)} Events`;
|
||||
const open = state.openRun === run.id;
|
||||
return `<tr class="run-row" data-run-id="${esc(run.id)}"><td>${clock(run.started_at)}<span class="run-meta">${new Date(run.started_at).toLocaleDateString('de-DE')}</span></td><td><span class="run-title">${esc(run.title)}</span><span class="run-meta">${esc(run.kind)}${run.trigger?` · ${esc(run.trigger)}`:''}</span></td><td><span class="status-pill ${statusClass(run.status)}">${esc(run.verdict||run.status)}</span><span class="run-meta">${esc(run.outcome||'')}</span></td><td><div class="delta-tags">${deltaTags(run.mutations)}</div></td><td>${esc(metricText)}</td><td>${run.status==='running'?'läuft':duration(run.duration_ms)}</td></tr>${open ? runDetails(run) : ''}`;
|
||||
}).join('');
|
||||
body.querySelectorAll('.run-row').forEach(row => row.addEventListener('click', () => { state.openRun = state.openRun === row.dataset.runId ? '' : row.dataset.runId; renderRuns(); }));
|
||||
}
|
||||
|
||||
function runDetails(run) {
|
||||
const events=(run.events||[]).slice().reverse();
|
||||
return `<tr class="run-detail-row"><td colspan="6"><div class="run-detail"><div><h3>Erklärung</h3><p>${esc(run.explanation||'Keine Erklärung vorhanden.')}</p><div class="delta-tags">${deltaTags(run.mutations)}</div><h3>Messwerte</h3><pre>${esc(JSON.stringify(run.metrics||{},null,2))}</pre><h3>Betroffene IDs</h3><pre>${esc(JSON.stringify({node_ids:run.node_ids||[],edge_ids:run.edge_ids||[]},null,2))}</pre></div><div><h3>Ereignisse dieses Laufs</h3><div class="event-mini-list">${events.map(event=>`<div class="event-mini"><b>${clock(event.activity.timestamp)} · ${esc(event.activity.type)}</b><small>${esc(event.activity.message||event.activity.phase||'')}</small><div class="delta-tags" style="margin-top:5px">${deltaTags(event.point?.delta)}</div>${event.change_count?`<small>${num(event.change_count)} konkrete Änderungen${event.changes_truncated?` · ${num(event.changes_truncated)} gekürzt`:''}</small>`:''}</div>`).join('')}</div></div></div></td></tr>`;
|
||||
}
|
||||
|
||||
function renderBarList(id, values, limit = 12) {
|
||||
const entries = Object.entries(values || {}).sort((a,b)=>b[1]-a[1]).slice(0,limit);
|
||||
const max = Math.max(1,...entries.map(entry=>Number(entry[1])));
|
||||
$(id).innerHTML = entries.length ? entries.map(([label,value])=>`<div class="bar-row"><span class="bar-label" title="${esc(label)}">${esc(label)}</span><span class="bar-track"><i class="bar-fill" style="width:${Math.max(2,Number(value)/max*100)}%"></i></span><span class="bar-value">${num(value)}</span></div>`).join('') : '<span class="empty-state">Keine Daten</span>';
|
||||
}
|
||||
function renderDistributions() {
|
||||
const graph=state.payload.graph||{};
|
||||
renderBarList('sourceDistribution',graph.node_sources,14);
|
||||
renderBarList('kindDistribution',graph.node_kinds,14);
|
||||
renderBarList('originDistribution',graph.node_origins,14);
|
||||
renderBarList('edgeTypeDistribution',graph.edge_types,14);
|
||||
}
|
||||
|
||||
function renderRelations() {
|
||||
const graph=state.payload.graph||{}, summary=graph.summary||{};
|
||||
$('relationAIEdges').textContent=num(summary.ai_edges);
|
||||
$('relationConfidence').textContent=graph.average_ai_confidence ? pct(graph.average_ai_confidence) : '–';
|
||||
$('relationSimilarity').textContent=graph.average_ai_similarity ? pct(graph.average_ai_similarity) : '–';
|
||||
$('relationContradictions').textContent=num(summary.contradictions);
|
||||
const observations=(state.payload.history?.events||[]).filter(event=>metadataValue(event.activity?.metadata,'semantic_similarity')!==undefined).slice(0,12);
|
||||
$('similarityList').innerHTML=observations.length?observations.map(event=>{const m=event.activity.metadata||{};return `<div class="compact-item"><header><b>${esc(event.activity.message||event.activity.type)}</b><span>${pct(Number(m.semantic_similarity))}</span></header><small>${when(event.activity.timestamp)} · ${num(m.candidate_comparisons||m.comparisons||0)} Vergleiche${m.confidence!==undefined?` · ${pct(m.confidence)} Konfidenz`:''}</small></div>`}).join(''):'<span class="empty-state">Noch keine Näheprüfungen im Zeitraum.</span>';
|
||||
}
|
||||
|
||||
function renderEmbeddings() {
|
||||
const graph=state.payload.graph||{}, totals=state.payload.history?.totals||{};
|
||||
$('embeddingRows').textContent=num(graph.vector_rows);
|
||||
$('embeddingCreated').textContent=num(totals.vectors_created);
|
||||
$('embeddingUpdated').textContent=num(totals.vectors_updated);
|
||||
$('embeddingDeleted').textContent=num(totals.vectors_deleted);
|
||||
$('embeddingCoverageBadge').className=`status-pill ${graph.vector_coverage>=.95?'success':graph.vector_coverage>=.7?'warning':'error'}`;
|
||||
$('embeddingCoverageBadge').textContent=pct(graph.vector_coverage);
|
||||
renderBarList('embeddingDimensions',graph.embedding_dimensions,8);
|
||||
const events=(state.payload.history?.events||[]).filter(event=>event.activity?.type?.includes('embedding')||event.activity?.type?.includes('.learned')).slice(0,12);
|
||||
$('embeddingEvents').innerHTML=events.length?events.map(event=>`<div class="compact-item"><header><b>${esc(event.activity.message||event.activity.type)}</b><span>${clock(event.activity.timestamp)}</span></header><small>${esc(event.activity.type)} · ${esc(metadataValue(event.activity.metadata,'model')||'')} ${metadataValue(event.activity.metadata,'dimensions')?`· ${num(metadataValue(event.activity.metadata,'dimensions'))} Dimensionen`:''}</small><div class="delta-tags" style="margin-top:5px">${deltaTags(event.point?.delta)}</div>${event.change_count?`<small>${num(event.change_count)} konkrete Änderungen${event.changes_truncated?` · ${num(event.changes_truncated)} gekürzt`:''}</small>`:''}</div>`).join(''):'<span class="empty-state">Keine Embedding-Ereignisse im Zeitraum.</span>';
|
||||
}
|
||||
|
||||
function researchMetrics() {
|
||||
const events=state.payload.history?.events||[];
|
||||
const metrics={results:0,fetched:0,accepted:0,rejected:0,articles:0,failures:0};
|
||||
events.forEach(event=>{const a=event.activity||{},m=a.metadata||{},type=a.type||'';if(type.includes('research.results'))metrics.results+=Number(m.result_count||m.research_search_results||0);if(type.includes('fetch.completed'))metrics.fetched++;if(type.includes('evidence.accepted'))metrics.accepted++;if(type.includes('evidence.rejected'))metrics.rejected++;if(type==='article.created'||(type==='autonomous.research.task.completed'&&m.article_created))metrics.articles++;if(type.includes('research')&&type.includes('failed'))metrics.failures++;});
|
||||
return metrics;
|
||||
}
|
||||
function renderResearch() {
|
||||
const metrics=researchMetrics();
|
||||
$('researchResults').textContent=num(metrics.results);$('researchFetched').textContent=num(metrics.fetched);$('researchAccepted').textContent=num(metrics.accepted);$('researchArticles').textContent=num(metrics.articles);
|
||||
$('researchBadge').className=`status-pill ${metrics.failures?'warning':metrics.accepted||metrics.results?'success':'neutral'}`;
|
||||
$('researchBadge').textContent=metrics.failures?`${num(metrics.failures)} Fehler`:metrics.accepted?`${num(metrics.accepted)} Belege`:'keine Aktivität';
|
||||
const runs=(state.payload.history?.runs||[]).filter(run=>['research','autonomous-research','searxng-test','opportunity-scan'].includes(run.kind)).slice(0,12);
|
||||
$('researchRuns').innerHTML=runs.length?runs.map(run=>`<div class="compact-item"><header><b>${esc(run.title)}</b><span class="status-pill ${statusClass(run.status)}">${esc(run.verdict||run.status)}</span></header><small>${when(run.started_at)} · ${duration(run.duration_ms)} · ${num(run.event_count)} Events</small><p>${esc(run.explanation||run.outcome||'')}</p><div class="delta-tags">${deltaTags(run.mutations)}</div></div>`).join(''):'<span class="empty-state">Keine Rechercheläufe im Zeitraum.</span>';
|
||||
}
|
||||
|
||||
function renderNewest() {
|
||||
const graph=state.payload.graph||{};
|
||||
$('newestNodes').innerHTML=(graph.newest_nodes||[]).map(node=>`<div class="entity-item"><header><b>${esc(node.label)}</b><small>${when(node.updated_at)}</small></header><p>${esc(node.summary||'Keine Zusammenfassung')}</p><div class="entity-tags"><span>${esc(node.kind)}</span><span>${esc(node.status||'kein Status')}</span><span>${esc(node.origin)}</span>${node.metadata?.source?`<span>source: ${esc(node.metadata.source)}</span>`:''}</div><code>${esc(node.id)}</code></div>`).join('')||'<span class="empty-state">Keine Nodes vorhanden.</span>';
|
||||
$('newestEdges').innerHTML=(graph.newest_edges||[]).map(edge=>`<div class="entity-item"><header><b>${esc(edge.type)}</b><small>${when(edge.updated_at)}</small></header><p>${esc(edge.explanation||'Keine Erklärung')} ${edge.confidence?`· Konfidenz ${pct(edge.confidence)}`:''}</p><div class="entity-tags"><span>${esc(edge.status||'kein Status')}</span><span>${esc(edge.origin)}</span></div><code>${esc(edge.source)} → ${esc(edge.target)}</code></div>`).join('')||'<span class="empty-state">Keine Edges vorhanden.</span>';
|
||||
}
|
||||
|
||||
function changeActionLabel(action) {
|
||||
return ({created:'erzeugt',updated:'aktualisiert',recalculated:'neu berechnet',deleted:'entfernt'})[action] || action || 'unbekannt';
|
||||
}
|
||||
|
||||
function renderChanges() {
|
||||
const history = state.payload?.history || {};
|
||||
const changes = history.changes || [];
|
||||
const kind = $('changeKindFilter').value;
|
||||
const action = $('changeActionFilter').value;
|
||||
const search = $('changeSearch').value.trim().toLowerCase();
|
||||
const filtered = changes.filter(change => (!kind || change.entity_kind === kind) && (!action || change.action === action) && (!search || JSON.stringify(change).toLowerCase().includes(search)));
|
||||
$('changeCount').textContent = `${num(filtered.length)} angezeigt · ${num(history.change_count)} insgesamt im Zeitraum`;
|
||||
const dropped = Number(history.dropped_detailed_changes || 0);
|
||||
const truncatedEvents = (history.events || []).reduce((sum, event) => sum + Number(event.changes_truncated || 0), 0);
|
||||
const notice = $('changeTruncationNotice');
|
||||
if (dropped || truncatedEvents || Number(history.change_count || 0) > changes.length) {
|
||||
notice.hidden = false;
|
||||
notice.textContent = `Es werden die neuesten ${num(changes.length)} Detailänderungen geladen. ${num(dropped || truncatedEvents)} weitere Detailzeilen wurden wegen des Schutzlimits nicht gespeichert; die Summen für Nodes, Edges und Vektoren bleiben vollständig.`;
|
||||
} else {
|
||||
notice.hidden = true;
|
||||
}
|
||||
const body = $('changesBody');
|
||||
if (!filtered.length) {
|
||||
body.innerHTML = '<tr><td colspan="7" class="empty-state">Keine konkreten Änderungen entsprechen den Filtern.</td></tr>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = filtered.map(change => {
|
||||
const details = change.details || {};
|
||||
const relation = change.relation_type || details.kind || '';
|
||||
const endpoints = details.source && details.target ? `${details.source} → ${details.target}` : '';
|
||||
const subtitle = change.label || endpoints || details.source || '';
|
||||
const sourceText = details.source && change.entity_kind === 'node' ? `source: ${details.source}` : '';
|
||||
const deltas = [];
|
||||
if (details.semantic_similarity !== undefined || details.previous_semantic_similarity !== undefined) deltas.push(`Nähe ${details.previous_semantic_similarity!==undefined?pct(details.previous_semantic_similarity):'–'} → ${details.semantic_similarity!==undefined?pct(details.semantic_similarity):'–'}`);
|
||||
if (details.confidence !== undefined && details.previous_confidence !== undefined) deltas.push(`Konfidenz ${pct(details.previous_confidence)} → ${pct(details.confidence)}`);
|
||||
if (details.dimensions !== undefined) deltas.push(details.previous_dimensions!==undefined?`Dimensionen ${num(details.previous_dimensions)} → ${num(details.dimensions)}`:`${num(details.dimensions)} Dimensionen`);
|
||||
const technical = [change.origin, sourceText, ...deltas].filter(Boolean).join(' · ');
|
||||
return `<tr class="change-row" data-event-id="${esc(change.event_id)}"><td>${when(change.timestamp)}</td><td><span class="change-action ${esc(change.action)}">${esc(changeActionLabel(change.action))}</span></td><td><b>${esc(change.entity_kind)}</b>${details.status?`<span class="run-meta">${esc(details.status)}</span>`:''}</td><td><code>${esc(change.entity_id)}</code>${subtitle?`<span class="run-meta">${esc(subtitle)}</span>`:''}</td><td>${esc(relation||'–')}<span class="run-meta">${esc(technical)}</span></td><td>${num(change.graph_version)}</td><td><button class="event-link" type="button" data-event-id="${esc(change.event_id)}">${esc(change.event_id || '–')}</button></td></tr>`;
|
||||
}).join('');
|
||||
body.querySelectorAll('.event-link').forEach(button => button.addEventListener('click', event => {
|
||||
event.stopPropagation();
|
||||
$('eventSearch').value = button.dataset.eventId;
|
||||
renderEvents();
|
||||
document.querySelector('.raw-events-section')?.scrollIntoView({behavior:'smooth',block:'start'});
|
||||
}));
|
||||
}
|
||||
|
||||
function renderEvents() {
|
||||
const events=state.payload.history?.events||[];
|
||||
const typeFilter=$('eventTypeFilter'); const known=new Set([...typeFilter.options].map(option=>option.value));
|
||||
[...new Set(events.map(event=>event.activity?.type).filter(Boolean))].sort().forEach(type=>{if(!known.has(type))typeFilter.add(new Option(type,type))});
|
||||
const type=typeFilter.value,search=$('eventSearch').value.trim().toLowerCase(),changesOnly=$('changesOnly').checked;
|
||||
const filtered=events.filter(event=>(!type||event.activity.type===type)&&(!changesOnly||hasMutation(event.point?.delta))&&(!search||JSON.stringify(event).toLowerCase().includes(search)));
|
||||
$('eventCount').textContent=`${num(filtered.length)} von ${num(events.length)} Ereignissen`;
|
||||
$('eventsList').innerHTML=filtered.length?filtered.map(event=>{const a=event.activity||{},open=state.openEvents.has(a.id);return `<article class="event-card ${open?'open':''}" data-event-id="${esc(a.id)}"><div class="event-summary"><span class="event-time">${when(a.timestamp)}</span><span class="event-type"><i class="status-dot ${eventSeverity(a)}"></i> ${esc(a.type)}</span><span class="event-message">${esc(a.message||a.query||a.phase||'')}</span><span class="event-delta"><span class="delta-tags">${deltaTags(event.point?.delta)}</span></span></div><div class="event-details"><div><b>Aktivität</b>${a.query?`<div class="query-box">${esc(a.query)}</div>`:''}<pre>${esc(JSON.stringify(a,null,2))}</pre></div><div><b>Graph-Checkpoint unmittelbar nach dem Event</b><pre>${esc(JSON.stringify({...event.point,change_count:event.change_count,changes_truncated:event.changes_truncated||0},null,2))}</pre></div></div></article>`}).join(''):'<div class="empty-state">Keine Ereignisse entsprechen den Filtern.</div>';
|
||||
$('eventsList').querySelectorAll('.event-card').forEach(card=>card.querySelector('.event-summary').addEventListener('click',()=>{const id=card.dataset.eventId;state.openEvents.has(id)?state.openEvents.delete(id):state.openEvents.add(id);renderEvents()}));
|
||||
}
|
||||
|
||||
function scheduleLiveRefresh() {
|
||||
if (!$('autoRefresh').checked) return;
|
||||
clearTimeout(state.refreshTimer);
|
||||
state.refreshTimer=setTimeout(()=>loadDashboard('live'),900);
|
||||
}
|
||||
function connectLive() {
|
||||
if (state.live) state.live.close();
|
||||
const source=new EventSource('/api/stream'); state.live=source;
|
||||
source.addEventListener('activity',scheduleLiveRefresh);
|
||||
source.onopen=()=>{if(!state.loading){$('connectionBanner').className='connection-banner live';$('connectionBanner').textContent='Live-Eventstream verbunden · wartet auf neue Hirnaktivität';}};
|
||||
source.onerror=()=>{if(!state.loading){$('connectionBanner').className='connection-banner error';$('connectionBanner').textContent='Live-Eventstream unterbrochen · Dashboard kann weiterhin manuell aktualisiert werden';}};
|
||||
}
|
||||
|
||||
$('refreshNow').addEventListener('click',()=>loadDashboard('manual'));
|
||||
$('rangeHours').addEventListener('change',()=>loadDashboard('range'));
|
||||
$('runKindFilter').addEventListener('change',renderRuns);$('runStatusFilter').addEventListener('change',renderRuns);$('runSearch').addEventListener('input',renderRuns);
|
||||
$('changeKindFilter').addEventListener('change',renderChanges);$('changeActionFilter').addEventListener('change',renderChanges);$('changeSearch').addEventListener('input',renderChanges);
|
||||
$('eventTypeFilter').addEventListener('change',renderEvents);$('eventSearch').addEventListener('input',renderEvents);$('changesOnly').addEventListener('change',renderEvents);
|
||||
$('autoRefresh').addEventListener('change',event=>{event.target.checked?connectLive():state.live?.close()});
|
||||
window.addEventListener('resize',()=>{if(state.payload)renderTimeline()});
|
||||
|
||||
loadDashboard('startup');
|
||||
connectLive();
|
||||
})();
|
||||
@@ -67,3 +67,4 @@ body.low-power .glass{backdrop-filter:blur(12px)}
|
||||
@media(max-width:620px){.view-selector{grid-template-columns:1fr}.metrics span[title]{display:none}}
|
||||
.filter-scope-summary{margin:2px 0 10px;line-height:1.45}.source-option.disabled{opacity:.38}.source-option.disabled span{cursor:not-allowed;border-style:dashed}.source-option span small{display:block;margin-top:2px;font-size:7px;letter-spacing:.04em;color:#8a6f79}.filter-scope-summary.warn{color:#ffb37f}.filter-scope-summary.ok{color:#7898aa}
|
||||
.autonomous-research-settings{flex:0 0 auto}.autonomous-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin:10px 0}.autonomous-grid label{display:block;padding:8px;border:1px solid rgba(133,200,255,.11);border-radius:10px;background:rgba(255,255,255,.02)}.autonomous-grid span{display:block;font-size:8px;color:#7893a6;margin-bottom:5px}.autonomous-grid input{width:100%;border:1px solid rgba(133,200,255,.14);background:rgba(2,8,17,.62);color:var(--text);border-radius:8px;padding:7px 8px;outline:0}.autonomous-actions{flex-wrap:wrap}.autonomous-actions button{flex:1 1 120px}.queue-title{margin-top:14px}.autonomous-queue{display:flex;flex-direction:column;gap:7px;max-height:260px;overflow:auto;padding-right:3px}.autonomous-task{position:relative;padding:9px 10px;border:1px solid rgba(133,200,255,.11);border-radius:11px;background:rgba(255,255,255,.024)}.autonomous-task.running{border-color:rgba(93,255,189,.3);box-shadow:0 0 18px rgba(93,255,189,.07)}.autonomous-task.failed{border-color:rgba(255,105,125,.24)}.autonomous-task.completed{opacity:.72}.autonomous-task-head{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.autonomous-task b{font-size:10px;color:#d9f3ff;line-height:1.35}.autonomous-task em{font-style:normal;font-size:8px;color:#82e7ff;border:1px solid rgba(82,231,255,.2);border-radius:999px;padding:2px 6px;white-space:nowrap}.autonomous-task p{margin:5px 0 0;color:#7893a6;font-size:8px;line-height:1.4}.autonomous-task-meta{display:flex;gap:5px;flex-wrap:wrap;margin-top:7px}.autonomous-task-meta span{font-size:7px;color:#86a3b7;background:rgba(255,255,255,.035);border-radius:999px;padding:3px 6px}.autonomous-task button{position:absolute;right:8px;bottom:8px;border:0;background:transparent;color:#ff8ba0;font-size:8px;cursor:pointer}.autonomous-task button:hover{color:#ffc0ca}@media(max-width:720px){.autonomous-grid{grid-template-columns:1fr}.autonomous-actions button{flex-basis:100%}}
|
||||
.dock .analysis-dashboard-link{display:inline-flex;align-items:center;border:1px solid rgba(82,231,255,.2);background:rgba(82,231,255,.055);color:#a9dbe8;border-radius:11px;padding:9px 11px;font-weight:700;letter-spacing:.08em;font-size:10px;white-space:nowrap}.dock .analysis-dashboard-link:hover{background:rgba(82,231,255,.12);color:#e5fbff}
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
<button id="toggleEco" title="Optimierter Renderpfad für schwächere Systeme">ECO</button>
|
||||
<button id="resetView">Zentrieren</button>
|
||||
<button id="openSettings" title="Exakte KB-source-Filter und Laufzeitsteuerung">FILTER</button>
|
||||
<a class="analysis-dashboard-link" href="/analysis.html" title="Technisches Analyse-Dashboard mit Lauf- und Graphhistorie">ANALYSE</a>
|
||||
<button id="enrichNow" class="think-action" title="Einen autonomen AI-THINK-Zyklus sofort starten"><span>AI-THINK</span><small>STARTEN</small></button>
|
||||
</nav>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user