superlocalmemory 3.7.8 → 3.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +129 -0
  4. package/README.md +205 -123
  5. package/package.json +12 -3
  6. package/plugin/.claude-plugin/plugin.json +2 -3
  7. package/plugin/CLAUDE.md +8 -8
  8. package/plugin/agents/slm-governance-advisor.md +80 -0
  9. package/plugin/agents/slm-loop-runner.md +71 -0
  10. package/plugin/agents/slm-memory-advisor.md +10 -5
  11. package/plugin/agents/slm-optimize-advisor.md +9 -3
  12. package/plugin/commands/slm-loop.md +31 -0
  13. package/plugin/hooks/hooks.json +79 -0
  14. package/plugin/requirements.txt +1 -1
  15. package/plugin/scripts/slm-launch +46 -7
  16. package/plugin/settings.json +9 -0
  17. package/plugin/skills/slm-cache/SKILL.md +9 -1
  18. package/plugin/skills/slm-compress/SKILL.md +8 -1
  19. package/plugin/skills/slm-governance/SKILL.md +248 -0
  20. package/plugin/skills/slm-graph/SKILL.md +17 -3
  21. package/plugin/skills/slm-loop/SKILL.md +99 -0
  22. package/plugin/skills/slm-mesh/SKILL.md +282 -0
  23. package/plugin/skills/slm-profile/SKILL.md +148 -0
  24. package/plugin/skills/slm-recall/SKILL.md +46 -10
  25. package/plugin/skills/slm-remember/SKILL.md +48 -1
  26. package/plugin/skills/slm-scope/SKILL.md +176 -0
  27. package/plugin/skills/slm-session/SKILL.md +24 -1
  28. package/plugin/skills/slm-status/SKILL.md +18 -1
  29. package/plugin-src/rules/AGENTS.md +57 -18
  30. package/plugin-src/skills/slm-cache/SKILL.md +9 -1
  31. package/plugin-src/skills/slm-compress/SKILL.md +8 -1
  32. package/plugin-src/skills/slm-graph/SKILL.md +17 -3
  33. package/plugin-src/skills/slm-recall/SKILL.md +46 -10
  34. package/plugin-src/skills/slm-remember/SKILL.md +48 -1
  35. package/plugin-src/skills/slm-session/SKILL.md +24 -1
  36. package/plugin-src/skills/slm-status/SKILL.md +18 -1
  37. package/pyproject.toml +2 -1
  38. package/scripts/postinstall/validation.js +2 -0
  39. package/scripts/postinstall-interactive.js +74 -2
  40. package/src/superlocalmemory/__init__.py +1 -1
  41. package/src/superlocalmemory/access/__init__.py +3 -0
  42. package/src/superlocalmemory/access/rbac.py +477 -0
  43. package/src/superlocalmemory/cli/commands.py +228 -17
  44. package/src/superlocalmemory/cli/compress_cmd.py +17 -7
  45. package/src/superlocalmemory/cli/daemon.py +7 -0
  46. package/src/superlocalmemory/cli/loop_cmd.py +187 -0
  47. package/src/superlocalmemory/cli/main.py +49 -8
  48. package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
  49. package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
  50. package/src/superlocalmemory/cli/pending_store.py +49 -13
  51. package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
  52. package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
  53. package/src/superlocalmemory/cli/setup_wizard.py +22 -13
  54. package/src/superlocalmemory/cli/version_banner.py +17 -3
  55. package/src/superlocalmemory/compliance/audit.py +6 -0
  56. package/src/superlocalmemory/compliance/gdpr.py +128 -138
  57. package/src/superlocalmemory/compliance/retention.py +176 -45
  58. package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
  59. package/src/superlocalmemory/core/community_summary.py +267 -0
  60. package/src/superlocalmemory/core/config.py +216 -3
  61. package/src/superlocalmemory/core/consolidation_engine.py +95 -22
  62. package/src/superlocalmemory/core/context_cache.py +61 -18
  63. package/src/superlocalmemory/core/embedding_worker.py +21 -7
  64. package/src/superlocalmemory/core/embeddings.py +131 -46
  65. package/src/superlocalmemory/core/engine.py +41 -22
  66. package/src/superlocalmemory/core/engine_ingestion.py +359 -43
  67. package/src/superlocalmemory/core/engine_wiring.py +13 -0
  68. package/src/superlocalmemory/core/entity_community.py +178 -0
  69. package/src/superlocalmemory/core/graph_analyzer.py +39 -2
  70. package/src/superlocalmemory/core/graph_pruner.py +13 -8
  71. package/src/superlocalmemory/core/ingestion_command.py +134 -25
  72. package/src/superlocalmemory/core/injection.py +12 -7
  73. package/src/superlocalmemory/core/key_expander.py +138 -0
  74. package/src/superlocalmemory/core/maintenance.py +23 -0
  75. package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
  76. package/src/superlocalmemory/core/modes.py +1 -1
  77. package/src/superlocalmemory/core/mutations.py +2 -2
  78. package/src/superlocalmemory/core/pii.py +105 -0
  79. package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
  80. package/src/superlocalmemory/core/recall_pipeline.py +7 -3
  81. package/src/superlocalmemory/core/recall_worker.py +20 -6
  82. package/src/superlocalmemory/core/scale_engine.py +60 -1
  83. package/src/superlocalmemory/core/security_primitives.py +40 -2
  84. package/src/superlocalmemory/core/store_pipeline.py +186 -29
  85. package/src/superlocalmemory/core/worker_pool.py +21 -6
  86. package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
  87. package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
  88. package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
  89. package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
  90. package/src/superlocalmemory/evolution/evolution_store.py +122 -45
  91. package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
  92. package/src/superlocalmemory/evolution/model_selection.py +160 -0
  93. package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
  94. package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
  95. package/src/superlocalmemory/evolution/triggers.py +22 -13
  96. package/src/superlocalmemory/graph/cozo_backend.py +43 -20
  97. package/src/superlocalmemory/hooks/adapter_base.py +5 -1
  98. package/src/superlocalmemory/hooks/auto_recall.py +13 -1
  99. package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
  100. package/src/superlocalmemory/hooks/codex_assets.py +64 -5
  101. package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
  102. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  103. package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
  104. package/src/superlocalmemory/hooks/portable_kit.py +148 -3
  105. package/src/superlocalmemory/infra/backup.py +12 -1
  106. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  107. package/src/superlocalmemory/infra/data_root.py +43 -4
  108. package/src/superlocalmemory/infra/event_bus.py +107 -24
  109. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  110. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  111. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  112. package/src/superlocalmemory/learning/cross_project.py +28 -19
  113. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  114. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  115. package/src/superlocalmemory/learning/reward.py +50 -0
  116. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  117. package/src/superlocalmemory/learning/source_quality.py +523 -1
  118. package/src/superlocalmemory/loops/__init__.py +56 -0
  119. package/src/superlocalmemory/loops/budget.py +58 -0
  120. package/src/superlocalmemory/loops/engine.py +164 -0
  121. package/src/superlocalmemory/loops/ledger.py +263 -0
  122. package/src/superlocalmemory/loops/models.py +152 -0
  123. package/src/superlocalmemory/loops/rules.py +52 -0
  124. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  125. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  126. package/src/superlocalmemory/mcp/profiles.py +103 -0
  127. package/src/superlocalmemory/mcp/server.py +32 -79
  128. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  129. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  130. package/src/superlocalmemory/mcp/tools_core.py +12 -4
  131. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  132. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  133. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  134. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  135. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  136. package/src/superlocalmemory/mesh/broker.py +237 -129
  137. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  138. package/src/superlocalmemory/optimize/NOTICE +1 -6
  139. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  140. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  141. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  142. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  143. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  144. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  145. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  146. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  147. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  148. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  149. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  150. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  151. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  152. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  153. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  154. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  155. package/src/superlocalmemory/retrieval/engine.py +221 -47
  156. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  157. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  158. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  159. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  160. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  161. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  162. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  163. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  164. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  165. package/src/superlocalmemory/server/api.py +4 -4
  166. package/src/superlocalmemory/server/config_file.py +90 -0
  167. package/src/superlocalmemory/server/origin.py +50 -0
  168. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  169. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  170. package/src/superlocalmemory/server/recall_health.py +24 -3
  171. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  172. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  173. package/src/superlocalmemory/server/routes/agents.py +128 -38
  174. package/src/superlocalmemory/server/routes/backup.py +317 -70
  175. package/src/superlocalmemory/server/routes/behavioral.py +349 -71
  176. package/src/superlocalmemory/server/routes/brain.py +69 -12
  177. package/src/superlocalmemory/server/routes/chat.py +10 -5
  178. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  179. package/src/superlocalmemory/server/routes/config_api.py +438 -0
  180. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  181. package/src/superlocalmemory/server/routes/entity.py +108 -26
  182. package/src/superlocalmemory/server/routes/events.py +24 -8
  183. package/src/superlocalmemory/server/routes/evolution.py +189 -68
  184. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  185. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  186. package/src/superlocalmemory/server/routes/insights.py +3 -3
  187. package/src/superlocalmemory/server/routes/learning.py +289 -118
  188. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  189. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  190. package/src/superlocalmemory/server/routes/memories.py +182 -57
  191. package/src/superlocalmemory/server/routes/mesh.py +200 -31
  192. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  193. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  194. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  195. package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
  196. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  197. package/src/superlocalmemory/server/routes/stats.py +103 -158
  198. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  199. package/src/superlocalmemory/server/routes/token.py +3 -13
  200. package/src/superlocalmemory/server/routes/v3_api.py +247 -89
  201. package/src/superlocalmemory/server/routes/ws.py +5 -2
  202. package/src/superlocalmemory/server/security_middleware.py +12 -5
  203. package/src/superlocalmemory/server/ui.py +20 -5
  204. package/src/superlocalmemory/server/unified_daemon.py +827 -72
  205. package/src/superlocalmemory/server/write_identity.py +38 -8
  206. package/src/superlocalmemory/storage/database.py +265 -53
  207. package/src/superlocalmemory/storage/migration_runner.py +132 -1
  208. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  209. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  210. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  211. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  212. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  213. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  214. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  215. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  216. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  217. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  218. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  219. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  220. package/src/superlocalmemory/storage/models.py +4 -0
  221. package/src/superlocalmemory/storage/schema.py +136 -1
  222. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  223. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  224. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  225. package/src/superlocalmemory/trust/gate.py +49 -8
  226. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  227. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  228. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  229. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  230. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  231. package/src/superlocalmemory/ui/favicon.svg +35 -4
  232. package/src/superlocalmemory/ui/index.html +303 -173
  233. package/src/superlocalmemory/ui/js/brain.js +5 -20
  234. package/src/superlocalmemory/ui/js/core.js +100 -41
  235. package/src/superlocalmemory/ui/js/dashboard.js +403 -65
  236. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  237. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  238. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  239. package/src/superlocalmemory/ui/js/memories.js +15 -4
  240. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  241. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  242. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  243. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  244. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  245. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  246. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  247. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  248. package/src/superlocalmemory/ui/js/od-backup.js +871 -0
  249. package/src/superlocalmemory/ui/js/od-brain.js +816 -0
  250. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  251. package/src/superlocalmemory/ui/js/od-graph.js +600 -0
  252. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  253. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  254. package/src/superlocalmemory/ui/js/od-memories.js +929 -0
  255. package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
  256. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  257. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  258. package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
  259. package/src/superlocalmemory/ui/js/od-shell.js +809 -0
  260. package/src/superlocalmemory/ui/js/od-skills.js +600 -0
  261. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  262. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  263. package/src/superlocalmemory/ui/js/settings.js +17 -3
  264. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  265. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  266. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  267. package/plugin-src/.mcp.json +0 -12
  268. package/plugin-src/agents/slm-memory-advisor.md +0 -44
  269. package/plugin-src/agents/slm-optimize-advisor.md +0 -38
  270. package/plugin-src/hooks/.gitkeep +0 -0
  271. package/plugin-src/hooks/hooks.json +0 -23
  272. package/plugin-src/manifest.json +0 -25
  273. package/plugin-src/requirements.txt +0 -1
  274. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  275. package/plugin-src/scripts/ensure-venv.bat +0 -122
  276. package/plugin-src/scripts/ensure-venv.sh +0 -105
  277. package/plugin-src/scripts/slm-launch +0 -23
  278. package/plugin-src/scripts/slm-launch.bat +0 -23
  279. package/plugin-src/settings.json +0 -16
  280. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -0,0 +1,871 @@
1
+ /* od-backup.js — SuperLocalMemory Backup & Cloud Dashboard v1.0
2
+ * Exposes:
3
+ * window.odRenderBackup(container) — render into a supplied pane
4
+ * window.odOpenBackupDashboard() — open a full-screen overlay (called from od-settings.js)
5
+ * Wired endpoints (all confirmed against live daemon):
6
+ * GET /api/backup/status
7
+ * POST /api/backup/create
8
+ * POST /api/backup/configure body: {interval_hours, max_backups, enabled}
9
+ * GET /api/backup/list
10
+ * GET /api/backup/destinations
11
+ * POST /api/backup/connect/github body: {pat, repo_name}
12
+ * DELETE /api/backup/disconnect/{id}
13
+ * POST /api/backup/sync
14
+ * POST /api/backup/export → FileResponse (.db.gz download)
15
+ * GET /api/backup/oauth/github/start → OAuth redirect or PAT form
16
+ * GET /api/backup/oauth/google/start → Google OAuth redirect
17
+ * No mock / seed data — every render goes to the live daemon.
18
+ * Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar — AGPL-3.0
19
+ */
20
+ (function () {
21
+ 'use strict';
22
+
23
+ var OVERLAY_ID = 'od-backup-overlay';
24
+ var P = 'od-bk';
25
+ var tokenPromise = null;
26
+
27
+ function getToken(forceRefresh, retriedEmpty) {
28
+ if (forceRefresh) tokenPromise = null;
29
+ if (!tokenPromise) {
30
+ tokenPromise = fetch('/internal/token', { credentials:'same-origin' })
31
+ .then(function (response) {
32
+ if (!response.ok) throw new Error('Dashboard authorization failed');
33
+ return response.json();
34
+ })
35
+ .then(function (data) {
36
+ if (!data.token && !retriedEmpty) {
37
+ tokenPromise = null;
38
+ return getToken(true, true);
39
+ }
40
+ if (!data.token) throw new Error('Dashboard authorization token is unavailable');
41
+ return data.token;
42
+ })
43
+ .catch(function (error) {
44
+ tokenPromise = null;
45
+ throw error;
46
+ });
47
+ }
48
+ return tokenPromise;
49
+ }
50
+ function authMutation(url, method, body) {
51
+ function send(forceRefresh) {
52
+ return getToken(forceRefresh).then(function (token) {
53
+ return fetch(url, {
54
+ method: method,
55
+ credentials: 'same-origin',
56
+ headers: { 'Content-Type':'application/json', 'X-Install-Token': token },
57
+ body: body === undefined ? undefined : JSON.stringify(body)
58
+ });
59
+ }).then(function (response) {
60
+ if (!forceRefresh && (response.status === 401 || response.status === 403)) {
61
+ return send(true);
62
+ }
63
+ if (!response.ok) {
64
+ return response.text().then(function (message) {
65
+ throw new Error(message || ('Request failed (' + response.status + ')'));
66
+ });
67
+ }
68
+ return response;
69
+ });
70
+ }
71
+ return send(false);
72
+ }
73
+
74
+ /* ── Tiny utilities ──────────────────────────────────────── */
75
+ function esc(s) {
76
+ return String(s == null ? '' : s)
77
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;')
78
+ .replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
79
+ }
80
+ function toast(msg, err) {
81
+ if (typeof window.showToast === 'function') { window.showToast(msg); return; }
82
+ var d = document.createElement('div');
83
+ d.textContent = msg;
84
+ Object.assign(d.style, { position:'fixed', bottom:'20px', right:'20px', zIndex:'99999',
85
+ background: err ? 'var(--danger)' : 'var(--violet)', color:'#fff',
86
+ padding:'10px 18px', borderRadius:'10px', fontSize:'13px',
87
+ boxShadow:'0 4px 16px rgba(0,0,0,.4)', maxWidth:'340px' });
88
+ document.body.appendChild(d);
89
+ setTimeout(function () { d.remove(); }, 3200);
90
+ }
91
+ function fmt(bytes) {
92
+ if (!bytes) return '0 MB';
93
+ var mb = bytes / 1048576;
94
+ return mb > 1024 ? (mb/1024).toFixed(1) + ' GB' : mb.toFixed(0) + ' MB';
95
+ }
96
+ function fmtDate(iso) {
97
+ if (!iso) return '—';
98
+ try { return new Date(iso).toLocaleString(); } catch (e) { return iso; }
99
+ }
100
+ function el(tag, attrs, css) {
101
+ var e = document.createElement(tag);
102
+ if (attrs) Object.keys(attrs).forEach(function (k) {
103
+ if (k === 'text') e.textContent = attrs[k]; else e.setAttribute(k, attrs[k]);
104
+ });
105
+ if (css) Object.assign(e.style, css);
106
+ return e;
107
+ }
108
+ function q(root, id) {
109
+ return (root || document).querySelector('#' + P + '-' + id);
110
+ }
111
+ function set(root, id, txt) {
112
+ var e = q(root, id); if (e) e.textContent = txt;
113
+ }
114
+
115
+ /* ── CSS injection (design-system classes from backup.html inline styles) ── */
116
+ function injectStyles() {
117
+ if (document.getElementById('od-bk-styles')) return;
118
+ var s = document.createElement('style');
119
+ s.id = 'od-bk-styles';
120
+ s.textContent = [
121
+ '.cloud-orb{width:52px;height:52px;border-radius:16px;display:grid;place-items:center;',
122
+ 'flex-shrink:0;background:linear-gradient(150deg,var(--violet),var(--cyan));',
123
+ 'box-shadow:0 6px 20px -6px var(--violet)}',
124
+ '.cloud-orb svg{width:26px;height:26px;color:#fff}',
125
+ '.cloud-orb.off{background:var(--card-2);box-shadow:none}',
126
+ '.cloud-orb.off svg{color:var(--fg-3)}',
127
+ '.bk-conn{display:flex;align-items:center;gap:13px;padding:14px 0;',
128
+ 'border-bottom:1px solid var(--border)}',
129
+ '.bk-conn:last-child{border-bottom:0}',
130
+ '.bk-conn-ic{width:38px;height:38px;border-radius:10px;flex-shrink:0;',
131
+ 'display:grid;place-items:center;color:#fff}',
132
+ '.bk-conn-ic.gh{background:#24292f}',
133
+ '.bk-conn-ic.goog{background:#fff;border:1px solid var(--border)}',
134
+ '.bk-conn-ic.s3{background:var(--warn)}',
135
+ '.bk-conn.off .bk-conn-ic{filter:grayscale(1);opacity:.6}',
136
+ '.bk-ctl{display:flex;align-items:center;justify-content:space-between;',
137
+ 'gap:16px;padding:13px 0;border-bottom:1px solid var(--border)}',
138
+ '.bk-ctl:last-child{border-bottom:0}'
139
+ ].join('');
140
+ document.head.appendChild(s);
141
+ }
142
+
143
+ /* ── Card factory ────────────────────────────────────────── */
144
+ // Returns {wrap, head, body} matching .card structure from design-system.css
145
+ function card(title, sub, ic) {
146
+ var wrap = el('div');
147
+ wrap.className = 'card';
148
+ Object.assign(wrap.style, { marginBottom:'16px' });
149
+ // card-head
150
+ var head = el('div');
151
+ head.className = 'card-head';
152
+ if (ic && typeof window.slmIcon === 'function') {
153
+ var icEl = el('span', null,
154
+ { color:'var(--violet)', width:'18px', height:'18px', display:'flex', flexShrink:'0' });
155
+ icEl.innerHTML = window.slmIcon(ic);
156
+ head.appendChild(icEl);
157
+ }
158
+ head.appendChild(el('h3', { text: title }));
159
+ if (sub) head.appendChild(el('span', { text: sub }, { className:'sub' }));
160
+ wrap.appendChild(head);
161
+ var body = el('div');
162
+ body.className = 'card-pad';
163
+ wrap.appendChild(body);
164
+ return { wrap: wrap, head: head, body: body };
165
+ }
166
+
167
+ /* ══════════════════════════════════════════════════════════
168
+ HERO SECTION (matches design: card.glass + cloud-orb + badge + stats)
169
+ ══════════════════════════════════════════════════════════ */
170
+ function buildHero(root) {
171
+ var hero = el('section');
172
+ hero.className = 'card glass';
173
+ Object.assign(hero.style, { padding:'24px 26px', marginBottom:'16px' });
174
+
175
+ var inner = el('div', null,
176
+ { display:'flex', alignItems:'center', gap:'18px', flexWrap:'wrap' });
177
+
178
+ // Cloud orb icon (gradient, matches design)
179
+ var orb = el('span', { id: P + '-orb' });
180
+ orb.className = 'cloud-orb';
181
+ if (typeof window.slmIcon === 'function') {
182
+ orb.innerHTML = window.slmIcon('cloud');
183
+ } else {
184
+ orb.innerHTML = '<svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor"' +
185
+ ' stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
186
+ '<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg>';
187
+ }
188
+ inner.appendChild(orb);
189
+
190
+ // Title + badge + sub
191
+ var titleArea = el('div', null, { flex:'1', minWidth:'200px' });
192
+ var titleRow = el('div', null, { display:'flex', alignItems:'center', gap:'10px' });
193
+ var titleEl = el('h3', { id: P + '-cloud-title', text:'Checking cloud backup' },
194
+ { fontSize:'17px', margin:'0' });
195
+ titleRow.appendChild(titleEl);
196
+ var syncBadge = el('span', { id: P + '-cloud-badge' });
197
+ syncBadge.className = 'badge neutral';
198
+ syncBadge.innerHTML = '<span class="dot"></span> Checking';
199
+ titleRow.appendChild(syncBadge);
200
+ titleArea.appendChild(titleRow);
201
+ var subEl = el('p', { id: P + '-cloud-sub', text:'Connecting to your cloud…' },
202
+ { fontSize:'13px', marginTop:'4px', color:'var(--fg-2)' });
203
+ titleArea.appendChild(subEl);
204
+ inner.appendChild(titleArea);
205
+
206
+ // Stats: Last backup / Next scheduled / on-disk representation
207
+ var statsRow = el('div', null, { display:'flex', gap:'26px', flexWrap:'wrap' });
208
+ [
209
+ { id:'hero-last', lbl:'Last backup' },
210
+ { id:'hero-next', lbl:'Next scheduled' },
211
+ { id:'hero-encrypt', lbl:'Backup format' }
212
+ ].forEach(function (s) {
213
+ var item = el('div');
214
+ item.appendChild(el('div', { text: s.lbl },
215
+ { fontSize:'11px', color:'var(--fg-3)' }));
216
+ item.appendChild(el('div', { id: P + '-' + s.id, text:'…' },
217
+ { fontSize:'16px', fontWeight:'650', marginTop:'2px' }));
218
+ statsRow.appendChild(item);
219
+ });
220
+ inner.appendChild(statsRow);
221
+
222
+ // Buttons
223
+ var btnGroup = el('div', null,
224
+ { display:'flex', flexDirection:'column', gap:'8px', alignItems:'flex-end' });
225
+ var nowBtn = el('button', { type:'button', id: P + '-btn-now' });
226
+ nowBtn.className = 'btn primary';
227
+ if (typeof window.slmIcon === 'function') {
228
+ nowBtn.innerHTML = window.slmIcon('cloud') + ' Back up now';
229
+ } else {
230
+ nowBtn.textContent = 'Back up now';
231
+ }
232
+ nowBtn.addEventListener('click', doBackupNow.bind(null, root));
233
+ btnGroup.appendChild(nowBtn);
234
+
235
+ var exportBtn = el('button', { type:'button' });
236
+ exportBtn.className = 'btn ghost';
237
+ exportBtn.textContent = 'Export .db.gz';
238
+ exportBtn.addEventListener('click', function () {
239
+ doExport(exportBtn);
240
+ });
241
+ btnGroup.appendChild(exportBtn);
242
+ inner.appendChild(btnGroup);
243
+
244
+ hero.appendChild(inner);
245
+ return hero;
246
+ }
247
+
248
+ /* ══════════════════════════════════════════════════════════
249
+ KPI STRIP (design uses .kpi-strip + .card.kpi structure)
250
+ ══════════════════════════════════════════════════════════ */
251
+ function buildKPIs() {
252
+ var kpis = [
253
+ { id:'kpi-total', lbl:'Total backups', icon:'cloud' },
254
+ { id:'kpi-size', lbl:'Local storage used', icon:'memories' },
255
+ { id:'kpi-retain',lbl:'Retention', icon:'clock' },
256
+ { id:'kpi-count', lbl:'Local snapshots', icon:'shield' }
257
+ ];
258
+ var strip = el('div', null, { marginBottom:'16px' });
259
+ strip.className = 'kpi-strip';
260
+ kpis.forEach(function (k) {
261
+ var tile = el('div');
262
+ tile.className = 'card kpi';
263
+ var lbl = el('div');
264
+ lbl.className = 'label';
265
+ if (typeof window.slmIcon === 'function') {
266
+ var icEl = el('span', null, { display:'contents' });
267
+ icEl.innerHTML = window.slmIcon(k.icon);
268
+ lbl.appendChild(icEl);
269
+ }
270
+ lbl.appendChild(document.createTextNode(' ' + k.lbl));
271
+ tile.appendChild(lbl);
272
+ var val = el('div', { id: P + '-' + k.id, text:'—' });
273
+ val.className = 'value num';
274
+ // Retention value ("10 snapshots") wraps at 30px — design overrides to 22px for this tile
275
+ if (k.id === 'kpi-retain') val.style.fontSize = '22px';
276
+ tile.appendChild(val);
277
+ strip.appendChild(tile);
278
+ });
279
+ return strip;
280
+ }
281
+
282
+ /* ══════════════════════════════════════════════════════════
283
+ CONNECTIONS — GitHub + Google OAuth + S3
284
+ Design: .bk-conn rows with .bk-conn-ic.gh/.goog/.s3 brand icons
285
+ ══════════════════════════════════════════════════════════ */
286
+ var PROVIDERS = [
287
+ { id:'github', label:'GitHub',
288
+ icClass:'gh', oauthPath:'/api/backup/oauth/github/start',
289
+ hint:'Connect as your GitHub account to mirror backups to a private repo.' },
290
+ { id:'google', label:'Google (Gmail)',
291
+ icClass:'goog', oauthPath:'/api/backup/oauth/google/start',
292
+ hint:'Back up to Google Drive via your Gmail account.' },
293
+ { id:'s3', label:'Custom S3 / WebDAV',
294
+ icClass:'s3', oauthPath:'/api/backup/oauth/s3/start',
295
+ hint:'Bring your own S3-compatible bucket (Wasabi, MinIO, Cloudflare R2).' }
296
+ ];
297
+
298
+ // Inject brand SVGs after DOM settles — same pattern as design's backup.html
299
+ function injectConnIcons(container) {
300
+ var gh = container.querySelector('.bk-conn-ic.gh');
301
+ if (gh && typeof window.slmIcon === 'function') gh.innerHTML = window.slmIcon('github');
302
+ var goog = container.querySelector('.bk-conn-ic.goog');
303
+ if (goog) goog.innerHTML =
304
+ '<svg viewBox="0 0 24 24" width="20" height="20">' +
305
+ '<path fill="#4285F4" d="M21.6 12.2c0-.6-.1-1.2-.2-1.8H12v3.4h5.4a4.6 4.6 0 0 1-2 3v2.5h3.2c1.9-1.7 3-4.3 3-7.1z"/>' +
306
+ '<path fill="#34A853" d="M12 22c2.7 0 5-.9 6.6-2.4l-3.2-2.5c-.9.6-2 .9-3.4.9-2.6 0-4.8-1.7-5.6-4.1H3.1v2.6A10 10 0 0 0 12 22z"/>' +
307
+ '<path fill="#FBBC05" d="M6.4 13.9a6 6 0 0 1 0-3.8V7.5H3.1a10 10 0 0 0 0 9z"/>' +
308
+ '<path fill="#EA4335" d="M12 6.6c1.5 0 2.8.5 3.8 1.5l2.8-2.8A10 10 0 0 0 3.1 7.5l3.3 2.6C7.2 8.3 9.4 6.6 12 6.6z"/>' +
309
+ '</svg>';
310
+ var s3 = container.querySelector('.bk-conn-ic.s3');
311
+ if (s3 && typeof window.slmIcon === 'function') s3.innerHTML = window.slmIcon('cloud');
312
+ }
313
+
314
+ function buildConnections(root) {
315
+ var c = card('Connections', 'connect your own accounts', 'mesh');
316
+ c.body.style.padding = '0 20px';
317
+ var connList = el('div', { id: P + '-conn-list' });
318
+ c.body.appendChild(connList);
319
+ return c.wrap;
320
+ }
321
+
322
+ function renderConnections(root, destinations) {
323
+ var connList = q(root, 'conn-list'); if (!connList) return;
324
+ connList.innerHTML = '';
325
+ var frag = document.createDocumentFragment();
326
+ PROVIDERS.forEach(function (prov) {
327
+ var dest = destinations.filter(function (d) {
328
+ var destinationType = d.destination_type || d.type || d.provider;
329
+ return destinationType === prov.id ||
330
+ (prov.id === 'google' && destinationType === 'google_drive');
331
+ })[0];
332
+ frag.appendChild(buildConnRow(root, prov, dest || null));
333
+ });
334
+ connList.appendChild(frag);
335
+ // Inject brand icons after DOM insertion
336
+ injectConnIcons(connList);
337
+ }
338
+
339
+ function buildConnRow(root, prov, dest) {
340
+ var row = el('div');
341
+ row.className = 'bk-conn' + (dest ? '' : ' off');
342
+ row.setAttribute('data-conn', prov.id);
343
+
344
+ // Brand icon
345
+ var iconWrap = el('span');
346
+ iconWrap.className = 'bk-conn-ic ' + prov.icClass;
347
+ row.appendChild(iconWrap);
348
+
349
+ // Info area
350
+ var info = el('div', null, { flex:'1' });
351
+ info.appendChild(el('b', { text: prov.label }));
352
+ var detailEl = el('div');
353
+ detailEl.className = 'dim';
354
+ Object.assign(detailEl.style, { fontSize:'12.5px' });
355
+ if (dest) {
356
+ var config = {};
357
+ try { config = typeof dest.config === 'string' ? JSON.parse(dest.config) : (dest.config || {}); }
358
+ catch (e) { config = {}; }
359
+ var repoOrBucket = dest.repo || dest.bucket || dest.folder_id ||
360
+ config.full_repo || config.repo || config.folder || '';
361
+ var syncStatus = dest.last_sync_status || 'never';
362
+ var statusText = syncStatus === 'success' && dest.last_sync_at
363
+ ? 'Last sync succeeded ' + fmtDate(dest.last_sync_at)
364
+ : syncStatus === 'failed'
365
+ ? 'Last sync failed'
366
+ : 'Connected · never synced';
367
+ detailEl.textContent = statusText + (repoOrBucket ? ' · ' + repoOrBucket : '');
368
+ } else {
369
+ detailEl.textContent = prov.hint;
370
+ }
371
+ info.appendChild(detailEl);
372
+ row.appendChild(info);
373
+
374
+ // Action button
375
+ var btnWrap = el('div', null, { display:'flex', gap:'7px', flexShrink:'0' });
376
+ if (dest) {
377
+ var syncBtn = el('button', { type:'button' });
378
+ syncBtn.className = 'btn sm ghost';
379
+ syncBtn.textContent = 'Sync all';
380
+ syncBtn.addEventListener('click', function () {
381
+ doSync(root);
382
+ });
383
+ btnWrap.appendChild(syncBtn);
384
+
385
+ var discBtn = el('button', { type:'button' });
386
+ discBtn.className = 'btn sm';
387
+ discBtn.textContent = 'Disconnect';
388
+ discBtn.addEventListener('click', function () {
389
+ doDisconnect(root, dest.id || dest.dest_id || prov.id);
390
+ });
391
+ btnWrap.appendChild(discBtn);
392
+ } else {
393
+ var connBtn = el('button', { type:'button' });
394
+ connBtn.className = 'btn sm primary';
395
+ connBtn.textContent = 'Connect';
396
+ connBtn.addEventListener('click', function () {
397
+ openOAuth(root, prov.oauthPath, prov.label);
398
+ });
399
+ btnWrap.appendChild(connBtn);
400
+ }
401
+ row.appendChild(btnWrap);
402
+ return row;
403
+ }
404
+
405
+ /* ══════════════════════════════════════════════════════════
406
+ BACKUP SCOPE + SCHEDULE
407
+ Design: .bk-ctl rows with .switch toggles; Schedule at bottom
408
+ ══════════════════════════════════════════════════════════ */
409
+ function buildScope() {
410
+ var c = card('Backup scope', 'current managed database set', 'operations');
411
+
412
+ // Scope items — matching design's 4 items with toggle switches
413
+ var scopeItems = [
414
+ { id:'scope-mem', lbl:'Memories',
415
+ sub:'<span class="mono">memory.db</span> · facts & conversations', on: true },
416
+ { id:'scope-learn', lbl:'Learning data',
417
+ sub:'<span class="mono">learning.db</span> · ranking model', on: true },
418
+ { id:'scope-audit', lbl:'Audit & code data',
419
+ sub:'<span class="mono">audit_chain.db · code_graph.db</span> · when present', on: true },
420
+ { id:'scope-pending', lbl:'Pending operations',
421
+ sub:'<span class="mono">pending.db</span> · when present', on: true }
422
+ ];
423
+
424
+ scopeItems.forEach(function (item) {
425
+ var row = el('div');
426
+ row.className = 'bk-ctl';
427
+
428
+ var labelGroup = el('div');
429
+ labelGroup.appendChild(el('b', { text: item.lbl }));
430
+ var subEl = el('div');
431
+ subEl.className = 'dim';
432
+ subEl.style.fontSize = '12.5px';
433
+ subEl.innerHTML = item.sub;
434
+ labelGroup.appendChild(subEl);
435
+ row.appendChild(labelGroup);
436
+
437
+ var sw = el('button', { type:'button', id: P + '-' + item.id });
438
+ sw.className = 'switch' + (item.on ? ' on' : '');
439
+ sw.setAttribute('role', 'switch');
440
+ sw.setAttribute('aria-checked', String(item.on));
441
+ sw.disabled = true;
442
+ sw.title = 'Managed database scope is fixed in this release';
443
+ row.appendChild(sw);
444
+ c.body.appendChild(row);
445
+ });
446
+
447
+ // Schedule row — matching design
448
+ var schedRow = el('div');
449
+ schedRow.className = 'bk-ctl';
450
+ schedRow.appendChild(el('b', { text: 'Schedule' }));
451
+
452
+ var segs = ['Manual', 'Daily', 'Weekly'];
453
+ var segWrap = el('div');
454
+ segWrap.className = 'seg';
455
+ segs.forEach(function (s) {
456
+ var b = el('button', { type:'button', id: P + '-seg-' + s.toLowerCase() });
457
+ b.textContent = s;
458
+ if (s === 'Daily') b.className = 'active';
459
+ b.addEventListener('click', function () {
460
+ segs.forEach(function (t) {
461
+ var tb = document.getElementById(P + '-seg-' + t.toLowerCase());
462
+ if (tb) tb.className = (t === s ? 'active' : '');
463
+ });
464
+ var schedule = s === 'Manual'
465
+ ? { enabled: false }
466
+ : { enabled: true, interval_hours: s === 'Daily' ? 24 : 168 };
467
+ authMutation('/api/backup/configure', 'POST', schedule)
468
+ .then(function (r) { return r.json(); })
469
+ .then(function () { toast(s + ' schedule saved'); })
470
+ .catch(function () { toast('Schedule save failed', true); });
471
+ });
472
+ segWrap.appendChild(b);
473
+ });
474
+ schedRow.appendChild(segWrap);
475
+ c.body.appendChild(schedRow);
476
+
477
+ return c.wrap;
478
+ }
479
+
480
+ /* ══════════════════════════════════════════════════════════
481
+ BACKUP HISTORY TABLE
482
+ Design: .tbl class, columns When/Scope/Size/Destination/Status
483
+ ══════════════════════════════════════════════════════════ */
484
+ // Map backup type from API to a human-readable scope string
485
+ function fmtScope(bk) {
486
+ var t = (bk.type || 'memory').toLowerCase();
487
+ if (t === 'full') return 'Memories · Learning · Config';
488
+ if (t === 'memory') return 'Memories';
489
+ if (t === 'learning') return 'Learning data';
490
+ if (t === 'config') return 'Config & profiles';
491
+ return esc(t);
492
+ }
493
+
494
+ function fmtDest(bk) {
495
+ var d = bk.destination || bk.dest || '';
496
+ if (!d) return 'local · ~/.slm/backups';
497
+ return esc(d);
498
+ }
499
+
500
+ var _histFilter = 'all'; // all | cloud | local
501
+
502
+ function buildHistory() {
503
+ var c = card('Backup history', 'local snapshots and upload evidence', 'health');
504
+
505
+ // Add filter segment to card-head (All / Cloud / Local)
506
+ var spacer = el('div');
507
+ spacer.className = 'spacer';
508
+ c.head.appendChild(spacer);
509
+
510
+ var seg = el('div');
511
+ seg.className = 'seg';
512
+ ['All', 'Cloud', 'Local'].forEach(function (f) {
513
+ var b = el('button', { type:'button', id: P + '-hist-' + f.toLowerCase() });
514
+ b.textContent = f;
515
+ if (f === 'All') b.className = 'active';
516
+ b.addEventListener('click', function () {
517
+ ['all','cloud','local'].forEach(function (k) {
518
+ var btn = document.getElementById(P + '-hist-' + k);
519
+ if (btn) btn.className = (k === f.toLowerCase() ? 'active' : '');
520
+ });
521
+ _histFilter = f.toLowerCase();
522
+ });
523
+ seg.appendChild(b);
524
+ });
525
+ c.head.appendChild(seg);
526
+
527
+ c.body.style.padding = '0';
528
+ var wrap = el('div', { id: P + '-history-wrap' });
529
+ c.body.appendChild(wrap);
530
+ return c.wrap;
531
+ }
532
+
533
+ function renderHistory(root, backups) {
534
+ var wrap = q(root, 'history-wrap'); if (!wrap) return;
535
+ if (!backups || backups.length === 0) {
536
+ wrap.innerHTML = '<div style="padding:28px;text-align:center;color:var(--fg-2);' +
537
+ 'font-size:13px">No backups yet. Click "Back up now" to create your first snapshot.</div>';
538
+ return;
539
+ }
540
+ var table = el('table');
541
+ table.className = 'tbl';
542
+ table.id = P + '-hist-tbl';
543
+
544
+ var thead = el('thead');
545
+ var headTr = el('tr');
546
+ ['When', 'Scope', 'Size', 'Destination', 'Status', ''].forEach(function (h) {
547
+ var th = el('th', { text: h });
548
+ headTr.appendChild(th);
549
+ });
550
+ thead.appendChild(headTr);
551
+ table.appendChild(thead);
552
+
553
+ var tbody = el('tbody');
554
+ backups.slice(0, 20).forEach(function (bk) {
555
+ var tr = el('tr');
556
+ // When
557
+ var tdWhen = el('td'); tdWhen.className = 'dim';
558
+ tdWhen.textContent = fmtDate(bk.created);
559
+ tr.appendChild(tdWhen);
560
+ // Scope
561
+ var tdScope = el('td');
562
+ tdScope.textContent = fmtScope(bk);
563
+ tr.appendChild(tdScope);
564
+ // Size
565
+ var tdSize = el('td'); tdSize.className = 'num';
566
+ tdSize.textContent = bk.size_mb ? bk.size_mb.toFixed(0) + ' MB' : '—';
567
+ tr.appendChild(tdSize);
568
+ // Destination
569
+ var tdDest = el('td'); tdDest.className = 'mono';
570
+ tdDest.style.fontSize = '12px';
571
+ tdDest.textContent = fmtDest(bk);
572
+ tr.appendChild(tdDest);
573
+ // Status badge
574
+ var tdStatus = el('td');
575
+ var statusBadge = el('span');
576
+ var statusTxt = bk.status || 'Complete';
577
+ statusBadge.className = 'badge ' +
578
+ (statusTxt === 'Complete' || statusTxt === 'ok' ? 'ok' :
579
+ statusTxt === 'Partial' ? 'warn' : 'danger');
580
+ statusBadge.innerHTML = '<span class="dot"></span>' + esc(statusTxt);
581
+ tdStatus.appendChild(statusBadge);
582
+ tr.appendChild(tdStatus);
583
+ // Restore is intentionally absent until the dashboard has a guarded,
584
+ // tested restore endpoint and a daemon-safe restart workflow.
585
+ var tdAct = el('td', null, { textAlign:'right' });
586
+ tr.appendChild(tdAct);
587
+ tbody.appendChild(tr);
588
+ });
589
+ table.appendChild(tbody);
590
+ wrap.innerHTML = '';
591
+ wrap.appendChild(table);
592
+ }
593
+
594
+ /* ══════════════════════════════════════════════════════════
595
+ ACTIONS: backup now, sync, disconnect, oauth
596
+ ══════════════════════════════════════════════════════════ */
597
+ function doBackupNow(root) {
598
+ var btn = q(root, 'btn-now'); if (btn) { btn.disabled = true; btn.textContent = 'Creating…'; }
599
+ authMutation('/api/backup/create', 'POST')
600
+ .then(function (r) { return r.json(); })
601
+ .then(function (d) {
602
+ toast(d.success ? 'Backup created: ' + esc(d.filename || '') : 'Backup failed', !d.success);
603
+ if (btn) { btn.disabled = false; btn.textContent = 'Back up now'; }
604
+ loadAll(root);
605
+ })
606
+ .catch(function () {
607
+ toast('Backup failed', true);
608
+ if (btn) { btn.disabled = false; btn.textContent = 'Back up now'; }
609
+ });
610
+ }
611
+
612
+ function doSync(root) {
613
+ authMutation('/api/backup/sync', 'POST')
614
+ .then(function (r) { return r.json(); })
615
+ .then(function (d) { toast(d.success ? 'Sync started' : 'Sync failed: ' + esc(d.error || ''), !d.success); })
616
+ .catch(function () { toast('Sync error', true); });
617
+ }
618
+
619
+ function doExport(button) {
620
+ if (button) button.disabled = true;
621
+ authMutation('/api/backup/export', 'POST')
622
+ .then(function (response) {
623
+ return response.blob().then(function (blob) {
624
+ return { blob: blob, disposition: response.headers.get('Content-Disposition') || '' };
625
+ });
626
+ })
627
+ .then(function (download) {
628
+ var match = download.disposition.match(/filename="?([^";]+)"?/i);
629
+ var link = document.createElement('a');
630
+ var objectUrl = URL.createObjectURL(download.blob);
631
+ link.href = objectUrl;
632
+ link.download = match ? match[1] : 'superlocalmemory-backup.db.gz';
633
+ link.style.display = 'none';
634
+ document.body.appendChild(link);
635
+ link.click();
636
+ link.remove();
637
+ URL.revokeObjectURL(objectUrl);
638
+ toast('Backup export downloaded');
639
+ })
640
+ .catch(function () { toast('Backup export failed', true); })
641
+ .finally(function () { if (button) button.disabled = false; });
642
+ }
643
+
644
+ function doDisconnect(root, destId) {
645
+ if (!window.confirm('Disconnect this cloud destination? Existing backups are not deleted.')) return;
646
+ authMutation('/api/backup/disconnect/' + encodeURIComponent(destId), 'DELETE')
647
+ .then(function (r) { return r.json(); })
648
+ .then(function (d) {
649
+ toast(d.success ? 'Disconnected' : 'Disconnect failed: ' + esc(d.error || ''), !d.success);
650
+ loadDestinations(root);
651
+ })
652
+ .catch(function () { toast('Disconnect failed', true); });
653
+ }
654
+
655
+ function openOAuth(root, oauthPath, providerLabel) {
656
+ var popup = window.open(oauthPath, providerLabel + '-oauth', 'width=560,height=700');
657
+ if (!popup) { toast('Popup blocked — allow popups for this page', true); return; }
658
+ var timer = setInterval(function () {
659
+ if (!popup || popup.closed) {
660
+ clearInterval(timer);
661
+ // A closed popup is not proof of success; refresh runtime truth only.
662
+ loadDestinations(root, true);
663
+ }
664
+ }, 800);
665
+ }
666
+
667
+ /* ══════════════════════════════════════════════════════════
668
+ DATA LOADING
669
+ ══════════════════════════════════════════════════════════ */
670
+ function loadStatus(root) {
671
+ fetch('/api/backup/status').then(function (r) { return r.ok ? r.json() : null; })
672
+ .then(function (d) {
673
+ if (!d) return;
674
+
675
+ // Hero stats
676
+ var lastStr = d.last_backup ? fmtDate(d.last_backup) : 'Never';
677
+ set(root, 'hero-last', lastStr);
678
+ var next = !d.enabled
679
+ ? 'Manual only'
680
+ : (d.last_backup && d.interval_hours
681
+ ? new Date(new Date(d.last_backup).getTime() + d.interval_hours * 3600000).toLocaleString()
682
+ : 'Scheduled');
683
+ set(root, 'hero-next', next);
684
+ set(root, 'hero-encrypt', 'Plain SQLite');
685
+
686
+ // Hero title + badge: reflect configured and witnessed sync state.
687
+ var hasCloud = d.cloud_destinations && d.cloud_destinations.length > 0;
688
+ var allSuccessful = hasCloud && d.cloud_destinations.every(function (dest) {
689
+ return dest.last_sync_status === 'success' && Boolean(dest.last_sync_at);
690
+ });
691
+ var failed = hasCloud && d.cloud_destinations.some(function (dest) {
692
+ return dest.last_sync_status === 'failed';
693
+ });
694
+ var titleEl = q(root, 'cloud-title');
695
+ if (titleEl) titleEl.textContent = hasCloud ? 'Cloud destination configured' : 'No cloud destination';
696
+ var badgeEl = q(root, 'cloud-badge');
697
+ if (badgeEl) {
698
+ badgeEl.className = 'badge ' + (failed ? 'danger' : allSuccessful ? 'ok' : 'warn');
699
+ badgeEl.replaceChildren();
700
+ var dot = el('span'); dot.className = 'dot';
701
+ badgeEl.appendChild(dot);
702
+ badgeEl.appendChild(document.createTextNode(
703
+ failed
704
+ ? ' One or more syncs failed'
705
+ : allSuccessful
706
+ ? ' Latest reported syncs succeeded'
707
+ : hasCloud ? ' Sync incomplete or pending' : ' Not connected'
708
+ ));
709
+ }
710
+ var subEl = q(root, 'cloud-sub');
711
+ if (subEl) {
712
+ if (hasCloud) {
713
+ var dest = d.cloud_destinations[0];
714
+ var destStr = dest.display_name || dest.destination_type || 'cloud';
715
+ subEl.textContent = 'Plain SQLite copies upload to your private ' + destStr +
716
+ '. Access protection comes from that provider account.';
717
+ } else {
718
+ subEl.textContent = 'Local snapshots are plaintext SQLite. Connect a private provider only if you accept its access controls.';
719
+ }
720
+ }
721
+ var orb = q(root, 'orb');
722
+ if (orb) orb.classList.toggle('off', !hasCloud);
723
+
724
+ // KPI values (matched to new KPI ids)
725
+ var cnt = d.backup_count || d.learning_backup_count || 0;
726
+ set(root, 'kpi-total', String(cnt));
727
+ var totalMb = d.total_size_mb || 0;
728
+ set(root, 'kpi-size', totalMb > 1024
729
+ ? (totalMb / 1024).toFixed(1) + ' GB'
730
+ : totalMb.toFixed(0) + ' MB');
731
+ set(root, 'kpi-retain', (d.max_backups || cnt) + ' snapshots');
732
+ set(root, 'kpi-count', String(cnt));
733
+
734
+ if (d.backups) renderHistory(root, d.backups);
735
+
736
+ // Sync schedule segment buttons
737
+ var hours = d.interval_hours || 168;
738
+ var active = !d.enabled ? 'manual' : hours <= 24 ? 'daily' : 'weekly';
739
+ ['daily','weekly','manual'].forEach(function (s) {
740
+ var b = document.getElementById(P + '-seg-' + s);
741
+ if (b) b.className = (s === active ? 'active' : '');
742
+ });
743
+ }).catch(function () {});
744
+ }
745
+
746
+ function loadDestinations(root, forceReload) {
747
+ fetch('/api/backup/destinations').then(function (r) { return r.ok ? r.json() : null; })
748
+ .then(function (d) {
749
+ var dests = d && d.destinations ? d.destinations : [];
750
+ renderConnections(root, dests);
751
+ if (forceReload) {
752
+ toast('Destination status refreshed');
753
+ // Re-load status without inferring whether the popup succeeded.
754
+ loadStatus(root);
755
+ }
756
+ }).catch(function () {});
757
+ }
758
+
759
+ function loadList(root) {
760
+ fetch('/api/backup/list').then(function (r) { return r.ok ? r.json() : null; })
761
+ .then(function (d) {
762
+ if (d && d.backups) renderHistory(root, d.backups);
763
+ }).catch(function () {});
764
+ }
765
+
766
+ function loadAll(root) {
767
+ loadStatus(root);
768
+ loadDestinations(root);
769
+ loadList(root);
770
+ }
771
+
772
+ /* ══════════════════════════════════════════════════════════
773
+ MAIN RENDER
774
+ ══════════════════════════════════════════════════════════ */
775
+ function odRenderBackup(container) {
776
+ if (!container) return;
777
+ injectStyles();
778
+ // Clear skeleton / prior render
779
+ Array.from(container.children).forEach(function (c) { c.style.display = 'none'; });
780
+
781
+ var hub = el('div', null, { padding:'26px', maxWidth:'960px' });
782
+
783
+ // Page head — matches design's <div class="page-head"> block
784
+ var pageHead = el('div');
785
+ pageHead.className = 'page-head';
786
+ pageHead.appendChild(el('h2', { text:'Backup & cloud sync' }));
787
+ var desc = el('p');
788
+ desc.textContent = 'Your memory lives on this machine as plaintext SQLite snapshots. ' +
789
+ 'You can copy snapshots to a private GitHub repository or your Google Drive. ' +
790
+ 'Those providers control remote access; this release does not encrypt backup files.';
791
+ pageHead.appendChild(desc);
792
+ hub.appendChild(pageHead);
793
+
794
+ hub.appendChild(buildHero(hub));
795
+ hub.appendChild(buildKPIs());
796
+
797
+ // Two-column area: Connections | Scope
798
+ var twoCol = el('div');
799
+ twoCol.className = 'grid';
800
+ twoCol.style.gridTemplateColumns = '1fr 1fr';
801
+ twoCol.style.alignItems = 'start';
802
+ twoCol.appendChild(buildConnections(hub));
803
+ twoCol.appendChild(buildScope());
804
+ hub.appendChild(twoCol);
805
+
806
+ hub.appendChild(buildHistory());
807
+ container.insertBefore(hub, container.firstChild);
808
+ loadAll(hub);
809
+ }
810
+
811
+ /* ══════════════════════════════════════════════════════════
812
+ OVERLAY — called from od-settings.js Backup group
813
+ ══════════════════════════════════════════════════════════ */
814
+ function odOpenBackupDashboard() {
815
+ var existing = document.getElementById(OVERLAY_ID);
816
+ if (existing) { existing.style.display = 'flex'; loadAll(existing); return; }
817
+
818
+ var overlay = el('div', { id: OVERLAY_ID }, {
819
+ position:'fixed', inset:'0', zIndex:'9000',
820
+ background:'var(--page)', overflowY:'auto',
821
+ display:'flex', flexDirection:'column'
822
+ });
823
+
824
+ // Top bar with close button
825
+ var topbar = el('div', null, { position:'sticky', top:'0', zIndex:'9001',
826
+ background:'var(--card)', borderBottom:'1px solid var(--border)',
827
+ display:'flex', alignItems:'center', gap:'12px', padding:'0 22px', height:'52px' });
828
+
829
+ var backBtn = el('button', { type:'button' }); backBtn.className = 'btn ghost sm';
830
+ backBtn.textContent = '← Settings';
831
+ backBtn.addEventListener('click', function () {
832
+ overlay.style.display = 'none';
833
+ });
834
+ topbar.appendChild(backBtn);
835
+ topbar.appendChild(el('span', { text:'Backup & Cloud' },
836
+ { fontWeight:'640', fontSize:'15px' }));
837
+
838
+ var escHint = el('span', { text:'Esc to close' },
839
+ { marginLeft:'auto', fontSize:'11.5px', color:'var(--fg-3)' });
840
+ topbar.appendChild(escHint);
841
+
842
+ overlay.appendChild(topbar);
843
+
844
+ // Content area
845
+ var content = el('div', null, { flex:'1', padding:'22px' });
846
+ overlay.appendChild(content);
847
+ document.body.appendChild(overlay);
848
+
849
+ // ESC key close
850
+ document.addEventListener('keydown', function onEsc(e) {
851
+ if (e.key === 'Escape' && overlay.style.display !== 'none') {
852
+ overlay.style.display = 'none';
853
+ }
854
+ });
855
+
856
+ odRenderBackup(content);
857
+ }
858
+
859
+ /* ══════════════════════════════════════════════════════════
860
+ BOOT
861
+ ══════════════════════════════════════════════════════════ */
862
+ window.odRenderBackup = odRenderBackup;
863
+ window.odOpenBackupDashboard = odOpenBackupDashboard;
864
+
865
+ document.addEventListener('DOMContentLoaded', function () {
866
+ // Wire into a backup-pane if the shell gains one in future
867
+ var pane = document.getElementById('backup-pane');
868
+ if (pane) odRenderBackup(pane);
869
+ });
870
+
871
+ }());