Update 8 - LowPowerMode + Bugfixes
release-tag / release-image (push) Successful in 2m25s

This commit is contained in:
2026-08-05 06:34:02 +02:00
parent 8dc4de3eae
commit 18255e4b98
18 changed files with 1271 additions and 290 deletions
+4 -2
View File
@@ -70,6 +70,7 @@ type Config struct {
ThinkingCategories []string
DefaultView string
MaxDisplayNodes int
LowPowerMode bool
RuntimeDefaultsConfigured bool
GLPIKBEnabled bool
@@ -162,6 +163,7 @@ func Load() (Config, error) {
ThinkingCategories: stringList("BRAIN_THINKING_CATEGORIES"),
DefaultView: strings.ToLower(env("BRAIN_DEFAULT_VIEW", "neural")),
MaxDisplayNodes: integer("BRAIN_MAX_DISPLAY_NODES", 0),
LowPowerMode: boolean("BRAIN_LOW_POWER_MODE", false),
RuntimeDefaultsConfigured: true,
GLPIKBEnabled: boolean("GLPI_KB_ENABLED", false),
GLPIURL: strings.TrimRight(strings.TrimSpace(os.Getenv("GLPI_URL")), "/"),
@@ -253,8 +255,8 @@ func Load() (Config, error) {
if cfg.ResearchEnabled && cfg.SearXNGURL == "" {
return Config{}, fmt.Errorf("BRAIN_RESEARCH_ENABLED requires SEARXNG_URL")
}
if cfg.DefaultView != "neural" && cfg.DefaultView != "honeycomb" {
return Config{}, fmt.Errorf("BRAIN_DEFAULT_VIEW must be neural or honeycomb")
if cfg.DefaultView != "neural" && cfg.DefaultView != "honeycomb" && cfg.DefaultView != "constellation" {
return Config{}, fmt.Errorf("BRAIN_DEFAULT_VIEW must be neural, honeycomb or constellation")
}
if cfg.MaxDisplayNodes < 0 || cfg.MaxDisplayNodes > 500000 {
return Config{}, fmt.Errorf("BRAIN_MAX_DISPLAY_NODES must be between 0 and 500000")
+3 -2
View File
@@ -45,13 +45,14 @@ func TestLoadRuntimeControlDefaultsAndFilters(t *testing.T) {
t.Setenv("BRAIN_LEARNING_CATEGORIES", "Netzwerk,GLPI KB")
t.Setenv("BRAIN_DISPLAY_CATEGORIES", "GLPI KB")
t.Setenv("BRAIN_THINKING_CATEGORIES", "Netzwerk")
t.Setenv("BRAIN_DEFAULT_VIEW", "honeycomb")
t.Setenv("BRAIN_DEFAULT_VIEW", "constellation")
t.Setenv("BRAIN_MAX_DISPLAY_NODES", "12000")
t.Setenv("BRAIN_LOW_POWER_MODE", "true")
cfg, err := Load()
if err != nil {
t.Fatal(err)
}
if cfg.LearningEnabled || cfg.ThinkingEnabled || cfg.DefaultView != "honeycomb" || cfg.MaxDisplayNodes != 12000 {
if cfg.LearningEnabled || cfg.ThinkingEnabled || cfg.DefaultView != "constellation" || cfg.MaxDisplayNodes != 12000 || !cfg.LowPowerMode {
t.Fatalf("unexpected runtime defaults: %+v", cfg)
}
if len(cfg.LearningCategories) != 2 || len(cfg.DisplayCategories) != 1 || len(cfg.ThinkingCategories) != 1 {
+9 -1
View File
@@ -1457,7 +1457,15 @@ func metadataString(metadata map[string]any, key string) string {
if metadata == nil {
return ""
}
return strings.TrimSpace(fmt.Sprint(metadata[key]))
value, ok := metadata[key]
if !ok || value == nil {
return ""
}
text := strings.TrimSpace(fmt.Sprint(value))
if text == "" || strings.EqualFold(text, "<nil>") || strings.EqualFold(text, "null") {
return ""
}
return text
}
func metadataStringSlice(metadata map[string]any, key string) []string {
@@ -89,3 +89,21 @@ func TestLoadResearchEvidenceCanReuseInMemoryQueuedRecord(t *testing.T) {
t.Fatalf("unexpected cached evidence: %+v", loaded)
}
}
func TestMetadataStringTreatsMissingAndNilAsEmpty(t *testing.T) {
cases := []map[string]any{
nil,
{},
{"evidence_path": nil},
{"evidence_path": "<nil>"},
{"evidence_path": "null"},
}
for _, metadata := range cases {
if got := metadataString(metadata, "evidence_path"); got != "" {
t.Fatalf("expected empty metadata string, got %q from %#v", got, metadata)
}
}
if got := metadataString(map[string]any{"evidence_path": " research-evidence/x.json "}, "evidence_path"); got != "research-evidence/x.json" {
t.Fatalf("unexpected normalized metadata string %q", got)
}
}
+4 -3
View File
@@ -392,13 +392,14 @@ func TestRuntimeSettingsDisableThinkingAndPersist(t *testing.T) {
LearningCategories: []string{"Netzwerk"},
DisplayCategories: []string{"GLPI KB"},
ThinkingCategories: []string{"Netzwerk"},
ViewMode: "honeycomb",
ViewMode: "constellation",
MaxDisplayNodes: 4321,
LowPowerMode: true,
})
if err != nil {
t.Fatal(err)
}
if updated.LearningEnabled || updated.ThinkingEnabled || updated.ViewMode != "honeycomb" || updated.MaxDisplayNodes != 4321 {
if updated.LearningEnabled || updated.ThinkingEnabled || updated.ViewMode != "constellation" || updated.MaxDisplayNodes != 4321 || !updated.LowPowerMode {
t.Fatalf("unexpected runtime settings: %+v", updated)
}
if e.RequestEnrich("manual") {
@@ -417,7 +418,7 @@ func TestRuntimeSettingsDisableThinkingAndPersist(t *testing.T) {
}
e2 := New(cfg, g2, activity.New(20))
loaded := e2.RuntimeSettings()
if loaded.LearningEnabled || loaded.ThinkingEnabled || loaded.ViewMode != "honeycomb" || loaded.MaxDisplayNodes != 4321 || len(loaded.DisplayCategories) != 1 {
if loaded.LearningEnabled || loaded.ThinkingEnabled || loaded.ViewMode != "constellation" || loaded.MaxDisplayNodes != 4321 || !loaded.LowPowerMode || len(loaded.DisplayCategories) != 1 {
t.Fatalf("runtime settings were not restored: %+v", loaded)
}
}
+4 -1
View File
@@ -20,6 +20,7 @@ type RuntimeSettings struct {
ThinkingCategories []string `json:"thinking_categories"`
ViewMode string `json:"view_mode"`
MaxDisplayNodes int `json:"max_display_nodes"`
LowPowerMode bool `json:"low_power_mode"`
}
type CategoryInfo struct {
@@ -36,6 +37,7 @@ func (e *Engine) defaultRuntimeSettings() RuntimeSettings {
ThinkingCategories: append([]string(nil), e.Cfg.ThinkingCategories...),
ViewMode: e.Cfg.DefaultView,
MaxDisplayNodes: e.Cfg.MaxDisplayNodes,
LowPowerMode: e.Cfg.LowPowerMode,
})
}
@@ -47,7 +49,7 @@ func normalizeRuntimeSettings(in RuntimeSettings) RuntimeSettings {
if in.ViewMode == "" {
in.ViewMode = "neural"
}
if in.ViewMode != "neural" && in.ViewMode != "honeycomb" {
if in.ViewMode != "neural" && in.ViewMode != "honeycomb" && in.ViewMode != "constellation" {
in.ViewMode = "neural"
}
if in.MaxDisplayNodes < 0 {
@@ -149,6 +151,7 @@ func (e *Engine) SetRuntimeSettings(settings RuntimeSettings) (RuntimeSettings,
"thinking_categories": len(settings.ThinkingCategories),
"view_mode": settings.ViewMode,
"max_display_nodes": settings.MaxDisplayNodes,
"low_power_mode": settings.LowPowerMode,
},
})
return settings, nil
+2 -2
View File
@@ -37,7 +37,7 @@ func TestRuntimeSettingsAndCategoriesAPI(t *testing.T) {
}, g, broker)
h := (&Server{Engine: eng, Graph: g, Broker: broker}).Handler()
body := `{"learning_enabled":false,"thinking_enabled":false,"learning_categories":["GLPI"],"display_categories":["Ollama"],"thinking_categories":["GLPI"],"view_mode":"honeycomb","max_display_nodes":1500}`
body := `{"learning_enabled":false,"thinking_enabled":false,"learning_categories":["GLPI"],"display_categories":["Ollama"],"thinking_categories":["GLPI"],"view_mode":"constellation","max_display_nodes":1500,"low_power_mode":true}`
req := httptest.NewRequest(http.MethodPut, "/api/runtime-settings", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
res := httptest.NewRecorder()
@@ -49,7 +49,7 @@ func TestRuntimeSettingsAndCategoriesAPI(t *testing.T) {
if err := json.NewDecoder(res.Body).Decode(&settings); err != nil {
t.Fatal(err)
}
if settings.LearningEnabled || settings.ThinkingEnabled || settings.ViewMode != "honeycomb" || settings.MaxDisplayNodes != 1500 {
if settings.LearningEnabled || settings.ThinkingEnabled || settings.ViewMode != "constellation" || settings.MaxDisplayNodes != 1500 || !settings.LowPowerMode {
t.Fatalf("unexpected settings: %+v", settings)
}
+15 -1
View File
@@ -39,7 +39,7 @@ body.honeycomb-view .legend{opacity:.58}body.honeycomb-view .mode-status small:a
.settings-header{display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);padding:2px 2px 13px}.settings-header strong{display:block;font-size:12px;letter-spacing:.17em}.settings-header small{display:block;margin-top:4px;color:var(--muted);font-size:10px}.settings-header button{border:0;background:transparent;color:var(--muted);font-size:24px;cursor:pointer;padding:0 5px}
.settings-section{padding:14px 2px;border-bottom:1px solid var(--line)}.settings-section h2{font-size:10px;letter-spacing:.17em;text-transform:uppercase;color:#9eb5c6;margin:0 0 10px}.settings-section-title{display:flex;justify-content:space-between;align-items:center}.settings-section-title span{font-size:9px;color:#668094}
.switch-row{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:9px 10px;margin:7px 0;border:1px solid rgba(133,200,255,.1);border-radius:12px;background:rgba(255,255,255,.022);cursor:pointer}.switch-row b{display:block;font-size:11px}.switch-row small{display:block;margin-top:3px;color:#718b9e;font-size:9px;line-height:1.35}.switch-row input{appearance:none;width:38px;height:21px;border-radius:999px;background:rgba(119,146,168,.25);position:relative;cursor:pointer;flex:0 0 auto;outline:1px solid rgba(255,255,255,.06)}.switch-row input:after{content:"";position:absolute;width:15px;height:15px;left:3px;top:3px;border-radius:50%;background:#8196a7;transition:transform .2s,background .2s,box-shadow .2s}.switch-row input:checked{background:rgba(82,231,255,.19)}.switch-row input:checked:after{transform:translateX(17px);background:var(--cyan);box-shadow:0 0 10px rgba(82,231,255,.7)}
.view-selector{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:10px}.view-selector button{border:1px solid rgba(133,200,255,.12);background:rgba(255,255,255,.025);color:#738da1;border-radius:10px;padding:9px;font-size:10px;font-weight:700;letter-spacing:.1em;cursor:pointer}.view-selector button.active{color:var(--green);border-color:rgba(93,255,189,.24);background:rgba(93,255,189,.07)}
.view-selector{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-top:10px}.view-selector button{border:1px solid rgba(133,200,255,.12);background:rgba(255,255,255,.025);color:#738da1;border-radius:10px;padding:9px;font-size:10px;font-weight:700;letter-spacing:.1em;cursor:pointer}.view-selector button.active{color:var(--green);border-color:rgba(93,255,189,.24);background:rgba(93,255,189,.07)}
.category-settings{flex:1;min-height:0;overflow:auto;padding-right:5px}.filter-search{display:block;margin-bottom:12px}.filter-search span{display:block;font-size:9px;color:#6d879a;margin-bottom:5px}.filter-search input{width:100%;border:1px solid rgba(133,200,255,.14);background:rgba(2,8,17,.6);color:var(--text);border-radius:10px;padding:9px 10px;outline:0}.filter-search input:focus{border-color:rgba(82,231,255,.38);box-shadow:0 0 0 3px rgba(82,231,255,.06)}
.filter-block{margin:12px 0 16px}.filter-heading{display:flex;align-items:center;justify-content:space-between;margin-bottom:7px}.filter-heading b{display:block;font-size:10px;color:#c8d9e4}.filter-heading small{display:block;margin-top:2px;color:#607b90;font-size:9px}.filter-heading button{border:1px solid rgba(82,231,255,.13);background:rgba(82,231,255,.04);color:#87a8bc;border-radius:8px;padding:5px 8px;font-size:8px;letter-spacing:.08em;cursor:pointer}.category-list{display:flex;flex-wrap:wrap;gap:5px;max-height:128px;overflow:auto;padding:1px}.category-option{position:relative}.category-option input{position:absolute;opacity:0;pointer-events:none}.category-option span{display:block;border:1px solid rgba(133,200,255,.12);background:rgba(255,255,255,.022);color:#7893a6;border-radius:999px;padding:5px 8px;font-size:9px;cursor:pointer;white-space:nowrap}.category-option input:checked+span{color:#dffaff;border-color:rgba(82,231,255,.32);background:rgba(82,231,255,.11);box-shadow:0 0 12px rgba(82,231,255,.05)}.category-option em{font-style:normal;opacity:.58;margin-left:4px}
.settings-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-top:13px}.settings-footer span{font-size:9px;color:#7f9aac;line-height:1.35}.settings-footer button{border:1px solid rgba(82,231,255,.3);background:rgba(82,231,255,.1);color:var(--cyan);border-radius:11px;padding:10px 13px;font-size:9px;font-weight:800;letter-spacing:.13em;cursor:pointer}.settings-footer button:disabled{opacity:.5;cursor:wait}
@@ -51,3 +51,17 @@ body.honeycomb-view .legend{opacity:.58}body.honeycomb-view .mode-status small:a
.activity-item .sources span{display:flex;align-items:flex-start;gap:6px;color:#b7d9ca;font-size:9px;line-height:1.35}
.activity-item .sources i{display:inline-flex;align-items:center;justify-content:center;flex:0 0 15px;height:15px;border-radius:50%;border:1px solid rgba(93,255,189,.24);background:rgba(93,255,189,.07);color:#86ffd0;font-style:normal;font-size:8px}
.research-settings{flex:0 0 auto}.research-status{margin:8px 0 10px;padding:9px 10px;border:1px solid rgba(133,200,255,.11);border-radius:11px;background:rgba(255,255,255,.022);color:#87a2b5;font-size:9px;line-height:1.45;word-break:break-word}.research-status.ok{border-color:rgba(93,255,189,.22);color:#91d9bc;background:rgba(93,255,189,.045)}.research-status.failed{border-color:rgba(255,105,125,.24);color:#ff9aab;background:rgba(255,105,125,.045)}.research-status strong{display:block;color:inherit;font-size:10px;margin-bottom:2px}.research-test-query{display:block}.research-test-query span{display:block;font-size:9px;color:#6d879a;margin-bottom:5px}.research-test-query input{width:100%;border:1px solid rgba(133,200,255,.14);background:rgba(2,8,17,.6);color:var(--text);border-radius:10px;padding:9px 10px;outline:0}.research-test-query input:focus{border-color:rgba(93,255,189,.4);box-shadow:0 0 0 3px rgba(93,255,189,.06)}.research-test-actions{display:flex;align-items:center;gap:9px;margin-top:8px}.research-test-actions button{border:1px solid rgba(93,255,189,.3);background:rgba(93,255,189,.08);color:var(--green);border-radius:10px;padding:8px 10px;font-size:8px;font-weight:800;letter-spacing:.12em;cursor:pointer}.research-test-actions button:disabled{opacity:.5;cursor:wait}.research-test-actions span{font-size:9px;color:#7893a6;line-height:1.35}.activity-item .error-detail{margin-top:7px;padding:7px 8px;border-left:2px solid rgba(255,105,125,.6);background:rgba(255,105,125,.055);color:#ff9aab;font-size:9px;line-height:1.45;white-space:pre-wrap;word-break:break-word}
/* Constellation view, Eco mode and transition diagnostics */
.dock #viewHoneycomb.active,.dock #viewNeural.active,.dock #viewConstellation.active{color:var(--green);border-color:rgba(93,255,189,.24);background:rgba(93,255,189,.07)}
.dock #toggleEco.active{color:var(--amber);border-color:rgba(255,180,82,.28);background:rgba(255,180,82,.08)}
body.constellation-view .mode-status small:after{content:" · Constellation"}
body.constellation-view .legend{opacity:.78}
body.low-power #brain{image-rendering:auto}
body.low-power .vignette{opacity:.78}
body.low-power .glass{backdrop-filter:blur(12px)}
.performance-subtitle{margin-top:13px}
.eco-switch{border-color:rgba(255,180,82,.14)}
.eco-switch input:checked{background:rgba(255,180,82,.2)}
.eco-switch input:checked:after{background:var(--amber);box-shadow:0 0 10px rgba(255,180,82,.72)}
@media(max-width:620px){.view-selector{grid-template-columns:1fr}.metrics span[title]{display:none}}
+889 -231
View File
File diff suppressed because it is too large Load Diff
+10 -1
View File
@@ -26,6 +26,7 @@
<span><b id="nodeCount">0</b> Nodes</span>
<span><b id="edgeCount">0</b> Edges</span>
<span title="Aktuell gerenderte Knoten"><b id="renderCount">0</b> Render</span>
<span title="Gemessene Renderfrequenz"><b id="fpsCount">0</b> FPS</span>
<span><b id="pulseCount">0</b> Impulse</span>
<span class="state" id="systemState"><i></i> verbindet</span>
</div>
@@ -47,6 +48,7 @@
<nav class="dock glass" aria-label="Ansichtssteuerung">
<button id="viewNeural" class="active" title="Semantischer Wissensgraph mit Edges">NEURAL</button>
<button id="viewHoneycomb" title="Gleichmäßig verteilte 3D-Wabenansicht ohne Edges">HONEYCOMB</button>
<button id="viewConstellation" title="Semantische Themen-Sternbilder mit leichten Clusterpfaden">CONSTELLATION</button>
<span class="dock-separator"></span>
<button id="toggleLearning" class="active" title="Lernen, Reindex und Embeddings aktivieren oder pausieren">LEARNING</button>
<button id="toggleThinking" class="active" title="Autonomes AI-THINK aktivieren oder pausieren">THINKING</button>
@@ -56,6 +58,7 @@
<button id="toggleRotate" class="active">Autopilot</button>
<button id="toggleCortex" class="active">Cortex</button>
<button id="toggleLOD" class="active" title="Hierarchische dynamische Verdichtung">LOD</button>
<button id="toggleEco" title="Optimierter Renderpfad für schwächere Systeme">ECO</button>
<button id="resetView">Zentrieren</button>
<button id="openSettings" title="Kategorie-Filter und Laufzeitsteuerung">FILTER</button>
<button id="enrichNow" class="think-action" title="Einen autonomen AI-THINK-Zyklus sofort starten"><span>AI-THINK</span><small>STARTEN</small></button>
@@ -89,11 +92,17 @@
<div class="view-selector" aria-label="Standardansicht">
<button id="settingsViewNeural" class="active" type="button">Neural</button>
<button id="settingsViewHoneycomb" type="button">Honeycomb</button>
<button id="settingsViewConstellation" type="button">Constellation</button>
</div>
</section>
<section class="settings-section performance-settings">
<div class="settings-section-title"><h2>GPU-Limit</h2><span>0 = unbegrenzt</span></div>
<div class="settings-section-title"><h2>Performance</h2><span>Browser-Rendering</span></div>
<label class="switch-row eco-switch" for="settingsLowPower">
<span><b>Eco-Modus</b><small>30 FPS, geringere Pixeldichte und günstigere Glow-Effekte. Aktivität, Rechercheanimationen und Fades bleiben sichtbar.</small></span>
<input id="settingsLowPower" type="checkbox">
</label>
<div class="settings-section-title performance-subtitle"><h2>GPU-Limit</h2><span>0 = unbegrenzt</span></div>
<label class="number-setting" for="settingsMaxDisplayNodes">
<span><b>Maximal angezeigte Nodes</b><small>Begrenzt Neural- und Honeycomb-Ansicht. Aktive Notes werden bei Bedarf temporär eingeblendet und ersetzen inaktive Nodes.</small></span>
<input id="settingsMaxDisplayNodes" type="number" min="0" max="500000" step="100" inputmode="numeric" value="0">