diff --git a/containers/libreportal/frontend/html/backup-content.html b/containers/libreportal/frontend/html/backup-content.html index d1679a0..fc9e6ec 100644 --- a/containers/libreportal/frontend/html/backup-content.html +++ b/containers/libreportal/frontend/html/backup-content.html @@ -111,21 +111,10 @@ -
- - - - - - - - - - - - -
AppHostLocationWhenIDActions
-
+ +
diff --git a/containers/libreportal/frontend/js/components/backup/backup-page.js b/containers/libreportal/frontend/js/components/backup/backup-page.js index 371d996..ca64b7f 100644 --- a/containers/libreportal/frontend/js/components/backup/backup-page.js +++ b/containers/libreportal/frontend/js/components/backup/backup-page.js @@ -234,16 +234,28 @@ class BackupPage { const restoreBtn = e.target.closest('[data-action="restore-snapshot"]'); if (restoreBtn) { + e.stopPropagation(); // don't also toggle the row's details panel this.openRestoreModal(restoreBtn.dataset.app, restoreBtn.dataset.loc, restoreBtn.dataset.snapshot); return; } const deleteBtn = e.target.closest('[data-action="delete-snapshot"]'); if (deleteBtn) { + e.stopPropagation(); this.openDeleteModal(deleteBtn.dataset.app, deleteBtn.dataset.loc, deleteBtn.dataset.snapshot); return; } + // Row header / Details button → toggle the .task-details panel. + // Matches the per-app Backups tab interaction. + const snapToggle = e.target.closest('[data-action="toggle-snapshot-row"]'); + if (snapToggle) { + const item = snapToggle.closest('.backup-snapshot-item'); + const details = item && item.querySelector('.task-details'); + if (details) details.classList.toggle('task-details-open'); + return; + } + const locEnable = e.target.closest('[data-action="toggle-location-enabled"]'); if (locEnable) { const cb = locEnable.querySelector('input[type="checkbox"]'); @@ -1045,8 +1057,8 @@ class BackupPage { } renderSnapshots() { - const tbody = document.getElementById('backup-snapshot-tbody'); - if (!tbody) return; + const list = document.getElementById('backup-snapshot-list'); + if (!list) return; const filter = (document.getElementById('backup-snapshot-filter')?.value || '').toLowerCase(); const locFilter = document.getElementById('backup-snapshot-repo')?.value || ''; @@ -1067,6 +1079,8 @@ class BackupPage { locName: locNameByIdx[locIdx] || `Loc ${locIdx}`, time: s.time, id: s.short_id || (s.id || '').slice(0, 8), + tags: Array.isArray(s.tags) ? s.tags : [], + paths: Array.isArray(s.paths) ? s.paths : [], }); }); }); @@ -1081,39 +1095,82 @@ class BackupPage { ) : rows; if (!filtered.length) { - tbody.innerHTML = `No backups yet.`; + list.innerHTML = `
No backups yet.
`; return; } - tbody.innerHTML = filtered.map(r => { - // Link the App and ID cells to the per-app Backups tab with the - // snapshot pre-expanded. "—" rows (snapshots without an - // app= tag, e.g. system config backups) stay plain text - // since there's no app page to open. - const hasApp = r.app && r.app !== '—'; - const deepLink = hasApp - ? `/app/${encodeURIComponent(r.app)}/backups?snapshot=${encodeURIComponent(r.id)}` - : null; - const appCell = hasApp - ? `${this.escape(r.app)}` - : this.escape(r.app); - const idCell = hasApp - ? `${this.escape(r.id)}` - : `${this.escape(r.id)}`; - return ` - - ${appCell} - ${this.escape(r.host)} - ${this.escape(r.locName)} - ${this.formatRelative(r.time)} - ${idCell} - - - - - - `; - }).join(''); + list.innerHTML = filtered.map(r => this._renderSnapshotRow(r)).join(''); + } + + // Render one global-Snapshots-tab backup as the same .task-item card + // the per-app Backups tab uses, so the two surfaces look identical. + // Extras vs the per-app card: + // - An app-name chip (because the global list isn't scoped to one app) + // that doubles as a deep-link to /app//backups?snapshot= + // - A Delete action alongside Restore (per-app card only offers + // Restore — delete lives in the global view) + _renderSnapshotRow(r) { + const hasApp = r.app && r.app !== '—'; + const deepLink = hasApp + ? `/app/${encodeURIComponent(r.app)}/backups?snapshot=${encodeURIComponent(r.id)}` + : null; + const iconUrl = hasApp ? `/icons/apps/${encodeURIComponent(r.app)}.svg` : '/icons/apps/libreportal.svg'; + const displayName = hasApp ? this.appMeta(r.app).displayName : 'System config'; + const appChip = hasApp + ? `${this.escape(displayName)}` + : `${this.escape(displayName)}`; + const sid = String(r.id); + return ` +
+
+
+ + ${this.escape(this.formatRelative(r.time))} + ${appChip} + ${this.escape(r.locName)} + ${this.escape(this._fmtShortTime(r.time))} + ${this.escape(sid)} +
+
+ + + +
+
+
+
+
App: ${this.escape(displayName)}${hasApp ? ` (${this.escape(r.app)})` : ''}
+
Backup ID: ${this.escape(sid)}
+
Location: ${this.escape(r.locName)}
+
When: ${this.escape(this._fmtFullTime(r.time))}
+
Host: ${this.escape(r.host)}
+ ${r.tags && r.tags.length ? `
Tags: ${r.tags.map(t => `${this.escape(t)}`).join(' ')}
` : ''} + ${r.paths && r.paths.length ? `
Paths:
${r.paths.map(p => this.escape(p)).join('
')}
` : ''} +
+
+
`; + } + + _fmtShortTime(iso) { + if (!iso) return ''; + const d = new Date(iso); + if (isNaN(d.getTime())) return String(iso); + return d.toLocaleString(); + } + _fmtFullTime(iso) { + if (!iso) return ''; + const d = new Date(iso); + if (isNaN(d.getTime())) return String(iso); + return d.toString(); } renderConfiguration() {