/** * Backup Manager * Handles automatic and manual backup/restore of all user data */ class BackupManager { constructor() { this.backupPrefix = 'webGame-backup-'; this.maxAutoBackups = 5; // Keep last 5 auto backups this.autoBackupInterval = 30 * 60 * 1000; // 30 minutes this.autoBackupTimer = null; this.init(); } init() { console.log('๐Ÿ›ก๏ธ Backup Manager initialized'); // Perform aggressive cleanup first this.performEmergencyCleanup(); this.cleanupOldBackups(); // Disable auto-backup to prevent quota issues // this.startAutoBackup(); console.log('โš ๏ธ Auto-backup disabled to prevent storage quota issues'); } /** * Create a complete backup of all user data */ createBackup(isAutomatic = false) { try { // Check storage usage before backup const currentUsage = this.calculateStorageUsage(); const maxStorage = 5 * 1024 * 1024; // 5MB typical localStorage limit console.log(`๐Ÿ“Š Current storage usage: ${(currentUsage / 1024 / 1024).toFixed(2)}MB`); if (currentUsage > maxStorage * 0.7) { // If over 70% usage console.warn('โš ๏ธ Storage usage high, performing preemptive cleanup...'); this.performEmergencyCleanup(); } const timestamp = new Date().toISOString(); const backupData = this.gatherAllUserData(); // Check backup size and clean if too large const testBackup = { timestamp, version: '1.0', isAutomatic, data: backupData, metadata: { totalItems: this.countDataItems(backupData), userAgent: navigator.userAgent, appVersion: this.getAppVersion() } }; const backupJson = JSON.stringify(testBackup); const backupSize = new Blob([backupJson]).size; const maxSize = 1 * 1024 * 1024; // Reduced to 1MB limit for safety console.log(`๐Ÿ“Š Backup size: ${(backupSize / 1024 / 1024).toFixed(2)}MB`); // Always clean photo data from backups to prevent quota issues console.log('๐Ÿงน Proactively cleaning photo data from backup...'); backupData.capturedPhotos = this.cleanLargePhotoData(backupData.capturedPhotos); backupData.photoGallery = this.cleanLargePhotoData(backupData.photoGallery); backupData.verificationPhotos = this.cleanLargePhotoData(backupData.verificationPhotos); // Update backup after cleaning testBackup.data = backupData; testBackup.metadata.totalItems = this.countDataItems(backupData); testBackup.metadata.photosReduced = true; // Recalculate size after cleaning const cleanedSize = new Blob([JSON.stringify(testBackup)]).size; console.log(`๐Ÿ“Š Cleaned backup size: ${(cleanedSize / 1024 / 1024).toFixed(2)}MB`); const backupKey = `${this.backupPrefix}${Date.now()}`; try { localStorage.setItem(backupKey, JSON.stringify(testBackup)); } catch (storageError) { if (storageError.name === 'QuotaExceededError') { console.warn('โš ๏ธ Storage quota exceeded, performing emergency cleanup...'); this.performEmergencyCleanup(); // Try again with minimal backup const minimalBackup = this.createMinimalBackup(isAutomatic); localStorage.setItem(backupKey, JSON.stringify(minimalBackup)); } else { throw storageError; } } console.log(`๐Ÿ›ก๏ธ ${isAutomatic ? 'Auto' : 'Manual'} backup created: ${backupKey}`); console.log(`๐Ÿ“Š Backup contains ${testBackup.metadata.totalItems} data items`); return backupKey; } catch (error) { console.error('โŒ Backup creation failed:', error); throw error; } } /** * Clean large photo data to reduce backup size */ cleanLargePhotoData(photoDataString) { if (!photoDataString) return photoDataString; try { const photoData = JSON.parse(photoDataString); if (Array.isArray(photoData)) { // Keep only metadata, remove actual image data const cleanedData = photoData.map(photo => ({ ...photo, data: photo.data ? '[IMAGE_DATA_REMOVED_FOR_BACKUP]' : photo.data, dataUrl: photo.dataUrl ? '[IMAGE_DATA_REMOVED_FOR_BACKUP]' : photo.dataUrl })); return JSON.stringify(cleanedData); } } catch (error) { console.warn('โš ๏ธ Error cleaning photo data:', error); } return photoDataString; } /** * Create minimal backup with only essential data */ createMinimalBackup(isAutomatic = false) { const essentialData = {}; // Only include essential game data, no photos const webGameData = localStorage.getItem('webGame-data'); if (webGameData) { essentialData.webGameData = JSON.parse(webGameData); } essentialData.playerStats = localStorage.getItem('playerStats'); essentialData.achievements = localStorage.getItem('achievements'); essentialData.selectedTheme = localStorage.getItem('selectedTheme'); essentialData.userPreferences = localStorage.getItem('userPreferences'); essentialData.disabledTasks = localStorage.getItem('disabledTasks'); return { timestamp: new Date().toISOString(), version: '1.0', isAutomatic, data: essentialData, metadata: { totalItems: this.countDataItems(essentialData), userAgent: navigator.userAgent, appVersion: this.getAppVersion(), isMinimal: true } }; } /** * Emergency cleanup when storage quota is exceeded */ performEmergencyCleanup() { try { console.log('๐Ÿšจ Starting emergency storage cleanup...'); // Delete ALL backups immediately const keys = Object.keys(localStorage); const backupKeys = keys.filter(key => key.startsWith(this.backupPrefix)); backupKeys.forEach(key => localStorage.removeItem(key)); console.log(`๐Ÿงน Emergency: Removed ${backupKeys.length} backups`); // Clean up ALL photos - don't keep any in localStorage localStorage.removeItem('verificationPhotos'); localStorage.removeItem('capturedPhotos'); localStorage.removeItem('photoGallery'); console.log(`๐Ÿงน Emergency: Removed all photo data from localStorage`); // Clean up any old individual photo keys const photoKeys = keys.filter(key => key.startsWith('verificationPhoto_') || key.startsWith('capturedPhoto_') || key.startsWith('photo_') ); photoKeys.forEach(key => { const value = localStorage.getItem(key); if (value && value.startsWith('blob:')) { URL.revokeObjectURL(value); } localStorage.removeItem(key); }); console.log(`๐Ÿงน Emergency: Removed ${photoKeys.length} old photo keys`); // Calculate storage usage after cleanup const usage = this.calculateStorageUsage(); console.log(`๐Ÿงน Emergency cleanup complete. Storage usage: ${(usage / 1024 / 1024).toFixed(2)}MB`); } catch (error) { console.error('โŒ Emergency cleanup failed:', error); } } /** * Calculate approximate localStorage usage */ calculateStorageUsage() { let totalSize = 0; try { for (let key in localStorage) { if (localStorage.hasOwnProperty(key)) { const value = localStorage.getItem(key); totalSize += new Blob([key + value]).size; } } } catch (error) { console.warn('โš ๏ธ Could not calculate storage usage:', error); } return totalSize; } /** * Gather all user data from various storage systems */ gatherAllUserData() { const data = {}; // Main application data const webGameData = localStorage.getItem('webGame-data'); if (webGameData) { data.webGameData = JSON.parse(webGameData); } // Media directories data.linkedImageDirectories = localStorage.getItem('linkedImageDirectories'); data.linkedVideoDirectories = localStorage.getItem('linkedVideoDirectories'); data.linkedAudioDirectories = localStorage.getItem('linkedAudioDirectories'); // Video library data data.unifiedVideoLibrary = localStorage.getItem('unifiedVideoLibrary'); data.backgroundVideos = localStorage.getItem('background-videos'); data.taskVideos = localStorage.getItem('task-videos'); data.rewardVideos = localStorage.getItem('reward-videos'); data.punishmentVideos = localStorage.getItem('punishment-videos'); // Player stats and achievements data.playerStats = localStorage.getItem('playerStats'); data.achievements = localStorage.getItem('achievements'); data.levelProgress = localStorage.getItem('levelProgress'); // Photo gallery (metadata only, no image data) data.photoGallery = localStorage.getItem('photoGallery'); data.capturedPhotos = localStorage.getItem('capturedPhotos'); data.verificationPhotos = localStorage.getItem('verificationPhotos'); // Custom content data.customMainTasks = localStorage.getItem('customMainTasks'); data.customConsequenceTasks = localStorage.getItem('customConsequenceTasks'); data.customImages = localStorage.getItem('customImages'); data.disabledTasks = localStorage.getItem('disabledTasks'); // Theme and preferences data.selectedTheme = localStorage.getItem('selectedTheme'); data.gameTheme = localStorage.getItem('gameTheme'); data.userPreferences = localStorage.getItem('userPreferences'); // Audio settings data.audioSettings = localStorage.getItem('audioSettings'); data.musicSettings = localStorage.getItem('musicSettings'); // Game state (if exists) data.autoSaveGameState = localStorage.getItem('autoSaveGameState'); data.lastSession = localStorage.getItem('lastSession'); return data; } /** * Count total data items for metadata */ countDataItems(data) { let count = 0; for (const key in data) { if (data[key] !== null) { count++; if (typeof data[key] === 'string') { try { const parsed = JSON.parse(data[key]); if (Array.isArray(parsed)) { count += parsed.length; } else if (typeof parsed === 'object') { count += Object.keys(parsed).length; } } catch (e) { // Not JSON, just count as 1 } } } } return count; } /** * Get application version */ getAppVersion() { return document.querySelector('meta[name="version"]')?.content || 'unknown'; } /** * List all available backups */ listBackups() { const backups = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith(this.backupPrefix)) { try { const backup = JSON.parse(localStorage.getItem(key)); backups.push({ key, timestamp: backup.timestamp, isAutomatic: backup.isAutomatic, version: backup.version, totalItems: backup.metadata?.totalItems || 0, size: localStorage.getItem(key).length }); } catch (e) { console.warn(`โš ๏ธ Invalid backup found: ${key}`); } } } // Sort by timestamp (newest first) return backups.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)); } /** * Restore data from a backup */ restoreBackup(backupKey, selective = null) { try { const backupData = localStorage.getItem(backupKey); if (!backupData) { throw new Error(`Backup ${backupKey} not found`); } const backup = JSON.parse(backupData); const data = backup.data; // Create a restore point before restoring const restorePointKey = this.createBackup(false); console.log(`๐Ÿ›ก๏ธ Restore point created: ${restorePointKey}`); let restoredCount = 0; // Restore all data or selective data for (const key in data) { if (data[key] !== null && (!selective || selective.includes(key))) { if (key === 'webGameData') { localStorage.setItem('webGame-data', JSON.stringify(data[key])); } else { localStorage.setItem(key, data[key]); } restoredCount++; } } console.log(`โœ… Backup restored: ${restoredCount} items from ${backup.timestamp}`); console.log(`๐Ÿ”„ Restart the application to fully apply restored data`); return { success: true, restoredItems: restoredCount, backupTimestamp: backup.timestamp, restorePointKey }; } catch (error) { console.error('โŒ Backup restore failed:', error); throw error; } } /** * Export backup to file for external storage */ async exportBackup(backupKey) { try { const backupData = localStorage.getItem(backupKey); if (!backupData) { throw new Error(`Backup ${backupKey} not found`); } const backup = JSON.parse(backupData); const filename = `webGame-backup-${backup.timestamp.replace(/[:.]/g, '-')}.json`; if (window.electronAPI && window.electronAPI.saveFile) { // Electron environment - use native file dialog const success = await window.electronAPI.saveFile(filename, backupData); if (success) { console.log(`๐Ÿ“ Backup exported to file: ${filename}`); return { success: true, filename }; } } else { // Browser environment - download file const blob = new Blob([backupData], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); console.log(`๐Ÿ“ Backup downloaded: ${filename}`); return { success: true, filename }; } } catch (error) { console.error('โŒ Backup export failed:', error); throw error; } } /** * Import backup from file */ async importBackup(file) { try { const text = await file.text(); const backup = JSON.parse(text); // Validate backup structure if (!backup.timestamp || !backup.data) { throw new Error('Invalid backup file format'); } // Create unique key for imported backup const importKey = `${this.backupPrefix}imported-${Date.now()}`; localStorage.setItem(importKey, text); console.log(`๐Ÿ“ฅ Backup imported: ${importKey} from ${backup.timestamp}`); return { success: true, backupKey: importKey, timestamp: backup.timestamp, totalItems: backup.metadata?.totalItems || 0 }; } catch (error) { console.error('โŒ Backup import failed:', error); throw error; } } /** * Delete a backup */ deleteBackup(backupKey) { try { if (localStorage.getItem(backupKey)) { localStorage.removeItem(backupKey); console.log(`๐Ÿ—‘๏ธ Backup deleted: ${backupKey}`); return true; } return false; } catch (error) { console.error('โŒ Backup deletion failed:', error); return false; } } /** * Start automatic backup timer */ startAutoBackup() { if (this.autoBackupTimer) { clearInterval(this.autoBackupTimer); } this.autoBackupTimer = setInterval(() => { try { this.createBackup(true); this.cleanupOldBackups(); } catch (error) { console.error('โŒ Auto-backup failed:', error); } }, this.autoBackupInterval); console.log(`๐Ÿ• Auto-backup scheduled every ${this.autoBackupInterval / 60000} minutes`); } /** * Stop automatic backups */ stopAutoBackup() { if (this.autoBackupTimer) { clearInterval(this.autoBackupTimer); this.autoBackupTimer = null; console.log('๐Ÿ›‘ Auto-backup stopped'); } } /** * Clean up old automatic backups */ cleanupOldBackups(maxToKeep = null) { const backups = this.listBackups(); const autoBackups = backups.filter(b => b.isAutomatic); const maxBackups = maxToKeep || this.maxAutoBackups; if (autoBackups.length > maxBackups) { const toDelete = autoBackups.slice(maxBackups); toDelete.forEach(backup => { this.deleteBackup(backup.key); }); console.log(`๐Ÿงน Cleaned up ${toDelete.length} old auto-backups (keeping ${maxBackups})`); } } /** * Get storage usage for backups */ getBackupStorageInfo() { const backups = this.listBackups(); const totalSize = backups.reduce((sum, backup) => sum + backup.size, 0); const autoBackups = backups.filter(b => b.isAutomatic).length; const manualBackups = backups.filter(b => !b.isAutomatic).length; return { totalBackups: backups.length, autoBackups, manualBackups, totalSize, formattedSize: this.formatBytes(totalSize), backups }; } /** * Format bytes to human readable format */ formatBytes(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Emergency backup before risky operations */ createEmergencyBackup(reason = 'Emergency backup') { try { const backupKey = this.createBackup(false); console.log(`๐Ÿšจ Emergency backup created: ${reason}`); return backupKey; } catch (error) { console.error('โŒ Emergency backup failed:', error); throw error; } } } // Export for use in other modules window.BackupManager = BackupManager;