From 14bc0c338602309819bd3aebf4a2de19df11f9f3 Mon Sep 17 00:00:00 2001 From: librelad Date: Wed, 27 May 2026 01:05:44 +0100 Subject: [PATCH] =?UTF-8?q?ui(backup):=20tile-click=20=E2=86=92=20Back-up?= =?UTF-8?q?=20checklist=20modal;=20LibrePortal=20icon=20on=20System=20tile?= =?UTF-8?q?;=202-up=20grid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reshape the dashboard's Backup status grid into a click-to-pick UI: - Removed the inline Back-up / Restore buttons from the System config tile. Same shape as an app tile now; LibrePortal app icon instead of the server-stack glyph. - Grid is 2 columns (was auto-fill min 220px). Tiles are wider, read better, and the System tile no longer needs to span a full row to fit inline buttons. - Click any tile (System or app) → opens a new "Back up" modal: * System config first (key=__system__, LibrePortal icon) * Every installed app, alphabetical * Checkbox per row + 'Select all' / 'Clear' shortcuts * The tile clicked is pre-ticked - Confirm queues backup tasks: * Everything ticked → single `libreportal backup all` (which also runs `backup system`) — one task instead of N * Subset → one task per ticked item (`backup system` and/or `backup app create `) Restore for System config used to live on the dashboard's inline 'Restore' button. It's now reachable via the Backups tab — system snapshots appear in the snapshot list with the standard per-row Restore action — same path apps already use. No new UI required; just one fewer dashboard button. Signed-off-by: librelad --- .../libreportal/frontend/css/backup.css | 5 +- .../frontend/html/backup-content.html | 14 ++ .../js/components/backup/backup-page.js | 158 ++++++++++++++---- 3 files changed, 147 insertions(+), 30 deletions(-) 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() {