// 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 `
${this.escape(this.formatRelative(r.time))} ${appChip} ${this.escape(r.locName)} ${this.escape(this._fmtShortTime(r.time))} ${this.escape(sid)}
${field('App', `${this.escape(displayName)}${hasApp ? ` ${this.escape(r.app)}` : ''}`)} ${field('Host', this.escape(r.host))} ${field('Location', `${this.escape(r.locName)}`)} ${field('Backup ID', `${this.escape(sid)}`)} ${field('When', `${this.escape(this._fmtNiceTime(r.time))}`)} ${engineName ? field('Engine', this.escape(engineName)) : ''}
${otherTags.length ? `
Tags
${otherTags.map(t => `${this.escape(t)}`).join('')}
` : ''} ${r.paths && r.paths.length ? `
Paths
    ${r.paths.map(p => `
  • ${this.escape(p)}
  • `).join('')}
` : ''}
`; }, _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' }); }, });