Faithful prototype-augment split of backup-page.js (2353->753 line base) into fetch-client, dashboard, snapshots, locations, location-fields, ssh-key, retention-presets, configuration, engine-details, location-modal, snapshot-actions, migrate (+ the earlier cron-schedule). Methods relocated verbatim (mechanical sed/awk extraction, no logic change); all augment BackupPage.prototype and load after the base via the ordered kernel loader. Verified: all 99 original methods present exactly once across base+clusters, no duplicates, all 14 files node --check clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
148 lines
7.5 KiB
JavaScript
148 lines
7.5 KiB
JavaScript
// Auto-extracted from backup-page.js (verbatim) — augments BackupPage.prototype. Loaded after the base.
|
|
Object.assign(BackupPage.prototype, {
|
|
openRestoreModal(app, locIdx, snapshot) {
|
|
const locName = this.locName(locIdx);
|
|
const modal = document.getElementById('backup-restore-modal');
|
|
const body = document.getElementById('backup-restore-modal-body');
|
|
if (!modal || !body) return;
|
|
body.innerHTML = `
|
|
<p>Restore <strong>${this.escape(app)}</strong> from backup <code>${this.escape(snapshot)}</code> at <strong>${this.escape(locName)}</strong>?</p>
|
|
<p class="backup-card-hint">The app will be stopped, its folder wiped, the backup restored in place, then the app started again. App-specific pre/post-restore hooks run if present.</p>
|
|
`;
|
|
modal.dataset.app = app;
|
|
modal.dataset.locIdx = locIdx;
|
|
modal.dataset.snapshot = snapshot;
|
|
modal.classList.add('open');
|
|
},
|
|
openDeleteModal(app, locIdx, snapshot) {
|
|
const locName = this.locName(locIdx);
|
|
const modal = document.getElementById('backup-delete-modal');
|
|
const body = document.getElementById('backup-delete-modal-body');
|
|
if (!modal || !body) return;
|
|
body.innerHTML = `
|
|
<p>Delete backup <code>${this.escape(snapshot)}</code> for <strong>${this.escape(app)}</strong> from <strong>${this.escape(locName)}</strong>?</p>
|
|
<p class="backup-card-hint">This cannot be undone. Append-only locations will reject the operation.</p>
|
|
`;
|
|
modal.dataset.app = app;
|
|
modal.dataset.locIdx = locIdx;
|
|
modal.dataset.snapshot = snapshot;
|
|
modal.classList.add('open');
|
|
},
|
|
async confirmRestore() {
|
|
const modal = document.getElementById('backup-restore-modal');
|
|
const { app, locIdx, snapshot } = modal.dataset;
|
|
this.closeAllModals();
|
|
await this.runTask(`libreportal restore app start ${app} ${snapshot} ${locIdx}`, 'restore', app);
|
|
},
|
|
async confirmDelete() {
|
|
const modal = document.getElementById('backup-delete-modal');
|
|
const { app, locIdx, snapshot } = modal.dataset;
|
|
this.closeAllModals();
|
|
await this.runTask(`libreportal backup app delete ${app} ${locIdx}:${snapshot}`, 'backup', app);
|
|
},
|
|
async runBackupAllApps() {
|
|
await this.runTask(`libreportal backup all`, 'backup', null);
|
|
},
|
|
async runBackupSystem() {
|
|
await this.runTask(`libreportal backup system`, 'backup', null);
|
|
},
|
|
async confirmRestoreSystem() {
|
|
if (!confirm('Restore the latest system-config snapshot?\n\nIt is restored into a staging folder (not applied) — review it, then copy what you need (e.g. backup-location credentials, login, settings) into the live config. Your running config is NOT overwritten.')) return;
|
|
await this.runTask(`libreportal restore system`, 'restore', null);
|
|
},
|
|
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__', '/core/icons/apps/libreportal.svg', 'Configs', 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. Requires at least one
|
|
// app so a system-only pick never collapses into "backup all".
|
|
if (wantsSystem && apps.length > 0 && 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);
|
|
}
|
|
},
|
|
});
|