diff --git a/containers/libreportal/frontend/css/backup.css b/containers/libreportal/frontend/css/backup.css index 801ba3c..be9b66e 100755 --- a/containers/libreportal/frontend/css/backup.css +++ b/containers/libreportal/frontend/css/backup.css @@ -205,7 +205,10 @@ .backup-app-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + /* 2 per row — clicking a tile opens the Back-up checklist modal, so we + no longer need room for inline action buttons. Wider tiles read + better and the System config tile fits one row alongside an app. */ + grid-template-columns: repeat(2, 1fr); gap: 12px; } diff --git a/containers/libreportal/frontend/html/backup-content.html b/containers/libreportal/frontend/html/backup-content.html index 1455527..b8d2796 100644 --- a/containers/libreportal/frontend/html/backup-content.html +++ b/containers/libreportal/frontend/html/backup-content.html @@ -174,6 +174,20 @@ +
+
+
+

Back up

+ +
+
+ +
+
+
diff --git a/containers/libreportal/frontend/js/components/backup/backup-page.js b/containers/libreportal/frontend/js/components/backup/backup-page.js index 707f9c5..329db86 100644 --- a/containers/libreportal/frontend/js/components/backup/backup-page.js +++ b/containers/libreportal/frontend/js/components/backup/backup-page.js @@ -186,6 +186,24 @@ class BackupPage { return; } + // Click any tile on the dashboard → open the Back-up checklist + // modal with that tile pre-ticked. System tile is data-system="1"; + // app tiles carry data-app="". Both share .backup-app-tile. + const tile = e.target.closest('.backup-app-tile'); + if (tile) { + if (tile.dataset.system) { + this.openBackupPickModal({ preTickSystem: true }); + } else if (tile.dataset.app) { + this.openBackupPickModal({ preTickApps: [tile.dataset.app] }); + } + return; + } + + if (e.target.closest('#backup-pick-confirm')) { + this.confirmBackupPick(); + return; + } + const restoreBtn = e.target.closest('[data-action="restore-snapshot"]'); if (restoreBtn) { this.openRestoreModal(restoreBtn.dataset.app, restoreBtn.dataset.loc, restoreBtn.dataset.snapshot); @@ -614,46 +632,25 @@ class BackupPage { } } - // System config tile — same shape as an app tile but with a server icon - // and two inline action buttons (the standalone System config card used - // to host these). Rendered first in the Backup status grid so it always - // sits at eye-level. + // System config tile — same shape as an app tile but with the LibrePortal + // app icon. Clicking any tile (system or app) opens the Back-up checklist + // modal with that tile pre-ticked; there are no inline action buttons + // anymore. Rendered first in the Backup status grid so the bare-metal + // prerequisite is always visible up top. renderSystemTile(sys) { const has = !!sys.latest_snapshot; const dot = has ? 'ok' : 'none'; const when = has ? 'Last backed up ' + this.formatRelative(sys.latest_time) : 'No backup yet'; return ` -
-
- - - - - - -
-
+
+ +
System config
${this.escape(when)}
-
- - -
`; } @@ -1833,6 +1830,109 @@ class BackupPage { await this.runTask(`libreportal restore system`, 'restore', null); } + /* ----- Back-up checklist modal ----- + Triggered by clicking any tile in the Backup status grid. Lists System + config first (special row, key="__system__"), then every installed app. + Pre-ticks the tile that was clicked. Confirm queues one backup task + per ticked item — except when EVERYTHING is ticked, in which case we + collapse to `libreportal backup all` (which also runs `backup system` + under the hood) so we only queue one task instead of N. */ + + openBackupPickModal(opts = {}) { + const modal = document.getElementById('backup-pick-modal'); + const body = document.getElementById('backup-pick-modal-body'); + if (!modal || !body) return; + + const apps = this.dashboard?.apps || []; + const sys = this.dashboard?.system || {}; + const preTickSystem = !!opts.preTickSystem; + const preTickApps = new Set(opts.preTickApps || []); + + // System row at the top — uses the LibrePortal icon to match the + // dashboard tile. Then every installed app, alphabetical. + const sortedApps = apps.slice().sort((a, b) => a.app.localeCompare(b.app)); + + const row = (key, iconSrc, label, sub, checked) => ` + + `; + + const sysSub = sys.latest_snapshot + ? 'Last backed up ' + this.formatRelative(sys.latest_time) + : 'No backup yet'; + + const rows = [ + row('__system__', '/icons/apps/libreportal.svg', 'System config', sysSub, preTickSystem), + ...sortedApps.map(app => { + const meta = this.appMeta(app.app); + const sub = app.latest_snapshot + ? 'Last backed up ' + this.formatRelative(app.latest_time) + : 'No backup yet'; + return row(app.app, meta.icon, meta.displayName, sub, preTickApps.has(app.app)); + }) + ].join(''); + + body.innerHTML = ` +

+ Pick what to snapshot. Each selection runs in the background and shows up on the Tasks page. +

+ +
${rows}
+ `; + + // The select-all / clear links live inside the modal body so we wire + // them once here per open (they get rebuilt every open, no listener + // leak). + body.querySelectorAll('[data-pick-action]').forEach(a => { + a.addEventListener('click', (e) => { + e.preventDefault(); + const all = a.dataset.pickAction === 'select-all'; + body.querySelectorAll('.backup-pick-cb').forEach(cb => { cb.checked = all; }); + }); + }); + + modal.classList.add('open'); + } + + async confirmBackupPick() { + const modal = document.getElementById('backup-pick-modal'); + if (!modal) return; + const selected = Array.from(modal.querySelectorAll('.backup-pick-cb:checked')) + .map(cb => cb.value); + if (!selected.length) { + this.notify('Pick at least one thing to back up.', 'error'); + return; + } + this.closeAllModals(); + + const apps = this.dashboard?.apps || []; + const totalThings = apps.length + 1; // +1 for system + const wantsSystem = selected.includes('__system__'); + const appSlugs = selected.filter(s => s !== '__system__'); + + // Whole-fleet shortcut — `backup all` queues a single task and also + // covers system, instead of N+1 separate tasks. + if (wantsSystem && appSlugs.length === apps.length) { + await this.runTask('libreportal backup all', 'backup', null); + return; + } + if (wantsSystem) { + await this.runTask('libreportal backup system', 'backup', null); + } + for (const slug of appSlugs) { + await this.runTask(`libreportal backup app create ${slug}`, 'backup', slug); + } + } + /* ----- Migrate (Phase 1: shared-backup) ----- */ renderMigrate() {