feat(account): add backup download and restore UI
Add a Data section to the account page with buttons to download a JSON backup and restore from a previously exported backup file.
This commit is contained in:
parent
6a0d94739b
commit
69b039b02d
1 changed files with 121 additions and 0 deletions
|
|
@ -184,6 +184,34 @@
|
|||
</button>
|
||||
</section>
|
||||
|
||||
<!-- Data -->
|
||||
<section>
|
||||
<h2 class="text-lg font-semibold text-amber-700 mb-3">Data</h2>
|
||||
<p class="text-sm text-stone-500 mb-3">Export all coffee data as JSON, or restore from a previous backup.</p>
|
||||
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button
|
||||
id="download-backup-btn"
|
||||
type="button"
|
||||
class="rounded-md bg-amber-600 px-4 py-2 text-sm font-medium text-amber-50 transition hover:bg-amber-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Download Backup
|
||||
</button>
|
||||
<button
|
||||
id="restore-backup-btn"
|
||||
type="button"
|
||||
class="rounded-md border border-stone-300 bg-white px-4 py-2 text-sm font-medium text-stone-600 transition hover:bg-stone-50 hover:text-stone-800"
|
||||
>
|
||||
Restore from Backup
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input type="file" id="restore-file-input" accept=".json" class="hidden" />
|
||||
|
||||
<div id="backup-status" class="mt-3 hidden rounded-lg border border-green-300 bg-green-50 p-4 text-sm text-green-800"></div>
|
||||
<div id="backup-error" class="mt-3 hidden rounded-md bg-red-100 border border-red-300 p-3 text-sm text-red-800"></div>
|
||||
</section>
|
||||
|
||||
<!-- Sign Out -->
|
||||
<section>
|
||||
<form method="post" action="/logout">
|
||||
|
|
@ -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 ---
|
||||
|
|
|
|||
Loading…
Reference in a new issue