// Auto-extracted from backup-page.js (verbatim) — augments BackupPage.prototype. Loaded after the base.
Object.assign(BackupPage.prototype, {
renderSnapshots() {
const list = document.getElementById('backup-snapshot-list');
if (!list) return;
const filter = (document.getElementById('backup-snapshot-filter')?.value || '').toLowerCase();
const locFilter = document.getElementById('backup-snapshot-repo')?.value || '';
const locNameByIdx = {};
(this.locations?.locations || []).forEach(l => { locNameByIdx[l.idx] = l.name; });
const rows = [];
Object.entries(this.snapshotsByLoc).forEach(([locIdx, data]) => {
if (locFilter && String(locFilter) !== String(locIdx)) return;
const snaps = Array.isArray(data?.snapshots) ? data.snapshots : [];
snaps.forEach(s => {
const app = (s.tags || []).map(t => /^app=/.test(t) ? t.slice(4) : null).find(Boolean) || '—';
rows.push({
app,
host: s.hostname || '—',
locIdx,
locName: locNameByIdx[locIdx] || `Loc ${locIdx}`,
time: s.time,
id: s.short_id || (s.id || '').slice(0, 8),
tags: Array.isArray(s.tags) ? s.tags : [],
paths: Array.isArray(s.paths) ? s.paths : [],
});
});
});
rows.sort((a, b) => String(b.time).localeCompare(String(a.time)));
const filtered = filter ? rows.filter(r =>
r.app.toLowerCase().includes(filter) ||
r.host.toLowerCase().includes(filter) ||
r.id.toLowerCase().includes(filter) ||
r.locName.toLowerCase().includes(filter)
) : rows;
if (!filtered.length) {
list.innerHTML = `
No backups yet.
`;
return;
}
list.innerHTML = filtered.map(r => this._renderSnapshotRow(r)).join('');
},
// Render one global-Snapshots-tab backup as the same .task-item card
// the per-app Backups tab uses, so the two surfaces look identical.
// Extras vs the per-app card:
// - An app-name chip (because the global list isn't scoped to one app)
// that doubles as a deep-link to /app//backups?snapshot=
// - A Delete action alongside Restore (per-app card only offers
// Restore — delete lives in the global view)
_renderSnapshotRow(r) {
const hasApp = r.app && r.app !== '—';
const deepLink = hasApp
? `/app/${encodeURIComponent(r.app)}/backups?snapshot=${encodeURIComponent(r.id)}`
: null;
const iconUrl = hasApp ? `/core/icons/apps/${encodeURIComponent(r.app)}.svg` : '/core/icons/apps/libreportal.svg';
const displayName = hasApp ? this.appMeta(r.app).displayName : 'Configs';
const appChip = hasApp
? `${this.escape(displayName)}`
: `${this.escape(displayName)}`;
const sid = String(r.id);
// Restic stamps app=/host=/engine= into the snapshot tags; surface
// those as their own fields and keep any remaining tags as chips.
const tagMap = {};
(r.tags || []).forEach(t => { const i = t.indexOf('='); if (i > 0) tagMap[t.slice(0, i)] = t.slice(i + 1); });
const engineName = tagMap.engine ? this.engineDisplayName(tagMap.engine) : null;
const otherTags = (r.tags || []).filter(t => !/^(app|host|engine|paths?)=/.test(t));
const field = (label, valueHtml) =>
`${label}${valueHtml}
`;
return `
`;
},
_fmtShortTime(iso) {
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return String(iso);
return d.toLocaleString();
},
_fmtFullTime(iso) {
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return String(iso);
return d.toString();
},
_fmtNiceTime(iso) {
if (!iso) return '';
const d = new Date(iso);
if (isNaN(d.getTime())) return String(iso);
return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' });
},
});