From 5d0247159b125bf035285d56c2b9bb58d6bb3029 Mon Sep 17 00:00:00 2001
From: Matthias Nott <mnott@mnsoft.org>
Date: Thu, 26 Feb 2026 14:39:34 +0100
Subject: [PATCH] feat: backup download endpoint + skip-backup sync option
---
static/js/app.js | 18 ++++++++++++++++--
1 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/static/js/app.js b/static/js/app.js
index a23028a..5621618 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -1,5 +1,5 @@
'use strict';
-const APP_VERSION = 'v14-20260222';
+const APP_VERSION = 'v15-20260226';
// ============================================================
// OPS Dashboard — Vanilla JS Application (v6)
@@ -827,6 +827,9 @@
const downloadBtn = (!b.hasLocal && b.hasOffsite)
? `<button class="btn btn-ghost btn-xs" style="color:#34d399;border-color:rgba(52,211,153,0.25);" onclick="downloadOffsiteBackup('${esc(b.project)}','${esc(b.env)}','${esc(b.name)}')">Download</button>`
: '';
+ const saveBtn = b.hasLocal
+ ? `<button class="btn btn-ghost btn-xs" style="color:#60a5fa;border-color:rgba(96,165,250,0.25);" onclick="downloadBackupFile('${esc(b.project)}','${esc(b.env)}','${esc(b.name)}')">Save</button>`
+ : '';
h += `<tr>
<td style="padding-left:0.75rem;"><input type="checkbox" class="backup-cb" value="${esc(b.name)}"${checked} onclick="toggleBackupSelect('${esc(b.name)}')" style="accent-color:#3b82f6;cursor:pointer;"></td>
<td>${locationBadge}</td>
@@ -834,6 +837,7 @@
<td>${esc(b.size_human || '\u2014')}</td>
<td style="white-space:nowrap;">
<button class="btn btn-danger btn-xs" onclick="openRestoreModal('${esc(b.project)}','${esc(b.env)}','${restoreSource}','${esc(b.name)}',${b.hasLocal},${b.hasOffsite})">Restore</button>
+ ${saveBtn}
${uploadBtn}
${downloadBtn}
${deleteBtn}
@@ -927,6 +931,10 @@
cachedBackups = null;
toast(`Deleted ${ok}${fail > 0 ? ', ' + fail + ' failed' : ''}`, fail > 0 ? 'warning' : 'success');
if (currentPage === 'backups') renderBackups();
+}
+
+function downloadBackupFile(project, env, name) {
+ window.open(`/api/backups/download/${encodeURIComponent(project)}/${encodeURIComponent(env)}/${encodeURIComponent(name)}?token=${encodeURIComponent(getToken())}`, '_blank');
}
async function uploadOffsiteBackup(project, env, name) {
@@ -1702,6 +1710,10 @@
ih += '<span style="font-size:0.75rem;color:#6b7280;margin-left:auto;">content flows up</span>';
ih += '</label>';
ih += '</div>';
+ ih += '<label style="display:flex;align-items:center;gap:0.5rem;font-size:0.875rem;color:#d1d5db;cursor:pointer;margin-top:0.875rem;">';
+ ih += '<input type="checkbox" id="ops-sync-skip-backup" style="width:1rem;height:1rem;accent-color:#f59e0b;">';
+ ih += 'Skip safety backup <span style="font-size:0.75rem;color:#f59e0b;margin-left:0.25rem;">(faster, no pre-sync snapshot)</span>';
+ ih += '</label>';
info.innerHTML = ih;
startBtn.className = 'btn btn-primary btn-sm';
@@ -1871,7 +1883,9 @@
if (type === 'promote') {
url = '/api/promote/' + encodeURIComponent(project) + '/' + encodeURIComponent(fromEnv) + '/' + encodeURIComponent(toEnv) + '?dry_run=' + dryRun + '&token=' + encodeURIComponent(getToken());
} else if (type === 'sync') {
- url = '/api/sync/' + encodeURIComponent(project) + '?from=' + encodeURIComponent(fromEnv) + '&to=' + encodeURIComponent(toEnv) + '&dry_run=' + dryRun + '&token=' + encodeURIComponent(getToken());
+ const skipBackupEl = document.getElementById('ops-sync-skip-backup');
+ const skipBackup = skipBackupEl ? skipBackupEl.checked : false;
+ url = '/api/sync/' + encodeURIComponent(project) + '?from=' + encodeURIComponent(fromEnv) + '&to=' + encodeURIComponent(toEnv) + '&dry_run=' + dryRun + (skipBackup ? '&skip_backup=true' : '') + '&token=' + encodeURIComponent(getToken());
} else if (type === 'restart' || type === 'rebuild') {
url = '/api/rebuild/' + encodeURIComponent(project) + '/' + encodeURIComponent(fromEnv)
+ '?action=' + encodeURIComponent(type) + '&token=' + encodeURIComponent(getToken());
--
Gitblit v1.3.1