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.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +69 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +94 -10
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +8 -1
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +183 -69
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +384 -56
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- 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,'&').replace(/</g,'<')
|
|
30
|
+
.replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
|
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
|
+
}());
|