// 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 + `
No apps installed yet.
`; } else { appGrid.innerHTML = systemTileHtml + apps.map(app => this.renderAppTile(app)).join(''); } if (!locs.length) { locSummary.innerHTML = `
No locations enabled.
`; } else { locSummary.innerHTML = locs.map(r => `
${this.escape(r.type)} ${this.escape(r.name)}
${this.formatBytes(parseInt(r.total_size_bytes) || 0)}
${r.total_files || 0} files
`).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 `
Configs
${this.escape(when)}
`; }, tile(label, value, detail) { return `
${this.escape(label)}
${this.escape(value)}
${this.escape(detail || '')}
`; }, 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 `
${this.escape(displayName)}
${when}
`; }, });