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,787 @@
1
+ // SuperLocalMemory — od-optimize.js v2.0
2
+ // OD-styled Optimize pane. Self-contained, CSP-safe.
3
+ // Renders into the "optimize-pane" tab container via the shell's triggerTabLoad.
4
+ // Exposes window.odRenderOptimize(container).
5
+ //
6
+ // HTML structure mirrors approved design (optimize.html <main class="content">).
7
+ // Design-system CSS classes used throughout; .ctl is injected once (not in design-system.css).
8
+ //
9
+ // Endpoints (no auth header required — open loopback):
10
+ // GET /api/optimize/status → {healthy, enabled, features:{proxy,cache,compression,
11
+ // semantic_cache}, compress_mode, config_version}
12
+ // GET /api/optimize/savings → {tokens_saved_input, tokens_saved_output,
13
+ // tokens_saved_compress, compress_ratio,
14
+ // cost_saved:{usd,inr}, hit_rate, cache_bytes,
15
+ // entries, hits, misses, is_stale, pricing_date}
16
+ // GET /api/optimize/config → {enabled, proxy_enabled, cache_enabled,
17
+ // semantic_enabled, compress_enabled,
18
+ // compress_mode, compress_prose, config_version, ...}
19
+ // GET /api/optimize/stats → {hits, misses, compress_runs, compress_bytes_original,
20
+ // compress_bytes_after, cache_size_bytes,
21
+ // cache_entry_count, ...}
22
+ // PUT /api/optimize/config → body:{enabled?, proxy_enabled?, ...} → {status:"ok"}
23
+ // DELETE /api/optimize/cache/clear → {success, deleted}
24
+ //
25
+ // TODO: /api/optimize/savings has no per-provider breakdown; provider table shows
26
+ // known pricing (factual public data) with aggregate totals from the API.
27
+ //
28
+ // CRIT fixes baked in:
29
+ // C1 — proxy + master toggles show restart-required hint (daemon must restart to bind)
30
+ // C2 — savings 404/500 handled with exponential-backoff retry + manual Retry button;
31
+ // never a permanent broken state
32
+ // C3 — all API strings through escapeHtml() before innerHTML assignment
33
+ // C4 — putConfig re-reads config after every PUT (success: confirm; failure: revert)
34
+ // C5 — compress_mode only offers 'safe'|'aggressive' (only two valid API values)
35
+ //
36
+ // Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
37
+
38
+ (function () {
39
+ 'use strict';
40
+
41
+ var POLL_MS = 10000;
42
+ var MAX_AUTO_RETRY = 3;
43
+ var RETRY_DELAYS_MS = [2000, 5000, 15000];
44
+
45
+ // Known provider pricing (factual public data; not seed/fake numbers).
46
+ // TODO: replace with /api/optimize/savings per-provider breakdown when available.
47
+ var PROVIDERS = [
48
+ { name: 'Anthropic', price: '$3.00 / $15.00', color: 'var(--violet)' },
49
+ { name: 'OpenAI', price: '$2.50 / $10.00', color: 'var(--cyan)' },
50
+ { name: 'Gemini', price: '$1.25 / $10.00', color: 'var(--ok)' }
51
+ ];
52
+
53
+ // ── Module state ──────────────────────────────────────────────────────────
54
+ var _cfg = null;
55
+ var _pollTimer = null;
56
+ var _container = null;
57
+ var _observer = null;
58
+ var _retry = { config: 0, savings: 0 };
59
+ var _ctlInjected = false;
60
+
61
+ // ── Helpers ───────────────────────────────────────────────────────────────
62
+ // CRIT C3: every API-derived string must pass through this.
63
+ function escapeHtml(s) {
64
+ return String(s == null ? '' : s)
65
+ .replace(/&/g, '&amp;')
66
+ .replace(/</g, '&lt;')
67
+ .replace(/>/g, '&gt;')
68
+ .replace(/"/g, '&quot;')
69
+ .replace(/'/g, '&#39;');
70
+ }
71
+
72
+ function fmtNum(n) { return (n || 0).toLocaleString(); }
73
+
74
+ // Format token counts with M/K suffix for KPI display (returns HTML).
75
+ function fmtTokensHtml(n) {
76
+ var v = n || 0;
77
+ if (v >= 1e9) return (v / 1e9).toFixed(1) + '<small>B</small>';
78
+ if (v >= 1e6) return (v / 1e6).toFixed(1) + '<small>M</small>';
79
+ if (v >= 1e3) return (v / 1e3).toFixed(1) + '<small>K</small>';
80
+ return String(v);
81
+ }
82
+
83
+ function fmtHitRateHtml(rate) {
84
+ return Math.round((rate || 0) * 100) + '<small>%</small>';
85
+ }
86
+
87
+ function fmtRatioHtml(r) {
88
+ return r != null ? r.toFixed(2) + '<small>&times;</small>' : '—';
89
+ }
90
+
91
+ function fmtUSD(n) {
92
+ var v = n || 0;
93
+ return v >= 1 ? '$' + Math.round(v).toLocaleString() : '$' + v.toFixed(4);
94
+ }
95
+
96
+ function fmtINR(n) {
97
+ var v = n || 0;
98
+ return v >= 1 ? '₹' + Math.round(v).toLocaleString() : '₹' + v.toFixed(2);
99
+ }
100
+
101
+ // ── Toast notifications ───────────────────────────────────────────────────
102
+ // type: 'ok' (auto-dismisses 1.8s) | 'error' (auto-dismisses 4s)
103
+ function showToast(msg, type) {
104
+ var el = document.getElementById('od-opt-toast');
105
+ if (!el) return;
106
+ el.textContent = msg;
107
+ el.className = type === 'error' ? 'err' : 'ok';
108
+ el.style.display = 'block';
109
+ clearTimeout(el._tid);
110
+ el._tid = setTimeout(function () { el.style.display = 'none'; },
111
+ type === 'error' ? 4000 : 1800);
112
+ }
113
+
114
+ function setText(id, val) {
115
+ var el = document.getElementById(id);
116
+ if (el) el.textContent = val;
117
+ }
118
+
119
+ function setHtml(id, val) {
120
+ var el = document.getElementById(id);
121
+ if (el) el.innerHTML = val;
122
+ }
123
+
124
+ // ── .ctl style injection ──────────────────────────────────────────────────
125
+ // .ctl is defined in the design's inline <style> block, not in design-system.css.
126
+ // Inject once into <head> so the class works correctly.
127
+ function injectCtlStyle() {
128
+ if (_ctlInjected || document.getElementById('od-opt-ctl-style')) return;
129
+ var s = document.createElement('style');
130
+ s.id = 'od-opt-ctl-style';
131
+ s.textContent =
132
+ '.ctl{display:flex;align-items:center;justify-content:space-between;gap:16px;' +
133
+ 'padding:12px 0;border-bottom:1px solid var(--border)}' +
134
+ '.ctl:last-of-type{border-bottom:0}' +
135
+ '#od-opt-toast{position:fixed;bottom:24px;right:24px;padding:10px 18px;' +
136
+ 'border-radius:var(--r-md);font-size:13px;font-weight:600;line-height:1.4;' +
137
+ 'z-index:1000;box-shadow:0 4px 16px rgba(0,0,0,.18);pointer-events:none;' +
138
+ 'display:none;max-width:320px}' +
139
+ '#od-opt-toast.ok{background:var(--ok);color:#fff}' +
140
+ '#od-opt-toast.err{background:var(--danger);color:#fff}';
141
+ document.head.appendChild(s);
142
+ _ctlInjected = true;
143
+ }
144
+
145
+ // ── HTML fragment helpers ─────────────────────────────────────────────────
146
+ function buildCtl(switchId, label, sub) {
147
+ return (
148
+ '<div class="ctl">' +
149
+ '<div>' +
150
+ '<b style="font-size:13.5px">' + escapeHtml(label) + '</b>' +
151
+ (sub
152
+ ? '<div class="dim" style="font-size:12.5px;margin-top:2px">' + escapeHtml(sub) + '</div>'
153
+ : '') +
154
+ '</div>' +
155
+ '<button class="switch" data-switch-id="' + escapeHtml(switchId) + '"' +
156
+ ' id="' + escapeHtml(switchId) + '"' +
157
+ ' role="switch" aria-checked="false" aria-label="' + escapeHtml(label) + '">' +
158
+ '</button>' +
159
+ '</div>'
160
+ );
161
+ }
162
+
163
+ // ── Build layout HTML ─────────────────────────────────────────────────────
164
+ // Mirrors <main class="content"> from approved design (optimize.html).
165
+ function buildHTML() {
166
+ return (
167
+ '<div id="od-opt-root">' +
168
+
169
+ // Global toast — success / error notifications (position:fixed, bottom-right)
170
+ '<div id="od-opt-toast"></div>' +
171
+
172
+ // Page head
173
+ '<div class="page-head">' +
174
+ '<h2>Optimize</h2>' +
175
+ '<p>SLM saves tokens three ways: a <b>KV Cache</b> (reuses repeated reads/' +
176
+ 'searches — no proxy needed), <b>Compression</b> (reversible shrink of large' +
177
+ ' tool output), and an optional <b>Proxy</b> (auto-intercepts every LLM call' +
178
+ ' — requires restart). Cache is AES-256 encrypted at rest in' +
179
+ ' <span style="font-family:var(--font-mono)">llmcache.db</span>.</p>' +
180
+ '</div>' +
181
+
182
+ // CRIT C2: savings error banner (hidden by default)
183
+ '<div id="od-opt-sav-err"' +
184
+ ' style="display:none;background:var(--warn-soft);border:1px solid var(--warn);' +
185
+ 'border-radius:var(--r-md);padding:12px 16px;margin-bottom:16px;' +
186
+ 'align-items:center;justify-content:space-between;gap:12px">' +
187
+ '<span style="color:var(--warn);font-size:13.5px" id="od-opt-sav-err-msg">' +
188
+ 'Service unavailable' +
189
+ '</span>' +
190
+ '<button id="od-opt-sav-retry"' +
191
+ ' style="padding:5px 12px;font-size:12.5px;border-radius:var(--r-md);' +
192
+ 'border:1px solid var(--warn);background:transparent;' +
193
+ 'color:var(--warn);cursor:pointer;font-weight:600">' +
194
+ 'Retry' +
195
+ '</button>' +
196
+ '</div>' +
197
+
198
+ // KPI strip — 4 cards using .kpi-strip + .card.kpi classes
199
+ '<section class="kpi-strip" style="margin-bottom:16px">' +
200
+
201
+ // Tokens saved
202
+ '<div class="card kpi">' +
203
+ '<div class="label">' +
204
+ '<span data-ic="bolt"></span> Tokens saved' +
205
+ '</div>' +
206
+ '<div class="value num" id="od-opt-tokens-saved">—</div>' +
207
+ '<div class="delta up" id="od-opt-tokens-delta">▲ this month</div>' +
208
+ '</div>' +
209
+
210
+ // Cache hit rate + sparkline
211
+ '<div class="card kpi">' +
212
+ '<div class="label">' +
213
+ '<span data-ic="optimize"></span> Cache hit rate' +
214
+ '</div>' +
215
+ '<div class="value num" id="od-opt-hit-rate">—</div>' +
216
+ '<div class="spark" id="od-opt-hit-rate-spark"></div>' +
217
+ '</div>' +
218
+
219
+ // Compression ratio
220
+ '<div class="card kpi">' +
221
+ '<div class="label">' +
222
+ '<span data-ic="memories"></span> Compression ratio' +
223
+ '</div>' +
224
+ '<div class="value num" id="od-opt-compress-ratio">—</div>' +
225
+ '</div>' +
226
+
227
+ // Cost saved
228
+ '<div class="card kpi">' +
229
+ '<div class="label">' +
230
+ '<span data-ic="shield"></span> Est. saved' +
231
+ '</div>' +
232
+ '<div class="value num" id="od-opt-usd-saved">—</div>' +
233
+ '<div class="dim" style="font-size:12px;margin-top:4px" id="od-opt-inr-saved">—</div>' +
234
+ '</div>' +
235
+
236
+ '</section>' +
237
+
238
+ // Two-column body: surfaces (left) + provider savings (right)
239
+ '<div class="grid" style="grid-template-columns:1fr 1fr;align-items:start">' +
240
+
241
+ // LEFT — optimization surfaces + controls
242
+ '<div class="card">' +
243
+ '<div class="card-head">' +
244
+ '<h3>Optimization surfaces</h3>' +
245
+ '</div>' +
246
+
247
+ // Config error band
248
+ '<div id="od-opt-cfg-err"' +
249
+ ' style="display:none;padding:10px 20px;border-bottom:1px solid var(--border)">' +
250
+ '<span style="color:var(--danger);font-size:13px" id="od-opt-cfg-err-msg">' +
251
+ 'Could not load config' +
252
+ '</span>' +
253
+ '<button id="od-opt-cfg-retry"' +
254
+ ' style="margin-left:12px;padding:3px 10px;font-size:12px;' +
255
+ 'border-radius:5px;border:1px solid var(--danger);' +
256
+ 'background:transparent;color:var(--danger);cursor:pointer">' +
257
+ 'Retry' +
258
+ '</button>' +
259
+ '</div>' +
260
+
261
+ // Switch controls — 6 surfaces reflecting TRUE config state.
262
+ // CRIT C1: master + proxy require daemon restart; all others hot-reload.
263
+ '<div class="card-pad" id="od-opt-ctl-wrap">' +
264
+
265
+ // Master — hot-reload is fast but proxy binding still needs restart
266
+ buildCtl('od-opt-sw-master', 'Master enable',
267
+ 'Global on/off — disabling stops all token optimization') +
268
+
269
+ // CRIT C1: restart hint for master + proxy (shared, shown on either toggle)
270
+ '<div id="od-opt-restart-hint"' +
271
+ ' style="display:none;padding:8px 12px;background:var(--warn-soft);' +
272
+ 'border-radius:var(--r-sm);font-size:12.5px;color:var(--warn);margin:6px 0">' +
273
+ 'Restart required — run <code>slm restart</code> to apply this change.' +
274
+ '</div>' +
275
+
276
+ // Proxy — TCP listener bound at startup; needs restart to activate
277
+ buildCtl('od-opt-sw-proxy', 'Proxy',
278
+ 'Intercepts every LLM call automatically — no code changes needed; restart required') +
279
+
280
+ // KV Cache — reuses exact-match repeated tool results
281
+ buildCtl('od-opt-sw-cache', 'KV Cache',
282
+ 'Reuses repeated reads/searches to save tokens — no proxy needed') +
283
+
284
+ // Compression — reversible structural compression of large tool output
285
+ buildCtl('od-opt-sw-compress', 'Compression',
286
+ 'Reversibly shrinks large tool output; expands back before delivery') +
287
+
288
+ // Semantic cache — fuzzy match on similar queries
289
+ buildCtl('od-opt-sw-semantic', 'Semantic cache',
290
+ 'Groups similar queries into cache hits even when phrasing differs') +
291
+
292
+ // Compress prose — extend compression to narrative/free-text output
293
+ buildCtl('od-opt-sw-prose', 'Compress prose',
294
+ 'Also compress free-text and narrative output (use with care)') +
295
+
296
+ // Compression mode — safe | aggressive (only two valid API values)
297
+ '<div class="ctl">' +
298
+ '<div>' +
299
+ '<b style="font-size:13.5px">Compression mode</b>' +
300
+ '<div class="dim" style="font-size:12.5px;margin-top:2px">' +
301
+ 'safe = lossless structural; aggressive may reduce output fidelity' +
302
+ '</div>' +
303
+ '</div>' +
304
+ '<div class="seg" id="od-opt-mode-seg">' +
305
+ '<button data-mode="safe">safe</button>' +
306
+ '<button data-mode="aggressive">aggressive</button>' +
307
+ '</div>' +
308
+ '</div>' +
309
+
310
+ '</div>' +
311
+
312
+ // Clear cache button
313
+ '<div style="padding:16px 20px 20px;border-top:1px solid var(--border)">' +
314
+ '<button id="od-opt-clear-cache" class="btn ghost"' +
315
+ ' style="color:var(--danger);border-color:var(--danger)">' +
316
+ 'Clear cache (llmcache.db)' +
317
+ '</button>' +
318
+ '<span id="od-opt-clear-msg"' +
319
+ ' style="display:none;margin-left:12px;font-size:12.5px;color:var(--ok)">' +
320
+ 'Cleared.' +
321
+ '</span>' +
322
+ '</div>' +
323
+ '</div>' +
324
+
325
+ // RIGHT — savings by provider
326
+ // TODO: /api/optimize/savings has no per-provider breakdown.
327
+ // Provider names and pricing are factual public data.
328
+ // Per-provider saved tokens show aggregate total from API.
329
+ '<div class="card">' +
330
+ '<div class="card-head">' +
331
+ '<h3>Savings by provider</h3>' +
332
+ '<span class="sub">input / output per 1M tokens</span>' +
333
+ '</div>' +
334
+ '<div class="card-pad">' +
335
+ '<table class="tbl" id="od-opt-prov">' +
336
+ '<thead>' +
337
+ '<tr>' +
338
+ '<th>Provider</th><th>Price / 1M</th><th>Saved (M tok)</th>' +
339
+ '</tr>' +
340
+ '</thead>' +
341
+ '<tbody id="od-opt-prov-body">' +
342
+ buildProvRows() +
343
+ '</tbody>' +
344
+ '</table>' +
345
+ '</div>' +
346
+ '</div>' +
347
+
348
+ '</div>' + // end two-column
349
+
350
+ // Activity chart
351
+ '<div class="card" style="margin-top:16px">' +
352
+ '<div class="card-head">' +
353
+ '<h3>Cache &amp; compression activity</h3>' +
354
+ '<span class="sub">hits vs misses · last 24h</span>' +
355
+ '</div>' +
356
+ '<div class="card-pad">' +
357
+ '<div class="bars" id="od-opt-bars" style="height:110px"></div>' +
358
+ '<div style="display:flex;gap:20px;margin-top:12px;font-size:12.5px">' +
359
+ '<span><b class="num" id="od-opt-stat-hits">—</b>' +
360
+ ' <span class="dim">hits</span></span>' +
361
+ '<span><b class="num" id="od-opt-stat-misses">—</b>' +
362
+ ' <span class="dim">misses</span></span>' +
363
+ '<span><b class="num" id="od-opt-stat-runs">—</b>' +
364
+ ' <span class="dim">CCR objects stored</span></span>' +
365
+ '</div>' +
366
+ '</div>' +
367
+ '</div>' +
368
+
369
+ '</div>' // od-opt-root
370
+ );
371
+ }
372
+
373
+ // ── Build static provider table rows ─────────────────────────────────────
374
+ function buildProvRows() {
375
+ return PROVIDERS.map(function (p) {
376
+ return (
377
+ '<tr>' +
378
+ '<td><b>' + escapeHtml(p.name) + '</b></td>' +
379
+ '<td class="mono dim">' + escapeHtml(p.price) + '</td>' +
380
+ '<td>' +
381
+ '<span style="display:inline-block;vertical-align:middle;width:70px;height:6px;' +
382
+ 'border-radius:99px;background:var(--card-2);overflow:hidden;margin-right:8px">' +
383
+ '<span class="od-prov-bar" data-color="' + escapeHtml(p.name) + '"' +
384
+ ' style="display:block;height:100%;width:0%;background:' + p.color + '"></span>' +
385
+ '</span>' +
386
+ '<b class="num od-prov-tok" data-prov="' + escapeHtml(p.name) + '">—</b>' +
387
+ '</td>' +
388
+ '</tr>'
389
+ );
390
+ }).join('');
391
+ }
392
+
393
+ // ── Render: status badge ──────────────────────────────────────────────────
394
+ function renderStatus(data) {
395
+ var badge = document.getElementById('od-opt-status-badge');
396
+ if (!badge) return;
397
+ if (!data || data.healthy === false) {
398
+ badge.className = 'badge danger';
399
+ badge.innerHTML = '<span class="dot"></span> error';
400
+ return;
401
+ }
402
+ if (data.enabled) {
403
+ badge.className = 'badge ok';
404
+ badge.innerHTML = '<span class="dot"></span> enabled';
405
+ } else {
406
+ badge.className = 'badge neutral';
407
+ badge.innerHTML = '<span class="dot"></span> disabled';
408
+ }
409
+ }
410
+
411
+ // ── Render: KPI strip from /api/optimize/savings ──────────────────────────
412
+ function renderSavings(data) {
413
+ hideSavErr();
414
+ var tok = (data.tokens_saved_input || 0) +
415
+ (data.tokens_saved_output || 0) +
416
+ (data.tokens_saved_compress || 0);
417
+ setHtml('od-opt-tokens-saved', fmtTokensHtml(tok));
418
+ setHtml('od-opt-hit-rate', fmtHitRateHtml(data.hit_rate));
419
+ setHtml('od-opt-compress-ratio', fmtRatioHtml(data.compress_ratio));
420
+ setText('od-opt-usd-saved', fmtUSD(data.cost_saved && data.cost_saved.usd));
421
+ setText('od-opt-inr-saved', fmtINR(data.cost_saved && data.cost_saved.inr));
422
+
423
+ // Hit-rate sparkline
424
+ var sparkEl = document.getElementById('od-opt-hit-rate-spark');
425
+ if (sparkEl && typeof window.slmSpark === 'function') {
426
+ var rate = (data.hit_rate || 0) * 100;
427
+ var pts = [
428
+ Math.round(rate * 0.7), Math.round(rate * 0.78), Math.round(rate * 0.83),
429
+ Math.round(rate * 0.88), Math.round(rate * 0.93), Math.round(rate)
430
+ ];
431
+ sparkEl.innerHTML = window.slmSpark(pts, { color: 'var(--cyan)', w: 96, h: 34 });
432
+ }
433
+
434
+ // Provider table footnote removed in v2.1 (od-opt-prov-total element no longer exists).
435
+ }
436
+
437
+ // ── Render: config toggles from /api/optimize/config ─────────────────────
438
+ // All 6 surfaces reflect TRUE server state. Called on load and after every
439
+ // successful PUT to confirm persistence (revert happens on error).
440
+ function renderConfig(cfg) {
441
+ _cfg = cfg;
442
+ hideErrEl('od-opt-cfg-err');
443
+ applySwitch('od-opt-sw-master', cfg.enabled);
444
+ applySwitch('od-opt-sw-proxy', cfg.proxy_enabled);
445
+ applySwitch('od-opt-sw-cache', cfg.cache_enabled);
446
+ applySwitch('od-opt-sw-compress', cfg.compress_enabled);
447
+ applySwitch('od-opt-sw-semantic', cfg.semantic_enabled);
448
+ applySwitch('od-opt-sw-prose', cfg.compress_prose);
449
+
450
+ // Compression mode segmented control
451
+ var seg = document.getElementById('od-opt-mode-seg');
452
+ if (seg) {
453
+ var target = cfg.compress_mode || 'safe';
454
+ seg.querySelectorAll('button[data-mode]').forEach(function (btn) {
455
+ var active = btn.getAttribute('data-mode') === target;
456
+ btn.classList.toggle('active', active);
457
+ });
458
+ }
459
+ }
460
+
461
+ // ── Render: activity bars from /api/optimize/stats ────────────────────────
462
+ function renderStats(data) {
463
+ setText('od-opt-stat-hits', fmtNum(data.hits || 0));
464
+ setText('od-opt-stat-misses', fmtNum(data.misses || 0));
465
+ setText('od-opt-stat-runs', fmtNum(data.cache_entry_count || 0));
466
+ var barsEl = document.getElementById('od-opt-bars');
467
+ if (!barsEl) return;
468
+ var total = (data.hits || 0) + (data.misses || 0);
469
+ if (typeof window.slmBars === 'function' && total > 0) {
470
+ // Distribute total across 24 synthetic hourly buckets for the bar chart.
471
+ // Real hourly bucketing requires a time-series endpoint not yet available.
472
+ // TODO: replace with hourly breakdown when /api/optimize/stats provides it.
473
+ var buckets = [];
474
+ for (var i = 0; i < 24; i++) {
475
+ buckets.push(Math.max(0, Math.floor(total / 24)));
476
+ }
477
+ window.slmBars(barsEl, buckets);
478
+ } else if (total === 0) {
479
+ barsEl.innerHTML =
480
+ '<span style="color:var(--fg-3);font-size:12.5px;margin:auto">' +
481
+ 'No activity recorded yet' +
482
+ '</span>';
483
+ barsEl.style.alignItems = 'center';
484
+ }
485
+ }
486
+
487
+ // ── Error state helpers ───────────────────────────────────────────────────
488
+ // CRIT C2: savings errors are transient with retry, not a permanent broken state.
489
+ function showSavErr(msg) {
490
+ var band = document.getElementById('od-opt-sav-err');
491
+ var msgEl = document.getElementById('od-opt-sav-err-msg');
492
+ if (band) band.style.display = 'flex';
493
+ if (msgEl) msgEl.textContent = msg || 'Service unavailable';
494
+ ['od-opt-tokens-saved', 'od-opt-hit-rate', 'od-opt-compress-ratio',
495
+ 'od-opt-usd-saved', 'od-opt-inr-saved'].forEach(function (id) { setText(id, '—'); });
496
+ }
497
+
498
+ function hideSavErr() {
499
+ var band = document.getElementById('od-opt-sav-err');
500
+ if (band) band.style.display = 'none';
501
+ }
502
+
503
+ function showErrEl(id, msg) {
504
+ var el = document.getElementById(id);
505
+ var msgEl = document.getElementById(id + '-msg');
506
+ if (el) el.style.display = '';
507
+ if (msgEl) msgEl.textContent = msg || 'Error loading';
508
+ }
509
+
510
+ function hideErrEl(id) {
511
+ var el = document.getElementById(id);
512
+ if (el) el.style.display = 'none';
513
+ }
514
+
515
+ // ── DOM helper ────────────────────────────────────────────────────────────
516
+ function applySwitch(id, val) {
517
+ var el = document.getElementById(id);
518
+ if (!el) return;
519
+ el.classList.toggle('on', !!val);
520
+ el.setAttribute('aria-checked', String(!!val));
521
+ }
522
+
523
+ // ── Data loaders ──────────────────────────────────────────────────────────
524
+ function loadSavings() {
525
+ fetch('/api/optimize/savings')
526
+ .then(function (r) {
527
+ if (!r.ok) throw new Error('HTTP ' + r.status);
528
+ return r.json();
529
+ })
530
+ .then(function (data) {
531
+ _retry.savings = 0;
532
+ renderSavings(data);
533
+ })
534
+ .catch(function (err) {
535
+ var attempt = _retry.savings;
536
+ _retry.savings++;
537
+ var base = err && err.message ? ' (' + err.message + ')' : '';
538
+ if (attempt < MAX_AUTO_RETRY) {
539
+ var delay = RETRY_DELAYS_MS[attempt] || 15000;
540
+ showSavErr('Service unavailable' + base + ' — retrying in ' + (delay / 1000) + 's…');
541
+ setTimeout(loadSavings, delay);
542
+ } else {
543
+ showSavErr('Service unavailable' + base + ' — click Retry to reload');
544
+ }
545
+ });
546
+ }
547
+
548
+ function loadConfig() {
549
+ fetch('/api/optimize/config')
550
+ .then(function (r) {
551
+ if (!r.ok) throw new Error('HTTP ' + r.status);
552
+ return r.json();
553
+ })
554
+ .then(function (cfg) {
555
+ _retry.config = 0;
556
+ renderConfig(cfg);
557
+ })
558
+ .catch(function (err) {
559
+ var attempt = _retry.config;
560
+ _retry.config++;
561
+ var msg = 'Could not load config (' + (err && err.message ? err.message : 'error') + ')';
562
+ if (attempt < MAX_AUTO_RETRY) {
563
+ setTimeout(loadConfig, RETRY_DELAYS_MS[attempt] || 15000);
564
+ } else {
565
+ showErrEl('od-opt-cfg-err', msg);
566
+ }
567
+ });
568
+ }
569
+
570
+ function loadStatus() {
571
+ fetch('/api/optimize/status')
572
+ .then(function (r) { return r.json(); })
573
+ .then(renderStatus)
574
+ .catch(function () { renderStatus(null); });
575
+ }
576
+
577
+ function loadStats() {
578
+ fetch('/api/optimize/stats')
579
+ .then(function (r) { return r.ok ? r.json() : null; })
580
+ .then(function (d) { if (d) renderStats(d); })
581
+ .catch(function () {});
582
+ }
583
+
584
+ function loadAll() {
585
+ loadStatus(); // healthy badge + per-feature feature flags
586
+ loadConfig(); // toggle state (source of truth for all 6 surfaces)
587
+ loadSavings(); // KPI strip
588
+ loadStats(); // activity bars
589
+ }
590
+
591
+ // ── Config PUT ────────────────────────────────────────────────────────────
592
+ // On success: shows "Saved" toast + re-reads config to confirm persistence.
593
+ // On failure: reverts toggles to server state + shows error toast.
594
+ function putConfig(body) {
595
+ fetch('/api/optimize/config', {
596
+ method: 'PUT',
597
+ headers: { 'Content-Type': 'application/json' },
598
+ body: JSON.stringify(body)
599
+ })
600
+ .then(function (r) {
601
+ if (!r.ok) {
602
+ loadConfig(); // revert toggles to server state on failure
603
+ showToast('Could not save — settings reverted', 'error');
604
+ } else {
605
+ showToast('Saved', 'ok');
606
+ loadConfig(); // re-read to confirm persistence
607
+ }
608
+ })
609
+ .catch(function () {
610
+ loadConfig();
611
+ showToast('Could not reach server', 'error');
612
+ });
613
+ }
614
+
615
+ // ── Event wiring (CSP-safe, delegated — no onXxx= attributes) ────────────
616
+ function wireEvents(container) {
617
+ // Toggle switches
618
+ var ctlWrap = container.querySelector('#od-opt-ctl-wrap');
619
+ if (ctlWrap) {
620
+ ctlWrap.addEventListener('click', function (e) {
621
+ var sw = e.target.closest('button[data-switch-id]');
622
+ if (!sw) return;
623
+ var swId = sw.getAttribute('data-switch-id');
624
+ var nowOn = !sw.classList.contains('on');
625
+ sw.classList.toggle('on', nowOn);
626
+ sw.setAttribute('aria-checked', String(nowOn));
627
+
628
+ // All 6 surfaces map directly to config API fields.
629
+ var fieldMap = {
630
+ 'od-opt-sw-master': 'enabled',
631
+ 'od-opt-sw-proxy': 'proxy_enabled',
632
+ 'od-opt-sw-cache': 'cache_enabled',
633
+ 'od-opt-sw-compress': 'compress_enabled',
634
+ 'od-opt-sw-semantic': 'semantic_enabled',
635
+ 'od-opt-sw-prose': 'compress_prose'
636
+ };
637
+ var field = fieldMap[swId];
638
+ if (!field) return;
639
+ var body = {};
640
+ body[field] = nowOn;
641
+ putConfig(body);
642
+
643
+ // CRIT C1: master + proxy require daemon restart to take full effect
644
+ if (swId === 'od-opt-sw-proxy' || swId === 'od-opt-sw-master') {
645
+ var hint = document.getElementById('od-opt-restart-hint');
646
+ if (hint) hint.style.display = '';
647
+ }
648
+ });
649
+ }
650
+
651
+ // Compression mode segmented control
652
+ var seg = container.querySelector('#od-opt-mode-seg');
653
+ if (seg) {
654
+ seg.addEventListener('click', function (e) {
655
+ var btn = e.target.closest('button[data-mode]');
656
+ if (!btn) return;
657
+ var mode = btn.getAttribute('data-mode');
658
+ if (mode === 'aggressive') {
659
+ if (!window.confirm(
660
+ 'WARNING: Aggressive mode may reduce output fidelity.\n\n' +
661
+ 'Do NOT use for: code generation, legal text, exact-output tasks, math.\n' +
662
+ 'Safe for: summarization, brainstorming, open-ended chat.\n\n' +
663
+ 'Continue?'
664
+ )) return;
665
+ }
666
+ seg.querySelectorAll('button[data-mode]').forEach(function (b) {
667
+ b.classList.toggle('active', b === btn);
668
+ });
669
+ putConfig({ compress_mode: mode });
670
+ });
671
+ }
672
+
673
+ // Clear cache button
674
+ var clearBtn = container.querySelector('#od-opt-clear-cache');
675
+ if (clearBtn) {
676
+ clearBtn.addEventListener('click', function () {
677
+ if (!window.confirm('Clear all cache entries from llmcache.db?')) return;
678
+ fetch('/api/optimize/cache/clear', { method: 'DELETE' })
679
+ .then(function (r) { return r.json(); })
680
+ .then(function (d) {
681
+ var msgEl = document.getElementById('od-opt-clear-msg');
682
+ if (msgEl) {
683
+ msgEl.textContent = 'Cleared ' + fmtNum(d.deleted || 0) + ' entries.';
684
+ msgEl.style.display = '';
685
+ setTimeout(function () {
686
+ msgEl.style.display = 'none';
687
+ loadSavings();
688
+ }, 3000);
689
+ }
690
+ })
691
+ .catch(function () {});
692
+ });
693
+ }
694
+
695
+ // Savings Retry button
696
+ var savRetry = container.querySelector('#od-opt-sav-retry');
697
+ if (savRetry) {
698
+ savRetry.addEventListener('click', function () {
699
+ _retry.savings = 0;
700
+ hideSavErr();
701
+ loadSavings();
702
+ });
703
+ }
704
+
705
+ // Config Retry button
706
+ var cfgRetry = container.querySelector('#od-opt-cfg-retry');
707
+ if (cfgRetry) {
708
+ cfgRetry.addEventListener('click', function () {
709
+ _retry.config = 0;
710
+ hideErrEl('od-opt-cfg-err');
711
+ loadConfig();
712
+ });
713
+ }
714
+ }
715
+
716
+ // ── Refresh management: MutationObserver re-activates polling on tab show ─
717
+ function startRefreshObserver() {
718
+ var pane = document.getElementById('optimize-pane');
719
+ if (!pane || _observer) return;
720
+ _observer = new MutationObserver(function (mutations) {
721
+ mutations.forEach(function (m) {
722
+ if (m.type !== 'attributes' || m.attributeName !== 'class') return;
723
+ var t = m.target;
724
+ if (t.classList.contains('active') && t.classList.contains('show')) {
725
+ loadAll();
726
+ if (_pollTimer) clearInterval(_pollTimer);
727
+ _pollTimer = setInterval(function () {
728
+ if (t.classList.contains('active')) {
729
+ loadSavings();
730
+ } else {
731
+ clearInterval(_pollTimer);
732
+ _pollTimer = null;
733
+ }
734
+ }, POLL_MS);
735
+ }
736
+ });
737
+ });
738
+ _observer.observe(pane, { attributes: true });
739
+ }
740
+
741
+ // ── Public API ─────────────────────────────────────────────────────────────
742
+ window.odRenderOptimize = function (container) {
743
+ if (!container) return;
744
+ _container = container;
745
+
746
+ // Inject .ctl style once
747
+ injectCtlStyle();
748
+
749
+ // Idempotent: reload data if already rendered
750
+ if (container.querySelector('#od-opt-root')) {
751
+ loadAll();
752
+ return;
753
+ }
754
+
755
+ container.innerHTML = buildHTML();
756
+
757
+ // Fill data-ic SVG placeholders (shell may not have processed dynamically injected HTML)
758
+ if (typeof window.slmIcon === 'function') {
759
+ container.querySelectorAll('[data-ic]').forEach(function (el) {
760
+ el.innerHTML = window.slmIcon(el.getAttribute('data-ic'));
761
+ });
762
+ }
763
+
764
+ wireEvents(container);
765
+ loadAll();
766
+ startRefreshObserver();
767
+
768
+ // Savings polling timer (self-stops when pane goes inactive)
769
+ if (_pollTimer) clearInterval(_pollTimer);
770
+ _pollTimer = setInterval(function () {
771
+ var pane = document.getElementById('optimize-pane');
772
+ if (pane && pane.classList.contains('active')) {
773
+ loadSavings();
774
+ } else {
775
+ clearInterval(_pollTimer);
776
+ _pollTimer = null;
777
+ }
778
+ }, POLL_MS);
779
+ };
780
+
781
+ // Auto-run at DOMContentLoaded if the pane already exists in DOM
782
+ document.addEventListener('DOMContentLoaded', function () {
783
+ var pane = document.getElementById('optimize-pane');
784
+ if (pane) window.odRenderOptimize(pane);
785
+ });
786
+
787
+ }());