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>
127 lines
6.6 KiB
JavaScript
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>
|
|
`;
|
|
},
|
|
});
|