superlocalmemory 3.8.0 → 3.8.2

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 (134) hide show
  1. package/CHANGELOG.md +112 -0
  2. package/README.md +32 -120
  3. package/package.json +9 -2
  4. package/plugin/.claude-plugin/plugin.json +1 -2
  5. package/plugin/CLAUDE.md +3 -3
  6. package/plugin/agents/slm-governance-advisor.md +1 -1
  7. package/plugin/agents/slm-loop-runner.md +1 -1
  8. package/plugin/agents/slm-memory-advisor.md +2 -2
  9. package/plugin/agents/slm-optimize-advisor.md +1 -1
  10. package/plugin/requirements.txt +1 -1
  11. package/plugin/skills/slm-cache/SKILL.md +1 -1
  12. package/plugin/skills/slm-compress/SKILL.md +1 -1
  13. package/plugin/skills/slm-governance/SKILL.md +1 -1
  14. package/plugin/skills/slm-graph/SKILL.md +1 -1
  15. package/plugin/skills/slm-loop/SKILL.md +1 -1
  16. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  17. package/plugin/skills/slm-profile/SKILL.md +1 -1
  18. package/plugin/skills/slm-recall/SKILL.md +3 -5
  19. package/plugin/skills/slm-remember/SKILL.md +1 -1
  20. package/plugin/skills/slm-scope/SKILL.md +1 -1
  21. package/plugin/skills/slm-session/SKILL.md +1 -1
  22. package/plugin/skills/slm-status/SKILL.md +1 -1
  23. package/plugin-src/rules/AGENTS.md +2 -1
  24. package/plugin-src/skills/slm-cache/SKILL.md +1 -1
  25. package/plugin-src/skills/slm-compress/SKILL.md +1 -1
  26. package/plugin-src/skills/slm-graph/SKILL.md +1 -1
  27. package/plugin-src/skills/slm-recall/SKILL.md +3 -5
  28. package/plugin-src/skills/slm-remember/SKILL.md +1 -1
  29. package/plugin-src/skills/slm-session/SKILL.md +1 -1
  30. package/plugin-src/skills/slm-status/SKILL.md +1 -1
  31. package/pyproject.toml +2 -1
  32. package/scripts/postinstall.js +7 -1
  33. package/src/superlocalmemory/__init__.py +1 -1
  34. package/src/superlocalmemory/cli/commands.py +494 -9
  35. package/src/superlocalmemory/cli/daemon.py +7 -0
  36. package/src/superlocalmemory/cli/loop_cmd.py +2 -7
  37. package/src/superlocalmemory/cli/main.py +72 -7
  38. package/src/superlocalmemory/cli/setup_wizard.py +142 -16
  39. package/src/superlocalmemory/cli/version_banner.py +17 -3
  40. package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
  41. package/src/superlocalmemory/core/component_healer.py +144 -0
  42. package/src/superlocalmemory/core/component_registry.py +487 -0
  43. package/src/superlocalmemory/core/config.py +21 -0
  44. package/src/superlocalmemory/core/embedding_worker.py +4 -5
  45. package/src/superlocalmemory/core/embeddings.py +132 -45
  46. package/src/superlocalmemory/core/engine.py +29 -22
  47. package/src/superlocalmemory/core/engine_ingestion.py +332 -45
  48. package/src/superlocalmemory/core/ingestion_command.py +154 -25
  49. package/src/superlocalmemory/core/injection.py +12 -7
  50. package/src/superlocalmemory/core/maintenance.py +43 -0
  51. package/src/superlocalmemory/core/maintenance_scheduler.py +44 -6
  52. package/src/superlocalmemory/core/recall_pipeline.py +42 -4
  53. package/src/superlocalmemory/core/store_pipeline.py +195 -20
  54. package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
  55. package/src/superlocalmemory/hooks/portable_kit.py +34 -2
  56. package/src/superlocalmemory/learning/model_rollback.py +3 -0
  57. package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
  58. package/src/superlocalmemory/learning/reward.py +50 -0
  59. package/src/superlocalmemory/learning/source_quality.py +523 -1
  60. package/src/superlocalmemory/loops/ledger.py +25 -5
  61. package/src/superlocalmemory/mcp/_daemon_proxy.py +6 -2
  62. package/src/superlocalmemory/mcp/_pool_adapter.py +4 -1
  63. package/src/superlocalmemory/mcp/server.py +11 -30
  64. package/src/superlocalmemory/mcp/tools_active.py +1 -1
  65. package/src/superlocalmemory/mcp/tools_core.py +21 -5
  66. package/src/superlocalmemory/mcp/tools_learning.py +2 -2
  67. package/src/superlocalmemory/retrieval/bridge_discovery.py +14 -0
  68. package/src/superlocalmemory/retrieval/engine.py +53 -21
  69. package/src/superlocalmemory/retrieval/reranker.py +3 -4
  70. package/src/superlocalmemory/retrieval/spreading_activation.py +68 -38
  71. package/src/superlocalmemory/server/config_file.py +90 -0
  72. package/src/superlocalmemory/server/origin.py +50 -0
  73. package/src/superlocalmemory/server/routes/backup.py +293 -70
  74. package/src/superlocalmemory/server/routes/behavioral.py +342 -61
  75. package/src/superlocalmemory/server/routes/brain.py +57 -16
  76. package/src/superlocalmemory/server/routes/config_api.py +84 -82
  77. package/src/superlocalmemory/server/routes/entity.py +100 -23
  78. package/src/superlocalmemory/server/routes/evolution.py +103 -100
  79. package/src/superlocalmemory/server/routes/learning.py +286 -105
  80. package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
  81. package/src/superlocalmemory/server/routes/memories.py +8 -3
  82. package/src/superlocalmemory/server/routes/mesh.py +121 -32
  83. package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
  84. package/src/superlocalmemory/server/routes/stats.py +93 -155
  85. package/src/superlocalmemory/server/routes/token.py +3 -13
  86. package/src/superlocalmemory/server/routes/v3_api.py +184 -20
  87. package/src/superlocalmemory/server/unified_daemon.py +732 -41
  88. package/src/superlocalmemory/storage/embedding_migrator.py +235 -0
  89. package/src/superlocalmemory/storage/migration_runner.py +79 -1
  90. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
  91. package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
  92. package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
  93. package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
  94. package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
  95. package/src/superlocalmemory/storage/schema.py +49 -1
  96. package/src/superlocalmemory/storage/schema_v32.py +2 -0
  97. package/src/superlocalmemory/storage/schema_v347.py +4 -0
  98. package/src/superlocalmemory/ui/index.html +6 -8
  99. package/src/superlocalmemory/ui/js/core.js +52 -9
  100. package/src/superlocalmemory/ui/js/dashboard.js +169 -82
  101. package/src/superlocalmemory/ui/js/od-backup.js +156 -65
  102. package/src/superlocalmemory/ui/js/od-brain.js +88 -51
  103. package/src/superlocalmemory/ui/js/od-components.js +147 -0
  104. package/src/superlocalmemory/ui/js/od-entities.js +65 -22
  105. package/src/superlocalmemory/ui/js/od-graph.js +46 -4
  106. package/src/superlocalmemory/ui/js/od-health.js +18 -0
  107. package/src/superlocalmemory/ui/js/od-memories.js +84 -5
  108. package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
  109. package/src/superlocalmemory/ui/js/od-operations.js +36 -0
  110. package/src/superlocalmemory/ui/js/od-settings.js +186 -63
  111. package/src/superlocalmemory/ui/js/od-shell.js +249 -33
  112. package/src/superlocalmemory/ui/js/od-skills.js +44 -17
  113. package/src/superlocalmemory/ui/js/settings.js +15 -1
  114. package/plugin-src/.mcp.json +0 -12
  115. package/plugin-src/agents/slm-governance-advisor.md +0 -80
  116. package/plugin-src/agents/slm-loop-runner.md +0 -71
  117. package/plugin-src/agents/slm-memory-advisor.md +0 -49
  118. package/plugin-src/agents/slm-optimize-advisor.md +0 -44
  119. package/plugin-src/commands/slm-loop.md +0 -31
  120. package/plugin-src/hooks/.gitkeep +0 -0
  121. package/plugin-src/hooks/hooks.json +0 -102
  122. package/plugin-src/manifest.json +0 -30
  123. package/plugin-src/requirements.txt +0 -1
  124. package/plugin-src/rules/CLAUDE.md.fragment +0 -44
  125. package/plugin-src/scripts/ensure-venv.bat +0 -122
  126. package/plugin-src/scripts/ensure-venv.sh +0 -105
  127. package/plugin-src/scripts/slm-launch +0 -62
  128. package/plugin-src/scripts/slm-launch.bat +0 -23
  129. package/plugin-src/settings.json +0 -25
  130. package/plugin-src/skills/slm-governance/SKILL.md +0 -248
  131. package/plugin-src/skills/slm-loop/SKILL.md +0 -99
  132. package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
  133. package/plugin-src/skills/slm-profile/SKILL.md +0 -148
  134. package/plugin-src/skills/slm-scope/SKILL.md +0 -176
@@ -11,7 +11,7 @@
11
11
  * POST /api/backup/connect/github body: {pat, repo_name}
12
12
  * DELETE /api/backup/disconnect/{id}
13
13
  * POST /api/backup/sync
14
- * GET /api/backup/export → FileResponse (.db.gz download)
14
+ * POST /api/backup/export → FileResponse (.db.gz download)
15
15
  * GET /api/backup/oauth/github/start → OAuth redirect or PAT form
16
16
  * GET /api/backup/oauth/google/start → Google OAuth redirect
17
17
  * No mock / seed data — every render goes to the live daemon.
@@ -22,6 +22,54 @@
22
22
 
23
23
  var OVERLAY_ID = 'od-backup-overlay';
24
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
+ }
25
73
 
26
74
  /* ── Tiny utilities ──────────────────────────────────────── */
27
75
  function esc(s) {
@@ -142,12 +190,12 @@
142
190
  // Title + badge + sub
143
191
  var titleArea = el('div', null, { flex:'1', minWidth:'200px' });
144
192
  var titleRow = el('div', null, { display:'flex', alignItems:'center', gap:'10px' });
145
- var titleEl = el('h3', { id: P + '-cloud-title', text:'Personal cloud connected' },
193
+ var titleEl = el('h3', { id: P + '-cloud-title', text:'Checking cloud backup' },
146
194
  { fontSize:'17px', margin:'0' });
147
195
  titleRow.appendChild(titleEl);
148
196
  var syncBadge = el('span', { id: P + '-cloud-badge' });
149
- syncBadge.className = 'badge ok';
150
- syncBadge.innerHTML = '<span class="dot"></span> Synced';
197
+ syncBadge.className = 'badge neutral';
198
+ syncBadge.innerHTML = '<span class="dot"></span> Checking';
151
199
  titleRow.appendChild(syncBadge);
152
200
  titleArea.appendChild(titleRow);
153
201
  var subEl = el('p', { id: P + '-cloud-sub', text:'Connecting to your cloud…' },
@@ -155,12 +203,12 @@
155
203
  titleArea.appendChild(subEl);
156
204
  inner.appendChild(titleArea);
157
205
 
158
- // Stats: Last backup / Next scheduled / Encryption
206
+ // Stats: Last backup / Next scheduled / on-disk representation
159
207
  var statsRow = el('div', null, { display:'flex', gap:'26px', flexWrap:'wrap' });
160
208
  [
161
209
  { id:'hero-last', lbl:'Last backup' },
162
210
  { id:'hero-next', lbl:'Next scheduled' },
163
- { id:'hero-encrypt', lbl:'Encryption' }
211
+ { id:'hero-encrypt', lbl:'Backup format' }
164
212
  ].forEach(function (s) {
165
213
  var item = el('div');
166
214
  item.appendChild(el('div', { text: s.lbl },
@@ -188,7 +236,7 @@
188
236
  exportBtn.className = 'btn ghost';
189
237
  exportBtn.textContent = 'Export .db.gz';
190
238
  exportBtn.addEventListener('click', function () {
191
- window.location.href = '/api/backup/export';
239
+ doExport(exportBtn);
192
240
  });
193
241
  btnGroup.appendChild(exportBtn);
194
242
  inner.appendChild(btnGroup);
@@ -203,9 +251,9 @@
203
251
  function buildKPIs() {
204
252
  var kpis = [
205
253
  { id:'kpi-total', lbl:'Total backups', icon:'cloud' },
206
- { id:'kpi-size', lbl:'Cloud storage used', icon:'memories' },
254
+ { id:'kpi-size', lbl:'Local storage used', icon:'memories' },
207
255
  { id:'kpi-retain',lbl:'Retention', icon:'clock' },
208
- { id:'kpi-count', lbl:'Restore points', icon:'shield' }
256
+ { id:'kpi-count', lbl:'Local snapshots', icon:'shield' }
209
257
  ];
210
258
  var strip = el('div', null, { marginBottom:'16px' });
211
259
  strip.className = 'kpi-strip';
@@ -277,7 +325,9 @@
277
325
  var frag = document.createDocumentFragment();
278
326
  PROVIDERS.forEach(function (prov) {
279
327
  var dest = destinations.filter(function (d) {
280
- return d.type === prov.id || d.provider === prov.id;
328
+ var destinationType = d.destination_type || d.type || d.provider;
329
+ return destinationType === prov.id ||
330
+ (prov.id === 'google' && destinationType === 'google_drive');
281
331
  })[0];
282
332
  frag.appendChild(buildConnRow(root, prov, dest || null));
283
333
  });
@@ -303,9 +353,18 @@
303
353
  detailEl.className = 'dim';
304
354
  Object.assign(detailEl.style, { fontSize:'12.5px' });
305
355
  if (dest) {
306
- var repoOrBucket = dest.repo || dest.bucket || dest.folder_id || '';
307
- detailEl.innerHTML = 'Connected' +
308
- (repoOrBucket ? ' · <span class="mono">' + esc(repoOrBucket) + '</span>' : '');
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 : '');
309
368
  } else {
310
369
  detailEl.textContent = prov.hint;
311
370
  }
@@ -317,9 +376,9 @@
317
376
  if (dest) {
318
377
  var syncBtn = el('button', { type:'button' });
319
378
  syncBtn.className = 'btn sm ghost';
320
- syncBtn.textContent = 'Sync';
379
+ syncBtn.textContent = 'Sync all';
321
380
  syncBtn.addEventListener('click', function () {
322
- doSync(root, dest.id || dest.dest_id || prov.id);
381
+ doSync(root);
323
382
  });
324
383
  btnWrap.appendChild(syncBtn);
325
384
 
@@ -348,7 +407,7 @@
348
407
  Design: .bk-ctl rows with .switch toggles; Schedule at bottom
349
408
  ══════════════════════════════════════════════════════════ */
350
409
  function buildScope() {
351
- var c = card('Backup scope', 'choose what mirrors to the cloud', 'operations');
410
+ var c = card('Backup scope', 'current managed database set', 'operations');
352
411
 
353
412
  // Scope items — matching design's 4 items with toggle switches
354
413
  var scopeItems = [
@@ -356,10 +415,10 @@
356
415
  sub:'<span class="mono">memory.db</span> · facts & conversations', on: true },
357
416
  { id:'scope-learn', lbl:'Learning data',
358
417
  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 }
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 }
363
422
  ];
364
423
 
365
424
  scopeItems.forEach(function (item) {
@@ -379,11 +438,8 @@
379
438
  sw.className = 'switch' + (item.on ? ' on' : '');
380
439
  sw.setAttribute('role', 'switch');
381
440
  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
- });
441
+ sw.disabled = true;
442
+ sw.title = 'Managed database scope is fixed in this release';
387
443
  row.appendChild(sw);
388
444
  c.body.appendChild(row);
389
445
  });
@@ -405,11 +461,11 @@
405
461
  var tb = document.getElementById(P + '-seg-' + t.toLowerCase());
406
462
  if (tb) tb.className = (t === s ? 'active' : '');
407
463
  });
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(); })
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(); })
413
469
  .then(function () { toast(s + ' schedule saved'); })
414
470
  .catch(function () { toast('Schedule save failed', true); });
415
471
  });
@@ -423,8 +479,7 @@
423
479
 
424
480
  /* ══════════════════════════════════════════════════════════
425
481
  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
482
+ Design: .tbl class, columns When/Scope/Size/Destination/Status
428
483
  ══════════════════════════════════════════════════════════ */
429
484
  // Map backup type from API to a human-readable scope string
430
485
  function fmtScope(bk) {
@@ -445,7 +500,7 @@
445
500
  var _histFilter = 'all'; // all | cloud | local
446
501
 
447
502
  function buildHistory() {
448
- var c = card('Backup history', 'every snapshot is restorable', 'health');
503
+ var c = card('Backup history', 'local snapshots and upload evidence', 'health');
449
504
 
450
505
  // Add filter segment to card-head (All / Cloud / Local)
451
506
  var spacer = el('div');
@@ -525,15 +580,9 @@
525
580
  statusBadge.innerHTML = '<span class="dot"></span>' + esc(statusTxt);
526
581
  tdStatus.appendChild(statusBadge);
527
582
  tr.appendChild(tdStatus);
528
- // Restore button
583
+ // Restore is intentionally absent until the dashboard has a guarded,
584
+ // tested restore endpoint and a daemon-safe restart workflow.
529
585
  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
586
  tr.appendChild(tdAct);
538
587
  tbody.appendChild(tr);
539
588
  });
@@ -547,7 +596,7 @@
547
596
  ══════════════════════════════════════════════════════════ */
548
597
  function doBackupNow(root) {
549
598
  var btn = q(root, 'btn-now'); if (btn) { btn.disabled = true; btn.textContent = 'Creating…'; }
550
- fetch('/api/backup/create', { method:'POST' })
599
+ authMutation('/api/backup/create', 'POST')
551
600
  .then(function (r) { return r.json(); })
552
601
  .then(function (d) {
553
602
  toast(d.success ? 'Backup created: ' + esc(d.filename || '') : 'Backup failed', !d.success);
@@ -560,17 +609,41 @@
560
609
  });
561
610
  }
562
611
 
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(); })
612
+ function doSync(root) {
613
+ authMutation('/api/backup/sync', 'POST')
614
+ .then(function (r) { return r.json(); })
567
615
  .then(function (d) { toast(d.success ? 'Sync started' : 'Sync failed: ' + esc(d.error || ''), !d.success); })
568
616
  .catch(function () { toast('Sync error', true); });
569
617
  }
570
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
+
571
644
  function doDisconnect(root, destId) {
572
645
  if (!window.confirm('Disconnect this cloud destination? Existing backups are not deleted.')) return;
573
- fetch('/api/backup/disconnect/' + encodeURIComponent(destId), { method:'DELETE' })
646
+ authMutation('/api/backup/disconnect/' + encodeURIComponent(destId), 'DELETE')
574
647
  .then(function (r) { return r.json(); })
575
648
  .then(function (d) {
576
649
  toast(d.success ? 'Disconnected' : 'Disconnect failed: ' + esc(d.error || ''), !d.success);
@@ -585,7 +658,7 @@
585
658
  var timer = setInterval(function () {
586
659
  if (!popup || popup.closed) {
587
660
  clearInterval(timer);
588
- // Reload destinations after OAuth popup closes
661
+ // A closed popup is not proof of success; refresh runtime truth only.
589
662
  loadDestinations(root, true);
590
663
  }
591
664
  }, 800);
@@ -602,29 +675,47 @@
602
675
  // Hero stats
603
676
  var lastStr = d.last_backup ? fmtDate(d.last_backup) : 'Never';
604
677
  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');
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');
608
683
  set(root, 'hero-next', next);
609
- set(root, 'hero-encrypt', 'AES-256');
684
+ set(root, 'hero-encrypt', 'Plain SQLite');
610
685
 
611
- // Hero title + badge: reflect connection state
686
+ // Hero title + badge: reflect configured and witnessed sync state.
612
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
+ });
613
694
  var titleEl = q(root, 'cloud-title');
614
- if (titleEl) titleEl.textContent = hasCloud ? 'Personal cloud connected' : 'No personal cloud';
695
+ if (titleEl) titleEl.textContent = hasCloud ? 'Cloud destination configured' : 'No cloud destination';
615
696
  var badgeEl = q(root, 'cloud-badge');
616
697
  if (badgeEl) {
617
- badgeEl.className = 'badge ' + (hasCloud ? 'ok' : 'warn');
618
- badgeEl.innerHTML = '<span class="dot"></span> ' + (hasCloud ? 'Synced' : 'Not connected');
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
+ ));
619
709
  }
620
710
  var subEl = q(root, 'cloud-sub');
621
711
  if (subEl) {
622
712
  if (hasCloud) {
623
713
  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.';
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.';
626
717
  } else {
627
- subEl.textContent = 'Connect a provider below to enable encrypted cloud mirroring.';
718
+ subEl.textContent = 'Local snapshots are plaintext SQLite. Connect a private provider only if you accept its access controls.';
628
719
  }
629
720
  }
630
721
  var orb = q(root, 'orb');
@@ -644,7 +735,7 @@
644
735
 
645
736
  // Sync schedule segment buttons
646
737
  var hours = d.interval_hours || 168;
647
- var active = hours <= 24 ? 'daily' : hours >= 9999 ? 'manual' : 'weekly';
738
+ var active = !d.enabled ? 'manual' : hours <= 24 ? 'daily' : 'weekly';
648
739
  ['daily','weekly','manual'].forEach(function (s) {
649
740
  var b = document.getElementById(P + '-seg-' + s);
650
741
  if (b) b.className = (s === active ? 'active' : '');
@@ -658,8 +749,8 @@
658
749
  var dests = d && d.destinations ? d.destinations : [];
659
750
  renderConnections(root, dests);
660
751
  if (forceReload) {
661
- toast(dests.length > 0 ? 'Connected successfully' : 'Connection complete');
662
- // Re-load status so hero reflects new connection state
752
+ toast('Destination status refreshed');
753
+ // Re-load status without inferring whether the popup succeeded.
663
754
  loadStatus(root);
664
755
  }
665
756
  }).catch(function () {});
@@ -694,9 +785,9 @@
694
785
  pageHead.className = 'page-head';
695
786
  pageHead.appendChild(el('h2', { text:'Backup & cloud sync' }));
696
787
  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.';
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.';
700
791
  pageHead.appendChild(desc);
701
792
  hub.appendChild(pageHead);
702
793