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
@@ -0,0 +1,816 @@
1
+ // Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
2
+ // od-brain.js — OD-themed "Living Brain" screen for SuperLocalMemory.
3
+ //
4
+ // Renders the approved design into window.odRenderBrain(container).
5
+ // ALL data from these confirmed live endpoints (no seed / no invented data):
6
+ // GET /api/learning/status → ranking_phase, stats, engagement, tech_preferences,
7
+ // workflow_patterns, persisted source-quality posteriors
8
+ // GET /api/behavioral/status → reward_telemetry, explicit outcome counts,
9
+ // patterns[], transfers, recent_outcomes[]
10
+ // GET /api/behavioral/assertions?category=skill_performance&limit=50 → assertions[]
11
+ // GET /api/behavioral/tool-events?limit=500 → events[].{created_at,tool_name,…}
12
+ //
13
+ // Heatmap is built from tool-events aggregated by UTC date — real activity density,
14
+ // not the random slmHeatmap() fallback.
15
+ //
16
+ // CSP-safe: no eval(), no innerHTML for API data, no inline event handlers.
17
+ // Immutable style: EL() always creates new nodes; no mutation of existing DOM.
18
+
19
+ /* global window, document, fetch, Promise, setTimeout, getComputedStyle */
20
+
21
+ (function () {
22
+ 'use strict';
23
+
24
+ // ======================================================================
25
+ // Fetch
26
+ // ======================================================================
27
+ function apiFetch(path) {
28
+ return fetch(path, { credentials: 'same-origin' })
29
+ .then(function (r) {
30
+ if (!r.ok) throw new Error(path + ' → ' + r.status);
31
+ return r.json();
32
+ });
33
+ }
34
+
35
+ function fetchAll() {
36
+ return Promise.all([
37
+ apiFetch('/api/learning/status'),
38
+ apiFetch('/api/behavioral/status'),
39
+ apiFetch('/api/behavioral/assertions?category=skill_performance&limit=50'),
40
+ apiFetch('/api/behavioral/tool-events?limit=500'),
41
+ ]);
42
+ }
43
+
44
+ // ======================================================================
45
+ // DOM helper — API strings always go through .text → textContent (XSS-safe)
46
+ // ======================================================================
47
+ function EL(tag, props, kids) {
48
+ var el = document.createElement(tag);
49
+ var p = props || {};
50
+ Object.keys(p).forEach(function (k) {
51
+ if (k === 'className') el.className = String(p[k]);
52
+ else if (k === 'text') el.textContent = p[k] == null ? '' : String(p[k]);
53
+ else el.setAttribute(k, String(p[k]));
54
+ });
55
+ (kids || []).forEach(function (c) { if (c != null) el.appendChild(c); });
56
+ return el;
57
+ }
58
+
59
+ // ======================================================================
60
+ // Formatters
61
+ // ======================================================================
62
+ function fmtNum(n) { return Number(n || 0).toLocaleString(); }
63
+
64
+ function fmtKB(kb) {
65
+ var v = Number(kb || 0);
66
+ if (v === 0) return '0 KB';
67
+ return v < 1024 ? v.toFixed(1) + ' KB' : (v / 1024).toFixed(1) + ' MB';
68
+ }
69
+
70
+ function phaseLabel(raw) {
71
+ return (raw || 'cold_start')
72
+ .replace(/_/g, '-')
73
+ .replace(/\b([a-z])/g, function (c) { return c.toUpperCase(); });
74
+ }
75
+
76
+ // ======================================================================
77
+ // Heatmap — real activity density from tool-events aggregated by UTC date
78
+ // ======================================================================
79
+ function buildDateMap(events) {
80
+ var map = {};
81
+ (events || []).forEach(function (e) {
82
+ if (!e.created_at) return;
83
+ var d = String(e.created_at).slice(0, 10);
84
+ map[d] = (map[d] || 0) + 1;
85
+ });
86
+ return map;
87
+ }
88
+
89
+ function signalLevel(count) {
90
+ if (count <= 0) return 0;
91
+ if (count <= 3) return 1;
92
+ if (count <= 10) return 2;
93
+ if (count <= 25) return 3;
94
+ return 4;
95
+ }
96
+
97
+ function buildHeatmap(el, dateMap, weeks) {
98
+ var totalDays = (weeks || 26) * 7;
99
+ var today = new Date(); today.setHours(0, 0, 0, 0);
100
+ el.replaceChildren();
101
+ for (var i = 0; i < totalDays; i++) {
102
+ var d = new Date(today);
103
+ d.setDate(today.getDate() - (totalDays - 1 - i));
104
+ var ds = d.toISOString().slice(0, 10);
105
+ var cnt = (dateMap && dateMap[ds]) || 0;
106
+ var cell = document.createElement('i');
107
+ cell.setAttribute('data-l', String(signalLevel(cnt)));
108
+ cell.setAttribute('title', ds + ': ' + cnt + ' event' + (cnt === 1 ? '' : 's'));
109
+ el.appendChild(cell);
110
+ }
111
+ }
112
+
113
+ // ======================================================================
114
+ // UI primitives
115
+ // ======================================================================
116
+ function meter(pct, color) {
117
+ var outer = EL('div', { className: 'meter' });
118
+ var fill = document.createElement('i');
119
+ fill.style.width = Math.min(100, Math.max(0, pct)) + '%';
120
+ if (color) fill.style.background = color;
121
+ outer.appendChild(fill);
122
+ return outer;
123
+ }
124
+
125
+ function inlineBar(pct) {
126
+ var wrap = EL('span', {
127
+ style: 'display:inline-block;vertical-align:middle;width:56px;height:6px;' +
128
+ 'border-radius:99px;background:var(--card-2);overflow:hidden;margin-right:6px',
129
+ });
130
+ wrap.appendChild(EL('span', {
131
+ style: 'display:block;height:100%;width:' + Math.min(100, pct) + '%;' +
132
+ 'background:linear-gradient(90deg,var(--cyan),var(--violet))',
133
+ }));
134
+ return wrap;
135
+ }
136
+
137
+ function heatLegend() {
138
+ var w = EL('div', { className: 'heat-legend' });
139
+ w.appendChild(document.createTextNode('less '));
140
+ for (var i = 0; i <= 4; i++) w.appendChild(EL('i', { 'data-l': String(i) }));
141
+ w.appendChild(document.createTextNode(' more'));
142
+ return w;
143
+ }
144
+
145
+ // isNumeric=true → class="value num" (tabular digits, 30px default from CSS)
146
+ // isNumeric=false → class="value" + font-size:24px (design uses smaller text for long labels)
147
+ function kpiCard(icon, label, value, delta, dUp, vColor, isNumeric) {
148
+ var card = EL('div', { className: 'card kpi' });
149
+ var lbl = EL('div', { className: 'label' });
150
+ if (typeof window.slmIcon === 'function') {
151
+ var ic = document.createElement('span');
152
+ ic.innerHTML = window.slmIcon(icon);
153
+ lbl.appendChild(ic);
154
+ }
155
+ lbl.appendChild(document.createTextNode(' ' + label));
156
+ var val = EL('div', { className: isNumeric ? 'value num' : 'value', text: value });
157
+ if (!isNumeric) val.style.fontSize = '24px';
158
+ if (vColor) val.style.color = vColor;
159
+ card.appendChild(lbl);
160
+ card.appendChild(val);
161
+ card.appendChild(EL('div', { className: 'delta ' + (dUp ? 'up' : ''), text: delta }));
162
+ return card;
163
+ }
164
+
165
+ // ======================================================================
166
+ // Phase style injection (once — not in design-system.css yet)
167
+ // ======================================================================
168
+ function injectPhaseStyle() {
169
+ if (document.getElementById('od-brain-phase-style')) return;
170
+ var st = document.createElement('style');
171
+ st.id = 'od-brain-phase-style';
172
+ st.textContent =
173
+ '.phase{flex:1;text-align:center;padding:10px 6px;border-radius:var(--r-md);' +
174
+ 'background:var(--card-2);border:1px solid var(--border)}' +
175
+ '.phase b{display:block;font-size:13px}' +
176
+ '.phase span{font-size:11px;color:var(--fg-3)}' +
177
+ '.phase.done{border-color:color-mix(in srgb,var(--ok) 40%,transparent)}' +
178
+ '.phase.done b{color:var(--ok)}' +
179
+ '.phase.now{border-color:var(--violet-line);background:var(--violet-soft)}' +
180
+ '.phase.now b{color:var(--violet)}';
181
+ document.head.appendChild(st);
182
+ }
183
+
184
+ // ======================================================================
185
+ // Tab: OVERVIEW
186
+ // ======================================================================
187
+ function buildOverview(learning, behavioral, dateMap) {
188
+ var sec = EL('section', { className: 'tabpane active', 'data-p': 'overview' });
189
+ var stats = (learning && learning.stats) || {};
190
+ var eng = (learning && learning.engagement) || {};
191
+ var beh = behavioral || {};
192
+ var ranker = (learning && learning.ranker_phase) || {};
193
+ var gates = ranker.gates || {};
194
+ var ruleGate = Math.max(1, Number(gates.rule_based_min_signals || 1));
195
+ var mlGate = Math.max(ruleGate, Number(gates.ml_model_min_signals || ruleGate));
196
+ var signals = Number(ranker.signals != null
197
+ ? ranker.signals : stats.ranker_signal_count || 0);
198
+ var pct = Math.min(100, Math.round(signals / mlGate * 100));
199
+ var phase = ranker.key || (learning && learning.ranking_phase) || 'baseline';
200
+ var phaseNumber = Number(ranker.phase || 1);
201
+ var modelActive = Boolean(ranker.model_active);
202
+ var phaseDelta = modelActive
203
+ ? 'Verified active model'
204
+ : signals < mlGate
205
+ ? fmtNum(mlGate - signals) + ' to ML data gate'
206
+ : 'ML data gate met · verified model required';
207
+ var healthStatus = (eng.health_status || 'INACTIVE').toUpperCase();
208
+ var healthColor = healthStatus === 'HEALTHY' ? 'var(--ok)'
209
+ : healthStatus === 'ACTIVE' ? 'var(--cyan)' : undefined;
210
+ var pCount = ((beh.patterns) || []).length;
211
+
212
+ // KPI strip
213
+ var strip = EL('div', { className: 'kpi-strip', style: 'margin-bottom:16px' });
214
+ // Ranking phase: text label → isNumeric=false (font-size:24px to match design)
215
+ strip.appendChild(kpiCard('skill', 'Ranking phase', phaseLabel(phase),
216
+ phaseDelta, modelActive, undefined, false));
217
+ // Feedback signals: numeric → isNumeric=true
218
+ strip.appendChild(kpiCard('optimize', 'Feedback signals', fmtNum(signals),
219
+ '▲ ' + fmtNum(stats.unique_queries || 0) + ' unique queries', true, undefined, true));
220
+ // Engagement health: text label → isNumeric=false
221
+ strip.appendChild(kpiCard('health', 'Engagement health',
222
+ healthStatus.charAt(0) + healthStatus.slice(1).toLowerCase(),
223
+ (eng.days_active || 0) + ' days active · ' + Number(eng.memories_per_day || 0).toFixed(1) + ' mem/day',
224
+ healthStatus === 'HEALTHY', healthColor, false));
225
+ // Patterns: numeric → isNumeric=true
226
+ strip.appendChild(kpiCard('brain', 'Patterns learned', String(pCount),
227
+ '▲ ' + (beh.cross_project_transfers || 0) + ' transferable', pCount > 0, undefined, true));
228
+ sec.appendChild(strip);
229
+
230
+ // 2-column grid
231
+ var grid = EL('div', { className: 'grid', style: 'grid-template-columns:1fr 1fr;align-items:start' });
232
+
233
+ // Progress card
234
+ var pcrd = EL('div', { className: 'card' });
235
+ var ph = EL('div', { className: 'card-head' });
236
+ ph.appendChild(EL('h3', { text: 'Adaptive ranking progress' }));
237
+ ph.appendChild(EL('span', { className: 'sub', text: 'baseline → rule-based → ML model' }));
238
+ pcrd.appendChild(ph);
239
+ var pb = EL('div', { className: 'card-pad' });
240
+ var pmeta = EL('div', {
241
+ style: 'display:flex;justify-content:space-between;font-size:12px;color:var(--fg-2);margin-bottom:8px',
242
+ });
243
+ pmeta.appendChild(EL('span', { text: fmtNum(signals) + ' / ' + fmtNum(mlGate) + ' signals' }));
244
+ pmeta.appendChild(EL('span', { className: 'num', text: pct + '%' }));
245
+ pb.appendChild(pmeta);
246
+ pb.appendChild(meter(pct));
247
+ var phasesRow = EL('div', {
248
+ style: 'display:flex;justify-content:space-between;margin-top:18px;gap:12px',
249
+ });
250
+ [
251
+ {
252
+ t: 'Baseline',
253
+ d: '0–' + Math.max(0, ruleGate - 1) + ' signals',
254
+ done: phaseNumber > 1,
255
+ now: phaseNumber === 1,
256
+ },
257
+ {
258
+ t: 'Rule-Based',
259
+ d: fmtNum(ruleGate) + '–' + fmtNum(Math.max(ruleGate, mlGate - 1)) + ' signals',
260
+ done: phaseNumber > 2,
261
+ now: phaseNumber === 2,
262
+ },
263
+ {
264
+ t: 'ML Model',
265
+ d: fmtNum(mlGate) + '+ · ' + (modelActive ? 'verified active' : 'verified model required'),
266
+ done: false,
267
+ now: phaseNumber === 3 && modelActive,
268
+ },
269
+ ].forEach(function (phI) {
270
+ var el = EL('div', {
271
+ className: 'phase' + (phI.done ? ' done' : '') + (phI.now ? ' now' : ''),
272
+ });
273
+ el.appendChild(EL('b', { text: phI.t }));
274
+ el.appendChild(EL('span', { text: phI.d }));
275
+ phasesRow.appendChild(el);
276
+ });
277
+ pb.appendChild(phasesRow);
278
+ pcrd.appendChild(pb);
279
+ grid.appendChild(pcrd);
280
+
281
+ // Privacy card
282
+ var priv = EL('div', { className: 'card' });
283
+ var prvh = EL('div', { className: 'card-head' });
284
+ prvh.appendChild(EL('h3', { text: 'Privacy & data' }));
285
+ prvh.appendChild(EL('span', { className: 'sub', text: 'learning.db — separate from memory.db' }));
286
+ priv.appendChild(prvh);
287
+ var prvb = EL('div', { className: 'card-pad', style: 'display:flex;flex-direction:column;gap:2px' });
288
+ [
289
+ ['Learning DB size', fmtKB(stats.db_size_kb)],
290
+ ['Patterns learned', String(pCount)],
291
+ ['Models trained', String(stats.models_trained || 0)],
292
+ ['Verified active models', String(stats.models_active_verified || 0)],
293
+ ['Sources tracked', String(stats.tracked_sources || 0)],
294
+ ].forEach(function (row) {
295
+ var r = EL('div', { className: 'list-row' });
296
+ r.appendChild(EL('span', { className: 'muted', style: 'flex:1', text: row[0] }));
297
+ r.appendChild(EL('b', { className: 'num', text: row[1] }));
298
+ prvb.appendChild(r);
299
+ });
300
+ var lr = EL('div', { className: 'list-row' });
301
+ lr.appendChild(EL('span', { className: 'muted', style: 'flex:1', text: 'Never leaves device' }));
302
+ var badge = EL('span', { className: 'badge ok' });
303
+ badge.appendChild(EL('span', { className: 'dot' }));
304
+ badge.appendChild(document.createTextNode(' local'));
305
+ lr.appendChild(badge);
306
+ prvb.appendChild(lr);
307
+ priv.appendChild(prvb);
308
+ grid.appendChild(priv);
309
+ sec.appendChild(grid);
310
+
311
+ // Activity heatmap (tool events are activity, never reward labels)
312
+ var hmc = EL('div', { className: 'card', style: 'margin-top:16px' });
313
+ var hmh = EL('div', { className: 'card-head' });
314
+ hmh.appendChild(EL('h3', { text: 'Memory activity' }));
315
+ hmh.appendChild(EL('span', { className: 'sub', text: 'tool events · last 26 weeks' }));
316
+ hmh.appendChild(EL('div', { className: 'spacer' }));
317
+ hmh.appendChild(heatLegend());
318
+ hmc.appendChild(hmh);
319
+ var hmb = EL('div', { className: 'card-pad', style: 'overflow-x:auto' });
320
+ var hmEl = EL('div', { className: 'heatmap', id: 'od-brain-heat1' });
321
+ buildHeatmap(hmEl, dateMap, 26);
322
+ hmb.appendChild(hmEl);
323
+ hmc.appendChild(hmb);
324
+ sec.appendChild(hmc);
325
+ return sec;
326
+ }
327
+
328
+ // ======================================================================
329
+ // Tab: REWARD SIGNAL
330
+ // ======================================================================
331
+ function buildReward(behavioral) {
332
+ var sec = EL('section', { className: 'tabpane', 'data-p': 'reward' });
333
+ var beh = behavioral || {};
334
+ var reward = beh.reward_telemetry || {};
335
+ var timeline = reward.timeline || [];
336
+ var rewardDateMap = {};
337
+ timeline.forEach(function (point) {
338
+ if (point && point.date) rewardDateMap[String(point.date)] = Number(point.count || 0);
339
+ });
340
+
341
+ // Density heatmap
342
+ var hmcr = EL('div', { className: 'card', style: 'margin-bottom:16px' });
343
+ var hmhr = EL('div', { className: 'card-head' });
344
+ hmhr.appendChild(EL('h3', { text: 'Reward signal density' }));
345
+ hmhr.appendChild(EL('span', {
346
+ className: 'sub',
347
+ text: 'settled numeric labels per day · last ' + Number(reward.window_days || 182) + ' days',
348
+ }));
349
+ hmhr.appendChild(EL('div', { className: 'spacer' }));
350
+ hmhr.appendChild(heatLegend());
351
+ hmcr.appendChild(hmhr);
352
+ var hmbr = EL('div', { className: 'card-pad', style: 'overflow-x:auto' });
353
+ var hmElR = EL('div', { className: 'heatmap', id: 'od-brain-heat2' });
354
+ buildHeatmap(hmElR, rewardDateMap, 26);
355
+ hmbr.appendChild(hmElR);
356
+ hmcr.appendChild(hmbr);
357
+ sec.appendChild(hmcr);
358
+
359
+ // 2-column: sparkline + outcome mix
360
+ var grid = EL('div', { className: 'grid', style: 'grid-template-columns:1fr 1fr;align-items:start' });
361
+
362
+ // Average settled reward and real daily series
363
+ var fbCard = EL('div', { className: 'card' });
364
+ var fbH = EL('div', { className: 'card-head' });
365
+ fbH.appendChild(EL('h3', { text: 'Average settled reward' }));
366
+ fbH.appendChild(EL('span', {
367
+ className: 'sub',
368
+ text: fmtNum(reward.count || 0) + ' finalized labels',
369
+ }));
370
+ fbCard.appendChild(fbH);
371
+ var fbB = EL('div', { className: 'card-pad' });
372
+ if (reward.average != null) {
373
+ fbB.appendChild(EL('div', {
374
+ className: 'value num',
375
+ style: 'font-size:30px;margin-bottom:12px',
376
+ text: Number(reward.average).toFixed(3),
377
+ }));
378
+ }
379
+ var fbSp = EL('div', { id: 'od-brain-sp-fb' });
380
+ var sparkVals = timeline.slice(-30).map(function (point) {
381
+ return Number(point.average || 0);
382
+ });
383
+ var hasSparkData = sparkVals.length > 0;
384
+ if (hasSparkData && typeof window.slmSpark === 'function') {
385
+ fbSp.innerHTML = window.slmSpark(sparkVals, { w: 600, h: 150, color: 'var(--violet)' });
386
+ var sv1 = fbSp.querySelector('svg'); if (sv1) sv1.style.height = '150px';
387
+ } else {
388
+ fbSp.appendChild(EL('p', {
389
+ className: 'muted',
390
+ style: 'padding:32px;text-align:center;font-size:13px',
391
+ text: 'No settled reward history is available yet.',
392
+ }));
393
+ }
394
+ fbB.appendChild(fbSp);
395
+ fbCard.appendChild(fbB);
396
+ grid.appendChild(fbCard);
397
+
398
+ // Settled reward distribution
399
+ var outCard = EL('div', { className: 'card' });
400
+ var outH = EL('div', { className: 'card-head' });
401
+ outH.appendChild(EL('h3', { text: 'Reward distribution' }));
402
+ outH.appendChild(EL('span', { className: 'sub', text: 'engagement-derived settled labels' }));
403
+ outCard.appendChild(outH);
404
+ var outB = EL('div', { className: 'card-pad', id: 'od-brain-outcomes' });
405
+ var total = Number(reward.count || 0);
406
+ var bd = reward.distribution || {};
407
+ if (total === 0) {
408
+ outB.appendChild(EL('p', {
409
+ className: 'muted',
410
+ style: 'padding:16px;text-align:center;font-size:13px',
411
+ text: 'No settled reward labels yet. Recall engagement will populate this view.',
412
+ }));
413
+ } else {
414
+ [
415
+ ['Positive (> 0.6)', bd.positive || 0, 'var(--ok)'],
416
+ ['Neutral (0.4–0.6)', bd.neutral || 0, 'var(--cyan)'],
417
+ ['Negative (< 0.4)', bd.negative || 0, 'var(--danger)'],
418
+ ].forEach(function (r) {
419
+ var p2 = Math.round(r[1] / total * 100);
420
+ var rw = EL('div', { style: 'margin-bottom:13px' });
421
+ var rm = EL('div', {
422
+ style: 'display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px',
423
+ });
424
+ rm.appendChild(EL('span', { text: r[0] }));
425
+ rm.appendChild(EL('b', { className: 'num', text: p2 + '%' }));
426
+ rw.appendChild(rm);
427
+ rw.appendChild(meter(p2, r[2]));
428
+ outB.appendChild(rw);
429
+ });
430
+ }
431
+ outCard.appendChild(outB);
432
+ grid.appendChild(outCard);
433
+ sec.appendChild(grid);
434
+ return sec;
435
+ }
436
+
437
+ // ======================================================================
438
+ // Tab: BEHAVIOUR
439
+ // ======================================================================
440
+ function buildBehaviour(learning, behavioral) {
441
+ var sec = EL('section', { className: 'tabpane', 'data-p': 'behaviour' });
442
+ var l = learning || {};
443
+ var beh = behavioral || {};
444
+ var grid1 = EL('div', { className: 'grid', style: 'grid-template-columns:1fr 1fr;align-items:start' });
445
+
446
+ // Tech preferences table
447
+ var tc = EL('div', { className: 'card' });
448
+ var tch = EL('div', { className: 'card-head' });
449
+ tch.appendChild(EL('h3', { text: 'Tech preferences' }));
450
+ tch.appendChild(EL('span', { className: 'sub', text: 'Layer 1 · confidence-weighted' }));
451
+ tc.appendChild(tch);
452
+ var tcb = EL('div', { className: 'card-pad' });
453
+ var techItems = l.tech_preferences || [];
454
+ if (techItems.length === 0) {
455
+ tcb.appendChild(EL('p', {
456
+ className: 'muted',
457
+ style: 'padding:16px;text-align:center;font-size:13px',
458
+ text: 'No tech preferences learned yet. These emerge after consistent cross-session usage.',
459
+ }));
460
+ } else {
461
+ var tbl = EL('table', { className: 'tbl' });
462
+ var thead = EL('thead');
463
+ var thr = EL('tr');
464
+ ['Preference', 'Value', 'Confidence', 'Evidence'].forEach(function (h) {
465
+ thr.appendChild(EL('th', { text: h }));
466
+ });
467
+ thead.appendChild(thr); tbl.appendChild(thead);
468
+ var tbody = EL('tbody');
469
+ techItems.slice(0, 10).forEach(function (t) {
470
+ var tr = EL('tr');
471
+ tr.appendChild(EL('td', { className: 'muted', text: String(t.name || t.preference || '—') }));
472
+ var vd = EL('td');
473
+ vd.appendChild(EL('b', { text: String(t.value || '—') }));
474
+ tr.appendChild(vd);
475
+ var cp = Math.round(Number(t.confidence || t.strength || 0) * 100);
476
+ var cd = EL('td');
477
+ cd.appendChild(inlineBar(cp));
478
+ cd.appendChild(document.createTextNode(cp + '%'));
479
+ tr.appendChild(cd);
480
+ tr.appendChild(EL('td', { className: 'num', text: String(t.evidence_count || t.count || 0) }));
481
+ tbody.appendChild(tr);
482
+ });
483
+ tbl.appendChild(tbody); tcb.appendChild(tbl);
484
+ }
485
+ tc.appendChild(tcb); grid1.appendChild(tc);
486
+
487
+ // Workflow patterns
488
+ var wc = EL('div', { className: 'card' });
489
+ var wch = EL('div', { className: 'card-head' });
490
+ wch.appendChild(EL('h3', { text: 'Workflow patterns' }));
491
+ wch.appendChild(EL('span', { className: 'sub', text: 'Layer 3 · sequence & temporal' }));
492
+ wc.appendChild(wch);
493
+ var wcb = EL('div', { className: 'card-pad' });
494
+ var wfPats = l.workflow_patterns || [];
495
+ if (wfPats.length === 0) {
496
+ wcb.appendChild(EL('p', {
497
+ className: 'muted',
498
+ style: 'padding:16px;text-align:center;font-size:13px',
499
+ text: 'No sequence or temporal patterns recorded yet. Interest signals are not shown as workflows.',
500
+ }));
501
+ } else {
502
+ wfPats.slice(0, 3).forEach(function (wf) {
503
+ var wd = EL('div', { style: 'margin-top:12px' });
504
+ wd.appendChild(EL('span', { className: 'badge violet', text: String(wf.type || 'pattern') }));
505
+ wd.appendChild(EL('div', {
506
+ style: 'margin-top:6px;font-size:13px',
507
+ text: String(wf.description || wf.pattern || '—'),
508
+ }));
509
+ wcb.appendChild(wd);
510
+ });
511
+ }
512
+ wc.appendChild(wcb); grid1.appendChild(wc);
513
+ sec.appendChild(grid1);
514
+
515
+ var grid2 = EL('div', {
516
+ className: 'grid',
517
+ style: 'grid-template-columns:1fr 1fr;align-items:start;margin-top:16px',
518
+ });
519
+
520
+ // Cross-project transfers
521
+ var xc = EL('div', { className: 'card' });
522
+ var xch = EL('div', { className: 'card-head' });
523
+ xch.appendChild(EL('h3', { text: 'Cross-project transfers' }));
524
+ xch.appendChild(EL('span', { className: 'sub', text: 'patterns reused across projects' }));
525
+ xc.appendChild(xch);
526
+ var xcb = EL('div', { className: 'card-pad' });
527
+ var xPats = beh.cross_project_patterns || [];
528
+ if (xPats.length === 0) {
529
+ xcb.appendChild(EL('p', {
530
+ className: 'muted',
531
+ style: 'padding:16px;text-align:center;font-size:13px',
532
+ text: 'No cross-project patterns yet. These appear when the same behaviour is detected in 2+ projects.',
533
+ }));
534
+ } else {
535
+ xPats.slice(0, 5).forEach(function (x) {
536
+ var item = EL('div', { className: 'feed-item' });
537
+ if (typeof window.slmIcon === 'function') {
538
+ var ic = EL('span', { className: 'ic', style: 'background:var(--violet-soft);color:var(--violet)' });
539
+ ic.innerHTML = window.slmIcon('link');
540
+ item.appendChild(ic);
541
+ }
542
+ var info = EL('div');
543
+ info.appendChild(EL('b', { text: String(x.pattern_key || x.name || '—') }));
544
+ info.appendChild(EL('div', {
545
+ className: 'dim',
546
+ style: 'font-size:12px;margin-top:2px',
547
+ text: String(x.description || x.pattern_type || '—'),
548
+ }));
549
+ item.appendChild(info); xcb.appendChild(item);
550
+ });
551
+ }
552
+ xc.appendChild(xcb); grid2.appendChild(xc);
553
+
554
+ // Recent outcomes
555
+ var rc = EL('div', { className: 'card' });
556
+ var rch = EL('div', { className: 'card-head' });
557
+ rch.appendChild(EL('h3', { text: 'Recent outcomes' }));
558
+ rc.appendChild(rch);
559
+ var rcb = EL('div', { className: 'card-pad' });
560
+ var recOuts = beh.recent_outcomes || [];
561
+ if (recOuts.length === 0) {
562
+ rcb.appendChild(EL('p', {
563
+ className: 'muted',
564
+ style: 'padding:16px;text-align:center;font-size:13px',
565
+ text: 'No outcome reports yet. Use /report-outcome to teach the ranker.',
566
+ }));
567
+ } else {
568
+ recOuts.slice(0, 8).forEach(function (r) {
569
+ var item = EL('div', { className: 'feed-item' });
570
+ var out = String(r.outcome || 'unknown');
571
+ var bcls = out === 'success' ? 'ok' : out === 'failure' ? 'danger' : 'warn';
572
+ var bdg = EL('span', { className: 'badge ' + bcls });
573
+ bdg.appendChild(EL('span', { className: 'dot' }));
574
+ bdg.appendChild(document.createTextNode(' ' + out));
575
+ item.appendChild(bdg);
576
+ item.appendChild(EL('span', {
577
+ style: 'flex:1;font-size:13px',
578
+ text: String(r.action_type || 'other'),
579
+ }));
580
+ item.appendChild(EL('time', { text: String(r.timestamp || '') }));
581
+ rcb.appendChild(item);
582
+ });
583
+ }
584
+ rc.appendChild(rcb); grid2.appendChild(rc);
585
+ sec.appendChild(grid2);
586
+ return sec;
587
+ }
588
+
589
+ // ======================================================================
590
+ // Tab: CONNECTED CLIENTS
591
+ // ======================================================================
592
+ function buildClients(dateMap) {
593
+ var sec = EL('section', { className: 'tabpane', 'data-p': 'clients' });
594
+
595
+ // Sparkline from tool-event activity (last 22 data-points matching design)
596
+ var evc = EL('div', { className: 'card', style: 'margin-bottom:16px' });
597
+ var evh = EL('div', { className: 'card-head' });
598
+ evh.appendChild(EL('h3', { text: 'Connected-client evolution' }));
599
+ evh.appendChild(EL('span', { className: 'sub', text: 'tool-event activity over time (proxy metric)' }));
600
+ evc.appendChild(evh);
601
+ var evb = EL('div', { className: 'card-pad' });
602
+ var today = new Date(); today.setHours(0, 0, 0, 0);
603
+ var cVals = [];
604
+ for (var ci = 21; ci >= 0; ci--) {
605
+ var cd = new Date(today); cd.setDate(today.getDate() - ci);
606
+ cVals.push((dateMap && dateMap[cd.toISOString().slice(0, 10)]) || 0);
607
+ }
608
+ var evSp = EL('div', { id: 'od-brain-sp-clients' });
609
+ var hasData = cVals.some(function (v) { return v > 0; });
610
+ if (hasData && typeof window.slmSpark === 'function') {
611
+ evSp.innerHTML = window.slmSpark(cVals, { w: 600, h: 150, color: 'var(--cyan)' });
612
+ var sv2 = evSp.querySelector('svg'); if (sv2) sv2.style.height = '150px';
613
+ } else {
614
+ evSp.appendChild(EL('p', { className: 'muted', style: 'padding:32px;text-align:center',
615
+ text: 'No event history in this period.' }));
616
+ }
617
+ evb.appendChild(evSp);
618
+ // TODO: GET /api/clients — no live connected-client session data available via these endpoints.
619
+ // tool-events captures tool invocations but not distinct client identities or session counts.
620
+ evb.appendChild(EL('p', {
621
+ className: 'muted',
622
+ style: 'margin-top:12px;font-size:13px',
623
+ text: 'Bars above represent all tool invocations logged to this daemon. ' +
624
+ 'A dedicated client-session endpoint is not yet exposed via the public API.',
625
+ }));
626
+ evc.appendChild(evb);
627
+ sec.appendChild(evc);
628
+
629
+ // Empty-state clients table
630
+ var tc = EL('div', { className: 'card' });
631
+ var tch = EL('div', { className: 'card-head' });
632
+ tch.appendChild(EL('h3', { text: 'Clients' }));
633
+ tc.appendChild(tch);
634
+ var tcb = EL('div', { className: 'card-pad' });
635
+ tcb.appendChild(EL('p', {
636
+ className: 'muted',
637
+ style: 'padding:16px;text-align:center;font-size:13px',
638
+ text: 'Client details are not yet available via API. Coming in a future daemon release.',
639
+ }));
640
+ tc.appendChild(tcb);
641
+ sec.appendChild(tc);
642
+ return sec;
643
+ }
644
+
645
+ // ======================================================================
646
+ // Tab: SOURCE QUALITY
647
+ // ======================================================================
648
+ function buildSourceQuality(learning) {
649
+ var sec = EL('section', { className: 'tabpane', 'data-p': 'sources' });
650
+ var scores = (learning && learning.source_scores) || {};
651
+ var entries = Object.keys(scores).sort(function (a, b) {
652
+ return Number(scores[b]) - Number(scores[a]);
653
+ });
654
+ var card = EL('div', { className: 'card' });
655
+ var ch = EL('div', { className: 'card-head' });
656
+ ch.appendChild(EL('h3', { text: 'Source quality' }));
657
+ ch.appendChild(EL('span', { className: 'sub', text: 'persisted source-outcome posterior · 0.0–1.0' }));
658
+ card.appendChild(ch);
659
+ var cb = EL('div', { className: 'card-pad' });
660
+ if (entries.length === 0) {
661
+ cb.appendChild(EL('p', {
662
+ className: 'muted',
663
+ style: 'padding:16px;text-align:center;font-size:13px',
664
+ text: 'No source-quality observations yet. Recall hits alone do not establish source quality.',
665
+ }));
666
+ } else {
667
+ entries.forEach(function (k) {
668
+ var v = Number(scores[k]);
669
+ var row = EL('div', { style: 'margin-bottom:14px' });
670
+ var meta = EL('div', {
671
+ style: 'display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px',
672
+ });
673
+ meta.appendChild(EL('span', { className: 'mono', text: k }));
674
+ meta.appendChild(EL('b', { className: 'num', text: v.toFixed(2) }));
675
+ row.appendChild(meta);
676
+ row.appendChild(meter(v * 100));
677
+ cb.appendChild(row);
678
+ });
679
+ }
680
+ card.appendChild(cb); sec.appendChild(card);
681
+ return sec;
682
+ }
683
+
684
+ // ======================================================================
685
+ // Tab row
686
+ // ======================================================================
687
+ function buildTabRow(pCount) {
688
+ var tabDiv = EL('div', { className: 'tabs', id: 'od-brain-tabs' });
689
+ [
690
+ ['overview', 'Overview', null],
691
+ ['reward', 'Reward signal', null],
692
+ ['behaviour', 'Behaviour', pCount],
693
+ ['clients', 'Connected clients', null],
694
+ ['sources', 'Source quality', null],
695
+ ].forEach(function (td, i) {
696
+ var btn = EL('button', { className: 'tab' + (i === 0 ? ' active' : ''), 'data-t': td[0] });
697
+ btn.appendChild(document.createTextNode(td[1]));
698
+ if (td[2] !== null) {
699
+ btn.appendChild(document.createTextNode(' '));
700
+ btn.appendChild(EL('span', { className: 'cnt', text: String(td[2]) }));
701
+ }
702
+ tabDiv.appendChild(btn);
703
+ });
704
+ return tabDiv;
705
+ }
706
+
707
+ // ======================================================================
708
+ // Wire tab switching — addEventListener only (CSP-safe, no onclick)
709
+ // ======================================================================
710
+ function wireTabs(container) {
711
+ var tabRow = container.querySelector('#od-brain-tabs');
712
+ if (!tabRow) return;
713
+ var btns = tabRow.querySelectorAll('.tab');
714
+ btns.forEach(function (btn) {
715
+ btn.addEventListener('click', function () {
716
+ btns.forEach(function (b) { b.classList.remove('active'); });
717
+ btn.classList.add('active');
718
+ var t = btn.getAttribute('data-t');
719
+ container.querySelectorAll('.tabpane').forEach(function (s) {
720
+ s.classList.toggle('active', s.getAttribute('data-p') === t);
721
+ });
722
+ });
723
+ });
724
+ }
725
+
726
+ // ======================================================================
727
+ // Loading / error
728
+ // ======================================================================
729
+ function showLoading(container) {
730
+ container.replaceChildren();
731
+ var w = EL('div', { style: 'padding:32px;text-align:center' });
732
+ var spin = EL('div', { className: 'spinner-border text-primary', role: 'status' });
733
+ spin.appendChild(EL('span', { className: 'visually-hidden', text: 'Loading' }));
734
+ w.appendChild(spin);
735
+ w.appendChild(EL('span', { className: 'text-muted ms-2', text: ' Loading Brain…' }));
736
+ container.appendChild(w);
737
+ }
738
+
739
+ function showError(container, err) {
740
+ container.replaceChildren();
741
+ var w = EL('div', { style: 'padding:24px' });
742
+ w.appendChild(EL('p', { className: 'text-danger fw-bold', text: 'Could not load Brain data.' }));
743
+ w.appendChild(EL('p', {
744
+ className: 'text-muted',
745
+ text: 'The daemon may be starting up. Details in the browser console.',
746
+ }));
747
+ var btn = EL('button', {
748
+ className: 'btn btn-sm btn-outline-secondary',
749
+ type: 'button',
750
+ text: 'Retry',
751
+ });
752
+ btn.addEventListener('click', function () { render(container); });
753
+ w.appendChild(btn);
754
+ container.appendChild(w);
755
+ if (err && window.console) window.console.debug('[od-brain]', err.message || String(err));
756
+ }
757
+
758
+ // ======================================================================
759
+ // Main render
760
+ // ======================================================================
761
+ function render(container) {
762
+ showLoading(container);
763
+ injectPhaseStyle();
764
+ fetchAll().then(function (results) {
765
+ var learning = results[0] || {};
766
+ var behavioral = results[1] || {};
767
+ var events = ((results[3] && results[3].events) || []);
768
+ var dateMap = buildDateMap(events);
769
+ var pCount = ((behavioral.patterns) || []).length;
770
+
771
+ var head = EL('div', { className: 'page-head' });
772
+ head.appendChild(EL('h2', { text: 'The living brain' }));
773
+ head.appendChild(EL('p', {
774
+ text: 'How your memory is getting smarter — ranking phase, the reward signal it learns from, ' +
775
+ 'and the behavioural patterns it has extracted. Everything trained on-device from your own usage.',
776
+ }));
777
+
778
+ container.replaceChildren(
779
+ head,
780
+ buildTabRow(pCount),
781
+ buildOverview(learning, behavioral, dateMap),
782
+ buildReward(behavioral),
783
+ buildBehaviour(learning, behavioral),
784
+ buildClients(dateMap),
785
+ buildSourceQuality(learning)
786
+ );
787
+ wireTabs(container);
788
+ }).catch(function (err) { showError(container, err); });
789
+ }
790
+
791
+ // ======================================================================
792
+ // Public API — od-shell.js calls window.odRenderBrain(pane)
793
+ // ======================================================================
794
+ window.odRenderBrain = function (container) { if (container) render(container); };
795
+
796
+ // Legacy compatibility — ng-shell.js calls window.loadBrain()
797
+ window.loadBrain = function () {
798
+ var c = document.getElementById('brain-pane');
799
+ if (c) render(c);
800
+ };
801
+
802
+ // Auto-boot when the pane is already visible (e.g., deep-link to #brain-pane)
803
+ (function boot() {
804
+ function tryBoot() {
805
+ var c = document.getElementById('brain-pane');
806
+ if (!c) return;
807
+ if (c.classList.contains('active') || c.classList.contains('show')) render(c);
808
+ }
809
+ if (document.readyState === 'loading') {
810
+ document.addEventListener('DOMContentLoaded', tryBoot);
811
+ } else {
812
+ tryBoot();
813
+ }
814
+ }());
815
+
816
+ }());