Skip to main content

max / goingson

12.0 KB · 329 lines History Blame Raw
1 /**
2 * GoingsOn - Export & Backup Module
3 * JSON/CSV/ICS export, backup creation, restore, and automatic backup settings
4 */
5
6 (function() {
7 'use strict';
8 const esc = GoingsOn.utils.escapeHtml;
9 const escAttr = GoingsOn.utils.escapeAttr;
10
11 // ============ Export Functions ============
12
13 /**
14 * Export all data as JSON.
15 */
16 async function exportJSON() {
17 try {
18 const { save } = window.__TAURI__.dialog;
19 const today = new Date().toISOString().slice(0, 10);
20
21 const filePath = await save({
22 defaultPath: `goingson-export-${today}.json`,
23 filters: [{ name: 'JSON', extensions: ['json'] }]
24 });
25
26 if (filePath) {
27 const result = await GoingsOn.api.export.json(filePath);
28 GoingsOn.ui.showToast(`Exported ${result.itemCount} items to JSON`);
29 }
30 } catch (err) {
31 GoingsOn.ui.showToast('Export failed: ' + GoingsOn.utils.getErrorMessage(err), 'error');
32 }
33 }
34
35 /**
36 * Export tasks as CSV.
37 */
38 async function exportTasksCSV() {
39 try {
40 const { save } = window.__TAURI__.dialog;
41 const today = new Date().toISOString().slice(0, 10);
42
43 const filePath = await save({
44 defaultPath: `goingson-tasks-${today}.csv`,
45 filters: [{ name: 'CSV', extensions: ['csv'] }]
46 });
47
48 if (filePath) {
49 const result = await GoingsOn.api.export.tasksCSV(filePath);
50 GoingsOn.ui.showToast(`Exported ${result.itemCount} tasks to CSV`);
51 }
52 } catch (err) {
53 GoingsOn.ui.showToast('Export failed: ' + GoingsOn.utils.getErrorMessage(err), 'error');
54 }
55 }
56
57 /**
58 * Export events as ICS calendar file.
59 */
60 async function exportEventsICS() {
61 try {
62 const { save } = window.__TAURI__.dialog;
63 const today = new Date().toISOString().slice(0, 10);
64
65 const filePath = await save({
66 defaultPath: `goingson-calendar-${today}.ics`,
67 filters: [{ name: 'iCalendar', extensions: ['ics'] }]
68 });
69
70 if (filePath) {
71 const result = await GoingsOn.api.export.eventsICS(filePath);
72 GoingsOn.ui.showToast(`Exported ${result.itemCount} events to ICS`);
73 }
74 } catch (err) {
75 GoingsOn.ui.showToast('Export failed: ' + GoingsOn.utils.getErrorMessage(err), 'error');
76 }
77 }
78
79 // ============ Backup Functions ============
80
81 /**
82 * Create a compressed backup.
83 */
84 async function createBackup() {
85 try {
86 const result = await GoingsOn.api.export.createBackup();
87 GoingsOn.ui.showToast(`Backup created with ${result.itemCount} items`);
88 } catch (err) {
89 GoingsOn.ui.showToast('Backup failed: ' + GoingsOn.utils.getErrorMessage(err), 'error');
90 }
91 }
92
93 /**
94 * Open the backups management modal.
95 */
96 async function openBackupsModal() {
97 GoingsOn.ui.closeModal();
98
99 let backups = [];
100 try {
101 backups = await GoingsOn.api.export.listBackups();
102 } catch (err) {
103 GoingsOn.ui.showToast('Failed to load backups: ' + GoingsOn.utils.getErrorMessage(err), 'error');
104 return;
105 }
106
107 const backupsList = backups.length === 0
108 ? '<p class="backups-empty">No backups found</p>'
109 : backups.map(backup => {
110 const date = new Date(backup.createdAt * 1000);
111 const dateStr = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
112 const sizeStr = formatBytes(backup.sizeBytes);
113
114 return `
115 <div class="backup-item">
116 <div>
117 <div class="backup-item-name">${esc(backup.fileName)}</div>
118 <div class="backup-item-meta">${dateStr} - ${sizeStr}</div>
119 </div>
120 <div class="backup-item-actions">
121 <button class="btn btn-sm btn-secondary" onclick="GoingsOn.export.restoreFromBackup('${escAttr(backup.filePath)}')">Restore</button>
122 <button class="btn btn-sm btn-danger" onclick="GoingsOn.export.deleteBackup('${escAttr(backup.filePath)}')">Delete</button>
123 </div>
124 </div>
125 `;
126 }).join('');
127
128 const content = `
129 <div style="max-height: 400px; overflow-y: auto;">
130 ${backupsList}
131 </div>
132
133 <div class="form-actions" style="margin-top: 1.5rem;">
134 <button class="btn btn-secondary" onclick="GoingsOn.export.createBackup(); GoingsOn.ui.closeModal(); setTimeout(() => GoingsOn.export.openBackupsModal(), 500);">
135 Create New Backup
136 </button>
137 <div class="flex-1"></div>
138 <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal()">Close</button>
139 </div>
140 `;
141
142 GoingsOn.ui.openModal('Manage Backups', content);
143 }
144
145 /**
146 * Restore data from a backup file after confirmation.
147 * @param {string} filePath - Absolute path to the backup file
148 */
149 async function restoreFromBackup(filePath) {
150 const confirmed = await GoingsOn.ui.confirmDelete(
151 'Restore from Backup',
152 'This will import data from the backup. Existing items with the same IDs will be skipped. Do you want to continue?'
153 );
154
155 if (!confirmed) return;
156
157 try {
158 const result = await GoingsOn.api.export.restoreBackup(filePath, { replaceAll: false });
159 const total = result.projectsRestored + result.tasksRestored + result.eventsRestored + result.emailsRestored;
160 GoingsOn.ui.showToast(`Restored ${total} items from backup`);
161 GoingsOn.ui.closeModal();
162
163 // Reload data
164 GoingsOn.projects.load();
165 GoingsOn.tasks.load();
166 GoingsOn.events.load();
167 GoingsOn.emails.load();
168 } catch (err) {
169 GoingsOn.ui.showToast('Restore failed: ' + GoingsOn.utils.getErrorMessage(err), 'error');
170 }
171 }
172
173 /**
174 * Delete a backup file after confirmation.
175 * @param {string} filePath - Absolute path to the backup file
176 */
177 async function deleteBackup(filePath) {
178 const confirmed = await GoingsOn.ui.confirmDelete(
179 'Delete Backup',
180 'Are you sure you want to delete this backup? This cannot be undone.'
181 );
182
183 if (!confirmed) return;
184
185 try {
186 await GoingsOn.api.export.deleteBackup(filePath);
187 GoingsOn.ui.showToast('Backup deleted');
188 // Refresh the backups list
189 openBackupsModal();
190 } catch (err) {
191 GoingsOn.ui.showToast('Delete failed: ' + GoingsOn.utils.getErrorMessage(err), 'error');
192 }
193 }
194
195 /**
196 * Format a byte count as a human-readable size string.
197 * @param {number} bytes - Number of bytes
198 * @returns {string} Formatted string (e.g., "1.5 MB", "256 KB")
199 */
200 function formatBytes(bytes) {
201 if (bytes === 0) return '0 Bytes';
202 const k = 1024;
203 const sizes = ['Bytes', 'KB', 'MB', 'GB'];
204 const i = Math.floor(Math.log(bytes) / Math.log(k));
205 return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
206 }
207
208 // ============ Backup Settings ============
209
210 /**
211 * Opens the automatic backup settings modal.
212 */
213 async function openBackupSettingsModal() {
214 let settings = null;
215 try {
216 settings = await GoingsOn.api.export.getBackupSettings();
217 } catch (err) {
218 console.error('Failed to load backup settings:', err);
219 settings = {
220 autoBackupEnabled: true,
221 backupFrequencyMinutes: 15,
222 maxBackupsToKeep: 1,
223 lastBackupAt: null,
224 };
225 }
226
227 const lastBackupText = settings.lastBackupAt
228 ? `Last backup: ${new Date(settings.lastBackupAt).toLocaleString()}`
229 : 'No backups yet';
230
231 const frequencyOptions = [
232 { value: 15, label: 'Every 15 minutes (Recommended)' },
233 { value: 30, label: 'Every 30 minutes' },
234 { value: 60, label: 'Every hour' },
235 { value: 360, label: 'Every 6 hours' },
236 { value: 1440, label: 'Daily' },
237 ];
238
239 const retentionOptions = [
240 { value: 1, label: 'Keep 1 backup (Recommended)' },
241 { value: 3, label: 'Keep 3 backups' },
242 { value: 7, label: 'Keep 7 backups' },
243 { value: 14, label: 'Keep 14 backups' },
244 { value: 0, label: 'Keep all backups' },
245 ];
246
247 const ff = GoingsOn.ui.renderFormField;
248 const content = `
249 <p class="export-desc">
250 Automatic backups protect your data by creating compressed snapshots on a schedule.
251 Once cloud sync is configured, backups will also sync to your cloud provider.
252 </p>
253
254 <div class="form-group">
255 <label class="form-checkbox-label">
256 <input type="checkbox" id="backup-enabled" ${settings.autoBackupEnabled ? 'checked' : ''}>
257 <span>Enable automatic backups</span>
258 </label>
259 </div>
260
261 ${ff({
262 kind: 'select',
263 name: 'backup-frequency',
264 id: 'backup-frequency',
265 label: 'Backup Frequency',
266 value: settings.backupFrequencyMinutes,
267 options: frequencyOptions.map(o => ({ value: String(o.value), label: o.label, selected: settings.backupFrequencyMinutes === o.value })),
268 })}
269
270 ${ff({
271 kind: 'select',
272 name: 'backup-retention',
273 id: 'backup-retention',
274 label: 'Retention Policy',
275 value: settings.maxBackupsToKeep,
276 options: retentionOptions.map(o => ({ value: String(o.value), label: o.label, selected: settings.maxBackupsToKeep === o.value })),
277 hint: 'Older backups are automatically deleted to save space.',
278 })}
279
280 <div class="export-note">
281 <p class="export-note-text">${esc(lastBackupText)}</p>
282 </div>
283
284 <div class="form-actions form-actions--spaced">
285 <button type="button" class="btn btn-secondary" onclick="GoingsOn.ui.closeModal(); GoingsOn.settings.open();">Cancel</button>
286 <button type="button" class="btn btn-primary" onclick="GoingsOn.export.saveBackupSettings()">Save Settings</button>
287 </div>
288 `;
289
290 GoingsOn.ui.openModal('Automatic Backup Settings', content);
291 }
292
293 /**
294 * Saves the backup settings (inline in settings page data section).
295 */
296 async function saveBackupSettings() {
297 const enabled = document.getElementById('backup-enabled')?.checked;
298 const frequency = parseInt(document.getElementById('backup-frequency')?.value, 10);
299 const retention = parseInt(document.getElementById('backup-retention')?.value, 10);
300
301 try {
302 await GoingsOn.api.export.saveBackupSettings({
303 autoBackupEnabled: enabled,
304 backupFrequencyMinutes: frequency,
305 maxBackupsToKeep: retention,
306 });
307 GoingsOn.ui.showToast('Backup settings saved');
308 } catch (err) {
309 GoingsOn.ui.showToast('Failed to save settings: ' + GoingsOn.utils.getErrorMessage(err), 'error');
310 }
311 }
312
313 // ============ Populate GoingsOn Namespace ============
314
315 GoingsOn.export = GoingsOn.export || {};
316 Object.assign(GoingsOn.export, {
317 exportJSON,
318 exportTasksCSV,
319 exportEventsICS,
320 createBackup,
321 openBackupsModal,
322 restoreFromBackup,
323 deleteBackup,
324 openBackupSettingsModal,
325 saveBackupSettings,
326 });
327
328 })();
329