Merge claude/2
This commit is contained in:
commit
5432f46fd0
@ -111,21 +111,10 @@
|
||||
<option value="">All locations</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="backup-snapshot-table-wrap">
|
||||
<table class="backup-snapshot-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>App</th>
|
||||
<th>Host</th>
|
||||
<th>Location</th>
|
||||
<th>When</th>
|
||||
<th>ID</th>
|
||||
<th class="backup-col-actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="backup-snapshot-tbody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Each backup is rendered by renderSnapshots() as a .task-item
|
||||
card, matching the per-app Backups tab so the two surfaces
|
||||
share one visual language. -->
|
||||
<div class="backup-snapshot-rows" id="backup-snapshot-list"></div>
|
||||
</section>
|
||||
|
||||
<section class="backup-tabpanel" id="backup-panel-locations">
|
||||
|
||||
@ -234,16 +234,28 @@ class BackupPage {
|
||||
|
||||
const restoreBtn = e.target.closest('[data-action="restore-snapshot"]');
|
||||
if (restoreBtn) {
|
||||
e.stopPropagation(); // don't also toggle the row's details panel
|
||||
this.openRestoreModal(restoreBtn.dataset.app, restoreBtn.dataset.loc, restoreBtn.dataset.snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteBtn = e.target.closest('[data-action="delete-snapshot"]');
|
||||
if (deleteBtn) {
|
||||
e.stopPropagation();
|
||||
this.openDeleteModal(deleteBtn.dataset.app, deleteBtn.dataset.loc, deleteBtn.dataset.snapshot);
|
||||
return;
|
||||
}
|
||||
|
||||
// Row header / Details button → toggle the .task-details panel.
|
||||
// Matches the per-app Backups tab interaction.
|
||||
const snapToggle = e.target.closest('[data-action="toggle-snapshot-row"]');
|
||||
if (snapToggle) {
|
||||
const item = snapToggle.closest('.backup-snapshot-item');
|
||||
const details = item && item.querySelector('.task-details');
|
||||
if (details) details.classList.toggle('task-details-open');
|
||||
return;
|
||||
}
|
||||
|
||||
const locEnable = e.target.closest('[data-action="toggle-location-enabled"]');
|
||||
if (locEnable) {
|
||||
const cb = locEnable.querySelector('input[type="checkbox"]');
|
||||
@ -1045,8 +1057,8 @@ class BackupPage {
|
||||
}
|
||||
|
||||
renderSnapshots() {
|
||||
const tbody = document.getElementById('backup-snapshot-tbody');
|
||||
if (!tbody) return;
|
||||
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 || '';
|
||||
@ -1067,6 +1079,8 @@ class BackupPage {
|
||||
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 : [],
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1081,39 +1095,82 @@ class BackupPage {
|
||||
) : rows;
|
||||
|
||||
if (!filtered.length) {
|
||||
tbody.innerHTML = `<tr><td colspan="6" class="backup-empty-state">No backups yet.</td></tr>`;
|
||||
list.innerHTML = `<div class="backup-empty-state">No backups yet.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = filtered.map(r => {
|
||||
// Link the App and ID cells to the per-app Backups tab with the
|
||||
// snapshot pre-expanded. "—" rows (snapshots without an
|
||||
// app=<slug> tag, e.g. system config backups) stay plain text
|
||||
// since there's no app page to open.
|
||||
const hasApp = r.app && r.app !== '—';
|
||||
const deepLink = hasApp
|
||||
? `/app/${encodeURIComponent(r.app)}/backups?snapshot=${encodeURIComponent(r.id)}`
|
||||
: null;
|
||||
const appCell = hasApp
|
||||
? `<a class="backup-snapshot-link" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}">${this.escape(r.app)}</a>`
|
||||
: this.escape(r.app);
|
||||
const idCell = hasApp
|
||||
? `<a class="backup-snapshot-link backup-snapshot-id" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}">${this.escape(r.id)}</a>`
|
||||
: `<span class="backup-snapshot-id">${this.escape(r.id)}</span>`;
|
||||
return `
|
||||
<tr>
|
||||
<td>${appCell}</td>
|
||||
<td>${this.escape(r.host)}</td>
|
||||
<td>${this.escape(r.locName)}</td>
|
||||
<td>${this.formatRelative(r.time)}</td>
|
||||
<td>${idCell}</td>
|
||||
<td class="backup-col-actions">
|
||||
<button class="backup-row-action-btn" data-action="restore-snapshot" data-app="${this.escape(r.app)}" data-loc="${r.locIdx}" data-snapshot="${this.escape(r.id)}">Restore</button>
|
||||
<button class="backup-row-action-btn danger" data-action="delete-snapshot" data-app="${this.escape(r.app)}" data-loc="${r.locIdx}" data-snapshot="${this.escape(r.id)}">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
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/<name>/backups?snapshot=<id>
|
||||
// - 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 ? `/icons/apps/${encodeURIComponent(r.app)}.svg` : '/icons/apps/libreportal.svg';
|
||||
const displayName = hasApp ? this.appMeta(r.app).displayName : 'System config';
|
||||
const appChip = hasApp
|
||||
? `<a class="backup-snapshot-link backup-snapshot-app-chip" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}" title="Open ${this.escape(displayName)} backups">${this.escape(displayName)}</a>`
|
||||
: `<span class="backup-snapshot-app-chip">${this.escape(displayName)}</span>`;
|
||||
const sid = String(r.id);
|
||||
return `
|
||||
<div class="task-item backup-snapshot-item" data-snapshot="${this.escape(sid)}" data-loc="${this.escape(String(r.locIdx))}">
|
||||
<div class="task-header" data-action="toggle-snapshot-row">
|
||||
<div class="task-info">
|
||||
<img src="${this.escape(iconUrl)}" alt="" class="task-app-icon" onerror="this.style.display='none'">
|
||||
<span class="task-title">${this.escape(this.formatRelative(r.time))}</span>
|
||||
${appChip}
|
||||
<span class="task-status backup-snapshot-loc-pill">${this.escape(r.locName)}</span>
|
||||
<span class="task-time" title="${this.escape(this._fmtFullTime(r.time))}">${this.escape(this._fmtShortTime(r.time))}</span>
|
||||
<span class="backup-snapshot-id-chip" title="Backup ID">${this.escape(sid)}</span>
|
||||
</div>
|
||||
<div class="task-actions">
|
||||
<button class="task-btn" data-action="restore-snapshot" data-app="${this.escape(r.app)}" data-loc="${this.escape(String(r.locIdx))}" data-snapshot="${this.escape(sid)}" title="Restore from this backup">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12a9 9 0 1 0 3-6.7"/><path d="M3 4v6h6"/></svg>
|
||||
<span class="task-btn-label">Restore</span>
|
||||
</button>
|
||||
<button class="task-btn delete" data-action="delete-snapshot" data-app="${this.escape(r.app)}" data-loc="${this.escape(String(r.locIdx))}" data-snapshot="${this.escape(sid)}" title="Delete this backup">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6M14 11v6"></path></svg>
|
||||
<span class="task-btn-label">Delete</span>
|
||||
</button>
|
||||
<button class="task-btn toggle-details" data-action="toggle-snapshot-row" title="Toggle details">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6,9 12,15 18,9"></polyline></svg>
|
||||
<span class="task-btn-label">Details</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="task-details">
|
||||
<div class="task-meta">
|
||||
<div class="meta-item"><strong>App:</strong> ${this.escape(displayName)}${hasApp ? ` (<code>${this.escape(r.app)}</code>)` : ''}</div>
|
||||
<div class="meta-item"><strong>Backup ID:</strong> <code>${this.escape(sid)}</code></div>
|
||||
<div class="meta-item"><strong>Location:</strong> ${this.escape(r.locName)}</div>
|
||||
<div class="meta-item"><strong>When:</strong> ${this.escape(this._fmtFullTime(r.time))}</div>
|
||||
<div class="meta-item"><strong>Host:</strong> ${this.escape(r.host)}</div>
|
||||
${r.tags && r.tags.length ? `<div class="meta-item"><strong>Tags:</strong> ${r.tags.map(t => `<span class="backup-snapshot-tag">${this.escape(t)}</span>`).join(' ')}</div>` : ''}
|
||||
${r.paths && r.paths.length ? `<div class="meta-item"><strong>Paths:</strong><br><code>${r.paths.map(p => this.escape(p)).join('<br>')}</code></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
_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();
|
||||
}
|
||||
|
||||
renderConfiguration() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user