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,258 @@
1
+ // od-team.js — Team & Access (RBAC / C3) card renderer.
2
+ // Renders into window.odRenderTeam(container). Mounted inside the Operations
3
+ // pane (#od-team-mount). Lets a non-technical admin manage who can use this
4
+ // workspace and at what role — entirely from the dashboard.
5
+ //
6
+ // Backend: /api/rbac/{whoami,status,login,logout,users,members,policy}
7
+ // Auth: same-origin loopback = machine owner (implicit root); a logged-in
8
+ // user carries an HttpOnly session cookie set by /api/rbac/login.
9
+ //
10
+ // CSP-safe: no inline handlers — every control is wired via addEventListener.
11
+ //
12
+ // Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
13
+
14
+ /* global window, document, fetch */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ function esc(s) {
19
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
20
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
21
+ });
22
+ }
23
+
24
+ function getJSON(url) {
25
+ return fetch(url, { cache: 'no-store' })
26
+ .then(function (r) { return r.ok ? r.json() : null; })
27
+ .catch(function () { return null; });
28
+ }
29
+
30
+ function postJSON(url, body, method) {
31
+ return fetch(url, {
32
+ method: method || 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify(body || {}),
35
+ }).then(function (r) {
36
+ return r.json().catch(function () { return {}; }).then(function (d) {
37
+ return { ok: r.ok, status: r.status, data: d };
38
+ });
39
+ });
40
+ }
41
+
42
+ var ROLES = ['admin', 'member', 'viewer'];
43
+
44
+ function roleOptions(selected) {
45
+ return ROLES.map(function (r) {
46
+ return '<option value="' + r + '"' + (r === selected ? ' selected' : '') + '>' +
47
+ r.charAt(0).toUpperCase() + r.slice(1) + '</option>';
48
+ }).join('');
49
+ }
50
+
51
+ // ─── Render ────────────────────────────────────────────────────────────────
52
+
53
+ function odRenderTeam(container) {
54
+ if (!container) return;
55
+ container.innerHTML =
56
+ '<div class="card-head"><h3>Team &amp; access</h3></div>' +
57
+ '<div class="card-pad" id="od-team-body">' +
58
+ '<p class="muted">Loading team &amp; access…</p></div>';
59
+
60
+ Promise.all([
61
+ getJSON('/api/rbac/whoami'),
62
+ getJSON('/api/rbac/status'),
63
+ ]).then(function (res) {
64
+ var who = res[0] || { kind: 'owner', permissions: [], role: 'owner' };
65
+ var status = res[1] || { rbac_active: false, require_login: false, user_count: 0 };
66
+ var canManage = (who.permissions || []).indexOf('manage') !== -1;
67
+
68
+ var extras = canManage
69
+ ? Promise.all([getJSON('/api/rbac/users'), getJSON('/api/rbac/members')])
70
+ : Promise.resolve([null, null]);
71
+
72
+ extras.then(function (mgmt) {
73
+ var users = (mgmt[0] && mgmt[0].users) || [];
74
+ var members = (mgmt[1] && mgmt[1].members) || [];
75
+ paint(container, who, status, canManage, users, members);
76
+ });
77
+ });
78
+ }
79
+
80
+ function identityLine(who) {
81
+ if (who.kind === 'user') {
82
+ return '<div class="row" style="justify-content:space-between;align-items:center">' +
83
+ '<div>Signed in as <strong>' + esc(who.display_name || who.username) + '</strong>' +
84
+ ' — role <strong>' + esc(who.role || 'none') + '</strong> on workspace <code>' +
85
+ esc(who.profile) + '</code></div>' +
86
+ '<button class="btn sm" id="od-team-logout">Sign out</button></div>';
87
+ }
88
+ return '<div>Operating as the <strong>machine owner</strong> (full control). ' +
89
+ 'Sign in as a user below to act with a specific role.</div>';
90
+ }
91
+
92
+ function loginForm() {
93
+ return '<div class="card" style="margin-top:12px;max-width:520px">' +
94
+ '<div class="card-head"><h4 style="margin:0">Sign in</h4></div>' +
95
+ '<div class="card-pad">' +
96
+ '<div class="row" style="gap:8px;flex-wrap:wrap;align-items:center">' +
97
+ '<input id="od-team-login-user" class="input sm" placeholder="Username" autocomplete="username">' +
98
+ '<input id="od-team-login-pass" class="input sm" type="password" placeholder="Password" autocomplete="current-password">' +
99
+ '<button class="btn sm primary" id="od-team-login-btn">Sign in</button>' +
100
+ '</div><p class="muted" id="od-team-login-msg" style="margin:8px 0 0"></p>' +
101
+ '</div></div>';
102
+ }
103
+
104
+ function usersTable(users, members) {
105
+ var roleByUser = {};
106
+ members.forEach(function (m) { roleByUser[m.user_id] = m.role; });
107
+ if (!users.length) {
108
+ return '<p class="muted">No users yet. Add your first admin below.</p>';
109
+ }
110
+ var rows = users.map(function (u) {
111
+ var role = roleByUser[u.user_id] || '';
112
+ return '<tr>' +
113
+ '<td>' + esc(u.username) + '</td>' +
114
+ '<td>' + esc(u.display_name || '') + '</td>' +
115
+ '<td><select class="input sm od-team-role" data-uid="' + esc(u.user_id) + '">' +
116
+ '<option value="">— none —</option>' + roleOptions(role) + '</select></td>' +
117
+ '<td>' + esc(u.status) + '</td>' +
118
+ '<td><button class="btn sm danger od-team-del" data-uid="' + esc(u.user_id) +
119
+ '" data-uname="' + esc(u.username) + '">Remove</button></td>' +
120
+ '</tr>';
121
+ }).join('');
122
+ return '<table class="tbl"><thead><tr>' +
123
+ '<th>Username</th><th>Name</th><th>Role (this workspace)</th><th>Status</th><th></th>' +
124
+ '</tr></thead><tbody>' + rows + '</tbody></table>';
125
+ }
126
+
127
+ function manageBlock(status, users, members) {
128
+ return '<div class="card" style="margin-top:12px">' +
129
+ '<div class="card-head"><h4 style="margin:0">Users &amp; roles</h4></div>' +
130
+ '<div class="card-pad">' +
131
+ '<div id="od-team-users">' + usersTable(users, members) + '</div>' +
132
+ '<div class="row" style="gap:8px;flex-wrap:wrap;align-items:center;margin-top:12px">' +
133
+ '<input id="od-team-new-user" class="input sm" placeholder="Username">' +
134
+ '<input id="od-team-new-name" class="input sm" placeholder="Display name (optional)">' +
135
+ '<input id="od-team-new-pass" class="input sm" type="password" placeholder="Password (min 8)">' +
136
+ '<select id="od-team-new-role" class="input sm">' + roleOptions('member') + '</select>' +
137
+ '<button class="btn sm primary" id="od-team-add-btn">Add user</button>' +
138
+ '</div><p class="muted" id="od-team-msg" style="margin:8px 0 0"></p>' +
139
+ '</div></div>' +
140
+ // Policy
141
+ '<div class="card" style="margin-top:12px;max-width:640px">' +
142
+ '<div class="card-head"><h4 style="margin:0">Access policy</h4></div>' +
143
+ '<div class="card-pad">' +
144
+ '<label class="row" style="gap:8px;align-items:center;cursor:pointer">' +
145
+ '<input type="checkbox" id="od-team-require-login"' +
146
+ (status.require_login ? ' checked' : '') + '>' +
147
+ '<span>Require every person to sign in before reading or writing memory ' +
148
+ '(company mode). The machine owner can always manage users.</span></label>' +
149
+ '<p class="muted" id="od-team-policy-msg" style="margin:8px 0 0"></p>' +
150
+ '</div></div>';
151
+ }
152
+
153
+ function paint(container, who, status, canManage, users, members) {
154
+ var body = container.querySelector('#od-team-body');
155
+ if (!body) return;
156
+ var html = identityLine(who);
157
+ if (who.kind !== 'user') html += loginForm();
158
+ if (canManage) html += manageBlock(status, users, members);
159
+ else if (who.kind === 'user') {
160
+ html += '<p class="muted" style="margin-top:12px">Your role does not include ' +
161
+ 'user administration. Ask a workspace admin to change roles.</p>';
162
+ }
163
+ body.innerHTML = html;
164
+ wire(container, who);
165
+ }
166
+
167
+ // ─── Wiring (CSP-safe) ───────────────────────────────────────────────────────
168
+
169
+ function wire(container, who) {
170
+ var q = function (id) { return container.querySelector('#' + id); };
171
+ var reload = function () { odRenderTeam(container); };
172
+
173
+ var loginBtn = q('od-team-login-btn');
174
+ if (loginBtn) {
175
+ loginBtn.addEventListener('click', function () {
176
+ var u = (q('od-team-login-user') || {}).value || '';
177
+ var p = (q('od-team-login-pass') || {}).value || '';
178
+ var msg = q('od-team-login-msg');
179
+ postJSON('/api/rbac/login', { username: u, password: p }).then(function (r) {
180
+ if (r.ok) { reload(); }
181
+ else if (msg) { msg.textContent = (r.data && r.data.detail) || 'Sign in failed.'; }
182
+ });
183
+ });
184
+ }
185
+
186
+ var logoutBtn = q('od-team-logout');
187
+ if (logoutBtn) {
188
+ logoutBtn.addEventListener('click', function () {
189
+ postJSON('/api/rbac/logout', {}).then(reload);
190
+ });
191
+ }
192
+
193
+ var addBtn = q('od-team-add-btn');
194
+ if (addBtn) {
195
+ addBtn.addEventListener('click', function () {
196
+ var msg = q('od-team-msg');
197
+ var body = {
198
+ username: (q('od-team-new-user') || {}).value || '',
199
+ password: (q('od-team-new-pass') || {}).value || '',
200
+ display_name: (q('od-team-new-name') || {}).value || '',
201
+ role: (q('od-team-new-role') || {}).value || 'member',
202
+ };
203
+ addBtn.disabled = true;
204
+ postJSON('/api/rbac/users', body).then(function (r) {
205
+ addBtn.disabled = false;
206
+ if (r.ok) { reload(); }
207
+ else if (msg) { msg.textContent = (r.data && r.data.detail) || 'Could not add user.'; }
208
+ });
209
+ });
210
+ }
211
+
212
+ // Role change dropdowns
213
+ container.querySelectorAll('.od-team-role').forEach(function (sel) {
214
+ sel.addEventListener('change', function () {
215
+ var uid = sel.getAttribute('data-uid');
216
+ var role = sel.value;
217
+ var msg = q('od-team-msg');
218
+ var done = function (r) {
219
+ if (!r.ok && msg) msg.textContent = (r.data && r.data.detail) || 'Role change failed.';
220
+ reload();
221
+ };
222
+ if (!role) {
223
+ postJSON('/api/rbac/members', { user_id: uid }, 'DELETE').then(done);
224
+ } else {
225
+ postJSON('/api/rbac/members', { user_id: uid, role: role }).then(done);
226
+ }
227
+ });
228
+ });
229
+
230
+ // Remove user
231
+ container.querySelectorAll('.od-team-del').forEach(function (btn) {
232
+ btn.addEventListener('click', function () {
233
+ var uid = btn.getAttribute('data-uid');
234
+ var uname = btn.getAttribute('data-uname');
235
+ if (!window.confirm('Remove user "' + uname + '"? This deletes their account and access.')) return;
236
+ postJSON('/api/rbac/users/' + encodeURIComponent(uid), {}, 'DELETE').then(reload);
237
+ });
238
+ });
239
+
240
+ // Require-login policy toggle
241
+ var reqLogin = q('od-team-require-login');
242
+ if (reqLogin) {
243
+ reqLogin.addEventListener('change', function () {
244
+ var msg = q('od-team-policy-msg');
245
+ postJSON('/api/rbac/policy', { require_login: reqLogin.checked }).then(function (r) {
246
+ if (msg) {
247
+ msg.textContent = r.ok
248
+ ? (reqLogin.checked ? 'Company mode on — everyone must sign in.'
249
+ : 'Company mode off — single-operator use.')
250
+ : ((r.data && r.data.detail) || 'Could not update policy.');
251
+ }
252
+ });
253
+ });
254
+ }
255
+ }
256
+
257
+ window.odRenderTeam = odRenderTeam;
258
+ }());
@@ -41,46 +41,169 @@ async function loadProfiles() {
41
41
  }
42
42
  }
43
43
 
44
- async function createProfile(nameOverride) {
45
- var name = nameOverride || document.getElementById('new-profile-name').value.trim();
46
- if (!name) {
47
- name = prompt('Enter new profile name:');
48
- if (!name || !name.trim()) return;
49
- name = name.trim();
50
- }
51
-
52
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
53
- showToast('Invalid name. Use letters, numbers, dashes, underscores.');
54
- return;
55
- }
44
+ var PROFILE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
56
45
 
46
+ // POST /api/profiles/create → { ok, status, detail }. Pure API step, no UI.
47
+ async function _postCreateProfile(name) {
57
48
  try {
58
49
  var response = await fetch('/api/profiles/create', {
59
50
  method: 'POST',
60
51
  headers: { 'Content-Type': 'application/json' },
61
52
  body: JSON.stringify({ profile_name: name })
62
53
  });
63
- var data = await response.json();
64
- if (response.status === 409) {
65
- showToast('Profile "' + name + '" already exists');
54
+ var data = {};
55
+ try { data = await response.json(); } catch (e) { /* empty body */ }
56
+ return { ok: response.ok, status: response.status, detail: data.detail || '' };
57
+ } catch (error) {
58
+ console.error('Error creating profile:', error);
59
+ return { ok: false, status: 0, detail: 'Network error creating profile' };
60
+ }
61
+ }
62
+
63
+ // Refresh every profile-aware surface after a successful create.
64
+ function _afterProfileMutation() {
65
+ // Small delay so the backend has persisted to both stores before re-read.
66
+ setTimeout(function () {
67
+ loadProfiles();
68
+ if (typeof loadProfilesTable === 'function') loadProfilesTable();
69
+ }, 300);
70
+ }
71
+
72
+ // Entry point used by the sidebar "+", the legacy Profiles-tab form, and the
73
+ // data-act-click="create-profile" button. Resolves a name from (a) an explicit
74
+ // override, (b) the legacy #new-profile-name input, or (c) an OD-styled modal —
75
+ // never the native prompt() (crude for the non-technical dashboard users).
76
+ async function createProfile(nameOverride) {
77
+ var name = (typeof nameOverride === 'string' && nameOverride) ? nameOverride : '';
78
+ if (!name) {
79
+ var input = document.getElementById('new-profile-name');
80
+ if (input && input.value.trim()) name = input.value.trim();
81
+ }
82
+ if (!name) {
83
+ openCreateProfileModal();
84
+ return;
85
+ }
86
+ if (!PROFILE_NAME_RE.test(name)) {
87
+ showToast('Invalid name. Use letters, numbers, dashes, underscores.');
88
+ return;
89
+ }
90
+ var res = await _postCreateProfile(name);
91
+ if (res.status === 409) { showToast('Profile "' + name + '" already exists'); return; }
92
+ if (!res.ok) { showToast(res.detail || 'Failed to create profile'); return; }
93
+ showToast('Profile "' + name + '" created');
94
+ var legacyInput = document.getElementById('new-profile-name');
95
+ if (legacyInput) legacyInput.value = '';
96
+ _afterProfileMutation();
97
+ }
98
+
99
+ // OD-styled create-profile dialog. Lazily built, reused across opens. Inline
100
+ // validation + 409 handling; ESC / backdrop / Cancel dismiss; Enter submits.
101
+ function openCreateProfileModal() {
102
+ var existing = document.getElementById('od-create-profile-overlay');
103
+ if (existing) { existing.remove(); }
104
+
105
+ var overlay = document.createElement('div');
106
+ overlay.id = 'od-create-profile-overlay';
107
+ overlay.setAttribute('role', 'dialog');
108
+ overlay.setAttribute('aria-modal', 'true');
109
+ overlay.setAttribute('aria-label', 'Create a new profile');
110
+ overlay.style.cssText =
111
+ 'position:fixed;inset:0;z-index:11000;display:flex;align-items:center;' +
112
+ 'justify-content:center;background:rgba(0,0,0,0.45);backdrop-filter:blur(2px);';
113
+
114
+ var card = document.createElement('div');
115
+ card.style.cssText =
116
+ 'width:min(420px,92vw);background:var(--card,#1a1f2e);color:var(--fg,#e8ecf3);' +
117
+ 'border:1px solid var(--border,rgba(255,255,255,0.1));border-radius:14px;' +
118
+ 'box-shadow:0 20px 60px rgba(0,0,0,0.4);padding:22px 22px 18px;';
119
+
120
+ var h = document.createElement('h3');
121
+ h.textContent = 'Create a new profile';
122
+ h.style.cssText = 'margin:0 0 4px;font-size:1.05rem;font-weight:600;';
123
+ var sub = document.createElement('p');
124
+ sub.textContent = 'Each profile is a fully isolated memory space.';
125
+ sub.style.cssText = 'margin:0 0 16px;font-size:0.8125rem;color:var(--fg-3,#8b93a7);';
126
+
127
+ var input = document.createElement('input');
128
+ input.type = 'text';
129
+ input.maxLength = 32;
130
+ input.placeholder = 'e.g. work, personal, project-x';
131
+ input.setAttribute('aria-label', 'Profile name');
132
+ input.style.cssText =
133
+ 'width:100%;box-sizing:border-box;padding:10px 12px;font-size:0.9rem;' +
134
+ 'background:var(--page,rgba(255,255,255,0.04));color:var(--fg,#e8ecf3);' +
135
+ 'border:1px solid var(--border,rgba(255,255,255,0.14));border-radius:8px;outline:none;';
136
+
137
+ var err = document.createElement('div');
138
+ err.style.cssText = 'min-height:18px;margin:6px 2px 0;font-size:0.75rem;color:#ff6b6b;';
139
+
140
+ var actions = document.createElement('div');
141
+ actions.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;margin-top:16px;';
142
+
143
+ var cancel = document.createElement('button');
144
+ cancel.type = 'button';
145
+ cancel.textContent = 'Cancel';
146
+ cancel.style.cssText =
147
+ 'padding:8px 14px;font-size:0.85rem;border-radius:8px;cursor:pointer;' +
148
+ 'background:transparent;color:var(--fg-3,#8b93a7);' +
149
+ 'border:1px solid var(--border,rgba(255,255,255,0.14));';
150
+
151
+ var create = document.createElement('button');
152
+ create.type = 'button';
153
+ create.textContent = 'Create profile';
154
+ create.style.cssText =
155
+ 'padding:8px 16px;font-size:0.85rem;border-radius:8px;cursor:pointer;' +
156
+ 'background:var(--violet,#7c5cff);color:#fff;border:1px solid transparent;font-weight:600;';
157
+
158
+ actions.appendChild(cancel);
159
+ actions.appendChild(create);
160
+ card.appendChild(h);
161
+ card.appendChild(sub);
162
+ card.appendChild(input);
163
+ card.appendChild(err);
164
+ card.appendChild(actions);
165
+ overlay.appendChild(card);
166
+ document.body.appendChild(overlay);
167
+ setTimeout(function () { input.focus(); }, 30);
168
+
169
+ function close() {
170
+ document.removeEventListener('keydown', onKey);
171
+ overlay.remove();
172
+ }
173
+ function onKey(e) {
174
+ if (e.key === 'Escape') { close(); }
175
+ else if (e.key === 'Enter') { submit(); }
176
+ }
177
+ async function submit() {
178
+ var name = input.value.trim();
179
+ if (!name) { err.textContent = 'Please enter a profile name.'; return; }
180
+ if (!PROFILE_NAME_RE.test(name)) {
181
+ err.textContent = 'Use only letters, numbers, dashes and underscores.';
66
182
  return;
67
183
  }
68
- if (!response.ok) {
69
- showToast(data.detail || 'Failed to create profile');
184
+ create.disabled = true;
185
+ create.textContent = 'Creating…';
186
+ err.textContent = '';
187
+ var res = await _postCreateProfile(name);
188
+ if (res.status === 409) {
189
+ err.textContent = 'A profile named "' + name + '" already exists.';
190
+ create.disabled = false; create.textContent = 'Create profile';
191
+ return;
192
+ }
193
+ if (!res.ok) {
194
+ err.textContent = res.detail || 'Failed to create profile.';
195
+ create.disabled = false; create.textContent = 'Create profile';
70
196
  return;
71
197
  }
198
+ close();
72
199
  showToast('Profile "' + name + '" created');
73
- var input = document.getElementById('new-profile-name');
74
- if (input) input.value = '';
75
- // Force reload with small delay to ensure backend has persisted
76
- setTimeout(function() {
77
- loadProfiles();
78
- if (typeof loadProfilesTable === 'function') loadProfilesTable();
79
- }, 300);
80
- } catch (error) {
81
- console.error('Error creating profile:', error);
82
- showToast('Error creating profile');
200
+ _afterProfileMutation();
83
201
  }
202
+
203
+ cancel.addEventListener('click', close);
204
+ create.addEventListener('click', submit);
205
+ overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); });
206
+ document.addEventListener('keydown', onKey);
84
207
  }
85
208
 
86
209
  async function deleteProfile(name) {
@@ -209,25 +332,15 @@ async function switchProfile(profileName) {
209
332
  var acknowledged = response.ok && data.success === true &&
210
333
  data.active_profile === profileName && Number.isInteger(data.generation);
211
334
  if (acknowledged) {
212
- showToast('Switched to profile: ' + profileName);
213
- loadProfiles();
214
- loadStats();
215
- if (typeof loadGraph === 'function') loadGraph();
216
- loadProfilesTable();
217
- // v2.7.4: Reload ALL tabs for new profile
218
- if (typeof loadLearning === 'function') loadLearning();
219
- if (typeof refreshFeedbackStats === 'function') refreshFeedbackStats();
220
- if (typeof loadLearningDataStats === 'function') loadLearningDataStats();
221
- if (typeof loadAgents === 'function') loadAgents();
222
- if (typeof loadMemories === 'function') loadMemories();
223
- if (typeof loadTimeline === 'function') loadTimeline();
224
- if (typeof loadEvents === 'function') loadEvents();
225
- // v2.8 tabs
226
- if (typeof loadLifecycle === 'function') loadLifecycle();
227
- if (typeof loadBehavioral === 'function') loadBehavioral();
228
- if (typeof loadCompliance === 'function') loadCompliance();
229
- var activeTab = document.querySelector('#mainTabs .nav-link.active');
230
- if (activeTab) activeTab.click();
335
+ // A profile switch is a full context change: reload the whole
336
+ // dashboard so EVERY pane, KPI, graph, and table reflects the new
337
+ // profile. Piecemeal per-pane refresh (the legacy loadX pile) left
338
+ // stale cross-profile data in any pane that wasn't re-fetched, and
339
+ // the OD dashboard's panes aren't driven by those legacy loaders.
340
+ showToast('Switched to profile: ' + profileName + ' — refreshing…');
341
+ setTimeout(function () {
342
+ try { window.location.reload(); } catch (e) { /* headless/jsdom */ }
343
+ }, 350);
231
344
  return true;
232
345
  } else {
233
346
  showToast(data.detail || 'Daemon did not acknowledge the requested profile');
@@ -380,7 +380,7 @@ function _updateNavbarWidget(destinations) {
380
380
  icon +
381
381
  '<span style="flex:1;color:#e0e0e0;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + name + '</span>' +
382
382
  '<span class="account-dest-badge ' + statusCls + '">' + statusText + '</span>' +
383
- '<button class="btn btn-sm" onclick="disconnectDestination(\'' + dest.id + '\')" style="padding:0;border:0;color:#555;font-size:12px;" title="Disconnect"><i class="bi bi-x"></i></button>' +
383
+ '<button class="btn btn-sm" data-act-click="disconnect-destination" data-dest-id="' + dest.id + '" style="padding:0;border:0;color:#555;font-size:12px;" title="Disconnect"><i class="bi bi-x"></i></button>' +
384
384
  '</div>';
385
385
  });
386
386
  accountList.innerHTML = html;
@@ -422,7 +422,7 @@ function renderCloudDestinations(destinations, container) {
422
422
  card.innerHTML = '<div class="card-body p-2">' +
423
423
  '<div class="d-flex justify-content-between align-items-center">' +
424
424
  '<div><i class="bi bi-' + icon + '"></i> <strong>' + dest.display_name + '</strong> ' + statusBadge + '</div>' +
425
- '<button class="btn btn-outline-danger btn-sm" onclick="disconnectDestination(\'' + dest.id + '\')"><i class="bi bi-x-circle"></i></button>' +
425
+ '<button class="btn btn-outline-danger btn-sm" data-act-click="disconnect-destination" data-dest-id="' + dest.id + '"><i class="bi bi-x-circle"></i></button>' +
426
426
  '</div>' +
427
427
  '<div class="small text-muted mt-1">Last sync: ' + lastSync + '</div>' +
428
428
  '</div>';
@@ -529,7 +529,21 @@ async function syncCloudNow() {
529
529
  async function exportBackup() {
530
530
  showToast('Preparing backup export...');
531
531
  try {
532
- window.location.href = '/api/backup/export';
532
+ var response = await fetch('/api/backup/export', { method: 'POST' });
533
+ if (!response.ok) throw new Error('Export rejected');
534
+ var blob = await response.blob();
535
+ var disposition = response.headers.get('Content-Disposition') || '';
536
+ var match = disposition.match(/filename="?([^";]+)"?/i);
537
+ var objectUrl = URL.createObjectURL(blob);
538
+ var link = document.createElement('a');
539
+ link.href = objectUrl;
540
+ link.download = match ? match[1] : 'superlocalmemory-backup.db.gz';
541
+ link.style.display = 'none';
542
+ document.body.appendChild(link);
543
+ link.click();
544
+ link.remove();
545
+ URL.revokeObjectURL(objectUrl);
546
+ showToast('Backup export downloaded');
533
547
  } catch (error) {
534
548
  showToast('Export failed');
535
549
  }
@@ -14,20 +14,49 @@ async function loadTimeline() {
14
14
  }
15
15
  }
16
16
 
17
+ // DASH-V5 (3.7.9): the /timeline endpoint returns individual events (with a
18
+ // `timestamp` but no `count`/`date`), while this chart needs per-day buckets.
19
+ // Reading a non-existent `count` produced a [0, NaN] y-domain and 300+ rects
20
+ // with height="NaN". Aggregate events into per-day counts client-side (and
21
+ // still accept a pre-aggregated {date,count} shape if the API ever provides it).
22
+ function _bucketTimeline(timeline) {
23
+ var first = timeline[0] || {};
24
+ if (first.count !== undefined && (first.date || first.period)) {
25
+ return timeline
26
+ .map(function(d) { return { date: d.date || d.period, count: +d.count || 0 }; })
27
+ .filter(function(d) { return d.date; });
28
+ }
29
+ var byDay = {};
30
+ timeline.forEach(function(e) {
31
+ var ts = e.timestamp || e.date || e.period || '';
32
+ var day = String(ts).slice(0, 10);
33
+ if (day) byDay[day] = (byDay[day] || 0) + 1;
34
+ });
35
+ return Object.keys(byDay).sort().map(function(day) {
36
+ return { date: day, count: byDay[day] };
37
+ });
38
+ }
39
+
17
40
  function renderTimeline(timeline) {
18
41
  var container = document.getElementById('timeline-chart');
19
42
  if (!timeline || timeline.length === 0) {
20
43
  showEmpty('timeline-chart', 'clock-history', 'No timeline data for the last 30 days.');
21
44
  return;
22
45
  }
46
+ var buckets = _bucketTimeline(timeline);
47
+ if (buckets.length === 0) {
48
+ showEmpty('timeline-chart', 'clock-history', 'No timeline data for the last 30 days.');
49
+ return;
50
+ }
23
51
  var margin = { top: 20, right: 20, bottom: 50, left: 50 };
24
- var width = container.clientWidth - margin.left - margin.right;
52
+ var width = Math.max(10, (container.clientWidth || 600) - margin.left - margin.right);
25
53
  var height = 300 - margin.top - margin.bottom;
26
54
  container.textContent = '';
27
55
  var svg = d3.select('#timeline-chart').append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom).append('g').attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
28
- var x = d3.scaleBand().range([0, width]).domain(timeline.map(function(d) { return d.date || d.period; })).padding(0.1);
29
- var y = d3.scaleLinear().range([height, 0]).domain([0, d3.max(timeline, function(d) { return d.count; })]);
56
+ var x = d3.scaleBand().range([0, width]).domain(buckets.map(function(d) { return d.date; })).padding(0.1);
57
+ var maxCount = d3.max(buckets, function(d) { return d.count; }) || 1;
58
+ var y = d3.scaleLinear().range([height, 0]).domain([0, maxCount]);
30
59
  svg.append('g').attr('transform', 'translate(0,' + height + ')').call(d3.axisBottom(x)).selectAll('text').attr('transform', 'rotate(-45)').style('text-anchor', 'end');
31
- svg.append('g').call(d3.axisLeft(y));
32
- svg.selectAll('.bar').data(timeline).enter().append('rect').attr('class', 'bar').attr('x', function(d) { return x(d.date || d.period); }).attr('y', function(d) { return y(d.count); }).attr('width', x.bandwidth()).attr('height', function(d) { return height - y(d.count); }).attr('fill', '#667eea').attr('rx', 3);
60
+ svg.append('g').call(d3.axisLeft(y).ticks(Math.min(maxCount, 8)));
61
+ svg.selectAll('.bar').data(buckets).enter().append('rect').attr('class', 'bar').attr('x', function(d) { return x(d.date); }).attr('y', function(d) { return y(d.count); }).attr('width', x.bandwidth()).attr('height', function(d) { return Math.max(0, height - y(d.count)); }).attr('fill', '#667eea').attr('rx', 3);
33
62
  }
@@ -2,8 +2,8 @@
2
2
  // Loads and displays Bayesian trust scores per agent and per fact.
3
3
  //
4
4
  // v3.4.21 (Operations pane): /api/v3/trust/dashboard returns
5
- // thousands of rows in one shot (3,900+ agents on Varun's live DB).
6
- // Client-side pagination keeps the table bounded; the user chooses
5
+ // thousands of rows in one shot; client-side pagination keeps the table bounded.
6
+ // The user chooses
7
7
  // sort order + page size; all data stays real (no mock fallback).
8
8
 
9
9
  (function trustDashboard() {
@@ -186,10 +186,12 @@ class LanceDBVectorBackend:
186
186
  if tier_filter is None:
187
187
  tier_filter = ["active", "warm"]
188
188
 
189
- # F-27: Validate tiers
190
- assert all(t in self.VALID_TIERS for t in tier_filter), (
191
- f"Invalid tier filter: {set(tier_filter) - self.VALID_TIERS}"
192
- )
189
+ # F-27 / LOW-1 (3.7.9): validate tiers with a real check, not assert —
190
+ # `python -O` strips asserts, which would let an invalid tier build an
191
+ # invalid LanceDB predicate.
192
+ invalid = set(tier_filter) - self.VALID_TIERS
193
+ if invalid:
194
+ raise ValueError(f"Invalid tier filter: {invalid}")
193
195
 
194
196
  try:
195
197
  search = self._table.search(query_vector).metric("cosine").limit(top_k)
@@ -289,7 +291,7 @@ class LanceDBVectorBackend:
289
291
  """Update tier for a single fact."""
290
292
  try:
291
293
  self._table.update(
292
- where=f"fact_id = '{fact_id}'",
294
+ where=self._fact_predicate(fact_id),
293
295
  values={"tier": new_tier},
294
296
  )
295
297
  except Exception as exc:
@@ -309,7 +311,7 @@ class LanceDBVectorBackend:
309
311
  for fact_id, tier in rows:
310
312
  try:
311
313
  self._table.update(
312
- where=f"fact_id = '{fact_id}'",
314
+ where=self._fact_predicate(fact_id),
313
315
  values={"tier": tier},
314
316
  )
315
317
  updated += 1
@@ -1,12 +0,0 @@
1
- {
2
- "mcpServers": {
3
- "superlocalmemory": {
4
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/slm-launch",
5
- "args": [],
6
- "env": {
7
- "SLM_MCP_PROFILE": "code",
8
- "SLM_DATA_DIR": "${CLAUDE_PLUGIN_DATA}"
9
- }
10
- }
11
- }
12
- }