/** * GoingsOn - Export & Backup Module * JSON/CSV/ICS export, backup creation, restore, and automatic backup settings */ (function() { 'use strict'; const esc = GoingsOn.utils.escapeHtml; const escAttr = GoingsOn.utils.escapeAttr; // ============ Export Functions ============ /** * Export all data as JSON. */ async function exportJSON() { try { const { save } = window.__TAURI__.dialog; const today = new Date().toISOString().slice(0, 10); const filePath = await save({ defaultPath: `goingson-export-${today}.json`, filters: [{ name: 'JSON', extensions: ['json'] }] }); if (filePath) { const result = await GoingsOn.api.export.json(filePath); GoingsOn.ui.showToast(`Exported ${result.itemCount} items to JSON`); } } catch (err) { GoingsOn.ui.showToast('Export failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } /** * Export tasks as CSV. */ async function exportTasksCSV() { try { const { save } = window.__TAURI__.dialog; const today = new Date().toISOString().slice(0, 10); const filePath = await save({ defaultPath: `goingson-tasks-${today}.csv`, filters: [{ name: 'CSV', extensions: ['csv'] }] }); if (filePath) { const result = await GoingsOn.api.export.tasksCSV(filePath); GoingsOn.ui.showToast(`Exported ${result.itemCount} tasks to CSV`); } } catch (err) { GoingsOn.ui.showToast('Export failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } /** * Export events as ICS calendar file. */ async function exportEventsICS() { try { const { save } = window.__TAURI__.dialog; const today = new Date().toISOString().slice(0, 10); const filePath = await save({ defaultPath: `goingson-calendar-${today}.ics`, filters: [{ name: 'iCalendar', extensions: ['ics'] }] }); if (filePath) { const result = await GoingsOn.api.export.eventsICS(filePath); GoingsOn.ui.showToast(`Exported ${result.itemCount} events to ICS`); } } catch (err) { GoingsOn.ui.showToast('Export failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } // ============ Backup Functions ============ /** * Create a compressed backup. */ async function createBackup() { try { const result = await GoingsOn.api.export.createBackup(); GoingsOn.ui.showToast(`Backup created with ${result.itemCount} items`); } catch (err) { GoingsOn.ui.showToast('Backup failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } /** * Open the backups management modal. */ async function openBackupsModal() { GoingsOn.ui.closeModal(); let backups = []; try { backups = await GoingsOn.api.export.listBackups(); } catch (err) { GoingsOn.ui.showToast('Failed to load backups: ' + GoingsOn.utils.getErrorMessage(err), 'error'); return; } const backupsList = backups.length === 0 ? '

No backups found

' : backups.map(backup => { const date = new Date(backup.createdAt * 1000); const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); const sizeStr = formatBytes(backup.sizeBytes); return `
${esc(backup.fileName)}
${dateStr} - ${sizeStr}
`; }).join(''); const content = `
${backupsList}
`; GoingsOn.ui.openModal('Manage Backups', content); } /** * Restore data from a backup file after confirmation. * @param {string} filePath - Absolute path to the backup file */ async function restoreFromBackup(filePath) { const confirmed = await GoingsOn.ui.confirmDelete( 'Restore from Backup', 'This will import data from the backup. Existing items with the same IDs will be skipped. Do you want to continue?' ); if (!confirmed) return; try { const result = await GoingsOn.api.export.restoreBackup(filePath, { replaceAll: false }); const total = result.projectsRestored + result.tasksRestored + result.eventsRestored + result.emailsRestored; GoingsOn.ui.showToast(`Restored ${total} items from backup`); GoingsOn.ui.closeModal(); // Reload data GoingsOn.projects.load(); GoingsOn.tasks.load(); GoingsOn.events.load(); GoingsOn.emails.load(); } catch (err) { GoingsOn.ui.showToast('Restore failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } /** * Delete a backup file after confirmation. * @param {string} filePath - Absolute path to the backup file */ async function deleteBackup(filePath) { const confirmed = await GoingsOn.ui.confirmDelete( 'Delete Backup', 'Are you sure you want to delete this backup? This cannot be undone.' ); if (!confirmed) return; try { await GoingsOn.api.export.deleteBackup(filePath); GoingsOn.ui.showToast('Backup deleted'); // Refresh the backups list openBackupsModal(); } catch (err) { GoingsOn.ui.showToast('Delete failed: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } /** * Format a byte count as a human-readable size string. * @param {number} bytes - Number of bytes * @returns {string} Formatted string (e.g., "1.5 MB", "256 KB") */ function 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(1)) + ' ' + sizes[i]; } // ============ Backup Settings ============ /** * Opens the automatic backup settings modal. */ async function openBackupSettingsModal() { let settings = null; try { settings = await GoingsOn.api.export.getBackupSettings(); } catch (err) { console.error('Failed to load backup settings:', err); settings = { autoBackupEnabled: true, backupFrequencyMinutes: 15, maxBackupsToKeep: 1, lastBackupAt: null, }; } const lastBackupText = settings.lastBackupAt ? `Last backup: ${new Date(settings.lastBackupAt).toLocaleString()}` : 'No backups yet'; const frequencyOptions = [ { value: 15, label: 'Every 15 minutes (Recommended)' }, { value: 30, label: 'Every 30 minutes' }, { value: 60, label: 'Every hour' }, { value: 360, label: 'Every 6 hours' }, { value: 1440, label: 'Daily' }, ]; const retentionOptions = [ { value: 1, label: 'Keep 1 backup (Recommended)' }, { value: 3, label: 'Keep 3 backups' }, { value: 7, label: 'Keep 7 backups' }, { value: 14, label: 'Keep 14 backups' }, { value: 0, label: 'Keep all backups' }, ]; const ff = GoingsOn.ui.renderFormField; const content = `

Automatic backups protect your data by creating compressed snapshots on a schedule. Once cloud sync is configured, backups will also sync to your cloud provider.

${ff({ kind: 'select', name: 'backup-frequency', id: 'backup-frequency', label: 'Backup Frequency', value: settings.backupFrequencyMinutes, options: frequencyOptions.map(o => ({ value: String(o.value), label: o.label, selected: settings.backupFrequencyMinutes === o.value })), })} ${ff({ kind: 'select', name: 'backup-retention', id: 'backup-retention', label: 'Retention Policy', value: settings.maxBackupsToKeep, options: retentionOptions.map(o => ({ value: String(o.value), label: o.label, selected: settings.maxBackupsToKeep === o.value })), hint: 'Older backups are automatically deleted to save space.', })}

${esc(lastBackupText)}

`; GoingsOn.ui.openModal('Automatic Backup Settings', content); } /** * Saves the backup settings (inline in settings page data section). */ async function saveBackupSettings() { const enabled = document.getElementById('backup-enabled')?.checked; const frequency = parseInt(document.getElementById('backup-frequency')?.value, 10); const retention = parseInt(document.getElementById('backup-retention')?.value, 10); try { await GoingsOn.api.export.saveBackupSettings({ autoBackupEnabled: enabled, backupFrequencyMinutes: frequency, maxBackupsToKeep: retention, }); GoingsOn.ui.showToast('Backup settings saved'); } catch (err) { GoingsOn.ui.showToast('Failed to save settings: ' + GoingsOn.utils.getErrorMessage(err), 'error'); } } // ============ Populate GoingsOn Namespace ============ GoingsOn.export = GoingsOn.export || {}; Object.assign(GoingsOn.export, { exportJSON, exportTasksCSV, exportEventsICS, createBackup, openBackupsModal, restoreFromBackup, deleteBackup, openBackupSettingsModal, saveBackupSettings, }); })();