From 9b631f527c60fd8626525ac939d23b6198b70a15 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 21 Aug 2026 15:15:13 +0200 Subject: [PATCH] Show which apps are in each backup + optional per-app manual backup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- platform/app.py | 19 +++++++-- platform/static/js/platform.js | 13 +++++- platform/templates/pages/backups.html | 57 ++++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/platform/app.py b/platform/app.py index 39a5330..9e29106 100644 --- a/platform/app.py +++ b/platform/app.py @@ -70,11 +70,11 @@ def _stream_restore(job_id, cmd): 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()} try: proc = subprocess.Popen( - ['bash', script_path], + ['bash', script_path] + (extra_args or []), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 ) @@ -442,8 +442,21 @@ def api_backup_run(): 'message': f'Backup script not found at {script}' }), 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()) - 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() return jsonify({'success': True, 'job_id': job_id, 'status': 'started'}) diff --git a/platform/static/js/platform.js b/platform/static/js/platform.js index 63a6db0..7adb33f 100644 --- a/platform/static/js/platform.js +++ b/platform/static/js/platform.js @@ -410,11 +410,15 @@ function renderBackupList(items, id, source) { const detailsBtn = typeof showBackupDetails === 'function' ? `` : ''; + const appsLabel = (b.apps && b.apps.length) + ? `${b.apps.length >= 5 ? 'All apps' : escapeHtml(b.apps.join(', '))}` + : ''; return `
${escapeHtml(name)} + ${appsLabel}
${detailsBtn} @@ -489,7 +493,14 @@ async function runManualBackup() { if (wrapper) wrapper.style.display = ''; if (logEl) logEl.innerHTML = ''; 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(); if (!d.success) throw new Error(d.message || 'Failed'); manualBackupJobId = d.job_id; diff --git a/platform/templates/pages/backups.html b/platform/templates/pages/backups.html index 7664284..f9cc961 100644 --- a/platform/templates/pages/backups.html +++ b/platform/templates/pages/backups.html @@ -252,7 +252,52 @@
Manual Backup
-

Manually trigger a backup of all containers

+

+ 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. +

+
+ + + + + +
@@ -288,6 +333,11 @@
{{ b.name }} + {% if b.apps %} + + {% if b.apps|length >= 5 %}All apps{% else %}{{ b.apps|join(', ') }}{% endif %} + + {% endif %}
@@ -313,6 +363,11 @@
{{ b.name }} + {% if b.apps %} + + {% if b.apps|length >= 5 %}All apps{% else %}{{ b.apps|join(', ') }}{% endif %} + + {% endif %}