superlocalmemory 2.7.3 → 2.7.4

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.
@@ -0,0 +1,333 @@
1
+ /**
2
+ * SuperLocalMemory V2 - Feedback Module (v2.7.4)
3
+ * Copyright (c) 2026 Varun Pratap Bhardwaj
4
+ * Licensed under MIT License
5
+ *
6
+ * Collects implicit and explicit feedback signals from dashboard
7
+ * interactions. All data stays 100% local.
8
+ *
9
+ * Signals:
10
+ * thumbs_up - User clicks thumbs up on a memory card
11
+ * thumbs_down - User clicks thumbs down on a memory card
12
+ * pin - User pins/bookmarks a memory
13
+ * dwell_time - Time spent viewing memory detail modal
14
+ * search_click - User clicks a search result (positive for clicked)
15
+ */
16
+
17
+ // Module state
18
+ var feedbackState = {
19
+ lastSearchQuery: '',
20
+ modalOpenTime: null,
21
+ modalMemoryId: null,
22
+ searchResultIds: [],
23
+ };
24
+
25
+ /**
26
+ * Record explicit feedback (thumbs up/down, pin).
27
+ * Sends POST /api/feedback to backend.
28
+ */
29
+ function recordFeedback(memoryId, feedbackType, query) {
30
+ if (!memoryId || !feedbackType) return;
31
+
32
+ fetch('/api/feedback', {
33
+ method: 'POST',
34
+ headers: {'Content-Type': 'application/json'},
35
+ body: JSON.stringify({
36
+ memory_id: memoryId,
37
+ feedback_type: feedbackType,
38
+ query: query || feedbackState.lastSearchQuery || '',
39
+ }),
40
+ })
41
+ .then(function(r) { return r.json(); })
42
+ .then(function(data) {
43
+ if (data.success) {
44
+ showFeedbackToast(feedbackType, memoryId);
45
+ refreshFeedbackStats();
46
+ }
47
+ })
48
+ .catch(function() {
49
+ // Silent failure — feedback should never break UI
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Start tracking dwell time when a memory detail modal opens.
55
+ */
56
+ function startDwellTracking(memoryId) {
57
+ feedbackState.modalOpenTime = Date.now();
58
+ feedbackState.modalMemoryId = memoryId;
59
+ }
60
+
61
+ /**
62
+ * Stop tracking and record dwell time when modal closes.
63
+ */
64
+ function stopDwellTracking() {
65
+ if (!feedbackState.modalOpenTime || !feedbackState.modalMemoryId) return;
66
+
67
+ var dwellMs = Date.now() - feedbackState.modalOpenTime;
68
+ var dwellSeconds = dwellMs / 1000;
69
+ var memId = feedbackState.modalMemoryId;
70
+
71
+ feedbackState.modalOpenTime = null;
72
+ feedbackState.modalMemoryId = null;
73
+
74
+ // Only record if meaningful (>1s)
75
+ if (dwellSeconds < 1.0) return;
76
+
77
+ fetch('/api/feedback/dwell', {
78
+ method: 'POST',
79
+ headers: {'Content-Type': 'application/json'},
80
+ body: JSON.stringify({
81
+ memory_id: memId,
82
+ dwell_time: dwellSeconds,
83
+ query: feedbackState.lastSearchQuery || '',
84
+ }),
85
+ })
86
+ .then(function(r) { return r.json(); })
87
+ .catch(function() {
88
+ // Silent failure
89
+ });
90
+ }
91
+
92
+ /**
93
+ * Track which search results are displayed.
94
+ */
95
+ function trackSearchResults(query, resultIds) {
96
+ feedbackState.lastSearchQuery = query;
97
+ feedbackState.searchResultIds = resultIds || [];
98
+ }
99
+
100
+ /**
101
+ * Create feedback buttons (thumbs up/down + pin) for a memory card.
102
+ * Returns a DOM element containing the buttons.
103
+ * Uses safe DOM methods (no innerHTML with user data).
104
+ */
105
+ function createFeedbackButtons(memoryId, query) {
106
+ var container = document.createElement('div');
107
+ container.className = 'feedback-buttons d-flex gap-1 align-items-center';
108
+ container.setAttribute('role', 'group');
109
+ container.setAttribute('aria-label', 'Feedback for memory ' + memoryId);
110
+
111
+ // Thumbs up
112
+ var upBtn = document.createElement('button');
113
+ upBtn.className = 'btn btn-outline-success btn-sm feedback-btn';
114
+ var upIcon = document.createElement('i');
115
+ upIcon.className = 'bi bi-hand-thumbs-up';
116
+ upBtn.appendChild(upIcon);
117
+ upBtn.title = 'This memory was useful';
118
+ upBtn.setAttribute('aria-label', 'Mark as useful');
119
+ upBtn.onclick = function(e) {
120
+ e.stopPropagation();
121
+ recordFeedback(memoryId, 'thumbs_up', query);
122
+ upBtn.classList.remove('btn-outline-success');
123
+ upBtn.classList.add('btn-success');
124
+ upBtn.disabled = true;
125
+ downBtn.disabled = true;
126
+ };
127
+
128
+ // Thumbs down
129
+ var downBtn = document.createElement('button');
130
+ downBtn.className = 'btn btn-outline-danger btn-sm feedback-btn';
131
+ var downIcon = document.createElement('i');
132
+ downIcon.className = 'bi bi-hand-thumbs-down';
133
+ downBtn.appendChild(downIcon);
134
+ downBtn.title = 'Not useful';
135
+ downBtn.setAttribute('aria-label', 'Mark as not useful');
136
+ downBtn.onclick = function(e) {
137
+ e.stopPropagation();
138
+ recordFeedback(memoryId, 'thumbs_down', query);
139
+ downBtn.classList.remove('btn-outline-danger');
140
+ downBtn.classList.add('btn-danger');
141
+ upBtn.disabled = true;
142
+ downBtn.disabled = true;
143
+ };
144
+
145
+ // Pin/Bookmark
146
+ var pinBtn = document.createElement('button');
147
+ pinBtn.className = 'btn btn-outline-warning btn-sm feedback-btn';
148
+ var pinIcon = document.createElement('i');
149
+ pinIcon.className = 'bi bi-pin-angle';
150
+ pinBtn.appendChild(pinIcon);
151
+ pinBtn.title = 'Pin this memory';
152
+ pinBtn.setAttribute('aria-label', 'Pin memory');
153
+ pinBtn.onclick = function(e) {
154
+ e.stopPropagation();
155
+ recordFeedback(memoryId, 'pin', query);
156
+ pinBtn.classList.remove('btn-outline-warning');
157
+ pinBtn.classList.add('btn-warning');
158
+ pinBtn.disabled = true;
159
+ };
160
+
161
+ container.appendChild(upBtn);
162
+ container.appendChild(downBtn);
163
+ container.appendChild(pinBtn);
164
+ return container;
165
+ }
166
+
167
+ /**
168
+ * Show a small toast notification after feedback.
169
+ */
170
+ function showFeedbackToast(type, memoryId) {
171
+ var messages = {
172
+ thumbs_up: 'Thanks! This helps improve future results.',
173
+ thumbs_down: 'Noted. Results will improve over time.',
174
+ pin: 'Memory pinned!',
175
+ };
176
+ var msg = messages[type] || 'Feedback recorded';
177
+
178
+ // Use existing showToast if available
179
+ if (typeof showToast === 'function') {
180
+ showToast(msg, 'success');
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Fetch and render feedback stats (progress bar, signal count).
186
+ */
187
+ function refreshFeedbackStats() {
188
+ fetch('/api/feedback/stats')
189
+ .then(function(r) { return r.json(); })
190
+ .then(function(data) {
191
+ renderFeedbackProgress(data);
192
+ })
193
+ .catch(function() {});
194
+ }
195
+
196
+ /**
197
+ * Render the feedback progress bar using safe DOM methods.
198
+ */
199
+ function renderFeedbackProgress(stats) {
200
+ var container = document.getElementById('feedback-progress');
201
+ if (!container) return;
202
+
203
+ var total = stats.total_signals || 0;
204
+ var phase = stats.ranking_phase || 'baseline';
205
+ var progress = stats.progress || 0;
206
+ var target = stats.target || 200;
207
+
208
+ var phaseLabels = {
209
+ baseline: 'Baseline (collecting data)',
210
+ rule_based: 'Rule-Based (learning your preferences)',
211
+ ml_model: 'ML-Powered (fully personalized)',
212
+ };
213
+ var phaseColors = {
214
+ baseline: 'bg-secondary',
215
+ rule_based: 'bg-info',
216
+ ml_model: 'bg-success',
217
+ };
218
+
219
+ // Clear container
220
+ while (container.firstChild) container.removeChild(container.firstChild);
221
+
222
+ // Header row
223
+ var headerRow = document.createElement('div');
224
+ headerRow.className = 'd-flex justify-content-between align-items-center mb-1';
225
+
226
+ var label = document.createElement('small');
227
+ label.className = 'text-muted';
228
+ var icon = document.createElement('i');
229
+ icon.className = 'bi bi-graph-up me-1';
230
+ label.appendChild(icon);
231
+ label.appendChild(document.createTextNode('Learning Progress'));
232
+
233
+ var badge = document.createElement('span');
234
+ badge.className = 'badge ' + (phaseColors[phase] || 'bg-secondary');
235
+ badge.textContent = phaseLabels[phase] || phase;
236
+
237
+ headerRow.appendChild(label);
238
+ headerRow.appendChild(badge);
239
+ container.appendChild(headerRow);
240
+
241
+ // Progress bar
242
+ var progressOuter = document.createElement('div');
243
+ progressOuter.className = 'progress';
244
+ progressOuter.style.height = '8px';
245
+
246
+ var progressBar = document.createElement('div');
247
+ progressBar.className = 'progress-bar ' + (phaseColors[phase] || 'bg-secondary');
248
+ progressBar.setAttribute('role', 'progressbar');
249
+ progressBar.style.width = progress + '%';
250
+ progressBar.setAttribute('aria-valuenow', String(total));
251
+ progressBar.setAttribute('aria-valuemin', '0');
252
+ progressBar.setAttribute('aria-valuemax', String(target));
253
+
254
+ progressOuter.appendChild(progressBar);
255
+ container.appendChild(progressOuter);
256
+
257
+ // Count text
258
+ var countText = document.createElement('small');
259
+ countText.className = 'text-muted';
260
+ countText.textContent = total + '/' + target + ' signals';
261
+ container.appendChild(countText);
262
+ }
263
+
264
+ /**
265
+ * Create the privacy notice banner using safe DOM methods.
266
+ */
267
+ function createPrivacyNotice() {
268
+ var container = document.getElementById('privacy-notice');
269
+ if (!container) return;
270
+
271
+ var alert = document.createElement('div');
272
+ alert.className = 'alert alert-light border d-flex align-items-center py-2 mb-3';
273
+ alert.setAttribute('role', 'alert');
274
+
275
+ var lockIcon = document.createElement('i');
276
+ lockIcon.className = 'bi bi-shield-lock me-2 text-success';
277
+
278
+ var text = document.createElement('small');
279
+ text.appendChild(document.createTextNode('Learning is 100% local. Your behavioral data never leaves this machine. '));
280
+
281
+ var link = document.createElement('a');
282
+ link.href = '#';
283
+ link.className = 'ms-1';
284
+ link.textContent = 'Learn more';
285
+ link.onclick = function(e) {
286
+ e.preventDefault();
287
+ showPrivacyDetails();
288
+ };
289
+ text.appendChild(link);
290
+
291
+ alert.appendChild(lockIcon);
292
+ alert.appendChild(text);
293
+ container.appendChild(alert);
294
+ }
295
+
296
+ /**
297
+ * Show privacy details in an alert (safe, no raw HTML injection).
298
+ */
299
+ function showPrivacyDetails() {
300
+ var info = 'What data is collected:\n' +
301
+ '- Which memories you find useful (thumbs up/down)\n' +
302
+ '- Time spent viewing memory details\n' +
303
+ '- Search patterns (queries are hashed, never stored as raw text)\n\n' +
304
+ 'Where it is stored:\n' +
305
+ '~/.claude-memory/learning.db (local SQLite file)\n\n' +
306
+ 'How to delete it:\n' +
307
+ 'Use "Reset Learning Data" in Settings, or run:\n' +
308
+ 'rm ~/.claude-memory/learning.db';
309
+ alert(info);
310
+ }
311
+
312
+ /**
313
+ * Reset all learning data.
314
+ */
315
+ function resetLearningData() {
316
+ if (!confirm('Reset all learning data? Your memories will be preserved.')) return;
317
+
318
+ fetch('/api/learning/reset', {method: 'POST'})
319
+ .then(function(r) { return r.json(); })
320
+ .then(function(data) {
321
+ if (data.success) {
322
+ if (typeof showToast === 'function') showToast('Learning data reset', 'success');
323
+ refreshFeedbackStats();
324
+ }
325
+ })
326
+ .catch(function() {});
327
+ }
328
+
329
+ // Initialize: load stats on page ready
330
+ document.addEventListener('DOMContentLoaded', function() {
331
+ createPrivacyNotice();
332
+ refreshFeedbackStats();
333
+ });
package/ui/js/learning.js CHANGED
@@ -24,12 +24,129 @@ function renderLearningStatus(data) {
24
24
  renderFeedbackCount(data.stats);
25
25
  renderEngagementHealth(data.engagement);
26
26
  renderProgressBar(data.stats ? data.stats.feedback_count : 0);
27
+ renderWhatWeLearned(data);
27
28
  renderTechPreferences(data.tech_preferences || []);
28
29
  renderWorkflowPatterns(data.workflow_patterns || []);
29
30
  renderSourceQuality(data.source_scores || {});
30
31
  renderPrivacyStats(data.stats);
31
32
  }
32
33
 
34
+ function renderWhatWeLearned(data) {
35
+ var container = document.getElementById('what-we-learned-content');
36
+ var profileBadge = document.getElementById('learned-profile-badge');
37
+ if (!container) return;
38
+ container.textContent = '';
39
+
40
+ // Show active profile
41
+ if (profileBadge && data.stats && data.stats.active_profile) {
42
+ profileBadge.textContent = data.stats.active_profile;
43
+ }
44
+
45
+ var insights = [];
46
+
47
+ // Collect tech preference insights
48
+ var techPrefs = data.tech_preferences || [];
49
+ var highConfTech = techPrefs.filter(function(p) { return p.confidence >= 0.6; });
50
+ if (highConfTech.length > 0) {
51
+ var techNames = highConfTech.map(function(p) { return p.value; }).slice(0, 5);
52
+ insights.push({
53
+ icon: 'bi-cpu',
54
+ color: 'text-primary',
55
+ text: 'You prefer: ' + techNames.join(', '),
56
+ detail: highConfTech.length + ' tech preferences learned',
57
+ });
58
+ }
59
+
60
+ // Collect workflow insights
61
+ var workflows = data.workflow_patterns || [];
62
+ var sequences = workflows.filter(function(w) { return w.type === 'sequence'; });
63
+ var temporal = workflows.filter(function(w) { return w.type === 'temporal'; });
64
+ if (sequences.length > 0) {
65
+ insights.push({
66
+ icon: 'bi-diagram-3',
67
+ color: 'text-info',
68
+ text: sequences.length + ' workflow sequence' + (sequences.length > 1 ? 's' : '') + ' detected',
69
+ detail: 'Common patterns in how you work',
70
+ });
71
+ }
72
+ if (temporal.length > 0) {
73
+ insights.push({
74
+ icon: 'bi-clock',
75
+ color: 'text-warning',
76
+ text: temporal.length + ' time-based pattern' + (temporal.length > 1 ? 's' : '') + ' found',
77
+ detail: 'When you tend to work on what',
78
+ });
79
+ }
80
+
81
+ // Source quality insights
82
+ var sources = data.source_scores || {};
83
+ var sourceCount = Object.keys(sources).length;
84
+ if (sourceCount > 0) {
85
+ var bestSource = Object.entries(sources).sort(function(a, b) { return b[1] - a[1]; })[0];
86
+ insights.push({
87
+ icon: 'bi-trophy',
88
+ color: 'text-success',
89
+ text: 'Best source: ' + bestSource[0] + ' (' + Math.round(bestSource[1] * 100) + '% quality)',
90
+ detail: sourceCount + ' sources tracked',
91
+ });
92
+ }
93
+
94
+ // Feedback volume insight
95
+ var feedbackCount = (data.stats && data.stats.feedback_count) || 0;
96
+ if (feedbackCount > 0) {
97
+ var phase = data.ranking_phase || 'baseline';
98
+ var phaseLabel = {baseline: 'collecting data', rule_based: 'learning your preferences', ml_model: 'fully personalized'}[phase] || phase;
99
+ insights.push({
100
+ icon: 'bi-graph-up',
101
+ color: 'text-success',
102
+ text: feedbackCount + ' feedback signals collected',
103
+ detail: 'Currently ' + phaseLabel,
104
+ });
105
+ }
106
+
107
+ // Render insights
108
+ if (insights.length === 0) {
109
+ var empty = document.createElement('div');
110
+ empty.className = 'text-center text-muted py-3';
111
+ var emptyIcon = document.createElement('i');
112
+ emptyIcon.className = 'bi bi-lightbulb';
113
+ emptyIcon.style.fontSize = '2rem';
114
+ empty.appendChild(emptyIcon);
115
+ var emptyText = document.createElement('p');
116
+ emptyText.className = 'mt-2 mb-0 small';
117
+ emptyText.textContent = 'Start using recall and giving feedback. SuperLocalMemory will learn your preferences automatically.';
118
+ empty.appendChild(emptyText);
119
+ container.appendChild(empty);
120
+ return;
121
+ }
122
+
123
+ for (var i = 0; i < insights.length; i++) {
124
+ var insight = insights[i];
125
+ var row = document.createElement('div');
126
+ row.className = 'd-flex align-items-start mb-2 p-2 rounded';
127
+ row.style.backgroundColor = 'var(--bs-body-bg)';
128
+
129
+ var icon = document.createElement('i');
130
+ icon.className = 'bi ' + insight.icon + ' ' + insight.color + ' me-3';
131
+ icon.style.fontSize = '1.3rem';
132
+ icon.style.marginTop = '2px';
133
+
134
+ var textDiv = document.createElement('div');
135
+ var mainText = document.createElement('div');
136
+ mainText.className = 'fw-semibold';
137
+ mainText.textContent = insight.text;
138
+ var detailText = document.createElement('small');
139
+ detailText.className = 'text-muted';
140
+ detailText.textContent = insight.detail;
141
+ textDiv.appendChild(mainText);
142
+ textDiv.appendChild(detailText);
143
+
144
+ row.appendChild(icon);
145
+ row.appendChild(textDiv);
146
+ container.appendChild(row);
147
+ }
148
+ }
149
+
33
150
  function renderPhase(data) {
34
151
  var phaseEl = document.getElementById('learning-phase');
35
152
  var phaseDetail = document.getElementById('learning-phase-detail');
package/ui/js/modal.js CHANGED
@@ -142,9 +142,26 @@ function openMemoryDetail(mem) {
142
142
  body.appendChild(actionsDiv);
143
143
  }
144
144
 
145
+ // v2.7.4: Add feedback buttons to modal body
146
+ if (typeof createFeedbackButtons === 'function' && mem && mem.id) {
147
+ var feedbackDiv = document.createElement('div');
148
+ feedbackDiv.className = 'mt-3 pt-2 border-top';
149
+ var feedbackLabel = document.createElement('small');
150
+ feedbackLabel.className = 'text-muted d-block mb-1';
151
+ feedbackLabel.textContent = 'Was this memory useful?';
152
+ feedbackDiv.appendChild(feedbackLabel);
153
+ feedbackDiv.appendChild(createFeedbackButtons(mem.id));
154
+ body.appendChild(feedbackDiv);
155
+ }
156
+
145
157
  var modalEl = document.getElementById('memoryDetailModal');
146
158
  var modal = new bootstrap.Modal(modalEl);
147
159
 
160
+ // v2.7.4: Start dwell time tracking
161
+ if (typeof startDwellTracking === 'function' && mem && mem.id) {
162
+ startDwellTracking(mem.id);
163
+ }
164
+
148
165
  // Focus first interactive element when modal opens
149
166
  modalEl.addEventListener('shown.bs.modal', function() {
150
167
  const firstButton = modalEl.querySelector('button, a[href]');
@@ -153,8 +170,12 @@ function openMemoryDetail(mem) {
153
170
  }
154
171
  }, { once: true });
155
172
 
156
- // Return focus when modal closes
173
+ // Return focus when modal closes + stop dwell tracking
157
174
  modalEl.addEventListener('hidden.bs.modal', function() {
175
+ // v2.7.4: Stop dwell time tracking
176
+ if (typeof stopDwellTracking === 'function') {
177
+ stopDwellTracking();
178
+ }
158
179
  if (window.lastFocusedElement && typeof window.lastFocusedElement.focus === 'function') {
159
180
  window.lastFocusedElement.focus();
160
181
  window.lastFocusedElement = null;
package/ui/js/profiles.js CHANGED
@@ -190,6 +190,14 @@ async function switchProfile(profileName) {
190
190
  loadStats();
191
191
  loadGraph();
192
192
  loadProfilesTable();
193
+ // v2.7.4: Reload ALL tabs for new profile
194
+ if (typeof loadLearning === 'function') loadLearning();
195
+ if (typeof refreshFeedbackStats === 'function') refreshFeedbackStats();
196
+ if (typeof loadLearningDataStats === 'function') loadLearningDataStats();
197
+ if (typeof loadAgents === 'function') loadAgents();
198
+ if (typeof loadMemories === 'function') loadMemories();
199
+ if (typeof loadTimeline === 'function') loadTimeline();
200
+ if (typeof loadEvents === 'function') loadEvents();
193
201
  var activeTab = document.querySelector('#mainTabs .nav-link.active');
194
202
  if (activeTab) activeTab.click();
195
203
  } else {
package/ui/js/settings.js CHANGED
@@ -5,6 +5,61 @@ async function loadSettings() {
5
5
  loadProfilesTable();
6
6
  loadBackupStatus();
7
7
  loadBackupList();
8
+ loadLearningDataStats();
9
+ }
10
+
11
+ async function loadLearningDataStats() {
12
+ try {
13
+ var response = await fetch('/api/feedback/stats');
14
+ var data = await response.json();
15
+ var container = document.getElementById('learning-data-stats');
16
+ if (!container) return;
17
+
18
+ container.textContent = '';
19
+ var row = document.createElement('div');
20
+ row.className = 'row g-2';
21
+
22
+ var stats = [
23
+ { value: String(data.total_signals || 0), label: 'Feedback Signals' },
24
+ { value: data.ranking_phase || 'baseline', label: 'Ranking Phase' },
25
+ { value: Math.round(data.progress || 0) + '%', label: 'ML Progress' },
26
+ ];
27
+
28
+ stats.forEach(function(s) {
29
+ var col = document.createElement('div');
30
+ col.className = 'col-4';
31
+ var stat = document.createElement('div');
32
+ stat.className = 'backup-stat';
33
+ var val = document.createElement('div');
34
+ val.className = 'value';
35
+ val.textContent = s.value;
36
+ var lbl = document.createElement('div');
37
+ lbl.className = 'label';
38
+ lbl.textContent = s.label;
39
+ stat.appendChild(val);
40
+ stat.appendChild(lbl);
41
+ col.appendChild(stat);
42
+ row.appendChild(col);
43
+ });
44
+ container.appendChild(row);
45
+ } catch (error) {
46
+ // Silent — learning stats are optional
47
+ }
48
+ }
49
+
50
+ async function backupLearningDb() {
51
+ try {
52
+ var response = await fetch('/api/learning/backup', { method: 'POST' });
53
+ var data = await response.json();
54
+ if (data.success) {
55
+ showToast('Learning DB backed up: ' + (data.filename || 'learning.db.bak'));
56
+ } else {
57
+ showToast('Backup created at ~/.claude-memory/learning.db.bak');
58
+ }
59
+ } catch (error) {
60
+ // Fallback: just tell user the manual path
61
+ showToast('Manual backup: cp ~/.claude-memory/learning.db ~/.claude-memory/learning.db.bak');
62
+ }
8
63
  }
9
64
 
10
65
  async function loadBackupStatus() {
@@ -92,7 +147,9 @@ async function saveBackupConfig() {
92
147
  })
93
148
  });
94
149
  var data = await response.json();
95
- renderBackupStatus(data);
150
+ // API wraps status inside data.status on configure response
151
+ var status = data.status || data;
152
+ renderBackupStatus(status);
96
153
  showToast('Backup settings saved');
97
154
  } catch (error) {
98
155
  console.error('Error saving backup config:', error);