superlocalmemory 3.7.8 → 3.8.1

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 (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +205 -123
  5. package/package.json +12 -3
  6. package/plugin/.claude-plugin/plugin.json +2 -3
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/rules/AGENTS.md +57 -18
  30. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  31. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  32. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  33. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  34. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  36. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  37. package/pyproject.toml +2 -1
  38. package/scripts/postinstall/validation.js +2 -0
  39. package/scripts/postinstall-interactive.js +74 -2
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/access/__init__.py +3 -0
  42. package/src/superlocalmemory/access/rbac.py +477 -0
  43. package/src/superlocalmemory/cli/commands.py +228 -17
  44. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  45. package/src/superlocalmemory/cli/daemon.py +7 -0
  46. package/src/superlocalmemory/cli/loop_cmd.py +187 -0
  47. package/src/superlocalmemory/cli/main.py +49 -8
  48. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  49. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  50. package/src/superlocalmemory/cli/pending_store.py +49 -13
  51. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  52. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  53. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  54. package/src/superlocalmemory/cli/version_banner.py +17 -3
  55. package/src/superlocalmemory/compliance/audit.py +6 -0
  56. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  57. package/src/superlocalmemory/compliance/retention.py +176 -45
  58. package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
  59. package/src/superlocalmemory/core/community_summary.py +267 -0
  60. package/src/superlocalmemory/core/config.py +216 -3
  61. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  62. package/src/superlocalmemory/core/context_cache.py +61 -18
  63. package/src/superlocalmemory/core/embedding_worker.py +21 -7
  64. package/src/superlocalmemory/core/embeddings.py +131 -46
  65. package/src/superlocalmemory/core/engine.py +41 -22
  66. package/src/superlocalmemory/core/engine_ingestion.py +359 -43
  67. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  68. package/src/superlocalmemory/core/entity_community.py +178 -0
  69. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  70. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  71. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  72. package/src/superlocalmemory/core/injection.py +12 -7
  73. package/src/superlocalmemory/core/key_expander.py +138 -0
  74. package/src/superlocalmemory/core/maintenance.py +23 -0
  75. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  76. package/src/superlocalmemory/core/modes.py +1 -1
  77. package/src/superlocalmemory/core/mutations.py +2 -2
  78. package/src/superlocalmemory/core/pii.py +105 -0
  79. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  80. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  81. package/src/superlocalmemory/core/recall_worker.py +20 -6
  82. package/src/superlocalmemory/core/scale_engine.py +60 -1
  83. package/src/superlocalmemory/core/security_primitives.py +40 -2
  84. package/src/superlocalmemory/core/store_pipeline.py +186 -29
  85. package/src/superlocalmemory/core/worker_pool.py +21 -6
  86. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  87. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  88. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  89. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  90. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  91. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  92. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  93. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  94. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  95. package/src/superlocalmemory/evolution/triggers.py +22 -13
  96. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  97. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  98. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  100. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  101. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  102. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  103. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  104. package/src/superlocalmemory/hooks/portable_kit.py +148 -3
  105. package/src/superlocalmemory/infra/backup.py +12 -1
  106. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  107. package/src/superlocalmemory/infra/data_root.py +43 -4
  108. package/src/superlocalmemory/infra/event_bus.py +107 -24
  109. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  110. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  111. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  112. package/src/superlocalmemory/learning/cross_project.py +28 -19
  113. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  114. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  115. package/src/superlocalmemory/learning/reward.py +50 -0
  116. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  117. package/src/superlocalmemory/learning/source_quality.py +523 -1
  118. package/src/superlocalmemory/loops/__init__.py +56 -0
  119. package/src/superlocalmemory/loops/budget.py +58 -0
  120. package/src/superlocalmemory/loops/engine.py +164 -0
  121. package/src/superlocalmemory/loops/ledger.py +263 -0
  122. package/src/superlocalmemory/loops/models.py +152 -0
  123. package/src/superlocalmemory/loops/rules.py +52 -0
  124. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  125. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  126. package/src/superlocalmemory/mcp/profiles.py +103 -0
  127. package/src/superlocalmemory/mcp/server.py +32 -79
  128. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  129. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  130. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  131. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  132. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  133. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  134. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  135. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  136. package/src/superlocalmemory/mesh/broker.py +237 -129
  137. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  138. package/src/superlocalmemory/optimize/NOTICE +1 -6
  139. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  140. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  141. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  142. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  143. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  144. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  145. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  146. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  147. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  148. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  149. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  150. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  151. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  152. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  153. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  154. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  155. package/src/superlocalmemory/retrieval/engine.py +221 -47
  156. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  157. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  158. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  159. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  160. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  161. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  162. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  163. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  164. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  165. package/src/superlocalmemory/server/api.py +4 -4
  166. package/src/superlocalmemory/server/config_file.py +90 -0
  167. package/src/superlocalmemory/server/origin.py +50 -0
  168. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  169. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  170. package/src/superlocalmemory/server/recall_health.py +24 -3
  171. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  172. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  173. package/src/superlocalmemory/server/routes/agents.py +128 -38
  174. package/src/superlocalmemory/server/routes/backup.py +317 -70
  175. package/src/superlocalmemory/server/routes/behavioral.py +349 -71
  176. package/src/superlocalmemory/server/routes/brain.py +69 -12
  177. package/src/superlocalmemory/server/routes/chat.py +10 -5
  178. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  179. package/src/superlocalmemory/server/routes/config_api.py +438 -0
  180. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  181. package/src/superlocalmemory/server/routes/entity.py +108 -26
  182. package/src/superlocalmemory/server/routes/events.py +24 -8
  183. package/src/superlocalmemory/server/routes/evolution.py +189 -68
  184. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  185. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  186. package/src/superlocalmemory/server/routes/insights.py +3 -3
  187. package/src/superlocalmemory/server/routes/learning.py +289 -118
  188. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  189. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  190. package/src/superlocalmemory/server/routes/memories.py +182 -57
  191. package/src/superlocalmemory/server/routes/mesh.py +200 -31
  192. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  193. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  194. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  195. package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
  196. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  197. package/src/superlocalmemory/server/routes/stats.py +103 -158
  198. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  199. package/src/superlocalmemory/server/routes/token.py +3 -13
  200. package/src/superlocalmemory/server/routes/v3_api.py +247 -89
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +827 -72
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +132 -1
  208. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  217. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  218. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  219. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  220. package/src/superlocalmemory/storage/models.py +4 -0
  221. package/src/superlocalmemory/storage/schema.py +136 -1
  222. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  223. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  224. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  225. package/src/superlocalmemory/trust/gate.py +49 -8
  226. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  227. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  228. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  229. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  230. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  231. package/src/superlocalmemory/ui/favicon.svg +35 -4
  232. package/src/superlocalmemory/ui/index.html +303 -173
  233. package/src/superlocalmemory/ui/js/brain.js +5 -20
  234. package/src/superlocalmemory/ui/js/core.js +100 -41
  235. package/src/superlocalmemory/ui/js/dashboard.js +403 -65
  236. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  237. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  238. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  239. package/src/superlocalmemory/ui/js/memories.js +15 -4
  240. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  241. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  242. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  243. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  244. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  245. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  246. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  247. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  248. package/src/superlocalmemory/ui/js/od-backup.js +871 -0
  249. package/src/superlocalmemory/ui/js/od-brain.js +816 -0
  250. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  251. package/src/superlocalmemory/ui/js/od-graph.js +600 -0
  252. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  253. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  254. package/src/superlocalmemory/ui/js/od-memories.js +929 -0
  255. package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
  256. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  257. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  258. package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
  259. package/src/superlocalmemory/ui/js/od-shell.js +809 -0
  260. package/src/superlocalmemory/ui/js/od-skills.js +600 -0
  261. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  262. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  263. package/src/superlocalmemory/ui/js/settings.js +17 -3
  264. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  265. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  266. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  267. package/plugin-src/.mcp.json +0 -12
  268. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  269. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  270. package/plugin-src/hooks/.gitkeep +0 -0
  271. package/plugin-src/hooks/hooks.json +0 -23
  272. package/plugin-src/manifest.json +0 -25
  273. package/plugin-src/requirements.txt +0 -1
  274. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  275. package/plugin-src/scripts/ensure-venv.bat +0 -122
  276. package/plugin-src/scripts/ensure-venv.sh +0 -105
  277. package/plugin-src/scripts/slm-launch +0 -23
  278. package/plugin-src/scripts/slm-launch.bat +0 -23
  279. package/plugin-src/settings.json +0 -16
  280. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -1,60 +1,391 @@
1
1
  // SuperLocalMemory V3 — Dashboard
2
2
  // Part of Qualixar | https://superlocalmemory.com
3
+ //
4
+ // OD integration (v3.8.0):
5
+ // loadDashboard() → GET /api/v3/dashboard → system card (#dashboard-mode etc.)
6
+ // loadDashboardStats() → GET /api/stats → OD KPI strip (#k-mem, #k-nodes, #k-edges)
7
+ // loadDashboardFeed() → GET /api/memories?limit=6 → activity feed (#feed)
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
+ };
3
22
 
4
23
  // Auto-refresh dashboard when tab becomes visible (fixes stale data after settings change)
5
24
  document.addEventListener('visibilitychange', function() {
6
- if (!document.hidden) loadDashboard();
25
+ if (!document.hidden) refreshDashboard();
7
26
  });
8
27
 
9
- // Also refresh when navigating back to dashboard tab in SPA
10
- window.addEventListener('hashchange', function() {
11
- if (window.location.hash === '' || window.location.hash === '#dashboard') {
12
- 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(); });
31
+
32
+ // Load when the Dashboard tab is shown via the sidebar. od-shell.js's activateTab()
33
+ // dispatches shown.bs.tab on the hidden #dashboard-tab button.
34
+ document.getElementById('dashboard-tab')?.addEventListener('shown.bs.tab', function() { refreshDashboard(); });
35
+
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;
13
44
  }
14
- });
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;
66
+ };
67
+
68
+ // ----------------------------------------------------------------
69
+ // loadDashboardStats — GET /api/stats → OD KPI strip
70
+ // ----------------------------------------------------------------
71
+ async function loadDashboardStats() {
72
+ try {
73
+ var r = await fetch('/api/stats', { credentials: 'same-origin' });
74
+ if (!r.ok) return false;
75
+ var data = await r.json();
76
+ var ov = data.overview || {};
77
+
78
+ var memories = ov.total_memories || 0;
79
+ var facts = ov.total_facts || 0;
80
+ var nodes = ov.graph_nodes || 0;
81
+ var edges = ov.graph_edges || 0;
82
+ var clusters = ov.total_clusters || 0;
83
+
84
+ // OD KPI strip
85
+ setKpiValue('k-mem', memories);
86
+ setKpiValue('k-nodes', nodes);
87
+ setKpiValue('k-edges', edges);
88
+ setKpiValue('k-facts', facts);
89
+ setKpiValue('k-clu', clusters);
90
+
91
+ // Legacy hidden compat IDs (core.js loadStats() may race — that's OK,
92
+ // the hidden IDs are only a data bridge, not visible).
93
+ setTextById('stat-memories', memories);
94
+ setTextById('stat-facts', facts);
95
+ setTextById('stat-nodes', nodes);
96
+ setTextById('stat-edges', edges);
97
+ loadDashboardSources(data);
98
+ return true;
99
+ } catch (e) {
100
+ // Non-fatal: KPI strip shows — values
101
+ return false;
102
+ }
103
+ }
104
+
105
+ // ----------------------------------------------------------------
106
+ // loadDashboardTimeline — renders real daily fact counts from /api/timeline
107
+ // ----------------------------------------------------------------
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);
129
+ }
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;
153
+ }
154
+ }
155
+
156
+ // ----------------------------------------------------------------
157
+ // loadDashboardFeed — GET /api/memories?limit=6 → activity feed
158
+ // ----------------------------------------------------------------
159
+ async function loadDashboardFeed() {
160
+ var feedEl = document.getElementById('feed');
161
+ if (!feedEl) return true;
162
+
163
+ var CATEGORY_COLORS = {
164
+ 'episodic': ['rgba(34,197,94,0.12)', '#22c55e'],
165
+ 'semantic': ['rgba(139,92,246,0.12)', '#8b5cf6'],
166
+ 'temporal': ['rgba(6,182,212,0.12)', '#06b6d4'],
167
+ 'opinion': ['rgba(245,158,11,0.12)', '#f59e0b'],
168
+ 'default': ['rgba(100,116,139,0.12)','#64748b'],
169
+ };
170
+ var CATEGORY_ICONS = {
171
+ 'episodic': 'bi bi-journal-text',
172
+ 'semantic': 'bi bi-diagram-3',
173
+ 'temporal': 'bi bi-clock',
174
+ 'opinion': 'bi bi-lightbulb',
175
+ 'default': 'bi bi-file-earmark',
176
+ };
177
+
178
+ try {
179
+ var r = await fetch('/api/memories?limit=6', { credentials: 'same-origin' });
180
+ if (!r.ok) {
181
+ feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No recent activity</div>';
182
+ return false;
183
+ }
184
+ var data = await r.json();
185
+ var items = data.memories || data.results || data || [];
186
+ if (!Array.isArray(items) || items.length === 0) {
187
+ feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No memories yet — start capturing!</div>';
188
+ return true;
189
+ }
190
+
191
+ // Build feed using DOM methods (XSS-safe — no innerHTML with user content)
192
+ feedEl.textContent = '';
193
+ items.slice(0, 6).forEach(function(mem) {
194
+ var cat = (mem.category || 'default').toLowerCase();
195
+ var cols = CATEGORY_COLORS[cat] || CATEGORY_COLORS['default'];
196
+ var icon = CATEGORY_ICONS[cat] || CATEGORY_ICONS['default'];
197
+ var text = (mem.content || mem.text || '').substring(0, 120);
198
+ var agent = mem.agent_id || mem.source || 'system';
199
+ var ts = mem.created_at || mem.updated_at || '';
200
+ var relTime = ts ? relativeTime(ts) : '';
201
+
202
+ var item = document.createElement('div');
203
+ item.className = 'feed-item';
204
+
205
+ var ic = document.createElement('span');
206
+ ic.className = 'ic';
207
+ ic.style.cssText = 'background:' + cols[0] + ';color:' + cols[1] + ';';
208
+ var iEl = document.createElement('i');
209
+ iEl.className = icon;
210
+ ic.appendChild(iEl);
211
+
212
+ var body = document.createElement('div');
213
+ body.style.flex = '1';
214
+ var textNode = document.createElement('div');
215
+ textNode.textContent = text;
216
+ var agentNode = document.createElement('div');
217
+ agentNode.className = 'dim';
218
+ agentNode.style.cssText = 'font-size:11.5px;margin-top:2px;font-family:SF Mono,Consolas,monospace;';
219
+ agentNode.textContent = agent + (cat !== 'default' ? ' · ' + cat : '');
220
+ body.appendChild(textNode);
221
+ body.appendChild(agentNode);
222
+
223
+ var time = document.createElement('time');
224
+ time.textContent = relTime;
15
225
 
16
- // Refresh on focus (covers alt-tab back to browser)
17
- window.addEventListener('focus', function() { loadDashboard(); });
226
+ item.appendChild(ic);
227
+ item.appendChild(body);
228
+ item.appendChild(time);
229
+ feedEl.appendChild(item);
230
+ });
231
+ return true;
232
+ } catch (e) {
233
+ feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">Activity unavailable</div>';
234
+ return false;
235
+ }
236
+ }
237
+
238
+ // ----------------------------------------------------------------
239
+ // loadDashboardSources — profile-scoped provenance returned by /api/stats
240
+ // ----------------------------------------------------------------
241
+ function loadDashboardSources(stats) {
242
+ var srcEl = document.getElementById('src');
243
+ if (!srcEl) return;
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
+ });
301
+ }
302
+
303
+ // ----------------------------------------------------------------
304
+ // relativeTime — converts ISO timestamp to "Xm ago" etc.
305
+ // ----------------------------------------------------------------
306
+ function relativeTime(ts) {
307
+ try {
308
+ var diff = (Date.now() - new Date(ts).getTime()) / 1000;
309
+ if (diff < 60) return Math.round(diff) + 's ago';
310
+ if (diff < 3600) return Math.round(diff / 60) + 'm ago';
311
+ if (diff < 86400) return Math.round(diff / 3600) + 'h ago';
312
+ return Math.round(diff / 86400) + 'd ago';
313
+ } catch (e) { return ''; }
314
+ }
315
+
316
+ // ----------------------------------------------------------------
317
+ // setKpiValue — formats large numbers with locale commas
318
+ // ----------------------------------------------------------------
319
+ function setKpiValue(id, val) {
320
+ var el = document.getElementById(id);
321
+ if (!el) return;
322
+ if (val === null || val === undefined) { el.textContent = '—'; return; }
323
+ el.textContent = Number(val).toLocaleString();
324
+ }
325
+
326
+ function setTextById(id, val) {
327
+ var el = document.getElementById(id);
328
+ if (el) el.textContent = String(val);
329
+ }
18
330
 
19
331
  async function loadDashboard() {
20
332
  var PANE_ID = 'dashboard-pane';
21
333
  try {
22
- var response = await fetch('/api/v3/dashboard');
334
+ var response = await fetch('/api/v3/dashboard', { credentials: 'same-origin' });
23
335
  if (!response.ok) {
24
336
  showPaneError(PANE_ID, paneErrorMessage(response.status), loadDashboard, true);
25
- return;
337
+ return false;
26
338
  }
27
339
  var data = await response.json();
28
340
 
29
341
  clearPaneError(PANE_ID, true);
30
342
 
31
- document.getElementById('dashboard-mode').textContent = 'Mode ' + data.mode.toUpperCase();
32
- document.getElementById('dashboard-mode-desc').textContent = data.mode_name + (data.provider !== 'none' ? ' — ' + data.provider : '');
33
- document.getElementById('dashboard-memory-count').textContent = data.fact_count || data.memory_count || '0';
34
- document.getElementById('dashboard-provider').textContent = data.provider === 'none' ? 'None' : data.provider;
35
- document.getElementById('dashboard-model').textContent = data.model || '';
36
- document.getElementById('dashboard-profile').textContent = data.profile || 'default';
37
- document.getElementById('dashboard-basedir').textContent = data.base_dir || '~/.superlocalmemory';
343
+ setTextById('dashboard-mode', 'Mode ' + data.mode.toUpperCase());
344
+ setTextById('dashboard-mode-desc', data.mode_name + (data.provider !== 'none' ? ' — ' + data.provider : ''));
345
+ setTextById('dashboard-memory-count', data.fact_count || data.memory_count || '0');
346
+ setTextById('dashboard-provider', data.provider === 'none' ? 'None' : data.provider);
347
+ setTextById('dashboard-model', data.model || '');
348
+ setTextById('dashboard-profile', data.profile || 'default');
349
+ setTextById('dashboard-basedir', data.base_dir || '~/.superlocalmemory');
350
+
38
351
  var ver = data.version || '';
39
352
  var dashVer = document.getElementById('dashboard-version');
40
353
  var settVer = document.getElementById('settings-version');
41
354
  if (dashVer) dashVer.textContent = ver;
42
355
  if (settVer) settVer.textContent = ver;
43
356
 
44
- // Update mode badge in navbar
357
+ // OD dashboard subtitle
358
+ var subtitle = document.getElementById('od-dash-subtitle');
359
+ if (subtitle && data.mode_name) {
360
+ subtitle.textContent = 'Mode ' + data.mode.toUpperCase() + ' · ' + data.mode_name +
361
+ ' · local-only · v' + (ver || '?');
362
+ }
363
+
364
+ // Update mode badge in sidebar (ng-premount hidden element)
45
365
  var badge = document.getElementById('mode-badge');
46
366
  if (badge) badge.textContent = 'Mode ' + data.mode.toUpperCase();
47
367
 
48
- // Highlight active mode button
368
+ // Highlight active mode button in OD system card
49
369
  document.querySelectorAll('.mode-btn').forEach(function(btn) {
50
370
  btn.classList.toggle('active', btn.dataset.mode === data.mode);
51
371
  });
372
+ return true;
52
373
  } catch (e) {
53
374
  showPaneError(PANE_ID, paneErrorMessage(0), loadDashboard, true);
54
375
  console.log('Dashboard load error:', e);
376
+ return false;
55
377
  }
56
378
  }
57
379
 
380
+ // Fire all OD + legacy loaders on first DOMContentLoaded (dashboard is the default pane)
381
+ document.addEventListener('DOMContentLoaded', function() {
382
+ // od-shell.js calls activateTab('dashboard-pane') which dispatches shown.bs.tab,
383
+ // but the listener may not yet be attached at that point. Call directly as well.
384
+ setTimeout(function() {
385
+ refreshDashboard({ force: true });
386
+ }, 200);
387
+ });
388
+
58
389
  // Mode switch buttons
59
390
  document.addEventListener('click', function(e) {
60
391
  if (e.target.classList.contains('mode-btn')) {
@@ -72,84 +403,91 @@ document.addEventListener('click', function(e) {
72
403
  }
73
404
  });
74
405
 
75
- async function dashboardInstallToken() {
76
- var key = 'slm_install_token';
77
- var cached = sessionStorage.getItem(key);
78
- if (cached) return cached;
79
- var response = await fetch('/internal/token', {credentials: 'same-origin'});
80
- if (!response.ok) return '';
81
- var payload = await response.json();
82
- var token = payload && payload.token ? payload.token : '';
83
- if (token) sessionStorage.setItem(key, token);
84
- return token;
85
- }
406
+ // B2 (3.7.9): token cached in a private closure, never sessionStorage.
407
+ var dashboardInstallToken = (function () {
408
+ var _cache = null;
409
+ return async function () {
410
+ if (_cache) return _cache;
411
+ var response = await fetch('/internal/token', {credentials: 'same-origin'});
412
+ if (!response.ok) return '';
413
+ var payload = await response.json();
414
+ var token = payload && payload.token ? payload.token : '';
415
+ if (token) _cache = token;
416
+ return token;
417
+ };
418
+ })();
86
419
 
87
420
  // Quick store
88
421
  document.getElementById('quick-store-btn')?.addEventListener('click', async function() {
89
- var input = document.getElementById('quick-store-input');
90
- var content = input.value.trim();
422
+ var btn = this, input = document.getElementById('quick-store-input');
423
+ var content = input ? input.value.trim() : '';
91
424
  if (!content) return;
92
- var token = await dashboardInstallToken();
93
- if (!token) {
94
- showToast('Store failed: local write credential unavailable', 'error');
95
- return;
96
- }
97
- fetch('/remember', {
98
- method: 'POST',
99
- credentials: 'same-origin',
100
- headers: {
101
- 'Content-Type': 'application/json',
102
- 'X-Install-Token': token
103
- },
104
- body: JSON.stringify({content: content})
105
- }).then(function(r) {
106
- if (!r.ok) return r.json().catch(function() { return {}; }).then(function(d) {
107
- showToast('Store failed: ' + (d.detail || d.error || r.status), 'error');
425
+ btn.disabled = true; btn.textContent = 'Storing…';
426
+ try {
427
+ var token = await dashboardInstallToken();
428
+ if (!token) throw new Error('local write credential unavailable');
429
+ var r = await fetch('/remember', {
430
+ method: 'POST', credentials: 'same-origin',
431
+ headers: {'Content-Type': 'application/json', 'X-Install-Token': token},
432
+ body: JSON.stringify({content: content})
108
433
  });
109
- return r.json();
110
- }).then(function(data) {
111
- if (!data) return;
112
- input.value = '';
113
- loadDashboard();
434
+ if (!r.ok) {
435
+ var d = await r.json().catch(function(){return{};});
436
+ throw new Error(d.detail || d.error || 'HTTP ' + r.status);
437
+ }
438
+ await r.json();
439
+ if (input) input.value = '';
440
+ refreshDashboard({ force: true });
114
441
  showToast('Stored!');
115
- }).catch(function() { showToast('Store failed: network error', 'error'); });
442
+ } catch(e) {
443
+ showToast('Store failed: ' + e.message);
444
+ } finally {
445
+ btn.disabled = false; btn.textContent = 'Store';
446
+ }
116
447
  });
117
448
 
118
449
  // Quick recall
119
450
  document.getElementById('quick-recall-btn')?.addEventListener('click', function() {
120
- var query = document.getElementById('quick-recall-input').value.trim();
451
+ var btn = this, qEl = document.getElementById('quick-recall-input');
452
+ var query = qEl ? qEl.value.trim() : '';
121
453
  if (!query) return;
454
+ var div = document.getElementById('quick-recall-results');
455
+ if (div) div.innerHTML = '<span style="color:var(--fg-2);font-size:12px">Searching…</span>';
456
+ btn.disabled = true;
122
457
  fetch('/api/search', {
123
- method: 'POST',
458
+ method: 'POST', credentials: 'same-origin',
124
459
  headers: {'Content-Type': 'application/json'},
460
+ slmInvalidatesCache: false,
461
+ slmRequiresWriteAuth: false,
125
462
  body: JSON.stringify({query: query, limit: 5})
126
463
  }).then(function(r) {
464
+ btn.disabled = false;
127
465
  if (!r.ok) throw new Error('HTTP ' + r.status);
128
466
  return r.json();
129
467
  }).then(function(data) {
130
468
  var div = document.getElementById('quick-recall-results');
469
+ if (!div) return;
131
470
  if (!data.results || data.results.length === 0) {
132
471
  div.textContent = 'No results found.';
133
472
  return;
134
473
  }
135
- // Build results using DOM methods for safety
136
474
  div.textContent = '';
137
475
  data.results.forEach(function(r, i) {
138
476
  var row = document.createElement('div');
139
- row.className = 'border-bottom py-1';
477
+ row.style.cssText = 'padding:5px 0;border-bottom:1px solid var(--border);font-size:12.5px';
140
478
  var strong = document.createElement('strong');
141
479
  strong.textContent = (i + 1) + '. ';
142
480
  row.appendChild(strong);
143
- var text = document.createTextNode((r.content || r.text || '').substring(0, 150) + ' ');
144
- row.appendChild(text);
145
- var scoreSpan = document.createElement('span');
146
- scoreSpan.className = 'text-muted';
147
- scoreSpan.textContent = '(' + (r.score || 0).toFixed(2) + ')';
148
- row.appendChild(scoreSpan);
481
+ row.appendChild(document.createTextNode((r.content || r.text || '').substring(0, 150)));
482
+ var sc = document.createElement('span');
483
+ sc.style.cssText = 'color:var(--fg-2);margin-left:6px;font-size:11.5px';
484
+ sc.textContent = '(' + (r.score || 0).toFixed(2) + ')';
485
+ row.appendChild(sc);
149
486
  div.appendChild(row);
150
487
  });
151
488
  }).catch(function(e) {
489
+ btn.disabled = false;
152
490
  var div = document.getElementById('quick-recall-results');
153
- if (div) div.textContent = 'Search failed. Is the daemon running?';
491
+ if (div) div.textContent = 'Search failed: ' + e.message;
154
492
  });
155
493
  });
@@ -0,0 +1,102 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
3
+ // B2 (3.7.9): CSP-safe event delegation. Replaces inline on*= handlers
4
+ // (which force script-src 'unsafe-inline') with data-act-* attributes
5
+ // dispatched by document-level listeners. Registry keys are an allowlist
6
+ // by construction — no reflective window[name] invocation.
7
+ (function () {
8
+ 'use strict';
9
+
10
+ // action-key -> handler(el, event). Handlers call existing globals.
11
+ const ACTIONS = {
12
+ // --- bespoke ---
13
+ 'scroll-into': (el) => { const t = document.getElementById(el.dataset.target); if (t) t.scrollIntoView({ behavior: 'smooth', block: 'start' }); },
14
+ 'sigma-clear-search': () => { sigmaClearSearch(); const i = document.getElementById('sigma-search-input'); if (i) i.value = ''; },
15
+ 'export-all': (el, e) => { if (e) e.preventDefault(); exportAll(el.dataset.format); },
16
+ 'export-search-results': (el, e) => { if (e) e.preventDefault(); exportSearchResults(); },
17
+ 'set-memory-filter': (el) => setMemoryFilter(el.dataset.filter),
18
+ 'community-source': (el) => handleCommunitySourceToggle(el.dataset.source),
19
+ // --- value (this.value) ---
20
+ 'handle-resolution-change': (el) => handleResolutionChange(el.value),
21
+ 'sigma-filter-by-category': (el) => sigmaFilterByCategory(el.value),
22
+ 'sigma-search': (el) => sigmaSearch(el.value),
23
+ // --- no-arg ---
24
+ 'backup-learning-db': () => backupLearningDb(),
25
+ 'clear-event-stream': () => clearEventStream(),
26
+ 'compact-dry-run': () => compactDryRun(),
27
+ 'compact-execute': () => compactExecute(),
28
+ 'connect-git-hub': () => connectGitHub(),
29
+ 'connect-google-drive': () => connectGoogleDrive(),
30
+ 'copy-memory-to-clipboard': () => copyMemoryToClipboard(),
31
+ 'create-backup-now': () => createBackupNow(),
32
+ 'create-profile': () => createProfile(),
33
+ 'create-retention-policy': () => createRetentionPolicy(),
34
+ 'evaluate-tiers-now': () => evaluateTiersNow(),
35
+ 'export-backup': () => exportBackup(),
36
+ 'export-memory-as-markdown': () => exportMemoryAsMarkdown(),
37
+ 'filter-events': () => filterEvents(),
38
+ 'load-agents': () => loadAgents(),
39
+ 'load-clusters': () => loadClusters(),
40
+ 'load-compliance': () => loadCompliance(),
41
+ 'load-entity-explorer': () => loadEntityExplorer(),
42
+ 'load-graph': () => loadGraph(),
43
+ 'load-health-monitor': () => loadHealthMonitor(),
44
+ 'load-ingestion-status': () => loadIngestionStatus(),
45
+ 'load-mesh-peers': () => loadMeshPeers(),
46
+ 'load-skill-evolution': () => loadSkillEvolution(),
47
+ 'refresh-dashboard': () => refreshDashboard(),
48
+ 'reset-learning-data': () => resetLearningData(),
49
+ 'run-community-detection': () => runCommunityDetection(),
50
+ 'save-backup-config': () => saveBackupConfig(),
51
+ 'search-memories': () => searchMemories(),
52
+ 'sigma-reset-view': () => sigmaResetView(),
53
+ 'sigma-zoom-in': () => sigmaZoomIn(),
54
+ 'sigma-zoom-out': () => sigmaZoomOut(),
55
+ 'sync-cloud-now': () => syncCloudNow(),
56
+ 'toggle-dark-mode': () => toggleDarkMode(),
57
+ // --- phase 2: handlers on JS-generated markup (dynamic args via data-*) ---
58
+ 'toggle-privacy-blur': () => togglePrivacyBlur(),
59
+ 'open-settings-tab': () => { const t = document.querySelector('[data-target=settings]'); if (t) t.click(); },
60
+ 'show-chat-panel': () => showChatPanel(),
61
+ 'show-detail-panel': () => showDetailPanel(),
62
+ 'send-chat-from-input': () => sendChatFromInput(),
63
+ 'cancel-chat': () => cancelChat(),
64
+ 'send-chat-query': (el) => sendChatQuery(el.dataset.query),
65
+ 'chat-submit-on-enter': (el, e) => { if (e && e.key === 'Enter') sendChatFromInput(); },
66
+ 'citation-click': (el, e) => { if (e) e.preventDefault(); _onCitationClick(el.dataset.factId); },
67
+ 'filter-entities': (el) => filterEntities(el.value),
68
+ 'navigate-entity-page': (el) => navigateEntityPage(parseInt(el.dataset.page, 10)),
69
+ 'show-entity-detail': (el) => showEntityDetail(el.dataset.entity),
70
+ 'recompile-entity': (el) => recompileEntity(el.dataset.entity),
71
+ 'close-entity-detail': () => { const p = document.getElementById('entity-detail-panel'); if (p) p.style.display = 'none'; },
72
+ 'trigger-evolution': () => triggerEvolution(),
73
+ 'enable-evolution': () => enableEvolution(),
74
+ 'save-evolution-config': () => saveEvolutionConfig(),
75
+ 'sigma-highlight-node': (el) => sigmaHighlightNode(el.dataset.nodeId),
76
+ 'open-memory-detail': (el) => openMemoryDetail({ id: el.dataset.memId, content: el.dataset.memContent, category: el.dataset.memCategory, importance: parseFloat(el.dataset.memImportance) }, 'graph'),
77
+ 'sigma-filter-community': (el) => sigmaFilterByCommunity(parseInt(el.dataset.communityId, 10), el.dataset.communitySrc),
78
+ 'odg-select': (el) => odgSelect(el.dataset.odgId), // od-graph.js node inspector + citation clicks
79
+ 'load-memories-page': (el) => loadMemories(parseInt(el.dataset.page, 10)),
80
+ 'disconnect-destination': (el) => disconnectDestination(el.dataset.destId),
81
+ 'adapter-action': (el) => adapterAction(el.dataset.adapter, el.dataset.adapterAction),
82
+ };
83
+
84
+ const ATTR = { click: 'actClick', change: 'actChange', input: 'actInput', keydown: 'actKeydown' };
85
+
86
+ function makeHandler(type) {
87
+ const key = ATTR[type];
88
+ return function (event) {
89
+ let el = event.target;
90
+ while (el && el.nodeType === 1) {
91
+ if (el.dataset && el.dataset[key]) {
92
+ const fn = ACTIONS[el.dataset[key]];
93
+ if (typeof fn === 'function') fn(el, event);
94
+ return;
95
+ }
96
+ el = el.parentElement;
97
+ }
98
+ };
99
+ }
100
+
101
+ ['click', 'change', 'input', 'keydown'].forEach((t) => document.addEventListener(t, makeHandler(t)));
102
+ })();