superlocalmemory 3.7.8 → 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 (260) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/ATTRIBUTION.md +1 -3
  3. package/CHANGELOG.md +69 -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 -1
  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 +94 -10
  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/backup.py +12 -1
  116. package/src/superlocalmemory/infra/daemon_identity.py +40 -4
  117. package/src/superlocalmemory/infra/data_root.py +43 -4
  118. package/src/superlocalmemory/infra/event_bus.py +107 -24
  119. package/src/superlocalmemory/infra/rate_limiter.py +93 -0
  120. package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
  121. package/src/superlocalmemory/ingestion/credentials.py +1 -1
  122. package/src/superlocalmemory/learning/cross_project.py +28 -19
  123. package/src/superlocalmemory/learning/reward_proxy.py +42 -9
  124. package/src/superlocalmemory/loops/__init__.py +56 -0
  125. package/src/superlocalmemory/loops/budget.py +58 -0
  126. package/src/superlocalmemory/loops/engine.py +164 -0
  127. package/src/superlocalmemory/loops/ledger.py +243 -0
  128. package/src/superlocalmemory/loops/models.py +152 -0
  129. package/src/superlocalmemory/loops/rules.py +52 -0
  130. package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
  131. package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
  132. package/src/superlocalmemory/mcp/profiles.py +103 -0
  133. package/src/superlocalmemory/mcp/server.py +21 -49
  134. package/src/superlocalmemory/mcp/tools_active.py +4 -7
  135. package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
  136. package/src/superlocalmemory/mcp/tools_core.py +8 -1
  137. package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
  138. package/src/superlocalmemory/mcp/tools_loops.py +300 -0
  139. package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
  140. package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
  141. package/src/superlocalmemory/mesh/broker.py +237 -129
  142. package/src/superlocalmemory/mesh/remote_sync.py +50 -8
  143. package/src/superlocalmemory/optimize/NOTICE +1 -6
  144. package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
  145. package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
  146. package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
  147. package/src/superlocalmemory/optimize/compress/align.py +32 -26
  148. package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
  149. package/src/superlocalmemory/optimize/compress/router.py +105 -22
  150. package/src/superlocalmemory/optimize/config/defaults.py +1 -1
  151. package/src/superlocalmemory/optimize/config/schema.py +87 -4
  152. package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
  153. package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
  154. package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
  155. package/src/superlocalmemory/optimize/storage/db.py +38 -9
  156. package/src/superlocalmemory/optimize/storage/schema.py +10 -0
  157. package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
  158. package/src/superlocalmemory/retrieval/agentic.py +1 -1
  159. package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
  160. package/src/superlocalmemory/retrieval/engine.py +168 -26
  161. package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
  162. package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
  163. package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
  164. package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
  165. package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
  166. package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
  167. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
  168. package/src/superlocalmemory/retrieval/time_window.py +181 -0
  169. package/src/superlocalmemory/server/api.py +4 -4
  170. package/src/superlocalmemory/server/profile_runtime.py +125 -8
  171. package/src/superlocalmemory/server/rbac_enforce.py +142 -0
  172. package/src/superlocalmemory/server/recall_health.py +24 -3
  173. package/src/superlocalmemory/server/recall_serializer.py +19 -1
  174. package/src/superlocalmemory/server/routes/abstraction.py +115 -0
  175. package/src/superlocalmemory/server/routes/agents.py +128 -38
  176. package/src/superlocalmemory/server/routes/backup.py +34 -10
  177. package/src/superlocalmemory/server/routes/behavioral.py +13 -12
  178. package/src/superlocalmemory/server/routes/brain.py +21 -5
  179. package/src/superlocalmemory/server/routes/chat.py +10 -5
  180. package/src/superlocalmemory/server/routes/compliance.py +171 -21
  181. package/src/superlocalmemory/server/routes/config_api.py +436 -0
  182. package/src/superlocalmemory/server/routes/data_io.py +30 -8
  183. package/src/superlocalmemory/server/routes/entity.py +9 -4
  184. package/src/superlocalmemory/server/routes/events.py +24 -8
  185. package/src/superlocalmemory/server/routes/evolution.py +135 -17
  186. package/src/superlocalmemory/server/routes/helpers.py +16 -1
  187. package/src/superlocalmemory/server/routes/ingest.py +7 -4
  188. package/src/superlocalmemory/server/routes/insights.py +3 -3
  189. package/src/superlocalmemory/server/routes/learning.py +14 -14
  190. package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
  191. package/src/superlocalmemory/server/routes/memories.py +182 -57
  192. package/src/superlocalmemory/server/routes/mesh.py +95 -15
  193. package/src/superlocalmemory/server/routes/optimize.py +33 -1
  194. package/src/superlocalmemory/server/routes/prewarm.py +2 -0
  195. package/src/superlocalmemory/server/routes/profiles.py +63 -17
  196. package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
  197. package/src/superlocalmemory/server/routes/rbac.py +367 -0
  198. package/src/superlocalmemory/server/routes/stats.py +13 -6
  199. package/src/superlocalmemory/server/routes/tiers.py +11 -9
  200. package/src/superlocalmemory/server/routes/v3_api.py +183 -69
  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 +384 -56
  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 +53 -0
  208. package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
  209. package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
  210. package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
  211. package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
  212. package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
  213. package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
  214. package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
  215. package/src/superlocalmemory/storage/models.py +4 -0
  216. package/src/superlocalmemory/storage/schema.py +87 -0
  217. package/src/superlocalmemory/storage/schema_v343.py +24 -12
  218. package/src/superlocalmemory/trust/gate.py +49 -8
  219. package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
  220. package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
  221. package/src/superlocalmemory/ui/css/design-system.css +621 -0
  222. package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
  223. package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
  224. package/src/superlocalmemory/ui/favicon.svg +35 -4
  225. package/src/superlocalmemory/ui/index.html +306 -173
  226. package/src/superlocalmemory/ui/js/brain.js +5 -20
  227. package/src/superlocalmemory/ui/js/core.js +47 -31
  228. package/src/superlocalmemory/ui/js/dashboard.js +314 -63
  229. package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
  230. package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
  231. package/src/superlocalmemory/ui/js/math-health.js +1 -1
  232. package/src/superlocalmemory/ui/js/memories.js +15 -4
  233. package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
  234. package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
  235. package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
  236. package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
  237. package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
  238. package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
  239. package/src/superlocalmemory/ui/js/od-agents.js +544 -0
  240. package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
  241. package/src/superlocalmemory/ui/js/od-backup.js +780 -0
  242. package/src/superlocalmemory/ui/js/od-brain.js +779 -0
  243. package/src/superlocalmemory/ui/js/od-entities.js +579 -0
  244. package/src/superlocalmemory/ui/js/od-graph.js +593 -0
  245. package/src/superlocalmemory/ui/js/od-health.js +539 -0
  246. package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
  247. package/src/superlocalmemory/ui/js/od-memories.js +887 -0
  248. package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
  249. package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
  250. package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
  251. package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
  252. package/src/superlocalmemory/ui/js/od-shell.js +593 -0
  253. package/src/superlocalmemory/ui/js/od-skills.js +573 -0
  254. package/src/superlocalmemory/ui/js/od-team.js +258 -0
  255. package/src/superlocalmemory/ui/js/profiles.js +159 -46
  256. package/src/superlocalmemory/ui/js/settings.js +2 -2
  257. package/src/superlocalmemory/ui/js/timeline.js +34 -5
  258. package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
  259. package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
  260. package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
@@ -0,0 +1,780 @@
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
+ * GET /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
+
26
+ /* ── Tiny utilities ──────────────────────────────────────── */
27
+ function esc(s) {
28
+ return String(s == null ? '' : s)
29
+ .replace(/&/g,'&amp;').replace(/</g,'&lt;')
30
+ .replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
31
+ }
32
+ function toast(msg, err) {
33
+ if (typeof window.showToast === 'function') { window.showToast(msg); return; }
34
+ var d = document.createElement('div');
35
+ d.textContent = msg;
36
+ Object.assign(d.style, { position:'fixed', bottom:'20px', right:'20px', zIndex:'99999',
37
+ background: err ? 'var(--danger)' : 'var(--violet)', color:'#fff',
38
+ padding:'10px 18px', borderRadius:'10px', fontSize:'13px',
39
+ boxShadow:'0 4px 16px rgba(0,0,0,.4)', maxWidth:'340px' });
40
+ document.body.appendChild(d);
41
+ setTimeout(function () { d.remove(); }, 3200);
42
+ }
43
+ function fmt(bytes) {
44
+ if (!bytes) return '0 MB';
45
+ var mb = bytes / 1048576;
46
+ return mb > 1024 ? (mb/1024).toFixed(1) + ' GB' : mb.toFixed(0) + ' MB';
47
+ }
48
+ function fmtDate(iso) {
49
+ if (!iso) return '—';
50
+ try { return new Date(iso).toLocaleString(); } catch (e) { return iso; }
51
+ }
52
+ function el(tag, attrs, css) {
53
+ var e = document.createElement(tag);
54
+ if (attrs) Object.keys(attrs).forEach(function (k) {
55
+ if (k === 'text') e.textContent = attrs[k]; else e.setAttribute(k, attrs[k]);
56
+ });
57
+ if (css) Object.assign(e.style, css);
58
+ return e;
59
+ }
60
+ function q(root, id) {
61
+ return (root || document).querySelector('#' + P + '-' + id);
62
+ }
63
+ function set(root, id, txt) {
64
+ var e = q(root, id); if (e) e.textContent = txt;
65
+ }
66
+
67
+ /* ── CSS injection (design-system classes from backup.html inline styles) ── */
68
+ function injectStyles() {
69
+ if (document.getElementById('od-bk-styles')) return;
70
+ var s = document.createElement('style');
71
+ s.id = 'od-bk-styles';
72
+ s.textContent = [
73
+ '.cloud-orb{width:52px;height:52px;border-radius:16px;display:grid;place-items:center;',
74
+ 'flex-shrink:0;background:linear-gradient(150deg,var(--violet),var(--cyan));',
75
+ 'box-shadow:0 6px 20px -6px var(--violet)}',
76
+ '.cloud-orb svg{width:26px;height:26px;color:#fff}',
77
+ '.cloud-orb.off{background:var(--card-2);box-shadow:none}',
78
+ '.cloud-orb.off svg{color:var(--fg-3)}',
79
+ '.bk-conn{display:flex;align-items:center;gap:13px;padding:14px 0;',
80
+ 'border-bottom:1px solid var(--border)}',
81
+ '.bk-conn:last-child{border-bottom:0}',
82
+ '.bk-conn-ic{width:38px;height:38px;border-radius:10px;flex-shrink:0;',
83
+ 'display:grid;place-items:center;color:#fff}',
84
+ '.bk-conn-ic.gh{background:#24292f}',
85
+ '.bk-conn-ic.goog{background:#fff;border:1px solid var(--border)}',
86
+ '.bk-conn-ic.s3{background:var(--warn)}',
87
+ '.bk-conn.off .bk-conn-ic{filter:grayscale(1);opacity:.6}',
88
+ '.bk-ctl{display:flex;align-items:center;justify-content:space-between;',
89
+ 'gap:16px;padding:13px 0;border-bottom:1px solid var(--border)}',
90
+ '.bk-ctl:last-child{border-bottom:0}'
91
+ ].join('');
92
+ document.head.appendChild(s);
93
+ }
94
+
95
+ /* ── Card factory ────────────────────────────────────────── */
96
+ // Returns {wrap, head, body} matching .card structure from design-system.css
97
+ function card(title, sub, ic) {
98
+ var wrap = el('div');
99
+ wrap.className = 'card';
100
+ Object.assign(wrap.style, { marginBottom:'16px' });
101
+ // card-head
102
+ var head = el('div');
103
+ head.className = 'card-head';
104
+ if (ic && typeof window.slmIcon === 'function') {
105
+ var icEl = el('span', null,
106
+ { color:'var(--violet)', width:'18px', height:'18px', display:'flex', flexShrink:'0' });
107
+ icEl.innerHTML = window.slmIcon(ic);
108
+ head.appendChild(icEl);
109
+ }
110
+ head.appendChild(el('h3', { text: title }));
111
+ if (sub) head.appendChild(el('span', { text: sub }, { className:'sub' }));
112
+ wrap.appendChild(head);
113
+ var body = el('div');
114
+ body.className = 'card-pad';
115
+ wrap.appendChild(body);
116
+ return { wrap: wrap, head: head, body: body };
117
+ }
118
+
119
+ /* ══════════════════════════════════════════════════════════
120
+ HERO SECTION (matches design: card.glass + cloud-orb + badge + stats)
121
+ ══════════════════════════════════════════════════════════ */
122
+ function buildHero(root) {
123
+ var hero = el('section');
124
+ hero.className = 'card glass';
125
+ Object.assign(hero.style, { padding:'24px 26px', marginBottom:'16px' });
126
+
127
+ var inner = el('div', null,
128
+ { display:'flex', alignItems:'center', gap:'18px', flexWrap:'wrap' });
129
+
130
+ // Cloud orb icon (gradient, matches design)
131
+ var orb = el('span', { id: P + '-orb' });
132
+ orb.className = 'cloud-orb';
133
+ if (typeof window.slmIcon === 'function') {
134
+ orb.innerHTML = window.slmIcon('cloud');
135
+ } else {
136
+ orb.innerHTML = '<svg viewBox="0 0 24 24" width="26" height="26" fill="none" stroke="currentColor"' +
137
+ ' stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
138
+ '<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/></svg>';
139
+ }
140
+ inner.appendChild(orb);
141
+
142
+ // Title + badge + sub
143
+ var titleArea = el('div', null, { flex:'1', minWidth:'200px' });
144
+ var titleRow = el('div', null, { display:'flex', alignItems:'center', gap:'10px' });
145
+ var titleEl = el('h3', { id: P + '-cloud-title', text:'Personal cloud connected' },
146
+ { fontSize:'17px', margin:'0' });
147
+ titleRow.appendChild(titleEl);
148
+ var syncBadge = el('span', { id: P + '-cloud-badge' });
149
+ syncBadge.className = 'badge ok';
150
+ syncBadge.innerHTML = '<span class="dot"></span> Synced';
151
+ titleRow.appendChild(syncBadge);
152
+ titleArea.appendChild(titleRow);
153
+ var subEl = el('p', { id: P + '-cloud-sub', text:'Connecting to your cloud…' },
154
+ { fontSize:'13px', marginTop:'4px', color:'var(--fg-2)' });
155
+ titleArea.appendChild(subEl);
156
+ inner.appendChild(titleArea);
157
+
158
+ // Stats: Last backup / Next scheduled / Encryption
159
+ var statsRow = el('div', null, { display:'flex', gap:'26px', flexWrap:'wrap' });
160
+ [
161
+ { id:'hero-last', lbl:'Last backup' },
162
+ { id:'hero-next', lbl:'Next scheduled' },
163
+ { id:'hero-encrypt', lbl:'Encryption' }
164
+ ].forEach(function (s) {
165
+ var item = el('div');
166
+ item.appendChild(el('div', { text: s.lbl },
167
+ { fontSize:'11px', color:'var(--fg-3)' }));
168
+ item.appendChild(el('div', { id: P + '-' + s.id, text:'…' },
169
+ { fontSize:'16px', fontWeight:'650', marginTop:'2px' }));
170
+ statsRow.appendChild(item);
171
+ });
172
+ inner.appendChild(statsRow);
173
+
174
+ // Buttons
175
+ var btnGroup = el('div', null,
176
+ { display:'flex', flexDirection:'column', gap:'8px', alignItems:'flex-end' });
177
+ var nowBtn = el('button', { type:'button', id: P + '-btn-now' });
178
+ nowBtn.className = 'btn primary';
179
+ if (typeof window.slmIcon === 'function') {
180
+ nowBtn.innerHTML = window.slmIcon('cloud') + ' Back up now';
181
+ } else {
182
+ nowBtn.textContent = 'Back up now';
183
+ }
184
+ nowBtn.addEventListener('click', doBackupNow.bind(null, root));
185
+ btnGroup.appendChild(nowBtn);
186
+
187
+ var exportBtn = el('button', { type:'button' });
188
+ exportBtn.className = 'btn ghost';
189
+ exportBtn.textContent = 'Export .db.gz';
190
+ exportBtn.addEventListener('click', function () {
191
+ window.location.href = '/api/backup/export';
192
+ });
193
+ btnGroup.appendChild(exportBtn);
194
+ inner.appendChild(btnGroup);
195
+
196
+ hero.appendChild(inner);
197
+ return hero;
198
+ }
199
+
200
+ /* ══════════════════════════════════════════════════════════
201
+ KPI STRIP (design uses .kpi-strip + .card.kpi structure)
202
+ ══════════════════════════════════════════════════════════ */
203
+ function buildKPIs() {
204
+ var kpis = [
205
+ { id:'kpi-total', lbl:'Total backups', icon:'cloud' },
206
+ { id:'kpi-size', lbl:'Cloud storage used', icon:'memories' },
207
+ { id:'kpi-retain',lbl:'Retention', icon:'clock' },
208
+ { id:'kpi-count', lbl:'Restore points', icon:'shield' }
209
+ ];
210
+ var strip = el('div', null, { marginBottom:'16px' });
211
+ strip.className = 'kpi-strip';
212
+ kpis.forEach(function (k) {
213
+ var tile = el('div');
214
+ tile.className = 'card kpi';
215
+ var lbl = el('div');
216
+ lbl.className = 'label';
217
+ if (typeof window.slmIcon === 'function') {
218
+ var icEl = el('span', null, { display:'contents' });
219
+ icEl.innerHTML = window.slmIcon(k.icon);
220
+ lbl.appendChild(icEl);
221
+ }
222
+ lbl.appendChild(document.createTextNode(' ' + k.lbl));
223
+ tile.appendChild(lbl);
224
+ var val = el('div', { id: P + '-' + k.id, text:'—' });
225
+ val.className = 'value num';
226
+ // Retention value ("10 snapshots") wraps at 30px — design overrides to 22px for this tile
227
+ if (k.id === 'kpi-retain') val.style.fontSize = '22px';
228
+ tile.appendChild(val);
229
+ strip.appendChild(tile);
230
+ });
231
+ return strip;
232
+ }
233
+
234
+ /* ══════════════════════════════════════════════════════════
235
+ CONNECTIONS — GitHub + Google OAuth + S3
236
+ Design: .bk-conn rows with .bk-conn-ic.gh/.goog/.s3 brand icons
237
+ ══════════════════════════════════════════════════════════ */
238
+ var PROVIDERS = [
239
+ { id:'github', label:'GitHub',
240
+ icClass:'gh', oauthPath:'/api/backup/oauth/github/start',
241
+ hint:'Connect as your GitHub account to mirror backups to a private repo.' },
242
+ { id:'google', label:'Google (Gmail)',
243
+ icClass:'goog', oauthPath:'/api/backup/oauth/google/start',
244
+ hint:'Back up to Google Drive via your Gmail account.' },
245
+ { id:'s3', label:'Custom S3 / WebDAV',
246
+ icClass:'s3', oauthPath:'/api/backup/oauth/s3/start',
247
+ hint:'Bring your own S3-compatible bucket (Wasabi, MinIO, Cloudflare R2).' }
248
+ ];
249
+
250
+ // Inject brand SVGs after DOM settles — same pattern as design's backup.html
251
+ function injectConnIcons(container) {
252
+ var gh = container.querySelector('.bk-conn-ic.gh');
253
+ if (gh && typeof window.slmIcon === 'function') gh.innerHTML = window.slmIcon('github');
254
+ var goog = container.querySelector('.bk-conn-ic.goog');
255
+ if (goog) goog.innerHTML =
256
+ '<svg viewBox="0 0 24 24" width="20" height="20">' +
257
+ '<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"/>' +
258
+ '<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"/>' +
259
+ '<path fill="#FBBC05" d="M6.4 13.9a6 6 0 0 1 0-3.8V7.5H3.1a10 10 0 0 0 0 9z"/>' +
260
+ '<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"/>' +
261
+ '</svg>';
262
+ var s3 = container.querySelector('.bk-conn-ic.s3');
263
+ if (s3 && typeof window.slmIcon === 'function') s3.innerHTML = window.slmIcon('cloud');
264
+ }
265
+
266
+ function buildConnections(root) {
267
+ var c = card('Connections', 'connect your own accounts', 'mesh');
268
+ c.body.style.padding = '0 20px';
269
+ var connList = el('div', { id: P + '-conn-list' });
270
+ c.body.appendChild(connList);
271
+ return c.wrap;
272
+ }
273
+
274
+ function renderConnections(root, destinations) {
275
+ var connList = q(root, 'conn-list'); if (!connList) return;
276
+ connList.innerHTML = '';
277
+ var frag = document.createDocumentFragment();
278
+ PROVIDERS.forEach(function (prov) {
279
+ var dest = destinations.filter(function (d) {
280
+ return d.type === prov.id || d.provider === prov.id;
281
+ })[0];
282
+ frag.appendChild(buildConnRow(root, prov, dest || null));
283
+ });
284
+ connList.appendChild(frag);
285
+ // Inject brand icons after DOM insertion
286
+ injectConnIcons(connList);
287
+ }
288
+
289
+ function buildConnRow(root, prov, dest) {
290
+ var row = el('div');
291
+ row.className = 'bk-conn' + (dest ? '' : ' off');
292
+ row.setAttribute('data-conn', prov.id);
293
+
294
+ // Brand icon
295
+ var iconWrap = el('span');
296
+ iconWrap.className = 'bk-conn-ic ' + prov.icClass;
297
+ row.appendChild(iconWrap);
298
+
299
+ // Info area
300
+ var info = el('div', null, { flex:'1' });
301
+ info.appendChild(el('b', { text: prov.label }));
302
+ var detailEl = el('div');
303
+ detailEl.className = 'dim';
304
+ Object.assign(detailEl.style, { fontSize:'12.5px' });
305
+ if (dest) {
306
+ var repoOrBucket = dest.repo || dest.bucket || dest.folder_id || '';
307
+ detailEl.innerHTML = 'Connected' +
308
+ (repoOrBucket ? ' · <span class="mono">' + esc(repoOrBucket) + '</span>' : '');
309
+ } else {
310
+ detailEl.textContent = prov.hint;
311
+ }
312
+ info.appendChild(detailEl);
313
+ row.appendChild(info);
314
+
315
+ // Action button
316
+ var btnWrap = el('div', null, { display:'flex', gap:'7px', flexShrink:'0' });
317
+ if (dest) {
318
+ var syncBtn = el('button', { type:'button' });
319
+ syncBtn.className = 'btn sm ghost';
320
+ syncBtn.textContent = 'Sync';
321
+ syncBtn.addEventListener('click', function () {
322
+ doSync(root, dest.id || dest.dest_id || prov.id);
323
+ });
324
+ btnWrap.appendChild(syncBtn);
325
+
326
+ var discBtn = el('button', { type:'button' });
327
+ discBtn.className = 'btn sm';
328
+ discBtn.textContent = 'Disconnect';
329
+ discBtn.addEventListener('click', function () {
330
+ doDisconnect(root, dest.id || dest.dest_id || prov.id);
331
+ });
332
+ btnWrap.appendChild(discBtn);
333
+ } else {
334
+ var connBtn = el('button', { type:'button' });
335
+ connBtn.className = 'btn sm primary';
336
+ connBtn.textContent = 'Connect';
337
+ connBtn.addEventListener('click', function () {
338
+ openOAuth(root, prov.oauthPath, prov.label);
339
+ });
340
+ btnWrap.appendChild(connBtn);
341
+ }
342
+ row.appendChild(btnWrap);
343
+ return row;
344
+ }
345
+
346
+ /* ══════════════════════════════════════════════════════════
347
+ BACKUP SCOPE + SCHEDULE
348
+ Design: .bk-ctl rows with .switch toggles; Schedule at bottom
349
+ ══════════════════════════════════════════════════════════ */
350
+ function buildScope() {
351
+ var c = card('Backup scope', 'choose what mirrors to the cloud', 'operations');
352
+
353
+ // Scope items — matching design's 4 items with toggle switches
354
+ var scopeItems = [
355
+ { id:'scope-mem', lbl:'Memories',
356
+ sub:'<span class="mono">memory.db</span> · facts & conversations', on: true },
357
+ { id:'scope-learn', lbl:'Learning data',
358
+ sub:'<span class="mono">learning.db</span> · ranking model', on: true },
359
+ { id:'scope-cfg', lbl:'Config & profiles',
360
+ sub:'<span class="mono">config.json</span> · modes, providers', on: true },
361
+ { id:'scope-cache', lbl:'Optimization cache',
362
+ sub:'<span class="mono">llmcache.db</span> · rebuildable', on: false }
363
+ ];
364
+
365
+ scopeItems.forEach(function (item) {
366
+ var row = el('div');
367
+ row.className = 'bk-ctl';
368
+
369
+ var labelGroup = el('div');
370
+ labelGroup.appendChild(el('b', { text: item.lbl }));
371
+ var subEl = el('div');
372
+ subEl.className = 'dim';
373
+ subEl.style.fontSize = '12.5px';
374
+ subEl.innerHTML = item.sub;
375
+ labelGroup.appendChild(subEl);
376
+ row.appendChild(labelGroup);
377
+
378
+ var sw = el('button', { type:'button', id: P + '-' + item.id });
379
+ sw.className = 'switch' + (item.on ? ' on' : '');
380
+ sw.setAttribute('role', 'switch');
381
+ sw.setAttribute('aria-checked', String(item.on));
382
+ sw.addEventListener('click', function () {
383
+ sw.classList.toggle('on');
384
+ sw.setAttribute('aria-checked', String(sw.classList.contains('on')));
385
+ // No dedicated /api/backup/scope endpoint yet
386
+ });
387
+ row.appendChild(sw);
388
+ c.body.appendChild(row);
389
+ });
390
+
391
+ // Schedule row — matching design
392
+ var schedRow = el('div');
393
+ schedRow.className = 'bk-ctl';
394
+ schedRow.appendChild(el('b', { text: 'Schedule' }));
395
+
396
+ var segs = ['Manual', 'Daily', 'Weekly'];
397
+ var segWrap = el('div');
398
+ segWrap.className = 'seg';
399
+ segs.forEach(function (s) {
400
+ var b = el('button', { type:'button', id: P + '-seg-' + s.toLowerCase() });
401
+ b.textContent = s;
402
+ if (s === 'Daily') b.className = 'active';
403
+ b.addEventListener('click', function () {
404
+ segs.forEach(function (t) {
405
+ var tb = document.getElementById(P + '-seg-' + t.toLowerCase());
406
+ if (tb) tb.className = (t === s ? 'active' : '');
407
+ });
408
+ var hours = s === 'Daily' ? 24 : s === 'Manual' ? 0 : 168;
409
+ fetch('/api/backup/configure', {
410
+ method:'POST', headers:{'Content-Type':'application/json'},
411
+ body:JSON.stringify({ interval_hours: hours })
412
+ }).then(function (r) { return r.json(); })
413
+ .then(function () { toast(s + ' schedule saved'); })
414
+ .catch(function () { toast('Schedule save failed', true); });
415
+ });
416
+ segWrap.appendChild(b);
417
+ });
418
+ schedRow.appendChild(segWrap);
419
+ c.body.appendChild(schedRow);
420
+
421
+ return c.wrap;
422
+ }
423
+
424
+ /* ══════════════════════════════════════════════════════════
425
+ BACKUP HISTORY TABLE
426
+ Design: .tbl class, columns When/Scope/Size/Destination/Status/Restore
427
+ card-head has sub "every snapshot is restorable" + All/Cloud/Local seg
428
+ ══════════════════════════════════════════════════════════ */
429
+ // Map backup type from API to a human-readable scope string
430
+ function fmtScope(bk) {
431
+ var t = (bk.type || 'memory').toLowerCase();
432
+ if (t === 'full') return 'Memories · Learning · Config';
433
+ if (t === 'memory') return 'Memories';
434
+ if (t === 'learning') return 'Learning data';
435
+ if (t === 'config') return 'Config & profiles';
436
+ return esc(t);
437
+ }
438
+
439
+ function fmtDest(bk) {
440
+ var d = bk.destination || bk.dest || '';
441
+ if (!d) return 'local · ~/.slm/backups';
442
+ return esc(d);
443
+ }
444
+
445
+ var _histFilter = 'all'; // all | cloud | local
446
+
447
+ function buildHistory() {
448
+ var c = card('Backup history', 'every snapshot is restorable', 'health');
449
+
450
+ // Add filter segment to card-head (All / Cloud / Local)
451
+ var spacer = el('div');
452
+ spacer.className = 'spacer';
453
+ c.head.appendChild(spacer);
454
+
455
+ var seg = el('div');
456
+ seg.className = 'seg';
457
+ ['All', 'Cloud', 'Local'].forEach(function (f) {
458
+ var b = el('button', { type:'button', id: P + '-hist-' + f.toLowerCase() });
459
+ b.textContent = f;
460
+ if (f === 'All') b.className = 'active';
461
+ b.addEventListener('click', function () {
462
+ ['all','cloud','local'].forEach(function (k) {
463
+ var btn = document.getElementById(P + '-hist-' + k);
464
+ if (btn) btn.className = (k === f.toLowerCase() ? 'active' : '');
465
+ });
466
+ _histFilter = f.toLowerCase();
467
+ });
468
+ seg.appendChild(b);
469
+ });
470
+ c.head.appendChild(seg);
471
+
472
+ c.body.style.padding = '0';
473
+ var wrap = el('div', { id: P + '-history-wrap' });
474
+ c.body.appendChild(wrap);
475
+ return c.wrap;
476
+ }
477
+
478
+ function renderHistory(root, backups) {
479
+ var wrap = q(root, 'history-wrap'); if (!wrap) return;
480
+ if (!backups || backups.length === 0) {
481
+ wrap.innerHTML = '<div style="padding:28px;text-align:center;color:var(--fg-2);' +
482
+ 'font-size:13px">No backups yet. Click "Back up now" to create your first snapshot.</div>';
483
+ return;
484
+ }
485
+ var table = el('table');
486
+ table.className = 'tbl';
487
+ table.id = P + '-hist-tbl';
488
+
489
+ var thead = el('thead');
490
+ var headTr = el('tr');
491
+ ['When', 'Scope', 'Size', 'Destination', 'Status', ''].forEach(function (h) {
492
+ var th = el('th', { text: h });
493
+ headTr.appendChild(th);
494
+ });
495
+ thead.appendChild(headTr);
496
+ table.appendChild(thead);
497
+
498
+ var tbody = el('tbody');
499
+ backups.slice(0, 20).forEach(function (bk) {
500
+ var tr = el('tr');
501
+ // When
502
+ var tdWhen = el('td'); tdWhen.className = 'dim';
503
+ tdWhen.textContent = fmtDate(bk.created);
504
+ tr.appendChild(tdWhen);
505
+ // Scope
506
+ var tdScope = el('td');
507
+ tdScope.textContent = fmtScope(bk);
508
+ tr.appendChild(tdScope);
509
+ // Size
510
+ var tdSize = el('td'); tdSize.className = 'num';
511
+ tdSize.textContent = bk.size_mb ? bk.size_mb.toFixed(0) + ' MB' : '—';
512
+ tr.appendChild(tdSize);
513
+ // Destination
514
+ var tdDest = el('td'); tdDest.className = 'mono';
515
+ tdDest.style.fontSize = '12px';
516
+ tdDest.textContent = fmtDest(bk);
517
+ tr.appendChild(tdDest);
518
+ // Status badge
519
+ var tdStatus = el('td');
520
+ var statusBadge = el('span');
521
+ var statusTxt = bk.status || 'Complete';
522
+ statusBadge.className = 'badge ' +
523
+ (statusTxt === 'Complete' || statusTxt === 'ok' ? 'ok' :
524
+ statusTxt === 'Partial' ? 'warn' : 'danger');
525
+ statusBadge.innerHTML = '<span class="dot"></span>' + esc(statusTxt);
526
+ tdStatus.appendChild(statusBadge);
527
+ tr.appendChild(tdStatus);
528
+ // Restore button
529
+ var tdAct = el('td', null, { textAlign:'right' });
530
+ var restoreBtn = el('button', { type:'button' });
531
+ restoreBtn.className = 'btn sm ghost';
532
+ restoreBtn.textContent = 'Restore';
533
+ restoreBtn.addEventListener('click', function () {
534
+ toast('Restore from ' + (bk.filename || 'snapshot') + ' — not yet implemented');
535
+ });
536
+ tdAct.appendChild(restoreBtn);
537
+ tr.appendChild(tdAct);
538
+ tbody.appendChild(tr);
539
+ });
540
+ table.appendChild(tbody);
541
+ wrap.innerHTML = '';
542
+ wrap.appendChild(table);
543
+ }
544
+
545
+ /* ══════════════════════════════════════════════════════════
546
+ ACTIONS: backup now, sync, disconnect, oauth
547
+ ══════════════════════════════════════════════════════════ */
548
+ function doBackupNow(root) {
549
+ var btn = q(root, 'btn-now'); if (btn) { btn.disabled = true; btn.textContent = 'Creating…'; }
550
+ fetch('/api/backup/create', { method:'POST' })
551
+ .then(function (r) { return r.json(); })
552
+ .then(function (d) {
553
+ toast(d.success ? 'Backup created: ' + esc(d.filename || '') : 'Backup failed', !d.success);
554
+ if (btn) { btn.disabled = false; btn.textContent = 'Back up now'; }
555
+ loadAll(root);
556
+ })
557
+ .catch(function () {
558
+ toast('Backup failed', true);
559
+ if (btn) { btn.disabled = false; btn.textContent = 'Back up now'; }
560
+ });
561
+ }
562
+
563
+ function doSync(root, destId) {
564
+ fetch('/api/backup/sync', { method:'POST', headers:{'Content-Type':'application/json'},
565
+ body:JSON.stringify({ destination_id: destId })
566
+ }).then(function (r) { return r.json(); })
567
+ .then(function (d) { toast(d.success ? 'Sync started' : 'Sync failed: ' + esc(d.error || ''), !d.success); })
568
+ .catch(function () { toast('Sync error', true); });
569
+ }
570
+
571
+ function doDisconnect(root, destId) {
572
+ if (!window.confirm('Disconnect this cloud destination? Existing backups are not deleted.')) return;
573
+ fetch('/api/backup/disconnect/' + encodeURIComponent(destId), { method:'DELETE' })
574
+ .then(function (r) { return r.json(); })
575
+ .then(function (d) {
576
+ toast(d.success ? 'Disconnected' : 'Disconnect failed: ' + esc(d.error || ''), !d.success);
577
+ loadDestinations(root);
578
+ })
579
+ .catch(function () { toast('Disconnect failed', true); });
580
+ }
581
+
582
+ function openOAuth(root, oauthPath, providerLabel) {
583
+ var popup = window.open(oauthPath, providerLabel + '-oauth', 'width=560,height=700');
584
+ if (!popup) { toast('Popup blocked — allow popups for this page', true); return; }
585
+ var timer = setInterval(function () {
586
+ if (!popup || popup.closed) {
587
+ clearInterval(timer);
588
+ // Reload destinations after OAuth popup closes
589
+ loadDestinations(root, true);
590
+ }
591
+ }, 800);
592
+ }
593
+
594
+ /* ══════════════════════════════════════════════════════════
595
+ DATA LOADING
596
+ ══════════════════════════════════════════════════════════ */
597
+ function loadStatus(root) {
598
+ fetch('/api/backup/status').then(function (r) { return r.ok ? r.json() : null; })
599
+ .then(function (d) {
600
+ if (!d) return;
601
+
602
+ // Hero stats
603
+ var lastStr = d.last_backup ? fmtDate(d.last_backup) : 'Never';
604
+ set(root, 'hero-last', lastStr);
605
+ var next = d.last_backup && d.interval_hours
606
+ ? new Date(new Date(d.last_backup).getTime() + d.interval_hours * 3600000).toLocaleString()
607
+ : (d.enabled ? 'Scheduled' : 'Manual only');
608
+ set(root, 'hero-next', next);
609
+ set(root, 'hero-encrypt', 'AES-256');
610
+
611
+ // Hero title + badge: reflect connection state
612
+ var hasCloud = d.cloud_destinations && d.cloud_destinations.length > 0;
613
+ var titleEl = q(root, 'cloud-title');
614
+ if (titleEl) titleEl.textContent = hasCloud ? 'Personal cloud connected' : 'No personal cloud';
615
+ var badgeEl = q(root, 'cloud-badge');
616
+ if (badgeEl) {
617
+ badgeEl.className = 'badge ' + (hasCloud ? 'ok' : 'warn');
618
+ badgeEl.innerHTML = '<span class="dot"></span> ' + (hasCloud ? 'Synced' : 'Not connected');
619
+ }
620
+ var subEl = q(root, 'cloud-sub');
621
+ if (subEl) {
622
+ if (hasCloud) {
623
+ var dest = d.cloud_destinations[0];
624
+ var destStr = dest.repo || dest.bucket || dest.folder_id || dest.provider || 'cloud';
625
+ subEl.innerHTML = 'Mirroring to <b>' + esc(destStr) + '</b> · encrypted with your local key.';
626
+ } else {
627
+ subEl.textContent = 'Connect a provider below to enable encrypted cloud mirroring.';
628
+ }
629
+ }
630
+ var orb = q(root, 'orb');
631
+ if (orb) orb.classList.toggle('off', !hasCloud);
632
+
633
+ // KPI values (matched to new KPI ids)
634
+ var cnt = d.backup_count || d.learning_backup_count || 0;
635
+ set(root, 'kpi-total', String(cnt));
636
+ var totalMb = d.total_size_mb || 0;
637
+ set(root, 'kpi-size', totalMb > 1024
638
+ ? (totalMb / 1024).toFixed(1) + ' GB'
639
+ : totalMb.toFixed(0) + ' MB');
640
+ set(root, 'kpi-retain', (d.max_backups || cnt) + ' snapshots');
641
+ set(root, 'kpi-count', String(cnt));
642
+
643
+ if (d.backups) renderHistory(root, d.backups);
644
+
645
+ // Sync schedule segment buttons
646
+ var hours = d.interval_hours || 168;
647
+ var active = hours <= 24 ? 'daily' : hours >= 9999 ? 'manual' : 'weekly';
648
+ ['daily','weekly','manual'].forEach(function (s) {
649
+ var b = document.getElementById(P + '-seg-' + s);
650
+ if (b) b.className = (s === active ? 'active' : '');
651
+ });
652
+ }).catch(function () {});
653
+ }
654
+
655
+ function loadDestinations(root, forceReload) {
656
+ fetch('/api/backup/destinations').then(function (r) { return r.ok ? r.json() : null; })
657
+ .then(function (d) {
658
+ var dests = d && d.destinations ? d.destinations : [];
659
+ renderConnections(root, dests);
660
+ if (forceReload) {
661
+ toast(dests.length > 0 ? 'Connected successfully' : 'Connection complete');
662
+ // Re-load status so hero reflects new connection state
663
+ loadStatus(root);
664
+ }
665
+ }).catch(function () {});
666
+ }
667
+
668
+ function loadList(root) {
669
+ fetch('/api/backup/list').then(function (r) { return r.ok ? r.json() : null; })
670
+ .then(function (d) {
671
+ if (d && d.backups) renderHistory(root, d.backups);
672
+ }).catch(function () {});
673
+ }
674
+
675
+ function loadAll(root) {
676
+ loadStatus(root);
677
+ loadDestinations(root);
678
+ loadList(root);
679
+ }
680
+
681
+ /* ══════════════════════════════════════════════════════════
682
+ MAIN RENDER
683
+ ══════════════════════════════════════════════════════════ */
684
+ function odRenderBackup(container) {
685
+ if (!container) return;
686
+ injectStyles();
687
+ // Clear skeleton / prior render
688
+ Array.from(container.children).forEach(function (c) { c.style.display = 'none'; });
689
+
690
+ var hub = el('div', null, { padding:'26px', maxWidth:'960px' });
691
+
692
+ // Page head — matches design's <div class="page-head"> block
693
+ var pageHead = el('div');
694
+ pageHead.className = 'page-head';
695
+ pageHead.appendChild(el('h2', { text:'Backup & cloud sync' }));
696
+ var desc = el('p');
697
+ desc.innerHTML = 'Your memory lives on this machine. Optionally encrypt and mirror it to' +
698
+ ' <b>your own</b> cloud — a private GitHub repo or your Google Drive via Gmail.' +
699
+ ' You hold the keys; SuperLocalMemory never sees your data.';
700
+ pageHead.appendChild(desc);
701
+ hub.appendChild(pageHead);
702
+
703
+ hub.appendChild(buildHero(hub));
704
+ hub.appendChild(buildKPIs());
705
+
706
+ // Two-column area: Connections | Scope
707
+ var twoCol = el('div');
708
+ twoCol.className = 'grid';
709
+ twoCol.style.gridTemplateColumns = '1fr 1fr';
710
+ twoCol.style.alignItems = 'start';
711
+ twoCol.appendChild(buildConnections(hub));
712
+ twoCol.appendChild(buildScope());
713
+ hub.appendChild(twoCol);
714
+
715
+ hub.appendChild(buildHistory());
716
+ container.insertBefore(hub, container.firstChild);
717
+ loadAll(hub);
718
+ }
719
+
720
+ /* ══════════════════════════════════════════════════════════
721
+ OVERLAY — called from od-settings.js Backup group
722
+ ══════════════════════════════════════════════════════════ */
723
+ function odOpenBackupDashboard() {
724
+ var existing = document.getElementById(OVERLAY_ID);
725
+ if (existing) { existing.style.display = 'flex'; loadAll(existing); return; }
726
+
727
+ var overlay = el('div', { id: OVERLAY_ID }, {
728
+ position:'fixed', inset:'0', zIndex:'9000',
729
+ background:'var(--page)', overflowY:'auto',
730
+ display:'flex', flexDirection:'column'
731
+ });
732
+
733
+ // Top bar with close button
734
+ var topbar = el('div', null, { position:'sticky', top:'0', zIndex:'9001',
735
+ background:'var(--card)', borderBottom:'1px solid var(--border)',
736
+ display:'flex', alignItems:'center', gap:'12px', padding:'0 22px', height:'52px' });
737
+
738
+ var backBtn = el('button', { type:'button' }); backBtn.className = 'btn ghost sm';
739
+ backBtn.textContent = '← Settings';
740
+ backBtn.addEventListener('click', function () {
741
+ overlay.style.display = 'none';
742
+ });
743
+ topbar.appendChild(backBtn);
744
+ topbar.appendChild(el('span', { text:'Backup & Cloud' },
745
+ { fontWeight:'640', fontSize:'15px' }));
746
+
747
+ var escHint = el('span', { text:'Esc to close' },
748
+ { marginLeft:'auto', fontSize:'11.5px', color:'var(--fg-3)' });
749
+ topbar.appendChild(escHint);
750
+
751
+ overlay.appendChild(topbar);
752
+
753
+ // Content area
754
+ var content = el('div', null, { flex:'1', padding:'22px' });
755
+ overlay.appendChild(content);
756
+ document.body.appendChild(overlay);
757
+
758
+ // ESC key close
759
+ document.addEventListener('keydown', function onEsc(e) {
760
+ if (e.key === 'Escape' && overlay.style.display !== 'none') {
761
+ overlay.style.display = 'none';
762
+ }
763
+ });
764
+
765
+ odRenderBackup(content);
766
+ }
767
+
768
+ /* ══════════════════════════════════════════════════════════
769
+ BOOT
770
+ ══════════════════════════════════════════════════════════ */
771
+ window.odRenderBackup = odRenderBackup;
772
+ window.odOpenBackupDashboard = odOpenBackupDashboard;
773
+
774
+ document.addEventListener('DOMContentLoaded', function () {
775
+ // Wire into a backup-pane if the shell gains one in future
776
+ var pane = document.getElementById('backup-pane');
777
+ if (pane) odRenderBackup(pane);
778
+ });
779
+
780
+ }());