main
js 872 lines 27 KB
Raw
1 import { createStore } from "/js/AlpineStore.js";
2 import {
3 formatDateTime,
4 getCurrentUserDateString,
5 getCurrentUserISOString,
6 getUserHour12,
7 getUserTimezone,
8 } from "/js/time-utils.js";
9
10 // Global function references
11 const sendJsonData = globalThis.sendJsonData;
12 const toast = globalThis.toast;
13 const fetchApi = globalThis.fetchApi;
14
15 // ⚠️ CRITICAL: The .env file contains API keys and essential configuration.
16 // This file is REQUIRED for Agent Zero to function and must be backed up.
17
18 const model = {
19 // State
20 mode: 'backup', // 'backup' or 'restore'
21 loading: false,
22 loadingMessage: '',
23 error: '',
24
25 // File operations log (shared between backup and restore)
26 fileOperationsLog: '',
27
28 // Backup state
29 backupMetadataConfig: null,
30 includeHidden: false,
31 previewStats: { total: 0, truncated: false },
32 backupEditor: null,
33
34 // Enhanced file preview state
35 previewMode: 'grouped', // 'grouped' or 'flat'
36 previewFiles: [],
37 previewGroups: [],
38 filteredPreviewFiles: [],
39 fileSearchFilter: '',
40 expandedGroups: new Set(),
41
42 // Progress state
43 progressData: null,
44 progressEventSource: null,
45
46 // Restore state
47 backupFile: null,
48 backupMetadata: null,
49 restorePatterns: '',
50 overwritePolicy: 'overwrite',
51 cleanBeforeRestore: false,
52 restoreEditor: null,
53 restoreResult: null,
54
55 // Initialization
56 async initBackup() {
57 this.mode = 'backup';
58 this.resetState();
59 await this.initBackupEditor();
60 await this.updatePreview();
61 },
62
63 async initRestore() {
64 this.mode = 'restore';
65 this.resetState();
66 await this.initRestoreEditor();
67 },
68
69 resetState() {
70 this.loading = false;
71 this.error = '';
72 this.backupFile = null;
73 this.backupMetadata = null;
74 this.restoreResult = null;
75 this.fileOperationsLog = '';
76 },
77
78 // File operations logging
79 addFileOperation(message) {
80 const timestamp = new Intl.DateTimeFormat(undefined, {
81 timeStyle: "medium",
82 hour12: getUserHour12(),
83 timeZone: getUserTimezone(),
84 }).format(new Date());
85 this.fileOperationsLog += `[${timestamp}] ${message}\n`;
86
87 // Auto-scroll to bottom - use setTimeout since $nextTick is not available in stores
88 setTimeout(() => {
89 const textarea = document.getElementById(this.mode === 'backup' ? 'backup-file-list' : 'restore-file-list');
90 if (textarea) {
91 textarea.scrollTop = textarea.scrollHeight;
92 }
93 }, 0);
94 },
95
96 clearFileOperations() {
97 this.fileOperationsLog = '';
98 },
99
100 createDownloadToastGroup(prefix) {
101 return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
102 },
103
104 showDownloadPreparingToast(group) {
105 window.toastFrontendInfo?.("Preparing download...", "Download", 0, group, undefined, true);
106 },
107
108 showDownloadStartedToast(group) {
109 window.toastFrontendInfo?.("Downloading...", "Download", 3, group, undefined, true);
110 },
111
112 showDownloadErrorToast(group, message) {
113 window.toastFrontendError?.(message || "Download failed", "Download Error", 8, group, undefined, true);
114 },
115
116 // Cleanup method for modal close
117 onClose() {
118 this.resetState();
119 if (this.backupEditor) {
120 this.backupEditor.destroy();
121 this.backupEditor = null;
122 }
123 if (this.restoreEditor) {
124 this.restoreEditor.destroy();
125 this.restoreEditor = null;
126 }
127 },
128
129 // Get default backup metadata with resolved patterns from backend
130 async getDefaultBackupMetadata() {
131 const timestamp = getCurrentUserISOString();
132
133 try {
134 // Get resolved default patterns from backend
135 const response = await sendJsonData("backup_get_defaults", {});
136
137 if (response.success) {
138 // Use patterns from backend with resolved absolute paths
139 const include_patterns = response.default_patterns.include_patterns;
140 const exclude_patterns = response.default_patterns.exclude_patterns;
141
142 return {
143 backup_name: `agent-zero-backup-${getCurrentUserDateString()}`,
144 include_hidden: true,
145 include_patterns: include_patterns,
146 exclude_patterns: exclude_patterns,
147 backup_config: {
148 compression_level: 6,
149 integrity_check: true
150 }
151 };
152 }
153 } catch (error) {
154 console.warn("Failed to get default patterns from backend, using fallback");
155 }
156
157 // Fallback patterns (will be overridden by backend on first use)
158 return {
159 backup_name: `agent-zero-backup-${timestamp.slice(0, 10)}`,
160 include_hidden: true,
161 include_patterns: [
162 // These will be replaced with resolved absolute paths by backend
163 "# Loading default patterns from backend..."
164 ],
165 exclude_patterns: [],
166 backup_config: {
167 compression_level: 6,
168 integrity_check: true
169 }
170 };
171 },
172
173 // Editor Management - Following Agent Zero ACE editor patterns
174 async initBackupEditor() {
175 const container = document.getElementById("backup-metadata-editor");
176 if (container) {
177 const editor = ace.edit("backup-metadata-editor");
178
179 const dark = localStorage.getItem("darkMode");
180 if (dark != "false") {
181 editor.setTheme("ace/theme/github_dark");
182 } else {
183 editor.setTheme("ace/theme/tomorrow");
184 }
185
186 editor.session.setMode("ace/mode/json");
187
188 // Initialize with default backup metadata
189 const defaultMetadata = await this.getDefaultBackupMetadata();
190 editor.setValue(JSON.stringify(defaultMetadata, null, 2));
191 editor.clearSelection();
192
193 // Auto-update preview on changes (debounced)
194 let timeout;
195 editor.on('change', () => {
196 clearTimeout(timeout);
197 timeout = setTimeout(() => {
198 this.updatePreview();
199 }, 1000);
200 });
201
202 this.backupEditor = editor;
203 }
204 },
205
206 async initRestoreEditor() {
207 const container = document.getElementById("restore-metadata-editor");
208 if (container) {
209 const editor = ace.edit("restore-metadata-editor");
210
211 const dark = localStorage.getItem("darkMode");
212 if (dark != "false") {
213 editor.setTheme("ace/theme/github_dark");
214 } else {
215 editor.setTheme("ace/theme/tomorrow");
216 }
217
218 editor.session.setMode("ace/mode/json");
219 editor.setValue('{}');
220 editor.clearSelection();
221
222 // Auto-validate JSON on changes
223 editor.on('change', () => {
224 this.validateRestoreMetadata();
225 });
226
227 this.restoreEditor = editor;
228 }
229 },
230
231 // Unified editor value getter (following MCP servers pattern)
232 getEditorValue() {
233 const editor = this.mode === 'backup' ? this.backupEditor : this.restoreEditor;
234 return editor ? editor.getValue() : '{}';
235 },
236
237 // Unified JSON formatting (following MCP servers pattern)
238 formatJson() {
239 const editor = this.mode === 'backup' ? this.backupEditor : this.restoreEditor;
240 if (!editor) return;
241
242 try {
243 const currentContent = editor.getValue();
244 const parsed = JSON.parse(currentContent);
245 const formatted = JSON.stringify(parsed, null, 2);
246
247 editor.setValue(formatted);
248 editor.clearSelection();
249 editor.navigateFileStart();
250 } catch (error) {
251 console.error("Failed to format JSON:", error);
252 this.error = "Invalid JSON: " + error.message;
253 }
254 },
255
256 // Enhanced File Preview Operations
257 async updatePreview() {
258 try {
259 const metadataText = this.getEditorValue();
260 const metadata = JSON.parse(metadataText);
261
262 if (!metadata.include_patterns || metadata.include_patterns.length === 0) {
263 this.previewStats = { total: 0, truncated: false };
264 this.previewFiles = [];
265 this.previewGroups = [];
266 return;
267 }
268
269 // Convert patterns arrays back to string format for API
270 const patternsString = this.convertPatternsToString(metadata.include_patterns, metadata.exclude_patterns);
271
272 // Get grouped preview for better UX
273 const response = await sendJsonData("backup_preview_grouped", {
274 patterns: patternsString,
275 include_hidden: metadata.include_hidden ?? true,
276 max_depth: 3,
277 search_filter: this.fileSearchFilter
278 });
279
280 if (response.success) {
281 this.previewGroups = response.groups;
282 this.previewStats = response.stats;
283
284 // Flatten groups for flat view
285 this.previewFiles = [];
286 response.groups.forEach(group => {
287 this.previewFiles.push(...group.files);
288 });
289
290 this.applyFileSearch();
291 } else {
292 this.error = response.error;
293 }
294 } catch (error) {
295 this.error = `Preview error: ${error.message}`;
296 }
297 },
298
299 // Convert pattern arrays to string format for backend API
300 convertPatternsToString(includePatterns, excludePatterns) {
301 const patterns = [];
302
303 // Add include patterns
304 if (includePatterns) {
305 patterns.push(...includePatterns);
306 }
307
308 // Add exclude patterns with '!' prefix
309 if (excludePatterns) {
310 excludePatterns.forEach(pattern => {
311 patterns.push(`!${pattern}`);
312 });
313 }
314
315 return patterns.join('\n');
316 },
317
318 // Validation for backup metadata
319 validateBackupMetadata() {
320 try {
321 const metadataText = this.getEditorValue();
322 const metadata = JSON.parse(metadataText);
323
324 // Validate required fields
325 if (!Array.isArray(metadata.include_patterns)) {
326 throw new Error('include_patterns must be an array');
327 }
328 if (!Array.isArray(metadata.exclude_patterns)) {
329 throw new Error('exclude_patterns must be an array');
330 }
331 if (!metadata.backup_name || typeof metadata.backup_name !== 'string') {
332 throw new Error('backup_name must be a non-empty string');
333 }
334
335 this.backupMetadataConfig = metadata;
336 this.error = '';
337 return true;
338 } catch (error) {
339 this.error = `Invalid backup metadata: ${error.message}`;
340 return false;
341 }
342 },
343
344 // File Preview UI Management
345 initFilePreview() {
346 this.fileSearchFilter = '';
347 this.expandedGroups.clear();
348 this.previewMode = localStorage.getItem('backupPreviewMode') || 'grouped';
349 },
350
351 togglePreviewMode() {
352 this.previewMode = this.previewMode === 'grouped' ? 'flat' : 'grouped';
353 localStorage.setItem('backupPreviewMode', this.previewMode);
354 },
355
356 toggleGroup(groupPath) {
357 if (this.expandedGroups.has(groupPath)) {
358 this.expandedGroups.delete(groupPath);
359 } else {
360 this.expandedGroups.add(groupPath);
361 }
362 },
363
364 isGroupExpanded(groupPath) {
365 return this.expandedGroups.has(groupPath);
366 },
367
368 debounceFileSearch() {
369 clearTimeout(this.searchTimeout);
370 this.searchTimeout = setTimeout(() => {
371 this.applyFileSearch();
372 }, 300);
373 },
374
375 clearFileSearch() {
376 this.fileSearchFilter = '';
377 this.applyFileSearch();
378 },
379
380 applyFileSearch() {
381 if (!this.fileSearchFilter.trim()) {
382 this.filteredPreviewFiles = this.previewFiles;
383 } else {
384 const search = this.fileSearchFilter.toLowerCase();
385 this.filteredPreviewFiles = this.previewFiles.filter(file =>
386 file.path.toLowerCase().includes(search)
387 );
388 }
389 },
390
391 async exportFileList() {
392 const fileList = this.previewFiles.map(f => f.path).join('\n');
393 const blob = new Blob([fileList], { type: 'text/plain' });
394 const url = URL.createObjectURL(blob);
395 const a = document.createElement('a');
396 a.href = url;
397 a.download = 'backup-file-list.txt';
398 a.click();
399 URL.revokeObjectURL(url);
400 },
401
402 async copyFileListToClipboard() {
403 const fileList = this.previewFiles.map(f => f.path).join('\n');
404 try {
405 await navigator.clipboard.writeText(fileList);
406 window.toastFrontendInfo('File list copied to clipboard', 'Clipboard');
407 } catch (error) {
408 window.toastFrontendError('Failed to copy to clipboard', 'Clipboard Error');
409 }
410 },
411
412 // Backup Creation using direct API call
413 async createBackup() {
414 // Validate backup metadata first
415 if (!this.validateBackupMetadata()) {
416 return;
417 }
418
419 const downloadToastGroup = this.createDownloadToastGroup("backup-create");
420
421 try {
422 this.loading = true;
423 this.loadingMessage = 'Creating backup...';
424 this.error = '';
425 this.clearFileOperations();
426 this.addFileOperation('Starting backup creation...');
427 this.showDownloadPreparingToast(downloadToastGroup);
428
429 const metadata = this.backupMetadataConfig;
430
431 // Use fetch directly since backup_create returns a file download, not JSON
432 const response = await fetchApi('/backup_create', {
433 method: 'POST',
434 headers: { 'Content-Type': 'application/json' },
435 body: JSON.stringify({
436 include_patterns: metadata.include_patterns,
437 exclude_patterns: metadata.exclude_patterns,
438 include_hidden: metadata.include_hidden ?? true,
439 backup_name: metadata.backup_name
440 })
441 });
442
443 if (response.ok) {
444 // Handle file download
445 const blob = await response.blob();
446 const url = window.URL.createObjectURL(blob);
447 const a = document.createElement('a');
448 a.href = url;
449 a.download = `${metadata.backup_name}.zip`;
450 a.click();
451 window.URL.revokeObjectURL(url);
452
453 this.addFileOperation('Backup created and downloaded successfully!');
454 this.showDownloadStartedToast(downloadToastGroup);
455 } else {
456 // Try to parse error response
457 const errorText = await response.text();
458 try {
459 const errorJson = JSON.parse(errorText);
460 this.error = errorJson.error || 'Backup creation failed';
461 } catch {
462 this.error = `Backup creation failed: ${response.status} ${response.statusText}`;
463 }
464 this.addFileOperation(`Error: ${this.error}`);
465 this.showDownloadErrorToast(downloadToastGroup, this.error);
466 }
467
468 } catch (error) {
469 this.error = `Backup error: ${error.message}`;
470 this.addFileOperation(`Error: ${error.message}`);
471 this.showDownloadErrorToast(downloadToastGroup, this.error);
472 } finally {
473 this.loading = false;
474 }
475 },
476
477 async downloadBackup(backupPath, backupName) {
478 const downloadToastGroup = this.createDownloadToastGroup("backup-download");
479
480 try {
481 this.showDownloadPreparingToast(downloadToastGroup);
482 const response = await fetchApi('/backup_download', {
483 method: 'POST',
484 headers: { 'Content-Type': 'application/json' },
485 body: JSON.stringify({ backup_path: backupPath })
486 });
487
488 if (response.ok) {
489 const blob = await response.blob();
490 const url = window.URL.createObjectURL(blob);
491 const a = document.createElement('a');
492 a.href = url;
493 a.download = `${backupName}.zip`;
494 a.click();
495 window.URL.revokeObjectURL(url);
496 this.showDownloadStartedToast(downloadToastGroup);
497 } else {
498 const errorText = await response.text();
499 this.error = errorText || `Download failed: ${response.status}`;
500 this.showDownloadErrorToast(downloadToastGroup, this.error);
501 }
502 } catch (error) {
503 console.error('Download error:', error);
504 this.error = error.message || 'Download failed';
505 this.showDownloadErrorToast(downloadToastGroup, this.error);
506 }
507 },
508
509 cancelBackup() {
510 if (this.progressEventSource) {
511 this.progressEventSource.close();
512 this.progressEventSource = null;
513 }
514 this.loading = false;
515 this.progressData = null;
516 },
517
518 resetToDefaults() {
519 this.getDefaultBackupMetadata().then(defaultMetadata => {
520 if (this.backupEditor) {
521 this.backupEditor.setValue(JSON.stringify(defaultMetadata, null, 2));
522 this.backupEditor.clearSelection();
523 }
524 this.updatePreview();
525 });
526 },
527
528 // Dry run functionality
529 async dryRun() {
530 if (this.mode === 'backup') {
531 await this.dryRunBackup();
532 } else if (this.mode === 'restore') {
533 await this.dryRunRestore();
534 }
535 },
536
537 async dryRunBackup() {
538 // Validate backup metadata first
539 if (!this.validateBackupMetadata()) {
540 return;
541 }
542
543 try {
544 this.loading = true;
545 this.loadingMessage = 'Performing dry run...';
546 this.error = '';
547 this.clearFileOperations();
548 this.addFileOperation('Starting backup dry run...');
549
550 const metadata = this.backupMetadataConfig;
551 const patternsString = this.convertPatternsToString(metadata.include_patterns, metadata.exclude_patterns);
552
553 const response = await sendJsonData("backup_test", {
554 patterns: patternsString,
555 include_hidden: metadata.include_hidden ?? true,
556 max_files: 10000
557 });
558
559 if (response.success) {
560 this.addFileOperation(`Found ${response.files.length} files that would be backed up:`);
561 response.files.forEach((file, index) => {
562 this.addFileOperation(`${index + 1}. ${file.path} (${this.formatFileSize(file.size)})`);
563 });
564 this.addFileOperation(`\nTotal: ${response.files.length} files, ${this.formatFileSize(response.files.reduce((sum, f) => sum + f.size, 0))}`);
565 this.addFileOperation('Dry run completed successfully.');
566 } else {
567 this.error = response.error;
568 this.addFileOperation(`Error: ${response.error}`);
569 }
570 } catch (error) {
571 this.error = `Dry run error: ${error.message}`;
572 this.addFileOperation(`Error: ${error.message}`);
573 } finally {
574 this.loading = false;
575 }
576 },
577
578 async dryRunRestore() {
579 if (!this.backupFile) {
580 this.error = 'Please select a backup file first';
581 return;
582 }
583
584 try {
585 this.loading = true;
586 this.loadingMessage = 'Performing restore dry run...';
587 this.error = '';
588 this.restoreResult = null;
589 this.clearFileOperations();
590 this.addFileOperation('Starting restore dry run...');
591
592 const formData = new FormData();
593 formData.append('backup_file', this.backupFile);
594 formData.append('metadata', this.getEditorValue());
595 formData.append('overwrite_policy', this.overwritePolicy);
596 formData.append('clean_before_restore', this.cleanBeforeRestore);
597
598 const response = await fetchApi('/backup_restore_preview', {
599 method: 'POST',
600 body: formData
601 });
602
603 const result = await response.json();
604
605 if (result.success) {
606 // Show delete operations if clean before restore is enabled
607 if (result.files_to_delete && result.files_to_delete.length > 0) {
608 this.addFileOperation(`Clean before restore - ${result.files_to_delete.length} files would be deleted:`);
609 result.files_to_delete.forEach((file, index) => {
610 this.addFileOperation(`${index + 1}. DELETE: ${file.path}`);
611 });
612 this.addFileOperation('');
613 }
614
615 // Show restore operations
616 if (result.files_to_restore && result.files_to_restore.length > 0) {
617 this.addFileOperation(`${result.files_to_restore.length} files would be restored:`);
618 result.files_to_restore.forEach((file, index) => {
619 this.addFileOperation(`${index + 1}. RESTORE: ${file.original_path} -> ${file.target_path}`);
620 });
621 }
622
623 // Show skipped files
624 if (result.skipped_files && result.skipped_files.length > 0) {
625 this.addFileOperation(`\nSkipped ${result.skipped_files.length} files:`);
626 result.skipped_files.forEach((file, index) => {
627 this.addFileOperation(`${index + 1}. ${file.original_path} (${file.reason})`);
628 });
629 }
630
631 const deleteCount = result.delete_count || 0;
632 const restoreCount = result.restore_count || 0;
633 const skippedCount = result.skipped_files?.length || 0;
634
635 this.addFileOperation(`\nSummary: ${deleteCount} to delete, ${restoreCount} to restore, ${skippedCount} skipped`);
636 this.addFileOperation('Dry run completed successfully.');
637 } else {
638 this.error = result.error;
639 this.addFileOperation(`Error: ${result.error}`);
640 }
641 } catch (error) {
642 this.error = `Dry run error: ${error.message}`;
643 this.addFileOperation(`Error: ${error.message}`);
644 } finally {
645 this.loading = false;
646 }
647 },
648
649 // Enhanced Restore Operations with Metadata Display
650 async handleFileUpload(event) {
651 const file = event.target.files[0];
652 if (!file) return;
653
654 this.backupFile = file;
655 this.error = '';
656 this.restoreResult = null;
657
658 try {
659 this.loading = true;
660 this.loadingMessage = 'Inspecting backup archive...';
661
662 const formData = new FormData();
663 formData.append('backup_file', file);
664
665 const response = await fetchApi('/backup_inspect', {
666 method: 'POST',
667 body: formData
668 });
669
670 const result = await response.json();
671
672 if (result.success) {
673 this.backupMetadata = result.metadata;
674
675 // Load complete metadata for JSON editing
676 this.restoreMetadata = JSON.parse(JSON.stringify(result.metadata)); // Deep copy
677
678 // Initialize restore editor with complete metadata JSON
679 if (this.restoreEditor) {
680 this.restoreEditor.setValue(JSON.stringify(this.restoreMetadata, null, 2));
681 this.restoreEditor.clearSelection();
682 }
683
684 // Validate backup compatibility
685 this.validateBackupCompatibility();
686 } else {
687 this.error = result.error;
688 this.backupMetadata = null;
689 }
690 } catch (error) {
691 this.error = `Inspection error: ${error.message}`;
692 this.backupMetadata = null;
693 } finally {
694 this.loading = false;
695 }
696 },
697
698 validateBackupCompatibility() {
699 if (!this.backupMetadata) return;
700
701 const warnings = [];
702
703 // Check Agent Zero version compatibility
704 // Note: Both backup and current versions are obtained via git.get_git_info()
705 const backupVersion = this.backupMetadata.agent_zero_version;
706 const currentVersion = globalThis.gitinfo.version; // Retrieved from git.get_git_info() on backend
707
708 if (backupVersion !== currentVersion && backupVersion !== "development") {
709 warnings.push(`Backup created with Agent Zero ${backupVersion}, current version is ${currentVersion}`);
710 }
711
712 // Check backup age
713 const backupDate = new Date(this.backupMetadata.timestamp);
714 const daysSinceBackup = (Date.now() - backupDate) / (1000 * 60 * 60 * 24);
715
716 if (daysSinceBackup > 30) {
717 warnings.push(`Backup is ${Math.floor(daysSinceBackup)} days old`);
718 }
719
720 // Check system compatibility
721 const systemInfo = this.backupMetadata.system_info;
722 if (systemInfo && systemInfo.system) {
723 // Could add platform-specific warnings here
724 }
725
726 if (warnings.length > 0) {
727 window.toastFrontendWarning(`Compatibility warnings: ${warnings.join(', ')}`, 'Backup Compatibility');
728 }
729 },
730
731 async performRestore() {
732 if (!this.backupFile) {
733 this.error = 'Please select a backup file';
734 return;
735 }
736
737 try {
738 this.loading = true;
739 this.loadingMessage = 'Restoring files...';
740 this.error = '';
741 this.restoreResult = null;
742 this.clearFileOperations();
743 this.addFileOperation('Starting file restoration...');
744
745 const formData = new FormData();
746 formData.append('backup_file', this.backupFile);
747 formData.append('metadata', this.getEditorValue());
748 formData.append('overwrite_policy', this.overwritePolicy);
749 formData.append('clean_before_restore', this.cleanBeforeRestore);
750
751 const response = await fetchApi('/backup_restore', {
752 method: 'POST',
753 body: formData
754 });
755
756 const result = await response.json();
757
758 if (result.success) {
759 // Log deleted files if clean before restore was enabled
760 if (result.deleted_files && result.deleted_files.length > 0) {
761 this.addFileOperation(`Clean before restore - Successfully deleted ${result.deleted_files.length} files:`);
762 result.deleted_files.forEach((file, index) => {
763 this.addFileOperation(`${index + 1}. DELETED: ${file.path}`);
764 });
765 this.addFileOperation('');
766 }
767
768 // Log restored files
769 this.addFileOperation(`Successfully restored ${result.restored_files.length} files:`);
770 result.restored_files.forEach((file, index) => {
771 this.addFileOperation(`${index + 1}. RESTORED: ${file.archive_path} -> ${file.target_path}`);
772 });
773
774 // Log skipped files
775 if (result.skipped_files && result.skipped_files.length > 0) {
776 this.addFileOperation(`\nSkipped ${result.skipped_files.length} files:`);
777 result.skipped_files.forEach((file, index) => {
778 this.addFileOperation(`${index + 1}. ${file.original_path} (${file.reason})`);
779 });
780 }
781
782 // Log errors
783 if (result.errors && result.errors.length > 0) {
784 this.addFileOperation(`\nErrors during restoration:`);
785 result.errors.forEach((error, index) => {
786 this.addFileOperation(`${index + 1}. ${error.original_path}: ${error.error}`);
787 });
788 }
789
790 const deletedCount = result.deleted_files?.length || 0;
791 const restoredCount = result.restored_files.length;
792 const skippedCount = result.skipped_files?.length || 0;
793 const errorCount = result.errors?.length || 0;
794
795 this.addFileOperation(`\nRestore completed: ${deletedCount} deleted, ${restoredCount} restored, ${skippedCount} skipped, ${errorCount} errors`);
796 this.restoreResult = result;
797 window.toastFrontendInfo('Restore completed successfully', 'Restore Status');
798 } else {
799 this.error = result.error;
800 this.addFileOperation(`Error: ${result.error}`);
801 }
802 } catch (error) {
803 this.error = `Restore error: ${error.message}`;
804 this.addFileOperation(`Error: ${error.message}`);
805 } finally {
806 this.loading = false;
807 }
808 },
809
810 // JSON Metadata Utilities
811 validateRestoreMetadata() {
812 try {
813 const metadataText = this.getEditorValue();
814 const metadata = JSON.parse(metadataText);
815
816 // Validate required fields
817 if (!Array.isArray(metadata.include_patterns)) {
818 throw new Error('include_patterns must be an array');
819 }
820 if (!Array.isArray(metadata.exclude_patterns)) {
821 throw new Error('exclude_patterns must be an array');
822 }
823
824 this.restoreMetadata = metadata;
825 this.error = '';
826 return true;
827 } catch (error) {
828 this.error = `Invalid JSON metadata: ${error.message}`;
829 return false;
830 }
831 },
832
833 getCurrentRestoreMetadata() {
834 if (this.validateRestoreMetadata()) {
835 return this.restoreMetadata;
836 }
837 return null;
838 },
839
840 // Restore Operations - Metadata Control
841 resetToOriginalMetadata() {
842 if (this.backupMetadata) {
843 this.restoreMetadata = JSON.parse(JSON.stringify(this.backupMetadata)); // Deep copy
844
845 if (this.restoreEditor) {
846 this.restoreEditor.setValue(JSON.stringify(this.restoreMetadata, null, 2));
847 this.restoreEditor.clearSelection();
848 }
849 }
850 },
851
852 // Utility
853 formatTimestamp(timestamp) {
854 if (!timestamp) return 'Unknown';
855 return formatDateTime(timestamp, "full");
856 },
857
858 formatFileSize(bytes) {
859 if (!bytes) return '0 B';
860 const sizes = ['B', 'KB', 'MB', 'GB'];
861 const i = Math.floor(Math.log(bytes) / Math.log(1024));
862 return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
863 },
864
865 formatDate(dateString) {
866 if (!dateString) return 'Unknown';
867 return formatDateTime(dateString, "date");
868 }
869 };
870
871 const store = createStore("backupStore", model);
872 export { store };