mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-05-23 12:44:31 +00:00
Add user-configurable timezone and 12/24-hour preferences, then wire them through settings, runtime snapshots, scheduler payloads, wait handling, notifications, backups, memory, plugin metadata, and frontend formatters. Keep UTC as the boundary for absolute instants while serializing user-facing dates in the configured or browser-resolved timezone. Preserve scheduler wall-clock inputs in the selected timezone, propagate TZ into desktop/runtime process environments, and restart active desktop sessions when the runtime timezone changes. Cover the risky paths with timezone regression tests for settings normalization, auto and fixed timezone resolution, scheduler round-trips, memory timestamp conversion, and desktop timezone sync.
872 lines
27 KiB
JavaScript
872 lines
27 KiB
JavaScript
import { createStore } from "/js/AlpineStore.js";
|
|
import {
|
|
formatDateTime,
|
|
getCurrentUserDateString,
|
|
getCurrentUserISOString,
|
|
getUserHour12,
|
|
getUserTimezone,
|
|
} from "/js/time-utils.js";
|
|
|
|
// Global function references
|
|
const sendJsonData = globalThis.sendJsonData;
|
|
const toast = globalThis.toast;
|
|
const fetchApi = globalThis.fetchApi;
|
|
|
|
// ⚠️ CRITICAL: The .env file contains API keys and essential configuration.
|
|
// This file is REQUIRED for Agent Zero to function and must be backed up.
|
|
|
|
const model = {
|
|
// State
|
|
mode: 'backup', // 'backup' or 'restore'
|
|
loading: false,
|
|
loadingMessage: '',
|
|
error: '',
|
|
|
|
// File operations log (shared between backup and restore)
|
|
fileOperationsLog: '',
|
|
|
|
// Backup state
|
|
backupMetadataConfig: null,
|
|
includeHidden: false,
|
|
previewStats: { total: 0, truncated: false },
|
|
backupEditor: null,
|
|
|
|
// Enhanced file preview state
|
|
previewMode: 'grouped', // 'grouped' or 'flat'
|
|
previewFiles: [],
|
|
previewGroups: [],
|
|
filteredPreviewFiles: [],
|
|
fileSearchFilter: '',
|
|
expandedGroups: new Set(),
|
|
|
|
// Progress state
|
|
progressData: null,
|
|
progressEventSource: null,
|
|
|
|
// Restore state
|
|
backupFile: null,
|
|
backupMetadata: null,
|
|
restorePatterns: '',
|
|
overwritePolicy: 'overwrite',
|
|
cleanBeforeRestore: false,
|
|
restoreEditor: null,
|
|
restoreResult: null,
|
|
|
|
// Initialization
|
|
async initBackup() {
|
|
this.mode = 'backup';
|
|
this.resetState();
|
|
await this.initBackupEditor();
|
|
await this.updatePreview();
|
|
},
|
|
|
|
async initRestore() {
|
|
this.mode = 'restore';
|
|
this.resetState();
|
|
await this.initRestoreEditor();
|
|
},
|
|
|
|
resetState() {
|
|
this.loading = false;
|
|
this.error = '';
|
|
this.backupFile = null;
|
|
this.backupMetadata = null;
|
|
this.restoreResult = null;
|
|
this.fileOperationsLog = '';
|
|
},
|
|
|
|
// File operations logging
|
|
addFileOperation(message) {
|
|
const timestamp = new Intl.DateTimeFormat(undefined, {
|
|
timeStyle: "medium",
|
|
hour12: getUserHour12(),
|
|
timeZone: getUserTimezone(),
|
|
}).format(new Date());
|
|
this.fileOperationsLog += `[${timestamp}] ${message}\n`;
|
|
|
|
// Auto-scroll to bottom - use setTimeout since $nextTick is not available in stores
|
|
setTimeout(() => {
|
|
const textarea = document.getElementById(this.mode === 'backup' ? 'backup-file-list' : 'restore-file-list');
|
|
if (textarea) {
|
|
textarea.scrollTop = textarea.scrollHeight;
|
|
}
|
|
}, 0);
|
|
},
|
|
|
|
clearFileOperations() {
|
|
this.fileOperationsLog = '';
|
|
},
|
|
|
|
createDownloadToastGroup(prefix) {
|
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
},
|
|
|
|
showDownloadPreparingToast(group) {
|
|
window.toastFrontendInfo?.("Preparing download...", "Download", 0, group, undefined, true);
|
|
},
|
|
|
|
showDownloadStartedToast(group) {
|
|
window.toastFrontendInfo?.("Downloading...", "Download", 3, group, undefined, true);
|
|
},
|
|
|
|
showDownloadErrorToast(group, message) {
|
|
window.toastFrontendError?.(message || "Download failed", "Download Error", 8, group, undefined, true);
|
|
},
|
|
|
|
// Cleanup method for modal close
|
|
onClose() {
|
|
this.resetState();
|
|
if (this.backupEditor) {
|
|
this.backupEditor.destroy();
|
|
this.backupEditor = null;
|
|
}
|
|
if (this.restoreEditor) {
|
|
this.restoreEditor.destroy();
|
|
this.restoreEditor = null;
|
|
}
|
|
},
|
|
|
|
// Get default backup metadata with resolved patterns from backend
|
|
async getDefaultBackupMetadata() {
|
|
const timestamp = getCurrentUserISOString();
|
|
|
|
try {
|
|
// Get resolved default patterns from backend
|
|
const response = await sendJsonData("backup_get_defaults", {});
|
|
|
|
if (response.success) {
|
|
// Use patterns from backend with resolved absolute paths
|
|
const include_patterns = response.default_patterns.include_patterns;
|
|
const exclude_patterns = response.default_patterns.exclude_patterns;
|
|
|
|
return {
|
|
backup_name: `agent-zero-backup-${getCurrentUserDateString()}`,
|
|
include_hidden: true,
|
|
include_patterns: include_patterns,
|
|
exclude_patterns: exclude_patterns,
|
|
backup_config: {
|
|
compression_level: 6,
|
|
integrity_check: true
|
|
}
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.warn("Failed to get default patterns from backend, using fallback");
|
|
}
|
|
|
|
// Fallback patterns (will be overridden by backend on first use)
|
|
return {
|
|
backup_name: `agent-zero-backup-${timestamp.slice(0, 10)}`,
|
|
include_hidden: true,
|
|
include_patterns: [
|
|
// These will be replaced with resolved absolute paths by backend
|
|
"# Loading default patterns from backend..."
|
|
],
|
|
exclude_patterns: [],
|
|
backup_config: {
|
|
compression_level: 6,
|
|
integrity_check: true
|
|
}
|
|
};
|
|
},
|
|
|
|
// Editor Management - Following Agent Zero ACE editor patterns
|
|
async initBackupEditor() {
|
|
const container = document.getElementById("backup-metadata-editor");
|
|
if (container) {
|
|
const editor = ace.edit("backup-metadata-editor");
|
|
|
|
const dark = localStorage.getItem("darkMode");
|
|
if (dark != "false") {
|
|
editor.setTheme("ace/theme/github_dark");
|
|
} else {
|
|
editor.setTheme("ace/theme/tomorrow");
|
|
}
|
|
|
|
editor.session.setMode("ace/mode/json");
|
|
|
|
// Initialize with default backup metadata
|
|
const defaultMetadata = await this.getDefaultBackupMetadata();
|
|
editor.setValue(JSON.stringify(defaultMetadata, null, 2));
|
|
editor.clearSelection();
|
|
|
|
// Auto-update preview on changes (debounced)
|
|
let timeout;
|
|
editor.on('change', () => {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(() => {
|
|
this.updatePreview();
|
|
}, 1000);
|
|
});
|
|
|
|
this.backupEditor = editor;
|
|
}
|
|
},
|
|
|
|
async initRestoreEditor() {
|
|
const container = document.getElementById("restore-metadata-editor");
|
|
if (container) {
|
|
const editor = ace.edit("restore-metadata-editor");
|
|
|
|
const dark = localStorage.getItem("darkMode");
|
|
if (dark != "false") {
|
|
editor.setTheme("ace/theme/github_dark");
|
|
} else {
|
|
editor.setTheme("ace/theme/tomorrow");
|
|
}
|
|
|
|
editor.session.setMode("ace/mode/json");
|
|
editor.setValue('{}');
|
|
editor.clearSelection();
|
|
|
|
// Auto-validate JSON on changes
|
|
editor.on('change', () => {
|
|
this.validateRestoreMetadata();
|
|
});
|
|
|
|
this.restoreEditor = editor;
|
|
}
|
|
},
|
|
|
|
// Unified editor value getter (following MCP servers pattern)
|
|
getEditorValue() {
|
|
const editor = this.mode === 'backup' ? this.backupEditor : this.restoreEditor;
|
|
return editor ? editor.getValue() : '{}';
|
|
},
|
|
|
|
// Unified JSON formatting (following MCP servers pattern)
|
|
formatJson() {
|
|
const editor = this.mode === 'backup' ? this.backupEditor : this.restoreEditor;
|
|
if (!editor) return;
|
|
|
|
try {
|
|
const currentContent = editor.getValue();
|
|
const parsed = JSON.parse(currentContent);
|
|
const formatted = JSON.stringify(parsed, null, 2);
|
|
|
|
editor.setValue(formatted);
|
|
editor.clearSelection();
|
|
editor.navigateFileStart();
|
|
} catch (error) {
|
|
console.error("Failed to format JSON:", error);
|
|
this.error = "Invalid JSON: " + error.message;
|
|
}
|
|
},
|
|
|
|
// Enhanced File Preview Operations
|
|
async updatePreview() {
|
|
try {
|
|
const metadataText = this.getEditorValue();
|
|
const metadata = JSON.parse(metadataText);
|
|
|
|
if (!metadata.include_patterns || metadata.include_patterns.length === 0) {
|
|
this.previewStats = { total: 0, truncated: false };
|
|
this.previewFiles = [];
|
|
this.previewGroups = [];
|
|
return;
|
|
}
|
|
|
|
// Convert patterns arrays back to string format for API
|
|
const patternsString = this.convertPatternsToString(metadata.include_patterns, metadata.exclude_patterns);
|
|
|
|
// Get grouped preview for better UX
|
|
const response = await sendJsonData("backup_preview_grouped", {
|
|
patterns: patternsString,
|
|
include_hidden: metadata.include_hidden ?? true,
|
|
max_depth: 3,
|
|
search_filter: this.fileSearchFilter
|
|
});
|
|
|
|
if (response.success) {
|
|
this.previewGroups = response.groups;
|
|
this.previewStats = response.stats;
|
|
|
|
// Flatten groups for flat view
|
|
this.previewFiles = [];
|
|
response.groups.forEach(group => {
|
|
this.previewFiles.push(...group.files);
|
|
});
|
|
|
|
this.applyFileSearch();
|
|
} else {
|
|
this.error = response.error;
|
|
}
|
|
} catch (error) {
|
|
this.error = `Preview error: ${error.message}`;
|
|
}
|
|
},
|
|
|
|
// Convert pattern arrays to string format for backend API
|
|
convertPatternsToString(includePatterns, excludePatterns) {
|
|
const patterns = [];
|
|
|
|
// Add include patterns
|
|
if (includePatterns) {
|
|
patterns.push(...includePatterns);
|
|
}
|
|
|
|
// Add exclude patterns with '!' prefix
|
|
if (excludePatterns) {
|
|
excludePatterns.forEach(pattern => {
|
|
patterns.push(`!${pattern}`);
|
|
});
|
|
}
|
|
|
|
return patterns.join('\n');
|
|
},
|
|
|
|
// Validation for backup metadata
|
|
validateBackupMetadata() {
|
|
try {
|
|
const metadataText = this.getEditorValue();
|
|
const metadata = JSON.parse(metadataText);
|
|
|
|
// Validate required fields
|
|
if (!Array.isArray(metadata.include_patterns)) {
|
|
throw new Error('include_patterns must be an array');
|
|
}
|
|
if (!Array.isArray(metadata.exclude_patterns)) {
|
|
throw new Error('exclude_patterns must be an array');
|
|
}
|
|
if (!metadata.backup_name || typeof metadata.backup_name !== 'string') {
|
|
throw new Error('backup_name must be a non-empty string');
|
|
}
|
|
|
|
this.backupMetadataConfig = metadata;
|
|
this.error = '';
|
|
return true;
|
|
} catch (error) {
|
|
this.error = `Invalid backup metadata: ${error.message}`;
|
|
return false;
|
|
}
|
|
},
|
|
|
|
// File Preview UI Management
|
|
initFilePreview() {
|
|
this.fileSearchFilter = '';
|
|
this.expandedGroups.clear();
|
|
this.previewMode = localStorage.getItem('backupPreviewMode') || 'grouped';
|
|
},
|
|
|
|
togglePreviewMode() {
|
|
this.previewMode = this.previewMode === 'grouped' ? 'flat' : 'grouped';
|
|
localStorage.setItem('backupPreviewMode', this.previewMode);
|
|
},
|
|
|
|
toggleGroup(groupPath) {
|
|
if (this.expandedGroups.has(groupPath)) {
|
|
this.expandedGroups.delete(groupPath);
|
|
} else {
|
|
this.expandedGroups.add(groupPath);
|
|
}
|
|
},
|
|
|
|
isGroupExpanded(groupPath) {
|
|
return this.expandedGroups.has(groupPath);
|
|
},
|
|
|
|
debounceFileSearch() {
|
|
clearTimeout(this.searchTimeout);
|
|
this.searchTimeout = setTimeout(() => {
|
|
this.applyFileSearch();
|
|
}, 300);
|
|
},
|
|
|
|
clearFileSearch() {
|
|
this.fileSearchFilter = '';
|
|
this.applyFileSearch();
|
|
},
|
|
|
|
applyFileSearch() {
|
|
if (!this.fileSearchFilter.trim()) {
|
|
this.filteredPreviewFiles = this.previewFiles;
|
|
} else {
|
|
const search = this.fileSearchFilter.toLowerCase();
|
|
this.filteredPreviewFiles = this.previewFiles.filter(file =>
|
|
file.path.toLowerCase().includes(search)
|
|
);
|
|
}
|
|
},
|
|
|
|
async exportFileList() {
|
|
const fileList = this.previewFiles.map(f => f.path).join('\n');
|
|
const blob = new Blob([fileList], { type: 'text/plain' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'backup-file-list.txt';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
|
|
async copyFileListToClipboard() {
|
|
const fileList = this.previewFiles.map(f => f.path).join('\n');
|
|
try {
|
|
await navigator.clipboard.writeText(fileList);
|
|
window.toastFrontendInfo('File list copied to clipboard', 'Clipboard');
|
|
} catch (error) {
|
|
window.toastFrontendError('Failed to copy to clipboard', 'Clipboard Error');
|
|
}
|
|
},
|
|
|
|
// Backup Creation using direct API call
|
|
async createBackup() {
|
|
// Validate backup metadata first
|
|
if (!this.validateBackupMetadata()) {
|
|
return;
|
|
}
|
|
|
|
const downloadToastGroup = this.createDownloadToastGroup("backup-create");
|
|
|
|
try {
|
|
this.loading = true;
|
|
this.loadingMessage = 'Creating backup...';
|
|
this.error = '';
|
|
this.clearFileOperations();
|
|
this.addFileOperation('Starting backup creation...');
|
|
this.showDownloadPreparingToast(downloadToastGroup);
|
|
|
|
const metadata = this.backupMetadataConfig;
|
|
|
|
// Use fetch directly since backup_create returns a file download, not JSON
|
|
const response = await fetchApi('/backup_create', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
include_patterns: metadata.include_patterns,
|
|
exclude_patterns: metadata.exclude_patterns,
|
|
include_hidden: metadata.include_hidden ?? true,
|
|
backup_name: metadata.backup_name
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Handle file download
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${metadata.backup_name}.zip`;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
|
|
this.addFileOperation('Backup created and downloaded successfully!');
|
|
this.showDownloadStartedToast(downloadToastGroup);
|
|
} else {
|
|
// Try to parse error response
|
|
const errorText = await response.text();
|
|
try {
|
|
const errorJson = JSON.parse(errorText);
|
|
this.error = errorJson.error || 'Backup creation failed';
|
|
} catch {
|
|
this.error = `Backup creation failed: ${response.status} ${response.statusText}`;
|
|
}
|
|
this.addFileOperation(`Error: ${this.error}`);
|
|
this.showDownloadErrorToast(downloadToastGroup, this.error);
|
|
}
|
|
|
|
} catch (error) {
|
|
this.error = `Backup error: ${error.message}`;
|
|
this.addFileOperation(`Error: ${error.message}`);
|
|
this.showDownloadErrorToast(downloadToastGroup, this.error);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async downloadBackup(backupPath, backupName) {
|
|
const downloadToastGroup = this.createDownloadToastGroup("backup-download");
|
|
|
|
try {
|
|
this.showDownloadPreparingToast(downloadToastGroup);
|
|
const response = await fetchApi('/backup_download', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ backup_path: backupPath })
|
|
});
|
|
|
|
if (response.ok) {
|
|
const blob = await response.blob();
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `${backupName}.zip`;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
this.showDownloadStartedToast(downloadToastGroup);
|
|
} else {
|
|
const errorText = await response.text();
|
|
this.error = errorText || `Download failed: ${response.status}`;
|
|
this.showDownloadErrorToast(downloadToastGroup, this.error);
|
|
}
|
|
} catch (error) {
|
|
console.error('Download error:', error);
|
|
this.error = error.message || 'Download failed';
|
|
this.showDownloadErrorToast(downloadToastGroup, this.error);
|
|
}
|
|
},
|
|
|
|
cancelBackup() {
|
|
if (this.progressEventSource) {
|
|
this.progressEventSource.close();
|
|
this.progressEventSource = null;
|
|
}
|
|
this.loading = false;
|
|
this.progressData = null;
|
|
},
|
|
|
|
resetToDefaults() {
|
|
this.getDefaultBackupMetadata().then(defaultMetadata => {
|
|
if (this.backupEditor) {
|
|
this.backupEditor.setValue(JSON.stringify(defaultMetadata, null, 2));
|
|
this.backupEditor.clearSelection();
|
|
}
|
|
this.updatePreview();
|
|
});
|
|
},
|
|
|
|
// Dry run functionality
|
|
async dryRun() {
|
|
if (this.mode === 'backup') {
|
|
await this.dryRunBackup();
|
|
} else if (this.mode === 'restore') {
|
|
await this.dryRunRestore();
|
|
}
|
|
},
|
|
|
|
async dryRunBackup() {
|
|
// Validate backup metadata first
|
|
if (!this.validateBackupMetadata()) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.loading = true;
|
|
this.loadingMessage = 'Performing dry run...';
|
|
this.error = '';
|
|
this.clearFileOperations();
|
|
this.addFileOperation('Starting backup dry run...');
|
|
|
|
const metadata = this.backupMetadataConfig;
|
|
const patternsString = this.convertPatternsToString(metadata.include_patterns, metadata.exclude_patterns);
|
|
|
|
const response = await sendJsonData("backup_test", {
|
|
patterns: patternsString,
|
|
include_hidden: metadata.include_hidden ?? true,
|
|
max_files: 10000
|
|
});
|
|
|
|
if (response.success) {
|
|
this.addFileOperation(`Found ${response.files.length} files that would be backed up:`);
|
|
response.files.forEach((file, index) => {
|
|
this.addFileOperation(`${index + 1}. ${file.path} (${this.formatFileSize(file.size)})`);
|
|
});
|
|
this.addFileOperation(`\nTotal: ${response.files.length} files, ${this.formatFileSize(response.files.reduce((sum, f) => sum + f.size, 0))}`);
|
|
this.addFileOperation('Dry run completed successfully.');
|
|
} else {
|
|
this.error = response.error;
|
|
this.addFileOperation(`Error: ${response.error}`);
|
|
}
|
|
} catch (error) {
|
|
this.error = `Dry run error: ${error.message}`;
|
|
this.addFileOperation(`Error: ${error.message}`);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
async dryRunRestore() {
|
|
if (!this.backupFile) {
|
|
this.error = 'Please select a backup file first';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.loading = true;
|
|
this.loadingMessage = 'Performing restore dry run...';
|
|
this.error = '';
|
|
this.restoreResult = null;
|
|
this.clearFileOperations();
|
|
this.addFileOperation('Starting restore dry run...');
|
|
|
|
const formData = new FormData();
|
|
formData.append('backup_file', this.backupFile);
|
|
formData.append('metadata', this.getEditorValue());
|
|
formData.append('overwrite_policy', this.overwritePolicy);
|
|
formData.append('clean_before_restore', this.cleanBeforeRestore);
|
|
|
|
const response = await fetchApi('/backup_restore_preview', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// Show delete operations if clean before restore is enabled
|
|
if (result.files_to_delete && result.files_to_delete.length > 0) {
|
|
this.addFileOperation(`Clean before restore - ${result.files_to_delete.length} files would be deleted:`);
|
|
result.files_to_delete.forEach((file, index) => {
|
|
this.addFileOperation(`${index + 1}. DELETE: ${file.path}`);
|
|
});
|
|
this.addFileOperation('');
|
|
}
|
|
|
|
// Show restore operations
|
|
if (result.files_to_restore && result.files_to_restore.length > 0) {
|
|
this.addFileOperation(`${result.files_to_restore.length} files would be restored:`);
|
|
result.files_to_restore.forEach((file, index) => {
|
|
this.addFileOperation(`${index + 1}. RESTORE: ${file.original_path} -> ${file.target_path}`);
|
|
});
|
|
}
|
|
|
|
// Show skipped files
|
|
if (result.skipped_files && result.skipped_files.length > 0) {
|
|
this.addFileOperation(`\nSkipped ${result.skipped_files.length} files:`);
|
|
result.skipped_files.forEach((file, index) => {
|
|
this.addFileOperation(`${index + 1}. ${file.original_path} (${file.reason})`);
|
|
});
|
|
}
|
|
|
|
const deleteCount = result.delete_count || 0;
|
|
const restoreCount = result.restore_count || 0;
|
|
const skippedCount = result.skipped_files?.length || 0;
|
|
|
|
this.addFileOperation(`\nSummary: ${deleteCount} to delete, ${restoreCount} to restore, ${skippedCount} skipped`);
|
|
this.addFileOperation('Dry run completed successfully.');
|
|
} else {
|
|
this.error = result.error;
|
|
this.addFileOperation(`Error: ${result.error}`);
|
|
}
|
|
} catch (error) {
|
|
this.error = `Dry run error: ${error.message}`;
|
|
this.addFileOperation(`Error: ${error.message}`);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// Enhanced Restore Operations with Metadata Display
|
|
async handleFileUpload(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
this.backupFile = file;
|
|
this.error = '';
|
|
this.restoreResult = null;
|
|
|
|
try {
|
|
this.loading = true;
|
|
this.loadingMessage = 'Inspecting backup archive...';
|
|
|
|
const formData = new FormData();
|
|
formData.append('backup_file', file);
|
|
|
|
const response = await fetchApi('/backup_inspect', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
this.backupMetadata = result.metadata;
|
|
|
|
// Load complete metadata for JSON editing
|
|
this.restoreMetadata = JSON.parse(JSON.stringify(result.metadata)); // Deep copy
|
|
|
|
// Initialize restore editor with complete metadata JSON
|
|
if (this.restoreEditor) {
|
|
this.restoreEditor.setValue(JSON.stringify(this.restoreMetadata, null, 2));
|
|
this.restoreEditor.clearSelection();
|
|
}
|
|
|
|
// Validate backup compatibility
|
|
this.validateBackupCompatibility();
|
|
} else {
|
|
this.error = result.error;
|
|
this.backupMetadata = null;
|
|
}
|
|
} catch (error) {
|
|
this.error = `Inspection error: ${error.message}`;
|
|
this.backupMetadata = null;
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
validateBackupCompatibility() {
|
|
if (!this.backupMetadata) return;
|
|
|
|
const warnings = [];
|
|
|
|
// Check Agent Zero version compatibility
|
|
// Note: Both backup and current versions are obtained via git.get_git_info()
|
|
const backupVersion = this.backupMetadata.agent_zero_version;
|
|
const currentVersion = globalThis.gitinfo.version; // Retrieved from git.get_git_info() on backend
|
|
|
|
if (backupVersion !== currentVersion && backupVersion !== "development") {
|
|
warnings.push(`Backup created with Agent Zero ${backupVersion}, current version is ${currentVersion}`);
|
|
}
|
|
|
|
// Check backup age
|
|
const backupDate = new Date(this.backupMetadata.timestamp);
|
|
const daysSinceBackup = (Date.now() - backupDate) / (1000 * 60 * 60 * 24);
|
|
|
|
if (daysSinceBackup > 30) {
|
|
warnings.push(`Backup is ${Math.floor(daysSinceBackup)} days old`);
|
|
}
|
|
|
|
// Check system compatibility
|
|
const systemInfo = this.backupMetadata.system_info;
|
|
if (systemInfo && systemInfo.system) {
|
|
// Could add platform-specific warnings here
|
|
}
|
|
|
|
if (warnings.length > 0) {
|
|
window.toastFrontendWarning(`Compatibility warnings: ${warnings.join(', ')}`, 'Backup Compatibility');
|
|
}
|
|
},
|
|
|
|
async performRestore() {
|
|
if (!this.backupFile) {
|
|
this.error = 'Please select a backup file';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
this.loading = true;
|
|
this.loadingMessage = 'Restoring files...';
|
|
this.error = '';
|
|
this.restoreResult = null;
|
|
this.clearFileOperations();
|
|
this.addFileOperation('Starting file restoration...');
|
|
|
|
const formData = new FormData();
|
|
formData.append('backup_file', this.backupFile);
|
|
formData.append('metadata', this.getEditorValue());
|
|
formData.append('overwrite_policy', this.overwritePolicy);
|
|
formData.append('clean_before_restore', this.cleanBeforeRestore);
|
|
|
|
const response = await fetchApi('/backup_restore', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
// Log deleted files if clean before restore was enabled
|
|
if (result.deleted_files && result.deleted_files.length > 0) {
|
|
this.addFileOperation(`Clean before restore - Successfully deleted ${result.deleted_files.length} files:`);
|
|
result.deleted_files.forEach((file, index) => {
|
|
this.addFileOperation(`${index + 1}. DELETED: ${file.path}`);
|
|
});
|
|
this.addFileOperation('');
|
|
}
|
|
|
|
// Log restored files
|
|
this.addFileOperation(`Successfully restored ${result.restored_files.length} files:`);
|
|
result.restored_files.forEach((file, index) => {
|
|
this.addFileOperation(`${index + 1}. RESTORED: ${file.archive_path} -> ${file.target_path}`);
|
|
});
|
|
|
|
// Log skipped files
|
|
if (result.skipped_files && result.skipped_files.length > 0) {
|
|
this.addFileOperation(`\nSkipped ${result.skipped_files.length} files:`);
|
|
result.skipped_files.forEach((file, index) => {
|
|
this.addFileOperation(`${index + 1}. ${file.original_path} (${file.reason})`);
|
|
});
|
|
}
|
|
|
|
// Log errors
|
|
if (result.errors && result.errors.length > 0) {
|
|
this.addFileOperation(`\nErrors during restoration:`);
|
|
result.errors.forEach((error, index) => {
|
|
this.addFileOperation(`${index + 1}. ${error.original_path}: ${error.error}`);
|
|
});
|
|
}
|
|
|
|
const deletedCount = result.deleted_files?.length || 0;
|
|
const restoredCount = result.restored_files.length;
|
|
const skippedCount = result.skipped_files?.length || 0;
|
|
const errorCount = result.errors?.length || 0;
|
|
|
|
this.addFileOperation(`\nRestore completed: ${deletedCount} deleted, ${restoredCount} restored, ${skippedCount} skipped, ${errorCount} errors`);
|
|
this.restoreResult = result;
|
|
window.toastFrontendInfo('Restore completed successfully', 'Restore Status');
|
|
} else {
|
|
this.error = result.error;
|
|
this.addFileOperation(`Error: ${result.error}`);
|
|
}
|
|
} catch (error) {
|
|
this.error = `Restore error: ${error.message}`;
|
|
this.addFileOperation(`Error: ${error.message}`);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
// JSON Metadata Utilities
|
|
validateRestoreMetadata() {
|
|
try {
|
|
const metadataText = this.getEditorValue();
|
|
const metadata = JSON.parse(metadataText);
|
|
|
|
// Validate required fields
|
|
if (!Array.isArray(metadata.include_patterns)) {
|
|
throw new Error('include_patterns must be an array');
|
|
}
|
|
if (!Array.isArray(metadata.exclude_patterns)) {
|
|
throw new Error('exclude_patterns must be an array');
|
|
}
|
|
|
|
this.restoreMetadata = metadata;
|
|
this.error = '';
|
|
return true;
|
|
} catch (error) {
|
|
this.error = `Invalid JSON metadata: ${error.message}`;
|
|
return false;
|
|
}
|
|
},
|
|
|
|
getCurrentRestoreMetadata() {
|
|
if (this.validateRestoreMetadata()) {
|
|
return this.restoreMetadata;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
// Restore Operations - Metadata Control
|
|
resetToOriginalMetadata() {
|
|
if (this.backupMetadata) {
|
|
this.restoreMetadata = JSON.parse(JSON.stringify(this.backupMetadata)); // Deep copy
|
|
|
|
if (this.restoreEditor) {
|
|
this.restoreEditor.setValue(JSON.stringify(this.restoreMetadata, null, 2));
|
|
this.restoreEditor.clearSelection();
|
|
}
|
|
}
|
|
},
|
|
|
|
// Utility
|
|
formatTimestamp(timestamp) {
|
|
if (!timestamp) return 'Unknown';
|
|
return formatDateTime(timestamp, "full");
|
|
},
|
|
|
|
formatFileSize(bytes) {
|
|
if (!bytes) return '0 B';
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
|
},
|
|
|
|
formatDate(dateString) {
|
|
if (!dateString) return 'Unknown';
|
|
return formatDateTime(dateString, "date");
|
|
}
|
|
};
|
|
|
|
const store = createStore("backupStore", model);
|
|
export { store };
|