superlocalmemory 3.8.0 → 3.8.2

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.
Files changed (134) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +32 -120
  3. package/package.json +9 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -2
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +2 -2
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +3 -5
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +2 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +3 -5
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +2 -1
  32. package/scripts/postinstall.js +7 -1
  33. package/src/superlocalmemory/__init__.py +1 -1
  34. package/src/superlocalmemory/cli/commands.py +494 -9
  35. package/src/superlocalmemory/cli/daemon.py +7 -0
  36. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  37. package/src/superlocalmemory/cli/main.py +72 -7
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -16
  39. package/src/superlocalmemory/cli/version_banner.py +17 -3
  40. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  41. package/src/superlocalmemory/core/component_healer.py +144 -0
  42. package/src/superlocalmemory/core/component_registry.py +487 -0
  43. package/src/superlocalmemory/core/config.py +21 -0
  44. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  45. package/src/superlocalmemory/core/embeddings.py +132 -45
  46. package/src/superlocalmemory/core/engine.py +29 -22
  47. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  48. package/src/superlocalmemory/core/ingestion_command.py +154 -25
  49. package/src/superlocalmemory/core/injection.py +12 -7
  50. package/src/superlocalmemory/core/maintenance.py +43 -0
  51. package/src/superlocalmemory/core/maintenance_scheduler.py +44 -6
  52. package/src/superlocalmemory/core/recall_pipeline.py +42 -4
  53. package/src/superlocalmemory/core/store_pipeline.py +195 -20
  54. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  55. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  56. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  57. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  58. package/src/superlocalmemory/learning/reward.py +50 -0
  59. package/src/superlocalmemory/learning/source_quality.py +523 -1
  60. package/src/superlocalmemory/loops/ledger.py +25 -5
  61. package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
  62. package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
  63. package/src/superlocalmemory/mcp/server.py +11 -30
  64. package/src/superlocalmemory/mcp/tools_active.py +1 -1
  65. package/src/superlocalmemory/mcp/tools_core.py +21 -5
  66. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  67. package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
  68. package/src/superlocalmemory/retrieval/engine.py +53 -21
  69. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  70. package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
  71. package/src/superlocalmemory/server/config_file.py +90 -0
  72. package/src/superlocalmemory/server/origin.py +50 -0
  73. package/src/superlocalmemory/server/routes/backup.py +293 -70
  74. package/src/superlocalmemory/server/routes/behavioral.py +342 -61
  75. package/src/superlocalmemory/server/routes/brain.py +57 -16
  76. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  77. package/src/superlocalmemory/server/routes/entity.py +100 -23
  78. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  79. package/src/superlocalmemory/server/routes/learning.py +286 -105
  80. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  81. package/src/superlocalmemory/server/routes/memories.py +8 -3
  82. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  83. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  84. package/src/superlocalmemory/server/routes/stats.py +93 -155
  85. package/src/superlocalmemory/server/routes/token.py +3 -13
  86. package/src/superlocalmemory/server/routes/v3_api.py +184 -20
  87. package/src/superlocalmemory/server/unified_daemon.py +732 -41
  88. package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
  89. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  90. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  91. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  92. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  93. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  94. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  95. package/src/superlocalmemory/storage/schema.py +49 -1
  96. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  97. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  98. package/src/superlocalmemory/ui/index.html +6 -8
  99. package/src/superlocalmemory/ui/js/core.js +52 -9
  100. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  101. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  102. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  103. package/src/superlocalmemory/ui/js/od-components.js +147 -0
  104. package/src/superlocalmemory/ui/js/od-entities.js +65 -22
  105. package/src/superlocalmemory/ui/js/od-graph.js +46 -4
  106. package/src/superlocalmemory/ui/js/od-health.js +18 -0
  107. package/src/superlocalmemory/ui/js/od-memories.js +84 -5
  108. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  109. package/src/superlocalmemory/ui/js/od-operations.js +36 -0
  110. package/src/superlocalmemory/ui/js/od-settings.js +186 -63
  111. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  112. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  113. package/src/superlocalmemory/ui/js/settings.js +15 -1
  114. package/plugin-src/.mcp.json +0 -12
  115. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  116. package/plugin-src/agents/slm-loop-runner.md +0 -71
  117. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  118. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  119. package/plugin-src/commands/slm-loop.md +0 -31
  120. package/plugin-src/hooks/.gitkeep +0 -0
  121. package/plugin-src/hooks/hooks.json +0 -102
  122. package/plugin-src/manifest.json +0 -30
  123. package/plugin-src/requirements.txt +0 -1
  124. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  125. package/plugin-src/scripts/ensure-venv.bat +0 -122
  126. package/plugin-src/scripts/ensure-venv.sh +0 -105
  127. package/plugin-src/scripts/slm-launch +0 -62
  128. package/plugin-src/scripts/slm-launch.bat +0 -23
  129. package/plugin-src/settings.json +0 -25
  130. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  131. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  132. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  133. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  134. package/plugin-src/skills/slm-scope/SKILL.md +0 -176
@@ -5,29 +5,64 @@
5
5
  // loadDashboard() → GET /api/v3/dashboard → system card (#dashboard-mode etc.)
6
6
  // loadDashboardStats() → GET /api/stats → OD KPI strip (#k-mem, #k-nodes, #k-edges)
7
7
  // loadDashboardFeed() → GET /api/memories?limit=6 → activity feed (#feed)
8
- // loadDashboardSources() → static from stats breakdown → capture-sources (#src)
9
- // loadDashboardSparklines() → builds sparklines from stats values via slmSpark
8
+ // loadDashboardSources() → real provenance from /api/stats → capture-sources (#src)
9
+ // loadDashboardTimeline() → real daily fact counts from /api/timeline #sp-big
10
+
11
+ var DASHBOARD_CACHE_MS = 30000;
12
+ var dashboardLastRefresh = 0;
13
+ var dashboardRefreshInFlight = null;
14
+ var dashboardCacheGeneration = 0;
15
+ var dashboardRefreshQueued = false;
16
+
17
+ window.slmInvalidateDashboardCache = function() {
18
+ dashboardCacheGeneration += 1;
19
+ dashboardLastRefresh = 0;
20
+ if (dashboardRefreshInFlight) dashboardRefreshQueued = true;
21
+ };
10
22
 
11
23
  // Auto-refresh dashboard when tab becomes visible (fixes stale data after settings change)
12
24
  document.addEventListener('visibilitychange', function() {
13
25
  if (!document.hidden) refreshDashboard();
14
26
  });
15
27
 
16
- // Refresh system card on focus (alt-tab back to browser).
17
- // Only calls loadDashboard() not the feed/sparklines, which would cause
18
- // DOM teardown and rebuild on every focus event (excessive churn).
19
- window.addEventListener('focus', function() { loadDashboard(); });
28
+ // Focus and tab navigation use a short cache window. This keeps normal
29
+ // browser switching from repeatedly querying a large local database.
30
+ window.addEventListener('focus', function() { refreshDashboard(); });
20
31
 
21
32
  // Load when the Dashboard tab is shown via the sidebar. od-shell.js's activateTab()
22
33
  // dispatches shown.bs.tab on the hidden #dashboard-tab button.
23
34
  document.getElementById('dashboard-tab')?.addEventListener('shown.bs.tab', function() { refreshDashboard(); });
24
35
 
25
- // refreshDashboard: fires all OD + legacy loaders in parallel (called by event-delegation.js 'refresh-dashboard')
26
- window.refreshDashboard = function() {
27
- loadDashboard();
28
- loadDashboardStats();
29
- loadDashboardFeed();
30
- loadDashboardSources();
36
+ // refreshDashboard coalesces concurrent startup/navigation events and keeps a
37
+ // 30-second snapshot. Mutations pass {force:true} to re-read daemon truth.
38
+ window.refreshDashboard = function(options) {
39
+ var force = options && options.force === true;
40
+ var now = Date.now();
41
+ if (dashboardRefreshInFlight) {
42
+ if (force) dashboardRefreshQueued = true;
43
+ return dashboardRefreshInFlight;
44
+ }
45
+ if (!force && dashboardLastRefresh && now - dashboardLastRefresh < DASHBOARD_CACHE_MS) {
46
+ return Promise.resolve();
47
+ }
48
+ var refreshGeneration = dashboardCacheGeneration;
49
+ dashboardRefreshInFlight = Promise.all([
50
+ loadDashboard(),
51
+ loadDashboardStats(),
52
+ loadDashboardTimeline(),
53
+ loadDashboardFeed()
54
+ ]).then(function(results) {
55
+ dashboardLastRefresh = refreshGeneration === dashboardCacheGeneration && results.every(Boolean)
56
+ ? Date.now()
57
+ : 0;
58
+ }).finally(function() {
59
+ dashboardRefreshInFlight = null;
60
+ }).then(function() {
61
+ if (!dashboardRefreshQueued) return;
62
+ dashboardRefreshQueued = false;
63
+ return window.refreshDashboard({ force: true });
64
+ });
65
+ return dashboardRefreshInFlight;
31
66
  };
32
67
 
33
68
  // ----------------------------------------------------------------
@@ -36,7 +71,7 @@ window.refreshDashboard = function() {
36
71
  async function loadDashboardStats() {
37
72
  try {
38
73
  var r = await fetch('/api/stats', { credentials: 'same-origin' });
39
- if (!r.ok) return;
74
+ if (!r.ok) return false;
40
75
  var data = await r.json();
41
76
  var ov = data.overview || {};
42
77
 
@@ -44,12 +79,14 @@ async function loadDashboardStats() {
44
79
  var facts = ov.total_facts || 0;
45
80
  var nodes = ov.graph_nodes || 0;
46
81
  var edges = ov.graph_edges || 0;
82
+ var clusters = ov.total_clusters || 0;
47
83
 
48
84
  // OD KPI strip
49
85
  setKpiValue('k-mem', memories);
50
86
  setKpiValue('k-nodes', nodes);
51
87
  setKpiValue('k-edges', edges);
52
88
  setKpiValue('k-facts', facts);
89
+ setKpiValue('k-clu', clusters);
53
90
 
54
91
  // Legacy hidden compat IDs (core.js loadStats() may race — that's OK,
55
92
  // the hidden IDs are only a data bridge, not visible).
@@ -57,51 +94,62 @@ async function loadDashboardStats() {
57
94
  setTextById('stat-facts', facts);
58
95
  setTextById('stat-nodes', nodes);
59
96
  setTextById('stat-edges', edges);
60
-
61
- // Sparklines — build synthetic ascending series from the real count
62
- // (no time-series endpoint exists yet; honest placeholder).
63
- // TODO: replace with GET /api/v3/stats/history when that endpoint ships.
64
- if (typeof window.slmSpark === 'function') {
65
- loadDashboardSparklines(memories, nodes, edges);
66
- }
97
+ loadDashboardSources(data);
98
+ return true;
67
99
  } catch (e) {
68
100
  // Non-fatal: KPI strip shows — values
101
+ return false;
69
102
  }
70
103
  }
71
104
 
72
105
  // ----------------------------------------------------------------
73
- // loadDashboardSparklinesbuilds inline SVG sparklines from totals
74
- // No time-series endpoint yet: we synthesise a plausible trailing
75
- // series anchored to the real current total. Honest placeholder.
106
+ // loadDashboardTimelinerenders real daily fact counts from /api/timeline
76
107
  // ----------------------------------------------------------------
77
- function loadDashboardSparklines(memories, nodes, edges) {
78
- function syntheticSeries(current, n, volatility) {
79
- var a = [];
80
- var v = current * 0.60;
81
- var step = (current - v) / n;
82
- for (var i = 0; i < n; i++) {
83
- v += step + (Math.random() - 0.45) * volatility * current;
84
- a.push(Math.max(0, Math.round(v)));
108
+ async function loadDashboardTimeline() {
109
+ var spBig = document.getElementById('sp-big');
110
+ if (!spBig) return true;
111
+ try {
112
+ var response = await fetch('/api/timeline?days=365&group_by=day&include_categories=false', {
113
+ credentials: 'same-origin'
114
+ });
115
+ if (!response.ok) throw new Error('timeline unavailable');
116
+ var stats = await response.json();
117
+ var points = Array.isArray(stats.timeline) ? stats.timeline : [];
118
+ var byDate = {};
119
+ points.forEach(function(point) {
120
+ if (point && point.period) byDate[String(point.period)] = Number(point.count) || 0;
121
+ });
122
+ var counts = [];
123
+ var today = new Date();
124
+ today.setHours(0, 0, 0, 0);
125
+ for (var dayOffset = 364; dayOffset >= 0; dayOffset--) {
126
+ var day = new Date(today);
127
+ day.setDate(today.getDate() - dayOffset);
128
+ counts.push(byDate[day.toISOString().slice(0, 10)] || 0);
85
129
  }
86
- a[n - 1] = current;
87
- return a;
88
- }
89
- var memSeries = syntheticSeries(memories || 1, 24, 0.015);
90
- var nodeSeries = syntheticSeries(nodes || 1, 24, 0.018);
91
- var edgeSeries = syntheticSeries(edges || 1, 24, 0.020);
92
- var bigSeries = syntheticSeries(memories || 1, 90, 0.012);
93
-
94
- var spMem = document.getElementById('sp-mem');
95
- var spNodes = document.getElementById('sp-nodes');
96
- var spEdges = document.getElementById('sp-edges');
97
- var spBig = document.getElementById('sp-big');
98
-
99
- if (spMem) spMem.innerHTML = window.slmSpark(memSeries, { color: 'var(--violet)' });
100
- if (spNodes) spNodes.innerHTML = window.slmSpark(nodeSeries, { color: 'var(--cyan)' });
101
- if (spEdges) spEdges.innerHTML = window.slmSpark(edgeSeries, { color: 'var(--violet)' });
102
- if (spBig) spBig.innerHTML = window.slmSpark(bigSeries, { w: 600, h: 140, color: 'var(--violet)' });
103
- if (spBig && spBig.querySelector('svg')) {
104
- spBig.querySelector('svg').style.cssText = 'width:100%;height:140px;display:block;';
130
+ var total = counts.reduce(function(sum, value) { return sum + value; }, 0);
131
+ var avgEl = document.getElementById('k-avg-day');
132
+ if (avgEl) avgEl.textContent = (total / 365).toFixed(1);
133
+
134
+ if (!total || typeof window.slmSpark !== 'function') {
135
+ spBig.textContent = 'No dated memory history recorded for this profile.';
136
+ spBig.className = 'muted';
137
+ return true;
138
+ }
139
+ spBig.className = '';
140
+ spBig.innerHTML = window.slmSpark(counts, {
141
+ w: 600,
142
+ h: 140,
143
+ color: 'var(--violet)'
144
+ });
145
+ if (spBig.querySelector('svg')) {
146
+ spBig.querySelector('svg').style.cssText = 'width:100%;height:140px;display:block;';
147
+ }
148
+ return true;
149
+ } catch (e) {
150
+ spBig.textContent = 'Memory history unavailable.';
151
+ spBig.className = 'muted';
152
+ return false;
105
153
  }
106
154
  }
107
155
 
@@ -110,7 +158,7 @@ function loadDashboardSparklines(memories, nodes, edges) {
110
158
  // ----------------------------------------------------------------
111
159
  async function loadDashboardFeed() {
112
160
  var feedEl = document.getElementById('feed');
113
- if (!feedEl) return;
161
+ if (!feedEl) return true;
114
162
 
115
163
  var CATEGORY_COLORS = {
116
164
  'episodic': ['rgba(34,197,94,0.12)', '#22c55e'],
@@ -131,13 +179,13 @@ async function loadDashboardFeed() {
131
179
  var r = await fetch('/api/memories?limit=6', { credentials: 'same-origin' });
132
180
  if (!r.ok) {
133
181
  feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No recent activity</div>';
134
- return;
182
+ return false;
135
183
  }
136
184
  var data = await r.json();
137
185
  var items = data.memories || data.results || data || [];
138
186
  if (!Array.isArray(items) || items.length === 0) {
139
187
  feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No memories yet — start capturing!</div>';
140
- return;
188
+ return true;
141
189
  }
142
190
 
143
191
  // Build feed using DOM methods (XSS-safe — no innerHTML with user content)
@@ -180,37 +228,76 @@ async function loadDashboardFeed() {
180
228
  item.appendChild(time);
181
229
  feedEl.appendChild(item);
182
230
  });
231
+ return true;
183
232
  } catch (e) {
184
233
  feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">Activity unavailable</div>';
234
+ return false;
185
235
  }
186
236
  }
187
237
 
188
238
  // ----------------------------------------------------------------
189
- // loadDashboardSources — static breakdown from available stats
190
- // TODO: GET /api/v3/ingestion/sources does not exist yet.
239
+ // loadDashboardSources — profile-scoped provenance returned by /api/stats
191
240
  // ----------------------------------------------------------------
192
- function loadDashboardSources() {
241
+ function loadDashboardSources(stats) {
193
242
  var srcEl = document.getElementById('src');
194
243
  if (!srcEl) return;
195
- // Honest static breakdown until endpoint ships.
196
- // These are representative percentages, not synthetic fake data —
197
- // they describe the general SLM capture pattern.
198
- var SRC = [
199
- ['MCP agents (Claude, Cursor)', 62, 'var(--violet)'],
200
- ['Auto-capture · decisions', 21, 'var(--cyan)'],
201
- ['Auto-capture · bugs', 9, 'var(--warn,#f59e0b)'],
202
- ['Manual / CLI', 8, 'var(--fg-3)'],
203
- ];
204
- srcEl.innerHTML = SRC.map(function(s) {
205
- return '<div style="margin-bottom:14px">' +
206
- '<div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px">' +
207
- '<span>' + s[0] + '</span><b class="num">' + s[1] + '%</b>' +
208
- '</div>' +
209
- '<div class="meter"><i style="width:' + s[1] + '%;background:' + s[2] + '"></i></div>' +
210
- '</div>';
211
- }).join('') + '<p style="font-size:11px;color:var(--fg-3);margin-top:8px">' +
212
- '<!-- TODO: wire to /api/v3/ingestion/sources when available -->' +
213
- '</p>';
244
+ var sourceStatus = stats && stats.ingestion_sources_status;
245
+ if (sourceStatus && sourceStatus.available === false) {
246
+ srcEl.textContent = 'Provenance metrics are temporarily unavailable.';
247
+ srcEl.className = 'muted';
248
+ return;
249
+ }
250
+ var sources = stats && Array.isArray(stats.ingestion_sources)
251
+ ? stats.ingestion_sources
252
+ : [];
253
+ if (!sources.length) {
254
+ srcEl.textContent = 'No provenance recorded for this profile.';
255
+ srcEl.className = 'muted';
256
+ return;
257
+ }
258
+
259
+ var LABELS = {
260
+ 'store': 'Historical / engine store',
261
+ 'http': 'Dashboard, hooks, and API',
262
+ 'http-observe': 'Auto-capture observations',
263
+ 'mcp-offline-worker': 'Queued MCP ingestion',
264
+ 'cli-sync': 'CLI sync',
265
+ 'unknown': 'Unclassified'
266
+ };
267
+ var COLORS = ['var(--violet)', 'var(--cyan)', 'var(--warn,#f59e0b)', 'var(--fg-3)'];
268
+ var total = sources.reduce(function(sum, source) {
269
+ return sum + (Number(source.count) || 0);
270
+ }, 0);
271
+
272
+ srcEl.className = '';
273
+ srcEl.textContent = '';
274
+ sources.forEach(function(source, index) {
275
+ var count = Number(source.count) || 0;
276
+ var percent = total ? Math.round((count / total) * 100) : 0;
277
+ var row = document.createElement('div');
278
+ row.style.marginBottom = '14px';
279
+
280
+ var heading = document.createElement('div');
281
+ heading.style.cssText = 'display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px';
282
+ var label = document.createElement('span');
283
+ label.textContent = LABELS[source.source_type] || source.source_type || 'Unclassified';
284
+ var value = document.createElement('b');
285
+ value.className = 'num';
286
+ value.textContent = count.toLocaleString() + ' · ' + percent + '%';
287
+ heading.appendChild(label);
288
+ heading.appendChild(value);
289
+
290
+ var meter = document.createElement('div');
291
+ meter.className = 'meter';
292
+ var fill = document.createElement('i');
293
+ fill.style.width = percent + '%';
294
+ fill.style.background = COLORS[index % COLORS.length];
295
+ meter.appendChild(fill);
296
+
297
+ row.appendChild(heading);
298
+ row.appendChild(meter);
299
+ srcEl.appendChild(row);
300
+ });
214
301
  }
215
302
 
216
303
  // ----------------------------------------------------------------
@@ -247,7 +334,7 @@ async function loadDashboard() {
247
334
  var response = await fetch('/api/v3/dashboard', { credentials: 'same-origin' });
248
335
  if (!response.ok) {
249
336
  showPaneError(PANE_ID, paneErrorMessage(response.status), loadDashboard, true);
250
- return;
337
+ return false;
251
338
  }
252
339
  var data = await response.json();
253
340
 
@@ -282,9 +369,11 @@ async function loadDashboard() {
282
369
  document.querySelectorAll('.mode-btn').forEach(function(btn) {
283
370
  btn.classList.toggle('active', btn.dataset.mode === data.mode);
284
371
  });
372
+ return true;
285
373
  } catch (e) {
286
374
  showPaneError(PANE_ID, paneErrorMessage(0), loadDashboard, true);
287
375
  console.log('Dashboard load error:', e);
376
+ return false;
288
377
  }
289
378
  }
290
379
 
@@ -293,10 +382,7 @@ document.addEventListener('DOMContentLoaded', function() {
293
382
  // od-shell.js calls activateTab('dashboard-pane') which dispatches shown.bs.tab,
294
383
  // but the listener may not yet be attached at that point. Call directly as well.
295
384
  setTimeout(function() {
296
- loadDashboard();
297
- loadDashboardStats();
298
- loadDashboardFeed();
299
- loadDashboardSources();
385
+ refreshDashboard({ force: true });
300
386
  }, 200);
301
387
  });
302
388
 
@@ -351,8 +437,7 @@ document.getElementById('quick-store-btn')?.addEventListener('click', async func
351
437
  }
352
438
  await r.json();
353
439
  if (input) input.value = '';
354
- loadDashboard();
355
- loadDashboardFeed();
440
+ refreshDashboard({ force: true });
356
441
  showToast('Stored!');
357
442
  } catch(e) {
358
443
  showToast('Store failed: ' + e.message);
@@ -372,6 +457,8 @@ document.getElementById('quick-recall-btn')?.addEventListener('click', function(
372
457
  fetch('/api/search', {
373
458
  method: 'POST', credentials: 'same-origin',
374
459
  headers: {'Content-Type': 'application/json'},
460
+ slmInvalidatesCache: false,
461
+ slmRequiresWriteAuth: false,
375
462
  body: JSON.stringify({query: query, limit: 5})
376
463
  }).then(function(r) {
377
464
  btn.disabled = false;