feat: Add Manual Backup button in Hub sidebar with drive detection and progress streaming

Agent-Logs-Url: https://github.com/naturallaw777/staging_alpha/sessions/14dc5955-19b2-4e5b-965a-2795285a22fd

Co-authored-by: naturallaw777 <99053422+naturallaw777@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-05 03:24:07 +00:00
committed by GitHub
parent d59b878906
commit d864402de2
5 changed files with 539 additions and 5 deletions

View File

@@ -360,3 +360,17 @@
border-color: #a8c8ff;
border-style: solid;
}
/* ── Manual Backup ───────────────────────────────────────────────── */
.support-backup-steps {
padding-left: 20px;
font-size: 0.85rem;
color: var(--text-secondary);
line-height: 1.8;
margin: 8px 0 0 0;
}
.support-backup-steps li {
margin-bottom: 4px;
}

View File

@@ -259,3 +259,215 @@ function closeSupportModal() {
stopSupportTimer();
stopWalletUnlockTimer();
}
// ── Manual Backup modal ───────────────────────────────────────────
var _backupPollTimer = null;
var _backupLogOffset = 0;
function openBackupModal() {
if (!$supportModal) return;
$supportModal.classList.add("open");
$supportBody.innerHTML = '<p class="creds-loading">Detecting external drives\u2026</p>';
detectDrivesAndRender();
}
async function detectDrivesAndRender() {
try {
// Check whether a backup is already in progress
var status = await apiFetch("/api/backup/status?offset=0");
if (status.running) {
renderBackupRunning();
_backupLogOffset = status.offset || 0;
if (status.log) {
var logDiv = document.getElementById("backup-log");
if (logDiv) { logDiv.insertAdjacentText("beforeend", status.log); logDiv.scrollTop = logDiv.scrollHeight; }
}
startBackupPoll();
return;
}
} catch (_) {}
try {
var data = await apiFetch("/api/backup/drives");
renderBackupReady(data.drives || []);
} catch (err) {
$supportBody.innerHTML = '<p class="creds-empty">Could not detect drives. Please try again.</p>';
}
}
function renderBackupReady(drives) {
var driveSelector = "";
if (drives.length > 0) {
driveSelector = [
'<label class="support-info-label" style="display:block;margin-bottom:6px;">Select drive:</label>',
'<select id="backup-drive-select" class="support-unlock-select" style="width:100%;margin-bottom:14px;">',
].join("");
for (var i = 0; i < drives.length; i++) {
var d = drives[i];
driveSelector += '<option value="' + escHtml(d.path) + '">' +
escHtml(d.name) + ' \u2014 ' + d.free_gb + ' GB free / ' + d.total_gb + ' GB total' +
'</option>';
}
driveSelector += '</select>';
driveSelector += '<button class="btn support-btn-enable" id="btn-start-backup">Start Backup</button>';
} else {
driveSelector = [
'<div class="support-wallet-box support-wallet-warning">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u26a0\ufe0f</span>',
'<span class="support-wallet-title">No External Drive Detected</span>',
'</div>',
'<p class="support-wallet-desc">',
'No USB drive was found under /run/media/. ',
'Make sure the drive is plugged in and mounted, then click Refresh.',
'</p>',
'</div>',
'<button class="btn support-btn-auditlog" id="btn-backup-refresh">&#x21bb; Refresh</button>',
].join("");
}
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">\ud83d\udcbe</div>',
'<h3 class="support-heading">Manual Backup</h3>',
'<p class="support-desc">Back up your Sovran_SystemsOS data to an external USB hard drive.</p>',
'<div class="support-steps">',
'<div class="support-steps-title">Requirements</div>',
'<ol class="support-backup-steps">',
'<li>USB hard drive plugged into one of the open USB ports on your Sovran Pro</li>',
'<li>At least 500 GB of free space on the drive</li>',
'<li>Drive must be formatted as <strong>exFAT</strong></li>',
'</ol>',
'</div>',
'<div class="support-steps">',
'<div class="support-steps-title">What gets backed up</div>',
'<ol class="support-backup-steps">',
'<li>NixOS configuration (<code>/etc/nixos</code>)</li>',
'<li>Bitcoin &amp; Lightning wallet data (<code>/var/lib/lnd</code>)</li>',
'<li>nix-bitcoin secrets (<code>/etc/nix-bitcoin-secrets</code>)</li>',
'<li>Domain configurations (<code>/var/lib/domains</code>)</li>',
'<li>Home directory (<code>/home</code>)</li>',
'</ol>',
'</div>',
'<div class="support-wallet-box support-wallet-protected">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u23f1\ufe0f</span>',
'<span class="support-wallet-title">Time Estimate</span>',
'</div>',
'<p class="support-wallet-desc">This backup can take <strong>up to 4 hours</strong> depending on the amount of data stored on your Sovran Pro and the speed of your external hard drive. Be patient\u2026</p>',
'</div>',
driveSelector,
'</div>',
].join("");
if (drives.length > 0) {
document.getElementById("btn-start-backup").addEventListener("click", startBackup);
} else {
document.getElementById("btn-backup-refresh").addEventListener("click", function() {
$supportBody.innerHTML = '<p class="creds-loading">Detecting external drives\u2026</p>';
detectDrivesAndRender();
});
}
}
async function startBackup() {
var btn = document.getElementById("btn-start-backup");
if (btn) { btn.disabled = true; btn.textContent = "Starting\u2026"; }
var sel = document.getElementById("backup-drive-select");
var target = sel ? sel.value : "";
try {
_backupLogOffset = 0;
await apiFetch("/api/backup/run" + (target ? "?target=" + encodeURIComponent(target) : ""), { method: "POST" });
renderBackupRunning();
startBackupPoll();
} catch (err) {
if (btn) { btn.disabled = false; btn.textContent = "Start Backup"; }
alert("Failed to start backup: " + (err.message || "Unknown error"));
}
}
function renderBackupRunning() {
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big support-active-icon">\ud83d\udcbe</div>',
'<h3 class="support-heading support-active-heading">Backup In Progress</h3>',
'<div class="support-wallet-box support-wallet-warning">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u26a0\ufe0f</span>',
'<span class="support-wallet-title">Do Not Unplug</span>',
'</div>',
'<p class="support-wallet-desc">Do not remove the USB drive while the backup is running. This could corrupt the backup and your drive.</p>',
'</div>',
'<div class="modal-log" id="backup-log" style="text-align:left;"></div>',
'</div>',
].join("");
}
function startBackupPoll() {
stopBackupPoll();
_backupPollTimer = setInterval(pollBackupStatus, 2000);
pollBackupStatus();
}
function stopBackupPoll() {
if (_backupPollTimer) { clearInterval(_backupPollTimer); _backupPollTimer = null; }
}
async function pollBackupStatus() {
try {
var data = await apiFetch("/api/backup/status?offset=" + _backupLogOffset);
var logDiv = document.getElementById("backup-log");
if (logDiv && data.log) {
logDiv.insertAdjacentText("beforeend", data.log);
logDiv.scrollTop = logDiv.scrollHeight;
}
_backupLogOffset = data.offset;
if (!data.running) {
stopBackupPoll();
renderBackupDone(data.result === "success");
}
} catch (_) {}
}
function renderBackupDone(success) {
var logDiv = document.getElementById("backup-log");
var logContent = logDiv ? logDiv.textContent : "";
if (success) {
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">\u2705</div>',
'<h3 class="support-heading">All Finished!</h3>',
'<div class="support-wallet-box support-wallet-protected">',
'<div class="support-wallet-header">',
'<span class="support-wallet-icon">\u23cf\ufe0f</span>',
'<span class="support-wallet-title">Eject Your Drive</span>',
'</div>',
'<p class="support-wallet-desc">Please eject the drive before removing it from your Sovran Pro.</p>',
'</div>',
'<div class="modal-log" id="backup-log-done" style="text-align:left;"></div>',
'<button class="btn support-btn-done" id="btn-backup-close">Close</button>',
'</div>',
].join("");
var doneLog = document.getElementById("backup-log-done");
if (doneLog) { doneLog.textContent = logContent; doneLog.scrollTop = doneLog.scrollHeight; }
} else {
$supportBody.innerHTML = [
'<div class="support-section">',
'<div class="support-icon-big">\u26a0\ufe0f</div>',
'<h3 class="support-heading">Backup Failed</h3>',
'<p class="support-desc">The backup did not complete successfully. Please check that the USB drive is still connected, has enough free space, and is formatted as exFAT. Then try again.</p>',
'<div class="modal-log" id="backup-log-fail" style="text-align:left;"></div>',
'<button class="btn support-btn-done" id="btn-backup-close">Close</button>',
'</div>',
].join("");
var failLog = document.getElementById("backup-log-fail");
if (failLog) { failLog.textContent = logContent; failLog.scrollTop = failLog.scrollHeight; }
}
document.getElementById("btn-backup-close").addEventListener("click", closeSupportModal);
}

View File

@@ -58,11 +58,22 @@ function renderSidebarSupport(supportServices) {
btn.addEventListener("click", function() { openSupportModal(); });
$sidebarSupport.appendChild(btn);
}
if (supportServices.length > 0) {
var hr = document.createElement("hr");
hr.className = "sidebar-divider";
$sidebarSupport.appendChild(hr);
}
// ── Manual Backup button
var backupBtn = document.createElement("button");
backupBtn.className = "sidebar-support-btn";
backupBtn.innerHTML =
'<span class="sidebar-support-icon">💾</span>' +
'<span class="sidebar-support-text">' +
'<span class="sidebar-support-title">Manual Backup</span>' +
'<span class="sidebar-support-hint">Back up to external drive</span>' +
'</span>';
backupBtn.addEventListener("click", function() { openBackupModal(); });
$sidebarSupport.appendChild(backupBtn);
var hr = document.createElement("hr");
hr.className = "sidebar-divider";
$sidebarSupport.appendChild(hr);
}
function buildTile(svc) {