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

127 lines
6.6 KiB
JavaScript

// Auto-extracted from backup-page.js (verbatim) — augments BackupPage.prototype. Loaded after the base.
Object.assign(BackupPage.prototype, {
renderDashboard() {
const summary = document.getElementById('backup-summary-row');
const appGrid = document.getElementById('backup-app-grid');
const locSummary = document.getElementById('backup-repo-list-summary');
if (!summary || !appGrid || !locSummary) return;
const d = this.dashboard || {};
const locs = d.locations || [];
const apps = d.apps || [];
const totalSnapshots = Object.values(this.snapshotsByLoc).reduce((acc, r) => {
return acc + (Array.isArray(r?.snapshots) ? r.snapshots.length : 0);
}, 0);
const protectedApps = apps.filter(a => a.latest_snapshot).length;
const totalSize = locs.reduce((acc, r) => acc + (parseInt(r.total_size_bytes) || 0), 0);
summary.innerHTML = `
${this.tile('Apps protected', `${protectedApps} / ${apps.length}`, 'with at least one backup')}
${this.tile('Backups', `${totalSnapshots}`, `across ${locs.length} location${locs.length === 1 ? '' : 's'}`)}
${this.tile('Total stored', this.formatBytes(totalSize), 'deduplicated, encrypted')}
`;
// Next-run hint in the "Backup status" card header — derived from
// CFG_BACKUP_CRONTAB_APP (the cron expression the app-backup
// scheduler uses). Pure client-side computation; no backend
// surface needed.
const nextRunEl = document.getElementById('backup-next-run');
if (nextRunEl) {
const cron = (window.systemConfigs?.CFG_BACKUP_CRONTAB_APP || '').trim();
const next = cron ? this.nextCronFireTime(cron) : null;
if (next) {
nextRunEl.textContent = `Next backup ${this.formatRelativeFuture(next)} · ${this.formatScheduleClock(next)}`;
nextRunEl.title = `Next scheduled backup: ${next.toLocaleString()}\nSchedule: ${cron}`;
} else if (cron) {
nextRunEl.textContent = `Schedule: ${cron}`;
nextRunEl.title = `Couldn't parse the schedule "${cron}" to compute the next run.`;
} else {
nextRunEl.textContent = 'No schedule set';
nextRunEl.title = 'CFG_BACKUP_CRONTAB_APP is empty — backups only run when triggered manually.';
}
}
// System config tile is rendered FIRST so the bare-metal-restore
// prerequisite is always at eye-level — without it, the user's
// backups exist but the credentials needed to reach them don't.
const systemTileHtml = this.renderSystemTile(d.system || {});
if (!apps.length) {
appGrid.innerHTML = systemTileHtml + `<div class="backup-empty-state">No apps installed yet.</div>`;
} else {
appGrid.innerHTML = systemTileHtml + apps.map(app => this.renderAppTile(app)).join('');
}
if (!locs.length) {
locSummary.innerHTML = `<div class="backup-empty-state">No locations enabled.</div>`;
} else {
locSummary.innerHTML = locs.map(r => `
<div class="backup-repo-row">
<div class="backup-repo-row-name">
<span class="backup-repo-type-pill">${this.escape(r.type)}</span>
${this.escape(r.name)}
</div>
<div class="backup-repo-row-meta">
${this.formatBytes(parseInt(r.total_size_bytes) || 0)}<br>
<span class="backup-card-hint">${r.total_files || 0} files</span>
</div>
</div>
`).join('');
}
},
// 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" title="Back up system config">
<img class="backup-app-tile-icon" src="/core/icons/apps/libreportal.svg" alt="" onerror="this.style.display='none'">
<div class="backup-app-tile-text">
<div class="backup-app-tile-name">Configs</div>
<div class="backup-app-tile-meta">
<span class="backup-status-dot ${dot}"></span>
<span>${this.escape(when)}</span>
</div>
</div>
<button type="button" class="backup-app-tile-action" data-action="backup-now" data-system="1" title="Back up now">
Back up
</button>
</div>
`;
},
tile(label, value, detail) {
return `
<div class="backup-summary-tile">
<div class="backup-summary-tile-label">${this.escape(label)}</div>
<div class="backup-summary-tile-value">${this.escape(value)}</div>
<div class="backup-summary-tile-detail">${this.escape(detail || '')}</div>
</div>
`;
},
renderAppTile(app) {
const has = !!app.latest_snapshot;
const dot = has ? 'ok' : 'none';
const when = has ? this.formatRelative(app.latest_time) : 'No backup yet';
const { icon, displayName } = this.appMeta(app.app);
return `
<div class="backup-app-tile" data-app="${this.escape(app.app)}" title="Open ${this.escape(displayName)} backup history">
<img class="backup-app-tile-icon" src="${this.escape(icon)}" alt="" onerror="this.src='/core/icons/apps/default.svg'">
<div class="backup-app-tile-text">
<div class="backup-app-tile-name">${this.escape(displayName)}</div>
<div class="backup-app-tile-meta">
<span class="backup-status-dot ${dot}"></span>
<span>${when}</span>
</div>
</div>
<button type="button" class="backup-app-tile-action" data-action="backup-now" data-app="${this.escape(app.app)}" title="Back up now">
Back up
</button>
</div>
`;
},
});