superlocalmemory 3.7.7 → 3.8.0

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 (262) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +85 -0
  4. package/README.md +199 -29
  5. package/package.json +4 -2
  6. package/plugin/.claude-plugin/plugin.json +2 -2
  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/agents/slm-governance-advisor.md +80 -0
  30. package/plugin-src/agents/slm-loop-runner.md +71 -0
  31. package/plugin-src/agents/slm-memory-advisor.md +10 -5
  32. package/plugin-src/agents/slm-optimize-advisor.md +9 -3
  33. package/plugin-src/commands/slm-loop.md +31 -0
  34. package/plugin-src/hooks/hooks.json +79 -0
  35. package/plugin-src/manifest.json +7 -2
  36. package/plugin-src/requirements.txt +1 -1
  37. package/plugin-src/rules/AGENTS.md +57 -18
  38. package/plugin-src/rules/CLAUDE.md.fragment +8 -8
  39. package/plugin-src/scripts/slm-launch +46 -7
  40. package/plugin-src/settings.json +9 -0
  41. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  42. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  43. package/plugin-src/skills/slm-governance/SKILL.md +248 -0
  44. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  45. package/plugin-src/skills/slm-loop/SKILL.md +99 -0
  46. package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
  47. package/plugin-src/skills/slm-profile/SKILL.md +148 -0
  48. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  49. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  50. package/plugin-src/skills/slm-scope/SKILL.md +176 -0
  51. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  52. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  53. package/pyproject.toml +1 -2
  54. package/scripts/postinstall/validation.js +2 -0
  55. package/scripts/postinstall-interactive.js +74 -2
  56. package/src/superlocalmemory/__init__.py +1 -1
  57. package/src/superlocalmemory/access/__init__.py +3 -0
  58. package/src/superlocalmemory/access/rbac.py +477 -0
  59. package/src/superlocalmemory/cli/commands.py +96 -12
  60. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  61. package/src/superlocalmemory/cli/loop_cmd.py +192 -0
  62. package/src/superlocalmemory/cli/main.py +39 -4
  63. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  64. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  65. package/src/superlocalmemory/cli/pending_store.py +49 -13
  66. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  67. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  68. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  69. package/src/superlocalmemory/compliance/audit.py +6 -0
  70. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  71. package/src/superlocalmemory/compliance/retention.py +176 -45
  72. package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
  73. package/src/superlocalmemory/core/community_summary.py +267 -0
  74. package/src/superlocalmemory/core/config.py +216 -3
  75. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  76. package/src/superlocalmemory/core/context_cache.py +61 -18
  77. package/src/superlocalmemory/core/embedding_worker.py +17 -2
  78. package/src/superlocalmemory/core/embeddings.py +12 -1
  79. package/src/superlocalmemory/core/engine.py +17 -1
  80. package/src/superlocalmemory/core/engine_ingestion.py +29 -0
  81. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  82. package/src/superlocalmemory/core/entity_community.py +178 -0
  83. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  84. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  85. package/src/superlocalmemory/core/key_expander.py +138 -0
  86. package/src/superlocalmemory/core/maintenance.py +23 -0
  87. package/src/superlocalmemory/core/modes.py +1 -1
  88. package/src/superlocalmemory/core/mutations.py +2 -2
  89. package/src/superlocalmemory/core/pii.py +105 -0
  90. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  91. package/src/superlocalmemory/core/recall_pipeline.py +2 -0
  92. package/src/superlocalmemory/core/recall_worker.py +20 -6
  93. package/src/superlocalmemory/core/scale_engine.py +60 -1
  94. package/src/superlocalmemory/core/security_primitives.py +40 -2
  95. package/src/superlocalmemory/core/store_pipeline.py +35 -11
  96. package/src/superlocalmemory/core/worker_pool.py +21 -6
  97. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  98. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  99. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  100. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  101. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  102. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  103. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  104. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  105. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  106. package/src/superlocalmemory/evolution/triggers.py +22 -13
  107. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  108. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  109. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  110. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  111. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  112. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  113. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  114. package/src/superlocalmemory/hooks/portable_kit.py +114 -1
  115. package/src/superlocalmemory/infra/auth_middleware.py +28 -0
  116. package/src/superlocalmemory/infra/backup.py +12 -1
  117. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  118. package/src/superlocalmemory/infra/data_root.py +43 -4
  119. package/src/superlocalmemory/infra/event_bus.py +107 -24
  120. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  121. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  122. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  123. package/src/superlocalmemory/learning/cross_project.py +28 -19
  124. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  125. package/src/superlocalmemory/loops/__init__.py +56 -0
  126. package/src/superlocalmemory/loops/budget.py +58 -0
  127. package/src/superlocalmemory/loops/engine.py +164 -0
  128. package/src/superlocalmemory/loops/ledger.py +243 -0
  129. package/src/superlocalmemory/loops/models.py +152 -0
  130. package/src/superlocalmemory/loops/rules.py +52 -0
  131. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  132. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  133. package/src/superlocalmemory/mcp/profiles.py +103 -0
  134. package/src/superlocalmemory/mcp/server.py +21 -49
  135. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  136. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  137. package/src/superlocalmemory/mcp/tools_core.py +50 -5
  138. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  139. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  140. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  141. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  142. package/src/superlocalmemory/mesh/broker.py +237 -129
  143. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  144. package/src/superlocalmemory/optimize/NOTICE +1 -6
  145. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  147. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  148. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  149. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  150. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  151. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  152. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  153. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  154. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  155. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  156. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  157. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  158. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  159. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  160. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  161. package/src/superlocalmemory/retrieval/engine.py +168 -26
  162. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  163. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  164. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  165. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  166. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  167. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  168. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  169. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  170. package/src/superlocalmemory/server/api.py +21 -4
  171. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  172. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  173. package/src/superlocalmemory/server/recall_health.py +24 -3
  174. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  175. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  176. package/src/superlocalmemory/server/routes/agents.py +128 -38
  177. package/src/superlocalmemory/server/routes/backup.py +34 -10
  178. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  179. package/src/superlocalmemory/server/routes/brain.py +21 -5
  180. package/src/superlocalmemory/server/routes/chat.py +72 -16
  181. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  182. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  183. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  184. package/src/superlocalmemory/server/routes/entity.py +9 -4
  185. package/src/superlocalmemory/server/routes/events.py +24 -8
  186. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  187. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  188. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  189. package/src/superlocalmemory/server/routes/insights.py +3 -3
  190. package/src/superlocalmemory/server/routes/learning.py +14 -14
  191. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  192. package/src/superlocalmemory/server/routes/memories.py +221 -49
  193. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  194. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  195. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  196. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  197. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  198. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  199. package/src/superlocalmemory/server/routes/stats.py +13 -6
  200. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  201. package/src/superlocalmemory/server/routes/v3_api.py +194 -81
  202. package/src/superlocalmemory/server/routes/ws.py +5 -2
  203. package/src/superlocalmemory/server/security_middleware.py +12 -5
  204. package/src/superlocalmemory/server/ui.py +30 -5
  205. package/src/superlocalmemory/server/unified_daemon.py +431 -75
  206. package/src/superlocalmemory/server/write_identity.py +38 -8
  207. package/src/superlocalmemory/storage/database.py +265 -53
  208. package/src/superlocalmemory/storage/migration_runner.py +53 -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/models.py +4 -0
  217. package/src/superlocalmemory/storage/schema.py +87 -0
  218. package/src/superlocalmemory/storage/schema_v32.py +0 -9
  219. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  220. package/src/superlocalmemory/trust/gate.py +49 -8
  221. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  222. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  223. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  224. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  225. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  226. package/src/superlocalmemory/ui/favicon.svg +35 -4
  227. package/src/superlocalmemory/ui/index.html +306 -173
  228. package/src/superlocalmemory/ui/js/brain.js +5 -20
  229. package/src/superlocalmemory/ui/js/core.js +47 -31
  230. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  231. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  232. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  233. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  234. package/src/superlocalmemory/ui/js/memories.js +15 -4
  235. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  236. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  237. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  238. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  239. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  240. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  241. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  242. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  243. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  244. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  245. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  246. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  247. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  248. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  249. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  250. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  251. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  252. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  253. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  254. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  255. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  256. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  257. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  258. package/src/superlocalmemory/ui/js/settings.js +2 -2
  259. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  260. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  261. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  262. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -0,0 +1,1053 @@
1
+ /* od-settings.js — SuperLocalMemory Settings Hub v2.1
2
+ * ALL 13 config groups wired to live daemon endpoints.
3
+ * Exposes: window.odRenderSettings(container)
4
+ * Auth: X-Install-Token cached from GET /internal/token (used on all new PUTs)
5
+ * Endpoints:
6
+ * GET/POST /api/v3/mode, /api/v3/mode/set
7
+ * GET/PUT /api/v3/embedding/config, POST /api/v3/embedding/test
8
+ * GET/PUT /api/v3/storage/config → restart_required
9
+ * GET/PUT /api/v3/scope/config
10
+ * GET/PUT /api/v3/auto-capture/config
11
+ * GET/PUT /api/v3/auto-recall/config
12
+ * GET/PUT /api/v3/auto-invoke/config
13
+ * GET/PUT /api/v3/forgetting/config + GET /api/v3/forgetting/stats
14
+ * GET/POST /api/evolution/status, /api/evolution/enable|disable|config
15
+ * GET/POST /api/backup/status, /api/backup/configure, /api/backup/create
16
+ * GET/PUT /api/v3/mesh/config
17
+ * GET/PUT /api/v3/trust/config
18
+ * GET/PUT /api/v3/daemon/config → restart_required
19
+ * CRIT fixes: (1) every control reads on render and persists on change;
20
+ * (2) all new PUTs include X-Install-Token via cached authPut helper;
21
+ * (3) restart_required surfaced inline after storage and daemon saves.
22
+ * Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
23
+ */
24
+ (function () {
25
+ 'use strict';
26
+
27
+ var ID = 'od-settings-hub';
28
+ var P = 'od-s';
29
+ var _captureConfig = {}; // preserves full capture config for merge-on-save
30
+ var _tokenCache = null;
31
+
32
+ /* ── Auth helper ──────────────────────────────────────────── */
33
+ function getToken() {
34
+ if (_tokenCache) return Promise.resolve(_tokenCache);
35
+ return fetch('/internal/token')
36
+ .then(function (r) { return r.json(); })
37
+ .then(function (d) { _tokenCache = d.token || ''; return _tokenCache; })
38
+ .catch(function () { return ''; });
39
+ }
40
+ /** PUT with X-Install-Token. Use for all new config endpoints. */
41
+ function authPut(url, body) {
42
+ return getToken().then(function (tok) {
43
+ return fetch(url, {
44
+ method: 'PUT',
45
+ headers: { 'Content-Type': 'application/json', 'X-Install-Token': tok },
46
+ body: JSON.stringify(body)
47
+ });
48
+ });
49
+ }
50
+
51
+ /* ── Helpers ──────────────────────────────────────────────── */
52
+ function esc(s) {
53
+ return String(s == null ? '' : s)
54
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;')
55
+ .replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
56
+ }
57
+ function toast(msg, err) {
58
+ if (typeof window.showToast === 'function') { window.showToast(msg); return; }
59
+ var d = document.createElement('div');
60
+ d.textContent = msg;
61
+ Object.assign(d.style, { position:'fixed', bottom:'20px', right:'20px', zIndex:'9999',
62
+ background: err ? 'var(--danger)' : 'var(--violet)', color:'#fff',
63
+ padding:'10px 18px', borderRadius:'10px', fontSize:'13px',
64
+ boxShadow:'0 4px 16px rgba(0,0,0,.4)', maxWidth:'340px' });
65
+ document.body.appendChild(d);
66
+ setTimeout(function () { d.remove(); }, 3200);
67
+ }
68
+ function q(s) { return document.getElementById(P + '-' + s); }
69
+ function el(tag, a, css) {
70
+ var e = document.createElement(tag);
71
+ if (a) Object.keys(a).forEach(function (k) {
72
+ if (k === 'text') e.textContent = a[k]; else e.setAttribute(k, a[k]);
73
+ });
74
+ if (css) Object.assign(e.style, css);
75
+ return e;
76
+ }
77
+ function setSt(id, msg, ok) {
78
+ var e = q(id + '-st'); if (!e) return;
79
+ e.textContent = msg;
80
+ e.style.color = ok === true ? 'var(--ok)' : ok === false ? 'var(--danger)' : 'var(--fg-2)';
81
+ if (ok === true) setTimeout(function () { if (e) e.textContent = ''; }, 2500);
82
+ }
83
+ function showRestartNote(id, needed) {
84
+ var e = q(id + '-restart'); if (!e) return;
85
+ e.style.display = needed ? 'flex' : 'none';
86
+ }
87
+
88
+ /* ── Control factories ────────────────────────────────────── */
89
+ function makeSwitch(id, on) {
90
+ var b = el('button', { type:'button', role:'switch',
91
+ 'aria-checked': String(!!on), id: P + '-' + id });
92
+ b.className = 'switch' + (on ? ' on' : '');
93
+ b.addEventListener('click', function () {
94
+ b.classList.toggle('on');
95
+ b.setAttribute('aria-checked', String(b.classList.contains('on')));
96
+ b.dispatchEvent(new CustomEvent('od-toggle',
97
+ { bubbles:true, detail:{ id:id, on:b.classList.contains('on') } }));
98
+ });
99
+ return b;
100
+ }
101
+ function makeSel(id, opts) {
102
+ var s = el('select', { id: P + '-' + id });
103
+ Object.assign(s.style, { height:'34px', borderRadius:'var(--r-md)',
104
+ border:'1px solid var(--border)', background:'var(--card-2)',
105
+ color:'var(--fg)', padding:'0 10px', fontSize:'13px',
106
+ minWidth:'140px', outline:'none' });
107
+ opts.forEach(function (o) {
108
+ var opt = document.createElement('option');
109
+ opt.value = o.v; opt.textContent = o.l; s.appendChild(opt);
110
+ });
111
+ return s;
112
+ }
113
+ function makeTin(id, ph, type) {
114
+ var i = el('input', { id: P + '-' + id, type: type || 'text', placeholder: ph || '' });
115
+ Object.assign(i.style, { height:'34px', borderRadius:'var(--r-md)',
116
+ border:'1px solid var(--border)', background:'var(--card-2)',
117
+ color:'var(--fg)', padding:'0 10px', fontSize:'13px',
118
+ minWidth:'160px', maxWidth:'260px', outline:'none' });
119
+ return i;
120
+ }
121
+ function makeRange(id, min, max, step, val) {
122
+ var w = el('div', null, { display:'flex', alignItems:'center', gap:'10px' });
123
+ var inp = el('input', { id: P + '-' + id, type:'range',
124
+ min:min, max:max, step:step, value:val });
125
+ inp.style.width = '110px';
126
+ var disp = el('span', { id: P + '-' + id + '-disp', text: val },
127
+ { fontFamily:'var(--font-mono)', fontSize:'12px', minWidth:'36px' });
128
+ inp.addEventListener('input', function () {
129
+ disp.textContent = Number(this.value).toFixed(parseFloat(step) < 1 ? 2 : 0);
130
+ });
131
+ w.appendChild(inp); w.appendChild(disp);
132
+ return w;
133
+ }
134
+ function makeBtn(id, txt, pri) {
135
+ var b = el('button', { type:'button', id: P + '-' + id });
136
+ b.className = 'btn sm ' + (pri ? 'primary' : 'ghost');
137
+ b.textContent = txt;
138
+ return b;
139
+ }
140
+ function makeSt(id) {
141
+ return el('span', { id: P + '-' + id + '-st' },
142
+ { fontSize:'12px', marginLeft:'8px', color:'var(--fg-2)' });
143
+ }
144
+ function bRow() {
145
+ var d = el('div', null, { display:'flex', gap:'8px', alignItems:'center', flexWrap:'wrap' });
146
+ Array.from(arguments).forEach(function (c) { d.appendChild(c); });
147
+ return d;
148
+ }
149
+ function restartDaemonFromNote(btn) {
150
+ // Same endpoint the Governance › System-control button uses. POST is
151
+ // mutating, so core.js attaches the X-Install-Token automatically.
152
+ btn.disabled = true;
153
+ btn.textContent = 'Restarting…';
154
+ fetch('/api/daemon/restart', { method: 'POST', credentials: 'same-origin' })
155
+ .then(function () {
156
+ btn.textContent = 'Restarting… reconnecting';
157
+ setTimeout(function () { window.location.reload(); }, 4000);
158
+ })
159
+ .catch(function () {
160
+ btn.disabled = false;
161
+ btn.textContent = 'Restart daemon now';
162
+ if (typeof showToast === 'function') showToast('Restart request failed');
163
+ });
164
+ }
165
+ function makeRestartNote(id) {
166
+ var wrap = el('div', { id: P + '-' + id + '-restart' }, {
167
+ display: 'none', alignItems: 'center', gap: '10px', flexWrap: 'wrap',
168
+ fontSize: '12px', fontWeight: '600', color: '#d97706',
169
+ padding: '5px 10px', background: 'rgba(217,119,6,.12)',
170
+ borderRadius: 'var(--r-md,6px)'
171
+ });
172
+ wrap.appendChild(el('span', { text: 'Config saved — restart the daemon to apply.' }));
173
+ var btn = el('button', { type: 'button', text: 'Restart daemon now' });
174
+ btn.className = 'btn sm';
175
+ btn.addEventListener('click', function () { restartDaemonFromNote(btn); });
176
+ wrap.appendChild(btn);
177
+ return wrap;
178
+ }
179
+
180
+ /* ── Layout ───────────────────────────────────────────────── */
181
+ function makeRow(lbl, desc, key, ctrl) {
182
+ var d = el('div', { 'data-set':'', 'data-txt':(lbl + ' ' + key).toLowerCase() });
183
+ Object.assign(d.style, { display:'flex', alignItems:'center',
184
+ justifyContent:'space-between', gap:'16px',
185
+ padding:'14px 20px', borderBottom:'1px solid var(--border)' });
186
+ var lv = el('div');
187
+ lv.appendChild(el('b', { text: lbl }));
188
+ if (desc) lv.appendChild(el('span', { text: desc },
189
+ { fontSize:'12.5px', color:'var(--fg-2)', display:'block' }));
190
+ if (key) lv.appendChild(el('div', { text: key },
191
+ { fontFamily:'var(--font-mono)', fontSize:'10.5px', color:'var(--fg-3)', marginTop:'3px' }));
192
+ d.appendChild(lv);
193
+ var cv = el('div', null, { flexShrink:'0' }); cv.appendChild(ctrl); d.appendChild(cv);
194
+ return d;
195
+ }
196
+ function makeGrp(title, ic, rows) {
197
+ var g = el('div', { 'data-grp':'' });
198
+ Object.assign(g.style, { background:'var(--card)', border:'1px solid var(--border)',
199
+ borderRadius:'var(--r-lg)', boxShadow:'var(--sh-md)', marginBottom:'14px' });
200
+ var head = el('div', null, { display:'flex', alignItems:'center', gap:'10px',
201
+ padding:'16px 20px', borderBottom:'1px solid var(--border)' });
202
+ if (ic && typeof window.slmIcon === 'function') {
203
+ var icEl = el('span', null,
204
+ { color:'var(--violet)', width:'18px', height:'18px', display:'flex', flexShrink:'0' });
205
+ icEl.innerHTML = window.slmIcon(ic);
206
+ head.appendChild(icEl);
207
+ }
208
+ head.appendChild(el('h3', { text: title },
209
+ { fontSize:'14.5px', fontWeight:'640', margin:'0' }));
210
+ g.appendChild(head);
211
+ rows.forEach(function (r) { g.appendChild(r); });
212
+ if (g.lastElementChild !== head) g.lastElementChild.style.borderBottom = '0';
213
+ return g;
214
+ }
215
+
216
+ /* ═══════════════════════════════════════════════════════════
217
+ GROUP 1 — Operating Mode & LLM
218
+ ═══════════════════════════════════════════════════════════ */
219
+ function buildMode() {
220
+ var modeSel = makeSel('mode', [
221
+ { v:'a', l:'Mode A — Local Guardian (zero cloud)' },
222
+ { v:'b', l:'Mode B — Smart Local (Ollama)' },
223
+ { v:'c', l:'Mode C — Full Power (cloud LLM)' }
224
+ ]);
225
+ var provSel = makeSel('provider', [
226
+ { v:'none', l:'None (Mode A)' }, { v:'ollama', l:'Ollama (local)' },
227
+ { v:'openrouter', l:'OpenRouter' }, { v:'openai', l:'OpenAI' },
228
+ { v:'anthropic', l:'Anthropic' }
229
+ ]);
230
+ var modelInp = makeTin('model', 'e.g. llama3.2');
231
+ var keyInp = makeTin('apikey', 'sk-... or your key', 'password');
232
+ var epInp = makeTin('endpoint', 'http://localhost:11434');
233
+ var saveBtn = makeBtn('mode-save', 'Save Mode', true);
234
+ var testBtn = makeBtn('mode-test', 'Test');
235
+ var st = makeSt('mode');
236
+ modeSel.addEventListener('change', function () {
237
+ var a = this.value === 'a';
238
+ [provSel, modelInp, keyInp, epInp].forEach(function (i) { i.disabled = a; });
239
+ });
240
+ saveBtn.addEventListener('click', saveMode);
241
+ testBtn.addEventListener('click', testMode);
242
+ return makeGrp('Operating Mode & LLM Provider', 'operations', [
243
+ makeRow('Mode', 'Select operating mode', 'mode', modeSel),
244
+ makeRow('Provider', 'LLM backend (Mode B/C)', 'llm.provider', provSel),
245
+ makeRow('Model', '', 'llm.model', modelInp),
246
+ makeRow('API Key', 'Stored locally, never sent to Qualixar', 'llm.api_key', keyInp),
247
+ makeRow('Base URL / Endpoint', 'Custom endpoint for Ollama, LM Studio', 'llm.base_url', epInp),
248
+ makeRow('Actions', '', '', bRow(saveBtn, testBtn, st))
249
+ ]);
250
+ }
251
+ function loadMode() {
252
+ fetch('/api/v3/mode').then(function (r) { return r.ok ? r.json() : null; })
253
+ .then(function (d) {
254
+ if (!d) return;
255
+ var m = q('mode');
256
+ if (m) { m.value = d.mode || 'a'; m.dispatchEvent(new Event('change')); }
257
+ var p = q('provider'); if (p) p.value = d.provider || 'none';
258
+ var mi = q('model'); if (mi) mi.value = d.model || '';
259
+ var k = q('apikey');
260
+ if (k) k.placeholder = d.has_key ? '(key saved — enter new to replace)' : 'sk-...';
261
+ var ep = q('endpoint'); if (ep) ep.value = d.endpoint || '';
262
+ }).catch(function () {});
263
+ }
264
+ function saveMode() {
265
+ setSt('mode', 'Saving…', null);
266
+ var body = { mode: q('mode') ? q('mode').value : 'a',
267
+ provider: q('provider') ? q('provider').value : 'none',
268
+ model: q('model') ? q('model').value : '' };
269
+ var k = q('apikey') ? q('apikey').value : '';
270
+ var ep = q('endpoint') ? q('endpoint').value : '';
271
+ if (k) { body.api_key = k; }
272
+ if (ep) { body.base_url = ep; body.endpoint = ep; }
273
+ fetch('/api/v3/mode/set', {
274
+ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)
275
+ }).then(function (r) { return r.json(); })
276
+ .then(function () { setSt('mode', 'Saved', true); toast('Mode saved'); loadMode(); })
277
+ .catch(function () { setSt('mode', 'Error', false); toast('Mode save failed', true); });
278
+ }
279
+ function testMode() {
280
+ setSt('mode', 'Testing…', null);
281
+ var body = { provider: q('provider') ? q('provider').value : '',
282
+ model: q('model') ? q('model').value : '' };
283
+ var k = q('apikey') ? q('apikey').value : '';
284
+ var ep = q('endpoint') ? q('endpoint').value : '';
285
+ if (k) { body.api_key = k; }
286
+ if (ep) { body.base_url = ep; body.endpoint = ep; }
287
+ fetch('/api/v3/provider/test', {
288
+ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)
289
+ }).then(function (r) { return r.json(); })
290
+ .then(function (d) {
291
+ setSt('mode', d.success ? esc(d.message || 'OK') : 'Failed: ' + esc(d.error || ''), d.success);
292
+ }).catch(function (e) { setSt('mode', 'Error: ' + e.message, false); });
293
+ }
294
+
295
+ /* ═══════════════════════════════════════════════════════════
296
+ GROUP 2 — Embeddings & Retrieval
297
+ ═══════════════════════════════════════════════════════════ */
298
+ function buildEmb() {
299
+ var provSel = makeSel('emb-prov', [
300
+ { v:'default', l:'Default (local nomic-embed-text)' },
301
+ { v:'openai', l:'Custom OpenAI-compatible endpoint' }
302
+ ]);
303
+ var modelInp = makeTin('emb-model', 'e.g. Qwen3-Embedding');
304
+ var dimInp = makeTin('emb-dim', '768', 'number');
305
+ dimInp.style.minWidth = '80px'; dimInp.style.maxWidth = '100px';
306
+ var epInp = makeTin('emb-endpoint', 'http://localhost:8045/v1/embeddings');
307
+ var keyInp = makeTin('emb-key', 'optional key', 'password');
308
+ var infoEl = el('span', { id: P + '-emb-info' }, { fontSize:'12px', color:'var(--fg-2)' });
309
+ var saveBtn = makeBtn('emb-save', 'Save Embedding', true);
310
+ var testBtn = makeBtn('emb-test', 'Test');
311
+ var st = makeSt('emb');
312
+ provSel.addEventListener('change', function () {
313
+ var c = this.value === 'openai';
314
+ [modelInp, dimInp, epInp, keyInp].forEach(function (i) { i.disabled = !c; });
315
+ });
316
+ saveBtn.addEventListener('click', saveEmb);
317
+ testBtn.addEventListener('click', testEmb);
318
+ return makeGrp('Embeddings & Retrieval', 'memories', [
319
+ makeRow('Provider', 'Local model is private and free', 'embedding.provider', provSel),
320
+ makeRow('Model Name', 'For custom endpoints', 'embedding.model_name', modelInp),
321
+ makeRow('Dimensions', '768 local · 1024/3072 cloud', 'embedding.dimension', dimInp),
322
+ makeRow('Endpoint', 'OpenAI-compatible URL', 'embedding.api_endpoint', epInp),
323
+ makeRow('API Key', 'optional', 'embedding.api_key', keyInp),
324
+ makeRow('Info', '', '', infoEl),
325
+ makeRow('Actions', '', '', bRow(saveBtn, testBtn, st))
326
+ ]);
327
+ }
328
+ function loadEmb() {
329
+ fetch('/api/v3/embedding/config').then(function (r) { return r.ok ? r.json() : null; })
330
+ .then(function (d) {
331
+ if (!d) return;
332
+ var p = q('emb-prov');
333
+ if (p) { p.value = d.is_openai_compatible ? 'openai' : 'default'; p.dispatchEvent(new Event('change')); }
334
+ var m = q('emb-model'); if (m) m.value = d.model_name || '';
335
+ var dim = q('emb-dim'); if (dim) dim.value = d.dimension || '';
336
+ var ep = q('emb-endpoint'); if (ep) ep.value = d.api_endpoint || '';
337
+ var inf = q('emb-info');
338
+ if (inf) inf.textContent = 'Current: ' + (d.model_name || 'nomic-embed-text') + ' (' + (d.dimension || 768) + 'd)';
339
+ }).catch(function () {});
340
+ }
341
+ function saveEmb() {
342
+ setSt('emb', 'Saving…', null);
343
+ var prov = q('emb-prov') ? q('emb-prov').value : 'default';
344
+ var body = prov === 'openai'
345
+ ? { embedding_provider:'openai',
346
+ embedding_endpoint: q('emb-endpoint') ? q('emb-endpoint').value : '',
347
+ embedding_model: q('emb-model') ? q('emb-model').value : '',
348
+ embedding_dimension: parseInt(q('emb-dim') ? q('emb-dim').value : '768') || 768,
349
+ embedding_key: q('emb-key') ? q('emb-key').value : '' }
350
+ : { embedding_provider:'default' };
351
+ fetch('/api/v3/mode/set', {
352
+ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)
353
+ }).then(function (r) { return r.json(); })
354
+ .then(function () { setSt('emb', 'Saved', true); toast('Embedding config saved'); })
355
+ .catch(function () { setSt('emb', 'Error', false); toast('Embedding save failed', true); });
356
+ }
357
+ function testEmb() {
358
+ setSt('emb', 'Testing…', null);
359
+ var ep = q('emb-endpoint') ? q('emb-endpoint').value : '';
360
+ if (!ep) { setSt('emb', 'Enter endpoint first', null); return; }
361
+ fetch('/api/v3/embedding/test', {
362
+ method:'POST', headers:{'Content-Type':'application/json'},
363
+ body:JSON.stringify({ api_endpoint: ep,
364
+ model_name: q('emb-model') ? q('emb-model').value : '',
365
+ api_key: q('emb-key') ? q('emb-key').value : '' })
366
+ }).then(function (r) { return r.json(); })
367
+ .then(function (d) {
368
+ setSt('emb', d.success ? esc(d.message || 'OK') : 'Failed: ' + esc(d.error || ''), d.success);
369
+ }).catch(function (e) { setSt('emb', 'Error: ' + e.message, false); });
370
+ }
371
+
372
+ /* ═══════════════════════════════════════════════════════════
373
+ GROUP 3 — Storage Backends
374
+ GET/PUT /api/v3/storage/config — PUT returns restart_required
375
+ ═══════════════════════════════════════════════════════════ */
376
+ function buildStorage() {
377
+ var gBk = makeSel('stor-graph', [
378
+ { v:'auto', l:'auto (recommended)' },
379
+ { v:'sqlite', l:'SQLite' },
380
+ { v:'cozo', l:'CozoDB' }
381
+ ]);
382
+ var vBk = makeSel('stor-vec', [
383
+ { v:'auto', l:'auto (recommended)' },
384
+ { v:'sqlite-vec', l:'sqlite-vec' },
385
+ { v:'lancedb', l:'LanceDB' }
386
+ ]);
387
+ var dirInp = makeTin('stor-dir', '~/.superlocalmemory');
388
+ dirInp.disabled = true;
389
+ Object.assign(dirInp.style, { opacity:'.55', cursor:'not-allowed' });
390
+ var saveBtn = makeBtn('stor-save', 'Save Backends', true);
391
+ var st = makeSt('stor');
392
+ var restNote = makeRestartNote('stor');
393
+ saveBtn.addEventListener('click', saveStorage);
394
+ return makeGrp('Storage Backends', 'operations', [
395
+ makeRow('Graph backend', 'Engine for relationship and knowledge-graph queries', 'graph_backend', gBk),
396
+ makeRow('Vector backend', 'Engine for semantic similarity search', 'vector_backend', vBk),
397
+ makeRow('Base directory', 'Where SLM stores its database files (read-only)', 'base_dir', dirInp),
398
+ makeRow('Actions', '', '', bRow(saveBtn, st, restNote))
399
+ ]);
400
+ }
401
+ function loadStorage() {
402
+ fetch('/api/v3/storage/config').then(function (r) { return r.ok ? r.json() : null; })
403
+ .then(function (d) {
404
+ if (!d) return;
405
+ var g = q('stor-graph'); if (g) g.value = d.graph_backend || 'auto';
406
+ var v = q('stor-vec'); if (v) v.value = d.vector_backend || 'auto';
407
+ var dir = q('stor-dir'); if (dir) dir.value = d.base_dir || '';
408
+ }).catch(function () {});
409
+ }
410
+ function saveStorage() {
411
+ setSt('stor', 'Saving…', null);
412
+ showRestartNote('stor', false);
413
+ authPut('/api/v3/storage/config', {
414
+ graph_backend: q('stor-graph') ? q('stor-graph').value : 'auto',
415
+ vector_backend: q('stor-vec') ? q('stor-vec').value : 'auto'
416
+ }).then(function (r) { return r.json(); })
417
+ .then(function (d) {
418
+ setSt('stor', 'Saved', true);
419
+ toast('Storage backends saved');
420
+ showRestartNote('stor', !!d.restart_required);
421
+ }).catch(function () { setSt('stor', 'Error', false); toast('Storage save failed', true); });
422
+ }
423
+
424
+ /* ═══════════════════════════════════════════════════════════
425
+ GROUP 4 — Memory Scope
426
+ GET/PUT /api/v3/scope/config
427
+ ═══════════════════════════════════════════════════════════ */
428
+ function buildScope() {
429
+ var defSel = makeSel('scope-def', [
430
+ { v:'personal', l:'Personal (recommended)' },
431
+ { v:'shared', l:'Shared' },
432
+ { v:'global', l:'Global' }
433
+ ]);
434
+ var shSw = makeSwitch('scope-shared', false);
435
+ var glSw = makeSwitch('scope-global', false);
436
+ var st = makeSt('scope');
437
+ function save() {
438
+ authPut('/api/v3/scope/config', {
439
+ default_scope: q('scope-def') ? q('scope-def').value : 'personal',
440
+ recall_include_shared: shSw.classList.contains('on'),
441
+ recall_include_global: glSw.classList.contains('on')
442
+ }).then(function (r) { return r.json(); })
443
+ .then(function (d) {
444
+ setSt('scope', d.success !== false ? 'Applied' : esc(d.error || 'Error'), d.success !== false);
445
+ })
446
+ .catch(function () { setSt('scope', 'Error', false); toast('Scope save failed', true); });
447
+ }
448
+ defSel.addEventListener('change', save);
449
+ shSw.addEventListener('od-toggle', save);
450
+ glSw.addEventListener('od-toggle', save);
451
+ return makeGrp('Memory Scope (auto-saves)', 'memories', [
452
+ makeRow('Default write scope', 'Where new memories are stored — Personal is privacy-safe', 'scope.default_scope', defSel),
453
+ makeRow('Include shared memories in recall', 'Surface memories shared across your agents', 'scope.recall_include_shared', shSw),
454
+ makeRow('Include global memories in recall', 'Surface system-wide shared facts', 'scope.recall_include_global', glSw),
455
+ makeRow('Status', '', '', st)
456
+ ]);
457
+ }
458
+ function loadScope() {
459
+ fetch('/api/v3/scope/config').then(function (r) { return r.ok ? r.json() : null; })
460
+ .then(function (d) {
461
+ if (!d) return;
462
+ var def = q('scope-def'); if (def) def.value = d.default_scope || 'personal';
463
+ var sh = q('scope-shared');
464
+ if (sh) { sh.classList.toggle('on', !!d.recall_include_shared); sh.setAttribute('aria-checked', String(!!d.recall_include_shared)); }
465
+ var gl = q('scope-global');
466
+ if (gl) { gl.classList.toggle('on', !!d.recall_include_global); gl.setAttribute('aria-checked', String(!!d.recall_include_global)); }
467
+ }).catch(function () {});
468
+ }
469
+
470
+ /* ═══════════════════════════════════════════════════════════
471
+ GROUP 5 — Auto-Capture
472
+ ═══════════════════════════════════════════════════════════ */
473
+ function buildCapture() {
474
+ var sw = makeSwitch('cap-en', true);
475
+ var swDec = makeSwitch('cap-dec', true);
476
+ var swBug = makeSwitch('cap-bug', true);
477
+ function save() {
478
+ // Merge with full loaded config to preserve server-side fields the UI doesn't expose
479
+ var body = Object.assign({}, _captureConfig, {
480
+ enabled: sw.classList.contains('on'),
481
+ capture_decisions: swDec.classList.contains('on'),
482
+ capture_bugs: swBug.classList.contains('on')
483
+ });
484
+ fetch('/api/v3/auto-capture/config', {
485
+ method:'PUT', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body)
486
+ }).catch(function () { toast('Auto-capture save failed', true); });
487
+ }
488
+ sw.addEventListener('od-toggle', save);
489
+ swDec.addEventListener('od-toggle', save);
490
+ swBug.addEventListener('od-toggle', save);
491
+ return makeGrp('Auto-Capture', 'brain', [
492
+ makeRow('Enable auto-capture', 'Silently remember key facts from agent sessions', 'auto_capture.enabled', sw),
493
+ makeRow('Capture decisions', 'Architecture and design choices', 'auto_capture.capture_decisions', swDec),
494
+ makeRow('Capture bug fixes', 'Error patterns and their resolutions', 'auto_capture.capture_bugs', swBug)
495
+ ]);
496
+ }
497
+ function loadCapture() {
498
+ fetch('/api/v3/auto-capture/config').then(function (r) { return r.ok ? r.json() : null; })
499
+ .then(function (d) {
500
+ if (!d || !d.config) return;
501
+ _captureConfig = Object.assign({}, d.config);
502
+ var c = d.config;
503
+ var map = { 'cap-en': c.enabled, 'cap-dec': c.capture_decisions, 'cap-bug': c.capture_bugs };
504
+ Object.keys(map).forEach(function (k) {
505
+ var e = q(k); if (!e) return;
506
+ e.classList.toggle('on', !!map[k]);
507
+ e.setAttribute('aria-checked', String(!!map[k]));
508
+ });
509
+ }).catch(function () {});
510
+ }
511
+
512
+ /* ═══════════════════════════════════════════════════════════
513
+ GROUP 6 — Auto-Recall
514
+ ═══════════════════════════════════════════════════════════ */
515
+ function buildRecall() {
516
+ var sw = makeSwitch('rec-en', true);
517
+ var swSess = makeSwitch('rec-sess', true);
518
+ function save() {
519
+ fetch('/api/v3/auto-recall/config', {
520
+ method:'PUT', headers:{'Content-Type':'application/json'},
521
+ body:JSON.stringify({
522
+ enabled: sw.classList.contains('on'),
523
+ on_session_start: swSess.classList.contains('on')
524
+ })
525
+ }).catch(function () { toast('Auto-recall save failed', true); });
526
+ }
527
+ sw.addEventListener('od-toggle', save);
528
+ swSess.addEventListener('od-toggle', save);
529
+ return makeGrp('Auto-Recall', 'brain', [
530
+ makeRow('Enable auto-recall', 'Inject relevant memory into agent context', 'auto_recall.enabled', sw),
531
+ makeRow('Recall on session start', 'Run recall automatically when a new session begins', 'auto_recall.on_session_start', swSess)
532
+ ]);
533
+ }
534
+ function loadRecall() {
535
+ fetch('/api/v3/auto-recall/config').then(function (r) { return r.ok ? r.json() : null; })
536
+ .then(function (d) {
537
+ if (!d || !d.config) return;
538
+ var c = d.config;
539
+ var en = q('rec-en');
540
+ if (en) { en.classList.toggle('on', !!c.enabled); en.setAttribute('aria-checked', String(!!c.enabled)); }
541
+ var se = q('rec-sess');
542
+ if (se) { se.classList.toggle('on', !!c.on_session_start); se.setAttribute('aria-checked', String(!!c.on_session_start)); }
543
+ }).catch(function () {});
544
+ }
545
+
546
+ /* ═══════════════════════════════════════════════════════════
547
+ GROUP 7 — Auto-Invoke (FOK Threshold)
548
+ ═══════════════════════════════════════════════════════════ */
549
+ function buildInvoke() {
550
+ var sw = makeSwitch('ai-en', true);
551
+ var range = makeRange('ai-fok', '0', '1', '0.01', '0.12');
552
+ var arSw = makeSwitch('ai-actr', false);
553
+ var st = makeSt('ai');
554
+ function save() {
555
+ fetch('/api/v3/auto-invoke/config', {
556
+ method:'PUT', headers:{'Content-Type':'application/json'},
557
+ body:JSON.stringify({
558
+ enabled: sw.classList.contains('on'),
559
+ min_score: parseFloat(q('ai-fok') ? q('ai-fok').value : '0.12'),
560
+ act_r_mode: arSw.classList.contains('on')
561
+ })
562
+ }).then(function (r) { return r.json(); })
563
+ .then(function () { setSt('ai', 'Saved', true); })
564
+ .catch(function () { toast('Auto-invoke save failed', true); });
565
+ }
566
+ sw.addEventListener('od-toggle', save);
567
+ arSw.addEventListener('od-toggle', save);
568
+ range.querySelector('input').addEventListener('change', save);
569
+ return makeGrp('Auto-Invoke (FOK Threshold)', 'optimize', [
570
+ makeRow('Enable auto-invoke', 'Run recall automatically when relevance score is high enough', 'auto_invoke.enabled', sw),
571
+ makeRow('FOK threshold', '0 = always recall · 1 = never recall', 'auto_invoke.min_score', range),
572
+ makeRow('ACT-R mode', 'Biologically-inspired memory activation formula', 'auto_invoke.act_r_mode', arSw),
573
+ makeRow('Status', '', '', st)
574
+ ]);
575
+ }
576
+ function loadInvoke() {
577
+ fetch('/api/v3/auto-invoke/config').then(function (r) { return r.ok ? r.json() : null; })
578
+ .then(function (d) {
579
+ if (!d) return;
580
+ var en = q('ai-en');
581
+ if (en) { en.classList.toggle('on', !!d.enabled); en.setAttribute('aria-checked', String(!!d.enabled)); }
582
+ var score = String(d.min_score != null ? d.min_score : 0.12);
583
+ var fok = q('ai-fok'); if (fok) fok.value = score;
584
+ var fokD = q('ai-fok-disp'); if (fokD) fokD.textContent = parseFloat(score).toFixed(2);
585
+ var ar = q('ai-actr');
586
+ if (ar) { ar.classList.toggle('on', !!d.act_r_mode); ar.setAttribute('aria-checked', String(!!d.act_r_mode)); }
587
+ }).catch(function () {});
588
+ }
589
+
590
+ /* ═══════════════════════════════════════════════════════════
591
+ GROUP 8 — Forgetting Engine
592
+ GET/PUT /api/v3/forgetting/config + GET /api/v3/forgetting/stats
593
+ ═══════════════════════════════════════════════════════════ */
594
+ function buildForgetting() {
595
+ var onSw = makeSwitch('forg-en', false);
596
+ var immSw = makeSwitch('forg-imm', true);
597
+ var arcR = makeRange('forg-arc', '0', '1', '0.01', '0.20');
598
+ var fgtR = makeRange('forg-fgt', '0', '1', '0.01', '0.05');
599
+ var intInp = makeTin('forg-int', '30', 'number');
600
+ intInp.style.minWidth = '80px'; intInp.style.maxWidth = '100px';
601
+ var statsEl = el('span', { id: P + '-forg-stats', text:'Loading…' },
602
+ { fontSize:'12px', color:'var(--fg-2)', fontFamily:'var(--font-mono)' });
603
+ var saveBtn = makeBtn('forg-save', 'Save Forgetting', true);
604
+ var st = makeSt('forg');
605
+ saveBtn.addEventListener('click', saveForgetting);
606
+ onSw.addEventListener('od-toggle', saveForgetting);
607
+ immSw.addEventListener('od-toggle', saveForgetting);
608
+ arcR.querySelector('input').addEventListener('change', saveForgetting);
609
+ fgtR.querySelector('input').addEventListener('change', saveForgetting);
610
+ intInp.addEventListener('change', saveForgetting);
611
+ return makeGrp('Forgetting Engine', 'health', [
612
+ makeRow('Enable forgetting', 'Gradually fade old or rarely-used memories (recommended on)', 'forgetting.enabled', onSw),
613
+ makeRow('Protect important memories', 'Core memories are immune from being forgotten', 'forgetting.core_memory_immune', immSw),
614
+ makeRow('Archive threshold', 'Memories below this strength move to cold archive', 'forgetting.archive_threshold', arcR),
615
+ makeRow('Forget threshold', 'Memories below this strength are permanently deleted', 'forgetting.forget_threshold', fgtR),
616
+ makeRow('Run every (minutes)', 'How often the forgetting engine runs in the background', 'forgetting.scheduler_interval_minutes', intInp),
617
+ makeRow('Memory zone stats', '', 'forgetting.stats', statsEl),
618
+ makeRow('Actions', '', '', bRow(saveBtn, st))
619
+ ]);
620
+ }
621
+ function loadForgetting() {
622
+ fetch('/api/v3/forgetting/config').then(function (r) { return r.ok ? r.json() : null; })
623
+ .then(function (d) {
624
+ if (!d) return;
625
+ var en = q('forg-en');
626
+ if (en) { en.classList.toggle('on', !!d.enabled); en.setAttribute('aria-checked', String(!!d.enabled)); }
627
+ var imm = q('forg-imm');
628
+ if (imm) { imm.classList.toggle('on', d.core_memory_immune !== false); imm.setAttribute('aria-checked', String(d.core_memory_immune !== false)); }
629
+ var arc = q('forg-arc');
630
+ if (arc) { arc.value = d.archive_threshold || 0.2; var dA = q('forg-arc-disp'); if (dA) dA.textContent = Number(d.archive_threshold || 0.2).toFixed(2); }
631
+ var fgt = q('forg-fgt');
632
+ if (fgt) { fgt.value = d.forget_threshold || 0.05; var dF = q('forg-fgt-disp'); if (dF) dF.textContent = Number(d.forget_threshold || 0.05).toFixed(2); }
633
+ var it = q('forg-int'); if (it) it.value = d.scheduler_interval_minutes || 30;
634
+ }).catch(function () {});
635
+ fetch('/api/v3/forgetting/stats').then(function (r) { return r.ok ? r.json() : null; })
636
+ .then(function (d) {
637
+ var e = q('forg-stats'); if (!d || !e) return;
638
+ var parts = [];
639
+ if (d.total != null) parts.push('total: ' + d.total);
640
+ if (d.zones) {
641
+ ['active','warm','cold','archive','forgotten'].forEach(function (z) {
642
+ if (d.zones[z] != null) parts.push(z + ': ' + d.zones[z]);
643
+ });
644
+ }
645
+ e.textContent = parts.join(' · ') || 'No data';
646
+ }).catch(function () {});
647
+ }
648
+ function saveForgetting() {
649
+ setSt('forg', 'Saving…', null);
650
+ authPut('/api/v3/forgetting/config', {
651
+ enabled: q('forg-en') ? q('forg-en').classList.contains('on') : false,
652
+ core_memory_immune: q('forg-imm') ? q('forg-imm').classList.contains('on') : true,
653
+ archive_threshold: parseFloat(q('forg-arc') ? q('forg-arc').value : '0.20'),
654
+ forget_threshold: parseFloat(q('forg-fgt') ? q('forg-fgt').value : '0.05'),
655
+ scheduler_interval_minutes: parseInt(q('forg-int') ? q('forg-int').value : '30') || 30
656
+ }).then(function (r) { return r.json(); })
657
+ .then(function () { setSt('forg', 'Saved', true); toast('Forgetting settings saved'); })
658
+ .catch(function () { setSt('forg', 'Error', false); toast('Forgetting save failed', true); });
659
+ }
660
+
661
+ /* ═══════════════════════════════════════════════════════════
662
+ GROUP 9 — Skill Evolution
663
+ POST /api/evolution/enable|disable (toggle); POST /api/evolution/config (backend)
664
+ ═══════════════════════════════════════════════════════════ */
665
+ function buildEvolution() {
666
+ var sw = makeSwitch('evo-en', false);
667
+ var bkSel = makeSel('evo-bk', [
668
+ { v:'auto', l:'auto (detect)' }, { v:'claude', l:'claude' },
669
+ { v:'ollama', l:'ollama' }, { v:'anthropic', l:'anthropic' },
670
+ { v:'openai', l:'openai' }
671
+ ]);
672
+ var saveBtn = makeBtn('evo-save', 'Save', true);
673
+ var st = makeSt('evo');
674
+ sw.addEventListener('od-toggle', function () {
675
+ var on = sw.classList.contains('on');
676
+ fetch(on ? '/api/evolution/enable' : '/api/evolution/disable', { method:'POST' })
677
+ .then(function (r) { return r.json(); })
678
+ .then(function (d) {
679
+ if (d.ok === false) {
680
+ toast('Failed: ' + esc(d.error || ''), true);
681
+ sw.classList.toggle('on', !on);
682
+ sw.setAttribute('aria-checked', String(!on));
683
+ } else {
684
+ toast(on ? 'Evolution engine enabled' : 'Evolution engine disabled');
685
+ loadEvolution();
686
+ }
687
+ }).catch(function () {
688
+ toast('Evolution toggle failed', true);
689
+ sw.classList.toggle('on', !on);
690
+ sw.setAttribute('aria-checked', String(!on));
691
+ });
692
+ });
693
+ saveBtn.addEventListener('click', function () {
694
+ setSt('evo', 'Saving…', null);
695
+ fetch('/api/evolution/config', {
696
+ method:'POST', headers:{'Content-Type':'application/json'},
697
+ body:JSON.stringify({ backend: q('evo-bk') ? q('evo-bk').value : 'auto' })
698
+ }).then(function (r) { return r.json(); })
699
+ .then(function () { setSt('evo', 'Saved', true); })
700
+ .catch(function () { setSt('evo', 'Error', false); toast('Evolution save failed', true); });
701
+ });
702
+ return makeGrp('Skill Evolution', 'skill', [
703
+ makeRow('Enable evolution engine', 'Off by default — makes background LLM calls to improve skills', 'evolution.enabled', sw),
704
+ makeRow('Backend', 'auto = cheapest capable model in your current mode', 'evolution.backend', bkSel),
705
+ makeRow('Actions', '', '', bRow(saveBtn, st))
706
+ ]);
707
+ }
708
+ function loadEvolution() {
709
+ fetch('/api/evolution/status').then(function (r) { return r.ok ? r.json() : null; })
710
+ .then(function (d) {
711
+ if (!d) return;
712
+ var sw = q('evo-en');
713
+ if (sw) { sw.classList.toggle('on', !!d.enabled); sw.setAttribute('aria-checked', String(!!d.enabled)); }
714
+ var bk = q('evo-bk');
715
+ if (bk && d.config && d.config.backend_setting) bk.value = d.config.backend_setting;
716
+ }).catch(function () {});
717
+ }
718
+
719
+ /* ═══════════════════════════════════════════════════════════
720
+ GROUP 10 — Backup
721
+ ═══════════════════════════════════════════════════════════ */
722
+ function buildBackup() {
723
+ var swEn = makeSwitch('bk-en', true);
724
+ var intSel = makeSel('bk-int', [{ v:'24', l:'Daily (24 h)' }, { v:'168', l:'Weekly (168 h)' }]);
725
+ var maxInp = makeTin('bk-max', '10', 'number');
726
+ maxInp.style.minWidth = '80px'; maxInp.style.maxWidth = '100px';
727
+ var saveBtn = makeBtn('bk-save', 'Save Backup Config', true);
728
+ var nowBtn = makeBtn('bk-now', 'Backup Now');
729
+ var st = makeSt('bk');
730
+ swEn.addEventListener('od-toggle', saveBackupConfig);
731
+ saveBtn.addEventListener('click', saveBackupConfig);
732
+ nowBtn.addEventListener('click', function () {
733
+ setSt('bk', 'Creating…', null);
734
+ fetch('/api/backup/create', { method:'POST' }).then(function (r) { return r.json(); })
735
+ .then(function (d) {
736
+ setSt('bk', d.success ? 'Done: ' + esc(d.filename || '') : 'Failed', d.success);
737
+ toast(d.success ? 'Backup created' : 'Backup failed', !d.success);
738
+ loadBackup();
739
+ }).catch(function () { setSt('bk', 'Error', false); toast('Backup failed', true); });
740
+ });
741
+ var cloudBtn = el('button', { type:'button' });
742
+ cloudBtn.className = 'btn sm ghost';
743
+ cloudBtn.textContent = 'Backup & Cloud →';
744
+ cloudBtn.addEventListener('click', function () {
745
+ if (typeof window.odOpenBackupDashboard === 'function') {
746
+ window.odOpenBackupDashboard();
747
+ } else {
748
+ toast('Backup dashboard module not loaded yet', true);
749
+ }
750
+ });
751
+ return makeGrp('Backup', 'health', [
752
+ makeRow('Auto-backup enabled', '', 'backup.enabled', swEn),
753
+ makeRow('Backup interval', '', 'backup.interval_hours', intSel),
754
+ makeRow('Max backups to keep', '', 'backup.max_backups', maxInp),
755
+ makeRow('Actions', '', '', bRow(saveBtn, nowBtn, st)),
756
+ makeRow('Cloud backup', 'Connect GitHub or Google Drive', '', cloudBtn)
757
+ ]);
758
+ }
759
+ function loadBackup() {
760
+ fetch('/api/backup/status').then(function (r) { return r.ok ? r.json() : null; })
761
+ .then(function (d) {
762
+ if (!d) return;
763
+ var en = q('bk-en');
764
+ if (en) { en.classList.toggle('on', d.enabled !== false); en.setAttribute('aria-checked', String(d.enabled !== false)); }
765
+ var it = q('bk-int'); if (it) it.value = (d.interval_hours || 168) <= 24 ? '24' : '168';
766
+ var mx = q('bk-max'); if (mx) mx.value = d.max_backups || 10;
767
+ }).catch(function () {});
768
+ }
769
+ function saveBackupConfig() {
770
+ setSt('bk', 'Saving…', null);
771
+ fetch('/api/backup/configure', {
772
+ method:'POST', headers:{'Content-Type':'application/json'},
773
+ body:JSON.stringify({
774
+ interval_hours: parseInt(q('bk-int') ? q('bk-int').value : '168'),
775
+ max_backups: parseInt(q('bk-max') ? q('bk-max').value : '10'),
776
+ enabled: q('bk-en') ? q('bk-en').classList.contains('on') : true
777
+ })
778
+ }).then(function (r) { return r.json(); })
779
+ .then(function () { setSt('bk', 'Saved', true); toast('Backup settings saved'); })
780
+ .catch(function () { setSt('bk', 'Error', false); toast('Backup save failed', true); });
781
+ }
782
+
783
+ /* ═══════════════════════════════════════════════════════════
784
+ GROUP 11 — Mesh Network
785
+ GET/PUT /api/v3/mesh/config {enabled}
786
+ ═══════════════════════════════════════════════════════════ */
787
+ function buildMesh() {
788
+ var sw = makeSwitch('mesh-en', false);
789
+ var st = makeSt('mesh');
790
+ sw.addEventListener('od-toggle', function () {
791
+ authPut('/api/v3/mesh/config', { enabled: sw.classList.contains('on') })
792
+ .then(function (r) { return r.json(); })
793
+ .then(function () { setSt('mesh', 'Saved', true); })
794
+ .catch(function () { setSt('mesh', 'Error', false); toast('Mesh save failed', true); });
795
+ });
796
+ return makeGrp('Mesh Network', 'mesh', [
797
+ makeRow('Enable mesh sync', 'Synchronise memories across devices on the same local network', 'mesh.enabled', sw),
798
+ makeRow('Status', '', '', st)
799
+ ]);
800
+ }
801
+ function loadMesh() {
802
+ fetch('/api/v3/mesh/config').then(function (r) { return r.ok ? r.json() : null; })
803
+ .then(function (d) {
804
+ if (!d) return;
805
+ var sw = q('mesh-en');
806
+ if (sw) { sw.classList.toggle('on', !!d.enabled); sw.setAttribute('aria-checked', String(!!d.enabled)); }
807
+ }).catch(function () {});
808
+ }
809
+
810
+ /* ═══════════════════════════════════════════════════════════
811
+ GROUP 12 — Trust & Source Quality
812
+ GET/PUT /api/v3/trust/config {use_trust_weighting, trust_first_party, promotion_min_trust}
813
+ ═══════════════════════════════════════════════════════════ */
814
+ function buildTrust() {
815
+ var twSw = makeSwitch('trust-tw', true);
816
+ var fpSw = makeSwitch('trust-fp', false);
817
+ var pmR = makeRange('trust-pm', '0', '1', '0.01', '0.5');
818
+ var saveBtn = makeBtn('trust-save', 'Save Trust', true);
819
+ var st = makeSt('trust');
820
+ saveBtn.addEventListener('click', saveTrust);
821
+ twSw.addEventListener('od-toggle', saveTrust);
822
+ fpSw.addEventListener('od-toggle', saveTrust);
823
+ pmR.querySelector('input').addEventListener('change', saveTrust);
824
+ return makeGrp('Trust & Source Quality', 'lock', [
825
+ makeRow('Weight recall by trust', 'Higher-trust sources appear first in recall results', 'trust.use_trust_weighting', twSw),
826
+ makeRow('Prefer first-party agents', 'Your own Claude/Gemini sessions rank higher than third-party', 'trust.trust_first_party', fpSw),
827
+ makeRow('Promotion min trust', 'Min trust score required to promote a memory to long-term storage', 'trust.promotion_min_trust', pmR),
828
+ makeRow('Actions', '', '', bRow(saveBtn, st))
829
+ ]);
830
+ }
831
+ function loadTrust() {
832
+ fetch('/api/v3/trust/config').then(function (r) { return r.ok ? r.json() : null; })
833
+ .then(function (d) {
834
+ if (!d) return;
835
+ var tw = q('trust-tw');
836
+ if (tw) { tw.classList.toggle('on', d.use_trust_weighting !== false); tw.setAttribute('aria-checked', String(d.use_trust_weighting !== false)); }
837
+ var fp = q('trust-fp');
838
+ if (fp) { fp.classList.toggle('on', !!d.trust_first_party); fp.setAttribute('aria-checked', String(!!d.trust_first_party)); }
839
+ var pm = q('trust-pm');
840
+ if (pm) { pm.value = d.promotion_min_trust != null ? d.promotion_min_trust : 0.5; var pmD = q('trust-pm-disp'); if (pmD) pmD.textContent = Number(d.promotion_min_trust != null ? d.promotion_min_trust : 0.5).toFixed(2); }
841
+ }).catch(function () {});
842
+ }
843
+ function saveTrust() {
844
+ setSt('trust', 'Saving…', null);
845
+ authPut('/api/v3/trust/config', {
846
+ use_trust_weighting: q('trust-tw') ? q('trust-tw').classList.contains('on') : true,
847
+ trust_first_party: q('trust-fp') ? q('trust-fp').classList.contains('on') : false,
848
+ promotion_min_trust: parseFloat(q('trust-pm') ? q('trust-pm').value : '0.5')
849
+ }).then(function (r) { return r.json(); })
850
+ .then(function () { setSt('trust', 'Saved', true); })
851
+ .catch(function () { setSt('trust', 'Error', false); toast('Trust save failed', true); });
852
+ }
853
+
854
+ /* ═══════════════════════════════════════════════════════════
855
+ GROUP 12b — Rate Limits (task #47)
856
+ GET/PUT /api/v3/ratelimit {write, read, window} → applied at runtime
857
+ ═══════════════════════════════════════════════════════════ */
858
+ function buildRateLimit() {
859
+ var wInp = makeTin('rl-write', '100', 'number');
860
+ wInp.style.minWidth = '90px'; wInp.style.maxWidth = '120px';
861
+ var rInp = makeTin('rl-read', '300', 'number');
862
+ rInp.style.minWidth = '90px'; rInp.style.maxWidth = '120px';
863
+ var winInp = makeTin('rl-window', '60', 'number');
864
+ winInp.style.minWidth = '90px'; winInp.style.maxWidth = '120px';
865
+ var lbInfo = el('span', { text: '—' }, { color:'var(--fg-2)', fontSize:'12px' });
866
+ lbInfo.id = P + '-rl-lb';
867
+ var saveBtn = makeBtn('rl-save', 'Save Limits', true);
868
+ var st = makeSt('rl');
869
+ saveBtn.addEventListener('click', saveRateLimit);
870
+ return makeGrp('Rate Limits', 'lock', [
871
+ makeRow('Write limit (per window)', 'Max write requests per client per window — raise this for heavy multi-system load', 'ratelimit.write', wInp),
872
+ makeRow('Read limit (per window)', 'Max read requests per client per window', 'ratelimit.read', rInp),
873
+ makeRow('Window (seconds)', 'Length of the sliding rate-limit window', 'ratelimit.window', winInp),
874
+ makeRow('Loopback (derived)', 'The local dashboard gets a generous multiple of these limits — updates automatically', 'ratelimit.loopback', lbInfo),
875
+ makeRow('Actions', 'Applied immediately — no restart needed', '', bRow(saveBtn, st))
876
+ ]);
877
+ }
878
+ function _rlSetLb(d) {
879
+ var lb = q('rl-lb');
880
+ if (lb && d) {
881
+ lb.textContent = 'write ' + (d.loopback_write != null ? d.loopback_write : '—') +
882
+ ' · read ' + (d.loopback_read != null ? d.loopback_read : '—');
883
+ }
884
+ }
885
+ function loadRateLimit() {
886
+ fetch('/api/v3/ratelimit').then(function (r) { return r.ok ? r.json() : null; })
887
+ .then(function (d) {
888
+ if (!d) return;
889
+ var w = q('rl-write'); if (w) w.value = d.write != null ? d.write : 100;
890
+ var r = q('rl-read'); if (r) r.value = d.read != null ? d.read : 300;
891
+ var win = q('rl-window'); if (win) win.value = d.window != null ? d.window : 60;
892
+ _rlSetLb(d);
893
+ }).catch(function () {});
894
+ }
895
+ function saveRateLimit() {
896
+ setSt('rl', 'Saving…', null);
897
+ authPut('/api/v3/ratelimit', {
898
+ write: parseInt(q('rl-write') ? q('rl-write').value : '100') || 100,
899
+ read: parseInt(q('rl-read') ? q('rl-read').value : '300') || 300,
900
+ window: parseInt(q('rl-window') ? q('rl-window').value : '60') || 60
901
+ }).then(function (r) { return r.json(); })
902
+ .then(function (d) { setSt('rl', 'Saved', true); _rlSetLb(d); })
903
+ .catch(function () { setSt('rl', 'Error', false); toast('Rate limit save failed', true); });
904
+ }
905
+
906
+ /* ═══════════════════════════════════════════════════════════
907
+ GROUP 13 — Daemon
908
+ GET/PUT /api/v3/daemon/config {idle_timeout, port, legacy_port, enable_legacy_port}
909
+ PUT returns restart_required
910
+ ═══════════════════════════════════════════════════════════ */
911
+ function buildDaemon() {
912
+ var portInp = makeTin('dmn-port', '8765', 'number');
913
+ portInp.style.minWidth = '90px'; portInp.style.maxWidth = '120px';
914
+ var lpInp = makeTin('dmn-lport', '8767', 'number');
915
+ lpInp.style.minWidth = '90px'; lpInp.style.maxWidth = '120px';
916
+ var lpSw = makeSwitch('dmn-lpen', true);
917
+ var toInp = makeTin('dmn-timeout', '0', 'number');
918
+ toInp.style.minWidth = '90px'; toInp.style.maxWidth = '120px';
919
+ var saveBtn = makeBtn('dmn-save', 'Save Daemon', true);
920
+ var st = makeSt('dmn');
921
+ var restNote = makeRestartNote('dmn');
922
+ saveBtn.addEventListener('click', saveDaemon);
923
+ return makeGrp('Daemon (port changes require restart)', 'settings', [
924
+ makeRow('API port', 'Port the SLM daemon listens on — changing this requires a restart', 'daemon.port', portInp),
925
+ makeRow('Legacy port', 'Backward-compatible API port for older integrations', 'daemon.legacy_port', lpInp),
926
+ makeRow('Enable legacy port', 'Keep the old API port open alongside the main port', 'daemon.enable_legacy_port', lpSw),
927
+ makeRow('Idle timeout (seconds)', '0 = daemon runs forever · >0 = auto-stop after N idle seconds', 'daemon.idle_timeout', toInp),
928
+ makeRow('Actions', '', '', bRow(saveBtn, st, restNote))
929
+ ]);
930
+ }
931
+ function loadDaemon() {
932
+ fetch('/api/v3/daemon/config').then(function (r) { return r.ok ? r.json() : null; })
933
+ .then(function (d) {
934
+ if (!d) return;
935
+ var p = q('dmn-port'); if (p) p.value = d.port || 8765;
936
+ var l = q('dmn-lport'); if (l) l.value = d.legacy_port || 8767;
937
+ var s = q('dmn-lpen');
938
+ if (s) { s.classList.toggle('on', d.enable_legacy_port !== false); s.setAttribute('aria-checked', String(d.enable_legacy_port !== false)); }
939
+ var t = q('dmn-timeout'); if (t) t.value = d.idle_timeout != null ? d.idle_timeout : 0;
940
+ }).catch(function () {});
941
+ }
942
+ function saveDaemon() {
943
+ setSt('dmn', 'Saving…', null);
944
+ showRestartNote('dmn', false);
945
+ authPut('/api/v3/daemon/config', {
946
+ port: parseInt(q('dmn-port') ? q('dmn-port').value : '8765') || 8765,
947
+ legacy_port: parseInt(q('dmn-lport') ? q('dmn-lport').value : '8767') || 8767,
948
+ enable_legacy_port: q('dmn-lpen') ? q('dmn-lpen').classList.contains('on') : true,
949
+ idle_timeout: parseInt(q('dmn-timeout') ? q('dmn-timeout').value : '0') || 0
950
+ }).then(function (r) { return r.json(); })
951
+ .then(function (d) {
952
+ setSt('dmn', 'Saved', true);
953
+ toast('Daemon settings saved');
954
+ showRestartNote('dmn', !!d.restart_required);
955
+ }).catch(function () { setSt('dmn', 'Error', false); toast('Daemon save failed', true); });
956
+ }
957
+
958
+ /* ═══════════════════════════════════════════════════════════
959
+ Filter + Load all
960
+ ═══════════════════════════════════════════════════════════ */
961
+ function filterSettings(query, hub) {
962
+ var q2 = (query || '').toLowerCase().trim();
963
+ hub.querySelectorAll('[data-grp]').forEach(function (g) {
964
+ var any = false;
965
+ g.querySelectorAll('[data-set]').forEach(function (r) {
966
+ var hit = !q2 || (r.dataset.txt || '').includes(q2);
967
+ r.style.display = hit ? '' : 'none';
968
+ if (hit) any = true;
969
+ });
970
+ g.style.display = any ? '' : 'none';
971
+ });
972
+ }
973
+ function loadAll() {
974
+ loadMode(); loadEmb(); loadStorage(); loadScope(); loadCapture();
975
+ loadRecall(); loadInvoke(); loadForgetting(); loadEvolution();
976
+ loadBackup(); loadMesh(); loadTrust(); loadRateLimit(); loadDaemon();
977
+ }
978
+
979
+ /* ═══════════════════════════════════════════════════════════
980
+ MAIN RENDER
981
+ ═══════════════════════════════════════════════════════════ */
982
+ function odRenderSettings(container) {
983
+ if (!container) return;
984
+ var existing = document.getElementById(ID);
985
+ if (existing) { loadAll(); return; }
986
+
987
+ Array.from(container.children).forEach(function (c) { c.style.display = 'none'; });
988
+
989
+ var hub = el('div', { id: ID });
990
+ Object.assign(hub.style, { padding:'26px', maxWidth:'920px' });
991
+
992
+ var headDiv = el('div', null, { marginBottom:'22px' });
993
+ headDiv.appendChild(el('h2', { text:'Settings' },
994
+ { fontSize:'22px', fontWeight:'650', letterSpacing:'-0.02em' }));
995
+ headDiv.appendChild(el('p', { text:'Every SuperLocalMemory configuration, grouped and searchable — no config files to edit by hand. Changes hot-reload the local daemon.' },
996
+ { color:'var(--fg-2)', marginTop:'5px', maxWidth:'62ch' }));
997
+ hub.appendChild(headDiv);
998
+
999
+ var sWrap = el('div', null, { display:'flex', alignItems:'center', gap:'9px', height:'38px',
1000
+ padding:'0 14px', background:'var(--card-2)', border:'1px solid var(--border)',
1001
+ borderRadius:'999px', color:'var(--fg-2)', marginBottom:'22px' });
1002
+ var sInp = el('input', { type:'search', placeholder:'Search every setting…', id: P + '-search' });
1003
+ Object.assign(sInp.style, { border:'0', background:'none', outline:'none',
1004
+ color:'var(--fg)', fontSize:'13.5px', width:'100%' });
1005
+ sInp.addEventListener('input', function () { filterSettings(this.value, hub); });
1006
+ sWrap.appendChild(sInp);
1007
+ hub.appendChild(sWrap);
1008
+
1009
+ var frag = document.createDocumentFragment();
1010
+ [buildMode(), buildEmb(), buildStorage(), buildScope(),
1011
+ buildCapture(), buildRecall(), buildInvoke(), buildForgetting(),
1012
+ buildEvolution(), buildBackup(), buildMesh(), buildTrust(),
1013
+ buildRateLimit(), buildDaemon()
1014
+ ].forEach(function (g) { frag.appendChild(g); });
1015
+ hub.appendChild(frag);
1016
+
1017
+ var foot = el('div', null, { display:'flex', gap:'10px', marginTop:'8px', position:'sticky',
1018
+ bottom:'0', padding:'14px 0',
1019
+ background:'linear-gradient(transparent, var(--page) 40%)' });
1020
+ var saveAllBtn = el('button', { type:'button' }); saveAllBtn.className = 'btn primary';
1021
+ saveAllBtn.textContent = 'Save all changes';
1022
+ var discardBtn = el('button', { type:'button' }); discardBtn.className = 'btn ghost';
1023
+ discardBtn.textContent = 'Discard';
1024
+ var syncBadge = el('span', null, { alignSelf:'center', marginLeft:'auto' });
1025
+ syncBadge.className = 'badge ok';
1026
+ syncBadge.innerHTML = '<span class="dot"></span> All synced';
1027
+ saveAllBtn.addEventListener('click', function () {
1028
+ ['mode-save','emb-save','stor-save','forg-save','trust-save','rl-save','evo-save','bk-save','dmn-save']
1029
+ .forEach(function (i) { var b = q(i); if (b) b.click(); });
1030
+ saveBackupConfig();
1031
+ syncBadge.className = 'badge ok';
1032
+ syncBadge.innerHTML = '<span class="dot"></span> Saved';
1033
+ setTimeout(function () { syncBadge.innerHTML = '<span class="dot"></span> All synced'; }, 3000);
1034
+ });
1035
+ discardBtn.addEventListener('click', function () { loadAll(); });
1036
+ foot.appendChild(saveAllBtn); foot.appendChild(discardBtn); foot.appendChild(syncBadge);
1037
+ hub.appendChild(foot);
1038
+
1039
+ container.insertBefore(hub, container.firstChild);
1040
+ loadAll();
1041
+ }
1042
+
1043
+ /* ═══════════════════════════════════════════════════════════
1044
+ BOOT
1045
+ ═══════════════════════════════════════════════════════════ */
1046
+ window.odRenderSettings = odRenderSettings;
1047
+
1048
+ document.addEventListener('DOMContentLoaded', function () {
1049
+ var pane = document.getElementById('settings-pane');
1050
+ if (pane && !document.getElementById(ID)) odRenderSettings(pane);
1051
+ });
1052
+
1053
+ }());