diff --git a/templates/account.html b/templates/account.html index 2dd4c3e..c819470 100644 --- a/templates/account.html +++ b/templates/account.html @@ -184,6 +184,34 @@ + + + Data + Export all coffee data as JSON, or restore from a previous backup. + + + + Download Backup + + + Restore from Backup + + + + + + + + + @@ -329,6 +357,99 @@ createTokenBtn.disabled = false; window.location.reload(); }); + + // --- Backup / Restore --- + const downloadBackupBtn = document.getElementById("download-backup-btn"); + const restoreBackupBtn = document.getElementById("restore-backup-btn"); + const restoreFileInput = document.getElementById("restore-file-input"); + const backupStatus = document.getElementById("backup-status"); + const backupError = document.getElementById("backup-error"); + + function showBackupStatus(message) { + backupError.classList.add("hidden"); + backupStatus.textContent = message; + backupStatus.classList.remove("hidden"); + } + + function showBackupError(message) { + backupStatus.classList.add("hidden"); + backupError.textContent = message; + backupError.classList.remove("hidden"); + } + + downloadBackupBtn.addEventListener("click", async function () { + downloadBackupBtn.disabled = true; + backupStatus.classList.add("hidden"); + backupError.classList.add("hidden"); + + try { + const response = await fetch("/api/v1/backup"); + if (!response.ok) { + throw new Error("Failed to download backup (HTTP " + response.status + ")."); + } + + const blob = await response.blob(); + const date = new Date().toISOString().slice(0, 10); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "brewlog-backup-" + date + ".json"; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + } catch (err) { + showBackupError(err.message); + } finally { + downloadBackupBtn.disabled = false; + } + }); + + restoreBackupBtn.addEventListener("click", function () { + restoreFileInput.click(); + }); + + restoreFileInput.addEventListener("change", async function () { + const file = restoreFileInput.files[0]; + if (!file) return; + restoreFileInput.value = ""; + + if (!confirm("Restore from backup? This will replace all data.\n\nThe database must be empty for restore to succeed.")) { + return; + } + + backupStatus.classList.add("hidden"); + backupError.classList.add("hidden"); + restoreBackupBtn.disabled = true; + + try { + const text = await file.text(); + JSON.parse(text); // validate JSON before sending + + const response = await fetch("/api/v1/backup/restore", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: text, + }); + + if (response.status === 409) { + throw new Error("Database is not empty. Restore requires an empty database."); + } + if (!response.ok) { + throw new Error("Restore failed (HTTP " + response.status + ")."); + } + + showBackupStatus("Backup restored successfully."); + } catch (err) { + if (err instanceof SyntaxError) { + showBackupError("Invalid JSON file."); + } else { + showBackupError(err.message); + } + } finally { + restoreBackupBtn.disabled = false; + } + }); }); // --- Global functions for inline onclick handlers ---
Export all coffee data as JSON, or restore from a previous backup.