superlocalmemory 3.6.20 → 3.6.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin-src/manifest.json +1 -1
- package/plugin-src/requirements.txt +1 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/mcp/tools_v3.py +4 -16
- package/src/superlocalmemory/server/routes/mesh.py +12 -4
- package/src/superlocalmemory/server/routes/v3_api.py +59 -47
- package/src/superlocalmemory/ui/js/auto-settings.js +8 -11
- package/src/superlocalmemory/ui/js/core.js +3 -0
- package/src/superlocalmemory/ui/js/dashboard.js +23 -6
- package/src/superlocalmemory/ui/js/knowledge-graph.js +4 -1
- package/src/superlocalmemory/ui/js/memories.js +1 -0
- package/src/superlocalmemory/ui/js/memory-chat.js +1 -1
- package/src/superlocalmemory/ui/js/ng-mesh.js +123 -21
- package/src/superlocalmemory/ui/js/optimize.js +6 -1
- package/src/superlocalmemory/ui/js/profiles.js +1 -1
- package/src/superlocalmemory/ui/js/recall-lab.js +9 -3
- package/src/superlocalmemory/ui/js/timeline.js +3 -2
- package/src/superlocalmemory.egg-info/PKG-INFO +1 -1
- package/src/superlocalmemory.egg-info/SOURCES.txt +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superlocalmemory",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.21",
|
|
4
4
|
"description": "Information-geometric agent memory with mathematical guarantees. 4-channel retrieval, Fisher-Rao similarity, zero-LLM mode, EU AI Act compliant. Works with Claude, Cursor, Windsurf, and 17+ AI tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-memory",
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.6.
|
|
1
|
+
superlocalmemory==3.6.21
|
package/plugin-src/manifest.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==3.6.
|
|
1
|
+
superlocalmemory==3.6.21
|
package/pyproject.toml
CHANGED
|
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
32
32
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
__version__ = "3.6.
|
|
35
|
+
__version__ = "3.6.21"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -65,25 +65,13 @@ def register_v3_tools(server, get_engine: Callable) -> None:
|
|
|
65
65
|
"error": f"Invalid mode '{mode}'. Use 'a', 'b', or 'c'.",
|
|
66
66
|
}
|
|
67
67
|
from superlocalmemory.core.config import SLMConfig
|
|
68
|
-
from superlocalmemory.storage.models import Mode
|
|
69
68
|
from superlocalmemory.mcp.server import reset_engine
|
|
70
69
|
|
|
71
|
-
|
|
70
|
+
# Use switch_mode() — the correct load-then-patch path that preserves
|
|
71
|
+
# all user-tuned config blocks (forgetting, injection, retrieval, scope,
|
|
72
|
+
# math, channel_weights, …). for_mode() resets them to hardcoded defaults.
|
|
72
73
|
old_config = SLMConfig.load()
|
|
73
|
-
config = SLMConfig.
|
|
74
|
-
mode_enum,
|
|
75
|
-
llm_provider=old_config.llm.provider,
|
|
76
|
-
llm_model=old_config.llm.model,
|
|
77
|
-
llm_api_key=old_config.llm.api_key,
|
|
78
|
-
llm_api_base=old_config.llm.api_base,
|
|
79
|
-
embedding_provider=old_config.embedding.provider,
|
|
80
|
-
embedding_endpoint=old_config.embedding.api_endpoint,
|
|
81
|
-
embedding_key=old_config.embedding.api_key,
|
|
82
|
-
embedding_model_name=old_config.embedding.model_name,
|
|
83
|
-
embedding_dimension=old_config.embedding.dimension,
|
|
84
|
-
)
|
|
85
|
-
config.active_profile = old_config.active_profile
|
|
86
|
-
config.save(mode_change=True)
|
|
74
|
+
config = SLMConfig.switch_mode(mode_lower)
|
|
87
75
|
|
|
88
76
|
# V3.3: Check if embedding model changed — flag for re-indexing
|
|
89
77
|
needs_reindex = (
|
|
@@ -87,15 +87,23 @@ def _get_broker(request: Request):
|
|
|
87
87
|
client_host = request.client.host if request.client else ""
|
|
88
88
|
if client_host not in ("127.0.0.1", "::1", "localhost"):
|
|
89
89
|
import hmac
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
#
|
|
90
|
+
from superlocalmemory.core.security_primitives import verify_install_token
|
|
91
|
+
|
|
92
|
+
# Path 1: install token — dashboard/browser callers hold this and
|
|
93
|
+
# should not need the mesh secret exposed in JS.
|
|
94
|
+
install_token = request.headers.get("x-install-token", "")
|
|
95
|
+
if install_token and verify_install_token(install_token):
|
|
96
|
+
return broker
|
|
97
|
+
|
|
98
|
+
# Path 2: mesh secret — remote agents / remote_sync.py / LAN peers.
|
|
99
|
+
# Accept X-Mesh-Secret (legacy v3.6.12 header) OR
|
|
100
|
+
# Authorization: Bearer <secret> (RFC 7617 canonical form).
|
|
93
101
|
presented = (
|
|
94
102
|
request.headers.get("x-mesh-secret")
|
|
95
103
|
or request.headers.get("authorization", "").removeprefix("Bearer ").strip()
|
|
96
104
|
)
|
|
97
105
|
if not presented or not hmac.compare_digest(presented, secret):
|
|
98
|
-
raise HTTPException(401, detail="invalid or missing
|
|
106
|
+
raise HTTPException(401, detail="invalid or missing credential")
|
|
99
107
|
return broker
|
|
100
108
|
|
|
101
109
|
|
|
@@ -123,20 +123,17 @@ async def set_mode(request: Request):
|
|
|
123
123
|
status_code=400,
|
|
124
124
|
)
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
new_config.active_profile = old_config.active_profile
|
|
139
|
-
new_config.save(mode_change=True)
|
|
126
|
+
# Apply new mode's structural presets (retrieval, math, channel_weights)
|
|
127
|
+
# by building a fresh template, then graft them onto the loaded config so
|
|
128
|
+
# all user-tuned blocks (forgetting, injection, consolidation, scope, …)
|
|
129
|
+
# are preserved across the mode switch.
|
|
130
|
+
_template = SLMConfig.for_mode(Mode(new_mode))
|
|
131
|
+
old_config.mode = Mode(new_mode)
|
|
132
|
+
old_config.retrieval = _template.retrieval
|
|
133
|
+
old_config.math = _template.math
|
|
134
|
+
old_config.channel_weights = _template.channel_weights
|
|
135
|
+
old_config.save(mode_change=True)
|
|
136
|
+
new_config = old_config
|
|
140
137
|
|
|
141
138
|
# Audit the change before we lose context — proves who/when/what.
|
|
142
139
|
# Captures the phantom-write case where `for_mode(C)` auto-defaults
|
|
@@ -184,35 +181,52 @@ async def set_full_config(request: Request):
|
|
|
184
181
|
if new_mode not in ("a", "b", "c"):
|
|
185
182
|
return JSONResponse({"error": "Invalid mode"}, status_code=400)
|
|
186
183
|
|
|
187
|
-
from superlocalmemory.core.config import SLMConfig
|
|
184
|
+
from superlocalmemory.core.config import SLMConfig, EmbeddingConfig, LLMConfig
|
|
188
185
|
from superlocalmemory.storage.models import Mode
|
|
189
186
|
from superlocalmemory.server.routes.helpers import log_mode_change
|
|
190
|
-
|
|
191
|
-
old_mode =
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
# llama.cpp / LM Studio / Azure-OpenAI endpoint configured in the
|
|
195
|
-
# dashboard could never be saved (Test Connection then probed the wrong
|
|
196
|
-
# URL → 401). Accept both base_url and endpoint; default ollama locally.
|
|
187
|
+
config = SLMConfig.load()
|
|
188
|
+
old_mode = config.mode.value
|
|
189
|
+
|
|
190
|
+
# v3.6.12 (settings-2): honor a custom endpoint for ANY provider.
|
|
197
191
|
_endpoint = (body.get("base_url", "") or body.get("endpoint", "")).strip()
|
|
198
192
|
if not _endpoint and provider == "ollama":
|
|
199
193
|
_endpoint = "http://localhost:11434"
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
embedding_dimension=int(body.get("embedding_dimension", 0) or 0),
|
|
194
|
+
|
|
195
|
+
# Mutate only the fields the dashboard sent — all other config blocks
|
|
196
|
+
# (forgetting, injection, retrieval, math, consolidation, scope, …) are
|
|
197
|
+
# preserved because we loaded the full existing config above.
|
|
198
|
+
config.mode = Mode(new_mode)
|
|
199
|
+
config.llm = LLMConfig(
|
|
200
|
+
provider=provider if provider != "none" else "",
|
|
201
|
+
model=model,
|
|
202
|
+
api_key=api_key,
|
|
203
|
+
api_base=_endpoint,
|
|
211
204
|
)
|
|
212
|
-
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
|
|
205
|
+
|
|
206
|
+
# Update embedding only when the dashboard explicitly sent those fields;
|
|
207
|
+
# absence means "leave it alone" (AIDEV-86 / broader fix).
|
|
208
|
+
_emb_fields = ("embedding_provider", "embedding_endpoint", "embedding_key",
|
|
209
|
+
"embedding_model", "embedding_dimension")
|
|
210
|
+
if any(k in body for k in _emb_fields):
|
|
211
|
+
config.embedding = EmbeddingConfig(
|
|
212
|
+
provider=body.get("embedding_provider", ""),
|
|
213
|
+
api_endpoint=body.get("embedding_endpoint", ""),
|
|
214
|
+
api_key=body.get("embedding_key", ""),
|
|
215
|
+
model_name=body.get("embedding_model", ""),
|
|
216
|
+
dimension=int(body.get("embedding_dimension", 0) or 0),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# When the mode actually changed, apply the new mode's structural presets
|
|
220
|
+
# (retrieval topology, math thresholds, channel weights) so the user gets
|
|
221
|
+
# the right runtime behaviour for their chosen mode.
|
|
222
|
+
if new_mode != old_mode:
|
|
223
|
+
_template = SLMConfig.for_mode(Mode(new_mode))
|
|
224
|
+
config.retrieval = _template.retrieval
|
|
225
|
+
config.math = _template.math
|
|
226
|
+
config.channel_weights = _template.channel_weights
|
|
227
|
+
|
|
228
|
+
# v3.6.12 (settings-1): mode_change=True is required to persist the new
|
|
229
|
+
# mode — save() without it hits a guard that preserves the old mode.
|
|
216
230
|
config.save(mode_change=True)
|
|
217
231
|
|
|
218
232
|
log_mode_change(
|
|
@@ -607,8 +621,7 @@ async def set_provider(request: Request):
|
|
|
607
621
|
model = body.get("model", "")
|
|
608
622
|
base_url = body.get("base_url", "")
|
|
609
623
|
|
|
610
|
-
from superlocalmemory.core.config import SLMConfig
|
|
611
|
-
from superlocalmemory.storage.models import Mode
|
|
624
|
+
from superlocalmemory.core.config import SLMConfig, LLMConfig
|
|
612
625
|
config = SLMConfig.load()
|
|
613
626
|
|
|
614
627
|
# Use preset base_url if not provided
|
|
@@ -619,15 +632,14 @@ async def set_provider(request: Request):
|
|
|
619
632
|
if not model:
|
|
620
633
|
model = preset.get("model", "")
|
|
621
634
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
635
|
+
# Mutate only the LLM block — all other config is preserved.
|
|
636
|
+
config.llm = LLMConfig(
|
|
637
|
+
provider=provider,
|
|
638
|
+
model=model,
|
|
639
|
+
api_key=api_key,
|
|
640
|
+
api_base=base_url,
|
|
628
641
|
)
|
|
629
|
-
|
|
630
|
-
new_config.save()
|
|
642
|
+
config.save()
|
|
631
643
|
|
|
632
644
|
return {"success": True, "provider": provider, "model": model}
|
|
633
645
|
except Exception as e:
|
|
@@ -52,17 +52,14 @@ function saveAutoRecallConfig() {
|
|
|
52
52
|
}).catch(function(e) { console.log('Save auto-recall error:', e); });
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
document.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
document.querySelectorAll('#auto-recall-toggle, #auto-recall-session').forEach(function(el) {
|
|
64
|
-
if (el) {
|
|
65
|
-
el.addEventListener('change', saveAutoRecallConfig);
|
|
55
|
+
// Use delegation so listeners work even when the settings pane is injected lazily.
|
|
56
|
+
document.addEventListener('change', function(e) {
|
|
57
|
+
var id = e.target && e.target.id;
|
|
58
|
+
if (!id) return;
|
|
59
|
+
if (id === 'auto-capture-toggle' || id === 'auto-capture-decisions' || id === 'auto-capture-bugs') {
|
|
60
|
+
saveAutoCaptureConfig();
|
|
61
|
+
} else if (id === 'auto-recall-toggle' || id === 'auto-recall-session') {
|
|
62
|
+
saveAutoRecallConfig();
|
|
66
63
|
}
|
|
67
64
|
});
|
|
68
65
|
|
|
@@ -416,6 +416,9 @@ function refreshDashboard() {
|
|
|
416
416
|
function populateFilters(categories, projects) {
|
|
417
417
|
var categorySelect = document.getElementById('filter-category');
|
|
418
418
|
var projectSelect = document.getElementById('filter-project');
|
|
419
|
+
// Clear existing options beyond the first placeholder to prevent duplicates on refresh
|
|
420
|
+
if (categorySelect) while (categorySelect.options.length > 1) categorySelect.remove(1);
|
|
421
|
+
if (projectSelect) while (projectSelect.options.length > 1) projectSelect.remove(1);
|
|
419
422
|
categories.forEach(function(cat) {
|
|
420
423
|
if (cat.category) {
|
|
421
424
|
var option = document.createElement('option');
|
|
@@ -63,7 +63,12 @@ document.addEventListener('click', function(e) {
|
|
|
63
63
|
method: 'PUT',
|
|
64
64
|
headers: {'Content-Type': 'application/json'},
|
|
65
65
|
body: JSON.stringify({mode: mode})
|
|
66
|
-
}).then(function() {
|
|
66
|
+
}).then(function(r) {
|
|
67
|
+
if (!r.ok) return r.json().catch(function() { return {}; }).then(function(d) {
|
|
68
|
+
showToast('Mode switch failed: ' + (d.error || d.detail || r.status), 'error');
|
|
69
|
+
});
|
|
70
|
+
return r.json().then(function() { loadDashboard(); });
|
|
71
|
+
}).catch(function() { showToast('Mode switch failed: network error', 'error'); });
|
|
67
72
|
}
|
|
68
73
|
});
|
|
69
74
|
|
|
@@ -72,15 +77,21 @@ document.getElementById('quick-store-btn')?.addEventListener('click', function()
|
|
|
72
77
|
var input = document.getElementById('quick-store-input');
|
|
73
78
|
var content = input.value.trim();
|
|
74
79
|
if (!content) return;
|
|
75
|
-
fetch('/
|
|
80
|
+
fetch('/remember', {
|
|
76
81
|
method: 'POST',
|
|
77
82
|
headers: {'Content-Type': 'application/json'},
|
|
78
83
|
body: JSON.stringify({content: content})
|
|
79
|
-
}).then(function(r) {
|
|
84
|
+
}).then(function(r) {
|
|
85
|
+
if (!r.ok) return r.json().catch(function() { return {}; }).then(function(d) {
|
|
86
|
+
showToast('Store failed: ' + (d.detail || d.error || r.status), 'error');
|
|
87
|
+
});
|
|
88
|
+
return r.json();
|
|
89
|
+
}).then(function(data) {
|
|
90
|
+
if (!data) return;
|
|
80
91
|
input.value = '';
|
|
81
92
|
loadDashboard();
|
|
82
|
-
|
|
83
|
-
});
|
|
93
|
+
showToast('Stored!');
|
|
94
|
+
}).catch(function() { showToast('Store failed: network error', 'error'); });
|
|
84
95
|
});
|
|
85
96
|
|
|
86
97
|
// Quick recall
|
|
@@ -91,7 +102,10 @@ document.getElementById('quick-recall-btn')?.addEventListener('click', function(
|
|
|
91
102
|
method: 'POST',
|
|
92
103
|
headers: {'Content-Type': 'application/json'},
|
|
93
104
|
body: JSON.stringify({query: query, limit: 5})
|
|
94
|
-
}).then(function(r) {
|
|
105
|
+
}).then(function(r) {
|
|
106
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
107
|
+
return r.json();
|
|
108
|
+
}).then(function(data) {
|
|
95
109
|
var div = document.getElementById('quick-recall-results');
|
|
96
110
|
if (!data.results || data.results.length === 0) {
|
|
97
111
|
div.textContent = 'No results found.';
|
|
@@ -113,5 +127,8 @@ document.getElementById('quick-recall-btn')?.addEventListener('click', function(
|
|
|
113
127
|
row.appendChild(scoreSpan);
|
|
114
128
|
div.appendChild(row);
|
|
115
129
|
});
|
|
130
|
+
}).catch(function(e) {
|
|
131
|
+
var div = document.getElementById('quick-recall-results');
|
|
132
|
+
if (div) div.textContent = 'Search failed. Is the daemon running?';
|
|
116
133
|
});
|
|
117
134
|
});
|
|
@@ -618,7 +618,10 @@ function loadGraphSigma() {
|
|
|
618
618
|
if (typeof showLoadingSpinner === 'function') showLoadingSpinner();
|
|
619
619
|
|
|
620
620
|
fetch('/api/graph?max_nodes=' + fetchLimit + '&min_importance=' + minImportance)
|
|
621
|
-
.then(function(r) {
|
|
621
|
+
.then(function(r) {
|
|
622
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
623
|
+
return r.json();
|
|
624
|
+
})
|
|
622
625
|
.then(function(data) {
|
|
623
626
|
// Store in shared globals
|
|
624
627
|
if (typeof window.graphData !== 'undefined') window.graphData = data;
|
|
@@ -41,6 +41,7 @@ async function loadMemories(page) {
|
|
|
41
41
|
showLoading('memories-list', 'Loading memories...');
|
|
42
42
|
try {
|
|
43
43
|
var response = await fetch(url);
|
|
44
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
44
45
|
var data = await response.json();
|
|
45
46
|
lastSearchResults = null;
|
|
46
47
|
var exportBtn = document.getElementById('export-search-btn');
|
|
@@ -157,7 +157,7 @@ function sendChatQuery(query) {
|
|
|
157
157
|
|
|
158
158
|
var currentEvent = '';
|
|
159
159
|
for (var i = 0; i < lines.length; i++) {
|
|
160
|
-
var line = lines[i];
|
|
160
|
+
var line = lines[i].replace(/\r$/, ''); // strip \r from \r\n streams
|
|
161
161
|
if (line.startsWith('event: ')) {
|
|
162
162
|
currentEvent = line.substring(7).trim();
|
|
163
163
|
} else if (line.startsWith('data: ')) {
|
|
@@ -8,56 +8,123 @@
|
|
|
8
8
|
var REFRESH_INTERVAL = 5000; // 5 seconds
|
|
9
9
|
var refreshTimer = null;
|
|
10
10
|
|
|
11
|
+
// ── Install-token bootstrap (mirrors brain.js) ───────────────
|
|
12
|
+
// Dashboard callers authenticate with the install token so we never
|
|
13
|
+
// embed the mesh secret in JS. Token is fetched from /internal/token
|
|
14
|
+
// (loopback-only endpoint) and cached in sessionStorage for the tab session.
|
|
15
|
+
var TOKEN_STORAGE_KEY = 'slm_install_token';
|
|
16
|
+
|
|
17
|
+
function readToken() {
|
|
18
|
+
try { return window.sessionStorage ? window.sessionStorage.getItem(TOKEN_STORAGE_KEY) : null; }
|
|
19
|
+
catch (e) { return null; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeToken(value) {
|
|
23
|
+
try { if (window.sessionStorage) window.sessionStorage.setItem(TOKEN_STORAGE_KEY, value); }
|
|
24
|
+
catch (e) { /* storage disabled */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fetchTokenFromServer() {
|
|
28
|
+
return fetch('/internal/token', { credentials: 'same-origin' })
|
|
29
|
+
.then(function(r) { return r.ok ? r.json() : null; })
|
|
30
|
+
.then(function(data) {
|
|
31
|
+
var tok = data && typeof data.token === 'string' ? data.token.trim() : '';
|
|
32
|
+
if (tok) { writeToken(tok); return tok; }
|
|
33
|
+
return null;
|
|
34
|
+
})
|
|
35
|
+
.catch(function() { return null; });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function ensureToken() {
|
|
39
|
+
var cached = readToken();
|
|
40
|
+
if (cached) return Promise.resolve(cached);
|
|
41
|
+
return fetchTokenFromServer();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Authenticated fetch: injects X-Install-Token; retries once on 401
|
|
45
|
+
// (token may have been rotated since the tab was opened).
|
|
46
|
+
function meshFetch(url) {
|
|
47
|
+
return ensureToken().then(function(token) {
|
|
48
|
+
var opts = token ? { headers: { 'X-Install-Token': token }, credentials: 'same-origin' } : {};
|
|
49
|
+
return fetch(url, opts).then(function(r) {
|
|
50
|
+
if (r.status === 401) {
|
|
51
|
+
return fetchTokenFromServer().then(function(freshToken) {
|
|
52
|
+
var retryOpts = freshToken
|
|
53
|
+
? { headers: { 'X-Install-Token': freshToken }, credentials: 'same-origin' }
|
|
54
|
+
: {};
|
|
55
|
+
return fetch(url, retryOpts);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return r;
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Public entry point ────────────────────────────────────────
|
|
11
64
|
window.loadMeshPeers = function() {
|
|
12
65
|
fetchMeshStatus();
|
|
13
66
|
fetchMeshPeers();
|
|
14
67
|
fetchMeshEvents();
|
|
15
68
|
fetchMeshState();
|
|
16
69
|
|
|
17
|
-
// Auto-refresh while tab is active
|
|
70
|
+
// Auto-refresh while tab is active; stop when pane loses focus
|
|
18
71
|
clearInterval(refreshTimer);
|
|
19
72
|
refreshTimer = setInterval(function() {
|
|
20
73
|
var pane = document.getElementById('mesh-pane');
|
|
21
74
|
if (pane && pane.classList.contains('active')) {
|
|
22
75
|
fetchMeshStatus();
|
|
23
76
|
fetchMeshPeers();
|
|
77
|
+
fetchMeshEvents();
|
|
78
|
+
fetchMeshState();
|
|
79
|
+
} else {
|
|
80
|
+
// Pane is no longer active — stop the timer to avoid background polling
|
|
81
|
+
clearInterval(refreshTimer);
|
|
82
|
+
refreshTimer = null;
|
|
24
83
|
}
|
|
25
84
|
}, REFRESH_INTERVAL);
|
|
26
85
|
};
|
|
27
86
|
|
|
28
87
|
// Try BOTH brokers: daemon (port 8765 /mesh/*) AND standalone slm-mesh (port 7899 /*)
|
|
29
|
-
var STANDALONE_PORT = null;
|
|
88
|
+
var STANDALONE_PORT = null;
|
|
89
|
+
|
|
90
|
+
function fetchStandaloneBroker(path) {
|
|
91
|
+
var ports = [7899];
|
|
92
|
+
return fetch('http://127.0.0.1:' + ports[0] + path, { signal: AbortSignal.timeout(2000) })
|
|
93
|
+
.then(function(r) {
|
|
94
|
+
if (r.ok) { STANDALONE_PORT = ports[0]; return r.json(); }
|
|
95
|
+
STANDALONE_PORT = null;
|
|
96
|
+
return null;
|
|
97
|
+
})
|
|
98
|
+
.catch(function() { STANDALONE_PORT = null; return null; });
|
|
99
|
+
}
|
|
30
100
|
|
|
31
101
|
function fetchMeshStatus() {
|
|
32
|
-
// Try daemon broker first, then standalone
|
|
33
102
|
Promise.all([
|
|
34
|
-
|
|
103
|
+
meshFetch('/mesh/status').then(function(r) {
|
|
104
|
+
if (!r.ok) return r.status === 401 ? { _auth_error: true } : null;
|
|
105
|
+
return r.json();
|
|
106
|
+
}).catch(function() { return null; }),
|
|
35
107
|
fetchStandaloneBroker('/health')
|
|
36
108
|
]).then(function(results) {
|
|
37
109
|
var daemon = results[0];
|
|
38
|
-
|
|
39
|
-
renderMeshStatus(daemon,
|
|
110
|
+
if (daemon && daemon._auth_error) { renderMeshStatusAuthError(); return; }
|
|
111
|
+
renderMeshStatus(daemon, results[1]);
|
|
40
112
|
});
|
|
41
113
|
}
|
|
42
114
|
|
|
43
|
-
function fetchStandaloneBroker(path) {
|
|
44
|
-
// Try port file first, then default 7899
|
|
45
|
-
var ports = [7899];
|
|
46
|
-
return fetch('http://127.0.0.1:' + ports[0] + path, { signal: AbortSignal.timeout(2000) })
|
|
47
|
-
.then(function(r) { STANDALONE_PORT = ports[0]; return r.json(); })
|
|
48
|
-
.catch(function() { return null; });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
115
|
function fetchMeshPeers() {
|
|
52
|
-
// Fetch from BOTH brokers and merge
|
|
53
116
|
Promise.all([
|
|
54
|
-
|
|
117
|
+
meshFetch('/mesh/peers').then(function(r) {
|
|
118
|
+
if (!r.ok) return r.status === 401 ? { _auth_error: true } : { peers: [] };
|
|
119
|
+
return r.json();
|
|
120
|
+
}).catch(function() { return { peers: [] }; }),
|
|
55
121
|
fetchStandaloneBroker('/peers')
|
|
56
122
|
]).then(function(results) {
|
|
57
|
-
var
|
|
123
|
+
var daemonResult = results[0];
|
|
124
|
+
if (daemonResult && daemonResult._auth_error) { renderMeshPeersAuthError(); return; }
|
|
125
|
+
var daemonPeers = (daemonResult && daemonResult.peers) || [];
|
|
58
126
|
var standalonePeers = (results[1] && (results[1].peers || results[1])) || [];
|
|
59
127
|
if (!Array.isArray(standalonePeers)) standalonePeers = [];
|
|
60
|
-
// Merge, dedup by peer_id
|
|
61
128
|
var seen = {};
|
|
62
129
|
var allPeers = [];
|
|
63
130
|
daemonPeers.concat(standalonePeers).forEach(function(p) {
|
|
@@ -66,7 +133,8 @@
|
|
|
66
133
|
});
|
|
67
134
|
renderMeshPeers(allPeers);
|
|
68
135
|
}).catch(function() {
|
|
69
|
-
document.getElementById('mesh-peers-list')
|
|
136
|
+
var el = document.getElementById('mesh-peers-list');
|
|
137
|
+
if (el) el.innerHTML =
|
|
70
138
|
'<div class="text-center" style="padding:24px;color:var(--ng-text-tertiary)">' +
|
|
71
139
|
'<i class="bi bi-wifi-off" style="font-size:2rem;display:block;margin-bottom:8px"></i>' +
|
|
72
140
|
'Mesh broker not reachable' +
|
|
@@ -75,17 +143,51 @@
|
|
|
75
143
|
}
|
|
76
144
|
|
|
77
145
|
function fetchMeshEvents() {
|
|
78
|
-
|
|
146
|
+
meshFetch('/mesh/events').then(function(r) {
|
|
147
|
+
if (!r.ok) return { events: [] };
|
|
148
|
+
return r.json();
|
|
149
|
+
}).then(function(data) {
|
|
79
150
|
renderMeshEvents(data.events || data || []);
|
|
80
151
|
}).catch(function() {});
|
|
81
152
|
}
|
|
82
153
|
|
|
83
154
|
function fetchMeshState() {
|
|
84
|
-
|
|
155
|
+
meshFetch('/mesh/state').then(function(r) {
|
|
156
|
+
if (!r.ok) return { state: {} };
|
|
157
|
+
return r.json();
|
|
158
|
+
}).then(function(data) {
|
|
85
159
|
renderMeshState(data.state || data || {});
|
|
86
160
|
}).catch(function() {});
|
|
87
161
|
}
|
|
88
162
|
|
|
163
|
+
// ── Auth-error renderers ──────────────────────────────────────
|
|
164
|
+
function renderMeshStatusAuthError() {
|
|
165
|
+
var el = document.getElementById('mesh-status-cards');
|
|
166
|
+
if (!el) return;
|
|
167
|
+
el.innerHTML =
|
|
168
|
+
'<div class="row g-3">' +
|
|
169
|
+
statusCard('Status', statusDot('error') + ' Auth required', 'bi-lock') +
|
|
170
|
+
statusCard('Peers', '—', 'bi-people') +
|
|
171
|
+
statusCard('Uptime', '—', 'bi-clock') +
|
|
172
|
+
statusCard('Brokers', '—', 'bi-hdd-stack') +
|
|
173
|
+
'</div>' +
|
|
174
|
+
'<div style="font-size:0.75rem;color:var(--ng-status-error);margin-top:8px;text-align:center">' +
|
|
175
|
+
'Mesh endpoints returned 401. The daemon may be starting — retrying automatically.' +
|
|
176
|
+
'</div>';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderMeshPeersAuthError() {
|
|
180
|
+
var el = document.getElementById('mesh-peers-list');
|
|
181
|
+
if (!el) return;
|
|
182
|
+
el.innerHTML =
|
|
183
|
+
'<div class="text-center" style="padding:32px;color:var(--ng-text-tertiary)">' +
|
|
184
|
+
'<i class="bi bi-lock" style="font-size:2.5rem;display:block;margin-bottom:12px;opacity:0.5"></i>' +
|
|
185
|
+
'<div style="font-size:0.9375rem;margin-bottom:4px">Authentication required</div>' +
|
|
186
|
+
'<div style="font-size:0.8125rem">Could not authenticate with the mesh broker. ' +
|
|
187
|
+
'The dashboard token may still be loading.</div>' +
|
|
188
|
+
'</div>';
|
|
189
|
+
}
|
|
190
|
+
|
|
89
191
|
function renderMeshStatus(daemonData, standaloneData) {
|
|
90
192
|
var el = document.getElementById('mesh-status-cards');
|
|
91
193
|
if (!el) return;
|
|
@@ -130,13 +130,18 @@
|
|
|
130
130
|
|
|
131
131
|
async function _putConfig(body) {
|
|
132
132
|
try {
|
|
133
|
-
await fetch('/api/optimize/config', {
|
|
133
|
+
var resp = await fetch('/api/optimize/config', {
|
|
134
134
|
method: 'PUT',
|
|
135
135
|
headers: {'Content-Type': 'application/json'},
|
|
136
136
|
body: JSON.stringify(body)
|
|
137
137
|
});
|
|
138
|
+
if (!resp.ok) {
|
|
139
|
+
console.warn('Config update failed: HTTP ' + resp.status);
|
|
140
|
+
_loadOptimizeConfig(); // revert toggles to server state
|
|
141
|
+
}
|
|
138
142
|
} catch (e) {
|
|
139
143
|
console.log('Config update error:', e);
|
|
144
|
+
_loadOptimizeConfig(); // revert toggles to server state
|
|
140
145
|
}
|
|
141
146
|
}
|
|
142
147
|
|
|
@@ -210,7 +210,7 @@ async function switchProfile(profileName) {
|
|
|
210
210
|
showToast('Switched to profile: ' + profileName);
|
|
211
211
|
loadProfiles();
|
|
212
212
|
loadStats();
|
|
213
|
-
loadGraph();
|
|
213
|
+
if (typeof loadGraph === 'function') loadGraph();
|
|
214
214
|
loadProfilesTable();
|
|
215
215
|
// v2.7.4: Reload ALL tabs for new profile
|
|
216
216
|
if (typeof loadLearning === 'function') loadLearning();
|
|
@@ -9,7 +9,9 @@ var recallLabState = {
|
|
|
9
9
|
synthesis: '',
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Delegation handles the case where the recall-lab tab pane is injected after parse time.
|
|
13
|
+
document.addEventListener('click', function(e) {
|
|
14
|
+
if (!e.target || e.target.id !== 'recall-lab-search') return;
|
|
13
15
|
var query = document.getElementById('recall-lab-query').value.trim();
|
|
14
16
|
if (!query) return;
|
|
15
17
|
|
|
@@ -287,6 +289,10 @@ function buildChannelBar(name, score, max, color) {
|
|
|
287
289
|
return row;
|
|
288
290
|
}
|
|
289
291
|
|
|
290
|
-
document.
|
|
291
|
-
if (e.
|
|
292
|
+
document.addEventListener('keydown', function(e) {
|
|
293
|
+
if (!e.target || e.target.id !== 'recall-lab-query') return;
|
|
294
|
+
if (e.key === 'Enter') {
|
|
295
|
+
var btn = document.getElementById('recall-lab-search');
|
|
296
|
+
if (btn) btn.click();
|
|
297
|
+
}
|
|
292
298
|
});
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
async function loadTimeline() {
|
|
5
5
|
showLoading('timeline-chart', 'Loading timeline...');
|
|
6
6
|
try {
|
|
7
|
-
var response = await fetch('/api/timeline
|
|
7
|
+
var response = await fetch('/api/v3/timeline/?range=30d&group_by=date&limit=1000');
|
|
8
|
+
if (!response.ok) throw new Error('HTTP ' + response.status);
|
|
8
9
|
var data = await response.json();
|
|
9
|
-
renderTimeline(data.timeline);
|
|
10
|
+
renderTimeline(data.events || data.timeline);
|
|
10
11
|
} catch (error) {
|
|
11
12
|
console.error('Error loading timeline:', error);
|
|
12
13
|
showEmpty('timeline-chart', 'clock-history', 'Failed to load timeline');
|