Show which apps are in each backup + optional per-app manual backup
Prompted by today's testing: running backup-k8s-apps.sh --apps X repeatedly
for isolated per-app verification left a bunch of single-app archives in
the list, all named identically (myapps-k8s-backup-TIMESTAMP.tar.gz) with
no visible indication of which app(s) each one actually contains — the
data was already there (the .meta.json sidecar's `apps` field, used for
the status-dot rolling average) but only surfaced in a hover tooltip.
Cleaned up today's test archives (local + VM + R2) and ran one fresh full
backup so the list reflects real state.
Two things added, backup/restore logic itself untouched per explicit
instruction:
1. A small visible badge next to each backup's name — "All apps" for a
full bundle, or the specific list (e.g. "odoo, n8n") for a partial one
— in both the Jinja-rendered list and platform.js's refresh path.
2. An optional app-picker on the "Run Backup Now" manual trigger, same
checkbox-grid pattern already used on the Restore page's app selector.
All checked (default) = exactly today's existing behavior, no --apps
flag, everything bundled into one archive. Unchecking some = a
deliberate one-off partial backup (--apps a,b) for e.g. backing up just
odoo before a risky change without waiting on the other 4. The nightly
cron and "backup all together" behavior are completely unaffected —
this only touches the manual/UI-triggered path.
Verified: manual full backup still produces one bundled archive (249M, 5
apps); the apps-badge renders correctly ("All apps") on the live standby;
the --apps arg-construction logic unit-tested directly (partial selection
-> ['--apps', 'a,b'], all-selected -> [] i.e. default bundle).
This commit is contained in:
@@ -70,11 +70,11 @@ def _stream_restore(job_id, cmd):
|
|||||||
restore_jobs[job_id]['status'] = 'error'
|
restore_jobs[job_id]['status'] = 'error'
|
||||||
|
|
||||||
|
|
||||||
def _stream_backup(job_id, script_path):
|
def _stream_backup(job_id, script_path, extra_args=None):
|
||||||
backup_jobs[job_id] = {'status': 'running', 'log': [], 'started': time.time()}
|
backup_jobs[job_id] = {'status': 'running', 'log': [], 'started': time.time()}
|
||||||
try:
|
try:
|
||||||
proc = subprocess.Popen(
|
proc = subprocess.Popen(
|
||||||
['bash', script_path],
|
['bash', script_path] + (extra_args or []),
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
text=True, bufsize=1
|
text=True, bufsize=1
|
||||||
)
|
)
|
||||||
@@ -442,8 +442,21 @@ def api_backup_run():
|
|||||||
'message': f'Backup script not found at {script}'
|
'message': f'Backup script not found at {script}'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
|
# Optional {"apps": [...]} body — omitted, empty, or "all apps checked"
|
||||||
|
# all mean the same thing as before this existed: no --apps flag, every
|
||||||
|
# app bundled into one archive together (the script's own default).
|
||||||
|
# Only becomes --apps a,b when the UI's app-picker has SOME apps
|
||||||
|
# unchecked, for a deliberate one-off single/partial-app backup.
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
raw_apps = data.get('apps', [])
|
||||||
|
selected = sorted({
|
||||||
|
a.strip().lower() for a in raw_apps
|
||||||
|
if isinstance(a, str) and a.strip().lower() in ALLOWED_RESTORE_APPS
|
||||||
|
})
|
||||||
|
extra_args = ['--apps', ','.join(selected)] if selected and len(selected) < len(ALLOWED_RESTORE_APPS) else []
|
||||||
|
|
||||||
job_id = str(uuid.uuid4())
|
job_id = str(uuid.uuid4())
|
||||||
t = threading.Thread(target=_stream_backup, args=(job_id, script), daemon=True)
|
t = threading.Thread(target=_stream_backup, args=(job_id, script, extra_args), daemon=True)
|
||||||
t.start()
|
t.start()
|
||||||
return jsonify({'success': True, 'job_id': job_id, 'status': 'started'})
|
return jsonify({'success': True, 'job_id': job_id, 'status': 'started'})
|
||||||
|
|
||||||
|
|||||||
@@ -410,11 +410,15 @@ function renderBackupList(items, id, source) {
|
|||||||
const detailsBtn = typeof showBackupDetails === 'function'
|
const detailsBtn = typeof showBackupDetails === 'function'
|
||||||
? `<button class="btn btn-ghost btn-sm" style="color:var(--text3);" onclick="showBackupDetails('${source}','${safe}',this)" title="Quick details"><i class="fas fa-circle-info"></i></button>`
|
? `<button class="btn btn-ghost btn-sm" style="color:var(--text3);" onclick="showBackupDetails('${source}','${safe}',this)" title="Quick details"><i class="fas fa-circle-info"></i></button>`
|
||||||
: '';
|
: '';
|
||||||
|
const appsLabel = (b.apps && b.apps.length)
|
||||||
|
? `<span class="badge" style="background:rgba(59,130,246,0.12);color:var(--accent2);font-size:10px;white-space:nowrap;">${b.apps.length >= 5 ? 'All apps' : escapeHtml(b.apps.join(', '))}</span>`
|
||||||
|
: '';
|
||||||
return `
|
return `
|
||||||
<div class="backup-item" id="bk-item-${source}-${name.replace(/[^a-z0-9]/gi, '_')}">
|
<div class="backup-item" id="bk-item-${source}-${name.replace(/[^a-z0-9]/gi, '_')}">
|
||||||
<div class="backup-item-main">
|
<div class="backup-item-main">
|
||||||
<span class="status-dot status-dot-${status}" title="${title}"></span>
|
<span class="status-dot status-dot-${status}" title="${title}"></span>
|
||||||
<span class="backup-name">${escapeHtml(name)}</span>
|
<span class="backup-name">${escapeHtml(name)}</span>
|
||||||
|
${appsLabel}
|
||||||
</div>
|
</div>
|
||||||
<div class="backup-actions">
|
<div class="backup-actions">
|
||||||
${detailsBtn}
|
${detailsBtn}
|
||||||
@@ -489,7 +493,14 @@ async function runManualBackup() {
|
|||||||
if (wrapper) wrapper.style.display = '';
|
if (wrapper) wrapper.style.display = '';
|
||||||
if (logEl) logEl.innerHTML = '';
|
if (logEl) logEl.innerHTML = '';
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/backups/run', { method: 'POST' });
|
const selectedApps = Array.from(
|
||||||
|
document.querySelectorAll('#backup-apps-checkbox-group input[type="checkbox"]:checked')
|
||||||
|
).map((cb) => cb.value);
|
||||||
|
const r = await fetch('/api/backups/run', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ apps: selectedApps }),
|
||||||
|
});
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
if (!d.success) throw new Error(d.message || 'Failed');
|
if (!d.success) throw new Error(d.message || 'Failed');
|
||||||
manualBackupJobId = d.job_id;
|
manualBackupJobId = d.job_id;
|
||||||
|
|||||||
@@ -252,7 +252,52 @@
|
|||||||
<div class="card-title"><i class="fas fa-shield-halved"></i> Manual Backup</div>
|
<div class="card-title"><i class="fas fa-shield-halved"></i> Manual Backup</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding:4px 0 12px;">
|
<div style="padding:4px 0 12px;">
|
||||||
<p style="color:var(--text2);font-size:13px;margin-bottom:14px;">Manually trigger a backup of all containers</p>
|
<p style="color:var(--text2);font-size:13px;margin-bottom:10px;">
|
||||||
|
All apps are backed up together into one archive by default. Uncheck any app to leave it out —
|
||||||
|
useful for a quick one-off backup of a single app before a risky change, without waiting on the rest.
|
||||||
|
</p>
|
||||||
|
<div class="app-picker-grid" id="backup-apps-checkbox-group" style="margin-bottom:14px;">
|
||||||
|
<label class="app-picker-card">
|
||||||
|
<input type="checkbox" value="frappe" checked>
|
||||||
|
<div class="app-picker-body">
|
||||||
|
<span class="app-picker-icon">🧾</span>
|
||||||
|
<div><div class="app-picker-name">Frappe / ERPNext</div></div>
|
||||||
|
<span class="app-picker-check"><i class="fas fa-check"></i></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="app-picker-card">
|
||||||
|
<input type="checkbox" value="odoo" checked>
|
||||||
|
<div class="app-picker-body">
|
||||||
|
<span class="app-picker-icon">🟣</span>
|
||||||
|
<div><div class="app-picker-name">Odoo</div></div>
|
||||||
|
<span class="app-picker-check"><i class="fas fa-check"></i></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="app-picker-card">
|
||||||
|
<input type="checkbox" value="nextcloud" checked>
|
||||||
|
<div class="app-picker-body">
|
||||||
|
<span class="app-picker-icon">☁️</span>
|
||||||
|
<div><div class="app-picker-name">Nextcloud</div></div>
|
||||||
|
<span class="app-picker-check"><i class="fas fa-check"></i></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="app-picker-card">
|
||||||
|
<input type="checkbox" value="mautic" checked>
|
||||||
|
<div class="app-picker-body">
|
||||||
|
<span class="app-picker-icon">📣</span>
|
||||||
|
<div><div class="app-picker-name">Mautic</div></div>
|
||||||
|
<span class="app-picker-check"><i class="fas fa-check"></i></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label class="app-picker-card">
|
||||||
|
<input type="checkbox" value="n8n" checked>
|
||||||
|
<div class="app-picker-body">
|
||||||
|
<span class="app-picker-icon">🔁</span>
|
||||||
|
<div><div class="app-picker-name">n8n</div></div>
|
||||||
|
<span class="app-picker-check"><i class="fas fa-check"></i></span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<button class="btn btn-primary" onclick="runManualBackup()" id="manual-backup-btn">
|
<button class="btn btn-primary" onclick="runManualBackup()" id="manual-backup-btn">
|
||||||
<i class="fas fa-play"></i> Run Backup Now
|
<i class="fas fa-play"></i> Run Backup Now
|
||||||
</button>
|
</button>
|
||||||
@@ -288,6 +333,11 @@
|
|||||||
<div class="backup-item-main">
|
<div class="backup-item-main">
|
||||||
<span class="status-dot status-dot-{{ b.status }}" title="{{ b.status }}{% if b.apps %} · apps: {{ b.apps|join(', ') }}{% endif %}{% if b.size_human %} · {{ b.size_human }}{% endif %}"></span>
|
<span class="status-dot status-dot-{{ b.status }}" title="{{ b.status }}{% if b.apps %} · apps: {{ b.apps|join(', ') }}{% endif %}{% if b.size_human %} · {{ b.size_human }}{% endif %}"></span>
|
||||||
<span class="backup-name">{{ b.name }}</span>
|
<span class="backup-name">{{ b.name }}</span>
|
||||||
|
{% if b.apps %}
|
||||||
|
<span class="badge" style="background:rgba(59,130,246,0.12);color:var(--accent2);font-size:10px;white-space:nowrap;">
|
||||||
|
{% if b.apps|length >= 5 %}All apps{% else %}{{ b.apps|join(', ') }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="backup-actions">
|
<div class="backup-actions">
|
||||||
<button class="btn btn-ghost btn-sm" style="color:var(--text3);" onclick="showBackupDetails('local','{{ b.name }}',this)" title="Quick details"><i class="fas fa-circle-info"></i></button>
|
<button class="btn btn-ghost btn-sm" style="color:var(--text3);" onclick="showBackupDetails('local','{{ b.name }}',this)" title="Quick details"><i class="fas fa-circle-info"></i></button>
|
||||||
@@ -313,6 +363,11 @@
|
|||||||
<div class="backup-item-main">
|
<div class="backup-item-main">
|
||||||
<span class="status-dot status-dot-{{ b.status }}" title="{{ b.status }}{% if b.apps %} · apps: {{ b.apps|join(', ') }}{% endif %}{% if b.size_human %} · {{ b.size_human }}{% endif %}"></span>
|
<span class="status-dot status-dot-{{ b.status }}" title="{{ b.status }}{% if b.apps %} · apps: {{ b.apps|join(', ') }}{% endif %}{% if b.size_human %} · {{ b.size_human }}{% endif %}"></span>
|
||||||
<span class="backup-name">{{ b.name }}</span>
|
<span class="backup-name">{{ b.name }}</span>
|
||||||
|
{% if b.apps %}
|
||||||
|
<span class="badge" style="background:rgba(59,130,246,0.12);color:var(--accent2);font-size:10px;white-space:nowrap;">
|
||||||
|
{% if b.apps|length >= 5 %}All apps{% else %}{{ b.apps|join(', ') }}{% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="backup-actions">
|
<div class="backup-actions">
|
||||||
<button class="btn btn-ghost btn-sm" style="color:var(--text3);" onclick="showBackupDetails('vm','{{ b.name }}',this)" title="Quick details"><i class="fas fa-circle-info"></i></button>
|
<button class="btn btn-ghost btn-sm" style="color:var(--text3);" onclick="showBackupDetails('vm','{{ b.name }}',this)" title="Quick details"><i class="fas fa-circle-info"></i></button>
|
||||||
|
|||||||
Reference in New Issue
Block a user