Skip to main content

max / goingson

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