librelad 82989069e2 refactor(backup): decompose backup-page god-file into 13 responsibility files
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>
2026-05-30 14:02:45 +01:00

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);
}
},
});