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