// Auto-extracted from backup-page.js (verbatim) — augments BackupPage.prototype. Loaded after the base.
Object.assign(BackupPage.prototype, {
renderMigrate() {
const body = document.getElementById('backup-migrate-body');
const empty = document.getElementById('backup-migrate-empty');
if (!body || !empty) return;
const data = this.migrate || {};
const locations = (data.locations || []).filter(l => (l.hosts || []).length > 0);
if (!locations.length) {
body.innerHTML = '';
empty.hidden = false;
return;
}
empty.hidden = true;
// Group: one card per source-host, with that host's apps listed underneath.
// We collapse across locations — if the same host appears in two locations,
// we still show it once with the union of apps (the per-app row carries
// which location it came from). Most setups have one shared location anyway.
const installed = new Set(data.destination?.installed_apps || []);
const html = locations.map(loc => `
${loc.hosts.map(host => {
const peerName = (this.hostnameToPeerName || {})[host.hostname];
const headerLabel = peerName
? `
${this.escape(peerName)} host: ${this.escape(host.hostname)} `
: `
${this.escape(host.hostname)} `;
return `
${headerLabel}
${(host.apps || []).length} app${host.apps.length === 1 ? '' : 's'} available
Migrate every app from this host
${(host.apps || []).map(app => {
const collide = installed.has(app.slug);
return `
${this.escape(app.slug)}
${collide ? ` ` : ''}
${app.snapshots} snapshot${app.snapshots === 1 ? '' : 's'} · latest ${this.escape(this.formatRelativeTime(app.latest_date))}
Migrate
`;
}).join('')}
`;
}).join('')}
`).join('');
body.innerHTML = html;
},
formatRelativeTime(iso) {
if (!iso) return 'never';
const t = Date.parse(iso);
if (!t) return iso;
const diff = Date.now() - t;
const minute = 60_000, hour = 60 * minute, day = 24 * hour;
if (diff < hour) return `${Math.max(1, Math.round(diff / minute))} min ago`;
if (diff < day) return `${Math.round(diff / hour)} h ago`;
if (diff < 7 * day) return `${Math.round(diff / day)} d ago`;
return new Date(t).toISOString().slice(0, 10);
},
openMigrateModal({ mode, locIdx, host, app }) {
const modal = document.getElementById('backup-migrate-modal');
const body = document.getElementById('backup-migrate-modal-body');
if (!modal || !body) return;
const dest = this.migrate?.destination || {};
const installed = new Set(dest.installed_apps || []);
const running = new Set(dest.running_apps || []);
const locName = this.locName(locIdx);
// App-mode: one specific app. Host-mode: every app from the host.
let targetApps = [];
if (mode === 'app') {
targetApps = [app];
} else {
const loc = (this.migrate?.locations || []).find(l => l.idx === locIdx);
const h = (loc?.hosts || []).find(x => x.hostname === host);
targetApps = (h?.apps || []).map(a => a.slug);
}
const collisions = targetApps.filter(a => installed.has(a));
const collisionsRunning = collisions.filter(a => running.has(a));
const intro = mode === 'app'
? `Migrate ${this.escape(app)} from ${this.escape(host)} via ${this.escape(locName)} onto this host.
`
: `Migrate every app (${targetApps.length}) from ${this.escape(host)} via ${this.escape(locName)} onto this host.
`;
let collisionNote = '';
if (collisions.length) {
collisionNote = `
⚠ Already installed here: ${collisions.map(c => `${this.escape(c)}`).join(', ')}.
These will be replaced .
${collisionsRunning.length ? `Currently running: ${collisionsRunning.map(c => `${this.escape(c)}`).join(', ')} — will be stopped first.` : ''}
`;
}
body.innerHTML = `
${intro}
${collisionNote}
Back up the destination's existing copy first
Safety net: snapshot the current ${mode === 'app' ? this.escape(app) : 'app'} into your first
enabled backup location (tagged pre-migrate) before wipe.
${collisions.length ? '' : 'No collision — nothing to back up.'}
Rewrite host-bound URLs to this host
Replaces CFG_*_URL, *_DOMAIN, *_HOSTNAME with this
host's values. Uncheck only if you want the moved app to keep claiming the source's hostname.
`;
modal.dataset.mode = mode;
modal.dataset.locIdx = String(locIdx);
modal.dataset.host = host;
modal.dataset.app = app || '';
modal.classList.add('open');
},
async confirmMigrate() {
const modal = document.getElementById('backup-migrate-modal');
if (!modal) return;
const { mode, locIdx, host, app } = modal.dataset;
const preBackup = document.getElementById('migrate-opt-pre-backup')?.checked;
const rewrite = document.getElementById('migrate-opt-rewrite-urls')?.checked;
// The bash CLI defaults are: pre-backup ON, URL rewrite ON. Opt-out flags
// only get appended when the user un-ticks; matches the kernel's defaults.
const opts = [];
if (preBackup === false) opts.push('--no-pre-backup');
if (rewrite === false) opts.push('--keep-urls');
const optStr = opts.length ? ' ' + opts.join(' ') : '';
this.closeAllModals();
if (mode === 'app') {
await this.runTask(
`libreportal restore migrate app ${app} ${host} ${locIdx}${optStr}`,
'restore', app);
} else {
await this.runTask(
`libreportal restore migrate system ${host} ${locIdx}${optStr}`,
'restore', null);
}
},
});