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 %}