ui(backup): tile-click → Back-up checklist modal; LibrePortal icon on System tile; 2-up grid
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 <slug>`)
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 <librelad@digitalangels.vip>
This commit is contained in:
parent
c49007d6e0
commit
14bc0c3386
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -174,6 +174,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="backup-modal" id="backup-pick-modal">
|
||||
<div class="backup-modal-inner">
|
||||
<div class="backup-modal-header">
|
||||
<h3>Back up</h3>
|
||||
<button class="backup-modal-close" data-close-modal>×</button>
|
||||
</div>
|
||||
<div class="backup-modal-body" id="backup-pick-modal-body"></div>
|
||||
<div class="backup-modal-footer">
|
||||
<button class="backup-secondary-btn" data-close-modal>Cancel</button>
|
||||
<button class="backup-primary-btn" id="backup-pick-confirm">Back up selected</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="backup-modal" id="backup-restore-modal">
|
||||
<div class="backup-modal-inner">
|
||||
<div class="backup-modal-header">
|
||||
|
||||
@ -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="<slug>". 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 `
|
||||
<div class="backup-app-tile backup-app-tile--system" data-system="1" style="grid-column: 1 / -1; display:flex; align-items:center; gap:14px">
|
||||
<div class="backup-app-tile-icon" style="display:flex; align-items:center; justify-content:center; opacity:.8">
|
||||
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="3" width="20" height="6" rx="1"></rect>
|
||||
<rect x="2" y="9" width="20" height="6" rx="1"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="12" x2="6.01" y2="12"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="backup-app-tile-text" style="flex:1; min-width:0">
|
||||
<div class="backup-app-tile backup-app-tile--system" data-system="1">
|
||||
<img class="backup-app-tile-icon" src="/icons/apps/libreportal.svg" alt="" onerror="this.style.display='none'">
|
||||
<div class="backup-app-tile-text">
|
||||
<div class="backup-app-tile-name">System config</div>
|
||||
<div class="backup-app-tile-meta">
|
||||
<span class="backup-status-dot ${dot}"></span>
|
||||
<span>${this.escape(when)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="backup-system-actions" style="display:flex; gap:6px; flex-shrink:0">
|
||||
<button type="button" class="backup-secondary-btn" data-action="backup-system" title="Snapshot the LibrePortal system config to every enabled location" style="padding:6px 10px; font-size:.85em">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="17 8 12 3 7 8"></polyline>
|
||||
<line x1="12" y1="3" x2="12" y2="15"></line>
|
||||
</svg>Back up
|
||||
</button>
|
||||
<button type="button" class="backup-secondary-btn" data-action="restore-system" title="Restore the latest system-config snapshot into a staging folder for review" style="padding:6px 10px; font-size:.85em">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px">
|
||||
<polyline points="23 4 23 10 17 10"></polyline>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
|
||||
</svg>Restore
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -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) => `
|
||||
<label class="backup-pick-row" style="display:flex; align-items:center; gap:12px; padding:10px 12px; border:1px solid rgba(var(--text-rgb),0.08); border-radius:8px; margin-bottom:6px; cursor:pointer">
|
||||
<input type="checkbox" class="backup-pick-cb" value="${this.escape(key)}" ${checked ? 'checked' : ''}>
|
||||
<img src="${this.escape(iconSrc)}" alt="" style="width:28px; height:28px; flex-shrink:0; border-radius:6px" onerror="this.style.display='none'">
|
||||
<div style="flex:1; min-width:0">
|
||||
<div style="font-weight:600">${this.escape(label)}</div>
|
||||
<div class="backup-card-hint" style="font-size:.82em">${this.escape(sub)}</div>
|
||||
</div>
|
||||
</label>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<p class="backup-card-hint" style="margin:0 0 10px">
|
||||
Pick what to snapshot. Each selection runs in the background and shows up on the Tasks page.
|
||||
</p>
|
||||
<div style="display:flex; gap:14px; margin-bottom:10px; font-size:.85em">
|
||||
<a href="#" data-pick-action="select-all" style="color:var(--accent); text-decoration:none">Select all</a>
|
||||
<a href="#" data-pick-action="select-none" style="color:var(--accent); text-decoration:none">Clear</a>
|
||||
</div>
|
||||
<div class="backup-pick-list" style="max-height:60vh; overflow-y:auto">${rows}</div>
|
||||
`;
|
||||
|
||||
// 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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user