Merge claude/2
This commit is contained in:
commit
5432f46fd0
@ -111,21 +111,10 @@
|
|||||||
<option value="">All locations</option>
|
<option value="">All locations</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="backup-snapshot-table-wrap">
|
<!-- Each backup is rendered by renderSnapshots() as a .task-item
|
||||||
<table class="backup-snapshot-table">
|
card, matching the per-app Backups tab so the two surfaces
|
||||||
<thead>
|
share one visual language. -->
|
||||||
<tr>
|
<div class="backup-snapshot-rows" id="backup-snapshot-list"></div>
|
||||||
<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>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="backup-tabpanel" id="backup-panel-locations">
|
<section class="backup-tabpanel" id="backup-panel-locations">
|
||||||
|
|||||||
@ -234,16 +234,28 @@ class BackupPage {
|
|||||||
|
|
||||||
const restoreBtn = e.target.closest('[data-action="restore-snapshot"]');
|
const restoreBtn = e.target.closest('[data-action="restore-snapshot"]');
|
||||||
if (restoreBtn) {
|
if (restoreBtn) {
|
||||||
|
e.stopPropagation(); // don't also toggle the row's details panel
|
||||||
this.openRestoreModal(restoreBtn.dataset.app, restoreBtn.dataset.loc, restoreBtn.dataset.snapshot);
|
this.openRestoreModal(restoreBtn.dataset.app, restoreBtn.dataset.loc, restoreBtn.dataset.snapshot);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteBtn = e.target.closest('[data-action="delete-snapshot"]');
|
const deleteBtn = e.target.closest('[data-action="delete-snapshot"]');
|
||||||
if (deleteBtn) {
|
if (deleteBtn) {
|
||||||
|
e.stopPropagation();
|
||||||
this.openDeleteModal(deleteBtn.dataset.app, deleteBtn.dataset.loc, deleteBtn.dataset.snapshot);
|
this.openDeleteModal(deleteBtn.dataset.app, deleteBtn.dataset.loc, deleteBtn.dataset.snapshot);
|
||||||
return;
|
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"]');
|
const locEnable = e.target.closest('[data-action="toggle-location-enabled"]');
|
||||||
if (locEnable) {
|
if (locEnable) {
|
||||||
const cb = locEnable.querySelector('input[type="checkbox"]');
|
const cb = locEnable.querySelector('input[type="checkbox"]');
|
||||||
@ -1045,8 +1057,8 @@ class BackupPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderSnapshots() {
|
renderSnapshots() {
|
||||||
const tbody = document.getElementById('backup-snapshot-tbody');
|
const list = document.getElementById('backup-snapshot-list');
|
||||||
if (!tbody) return;
|
if (!list) return;
|
||||||
|
|
||||||
const filter = (document.getElementById('backup-snapshot-filter')?.value || '').toLowerCase();
|
const filter = (document.getElementById('backup-snapshot-filter')?.value || '').toLowerCase();
|
||||||
const locFilter = document.getElementById('backup-snapshot-repo')?.value || '';
|
const locFilter = document.getElementById('backup-snapshot-repo')?.value || '';
|
||||||
@ -1067,6 +1079,8 @@ class BackupPage {
|
|||||||
locName: locNameByIdx[locIdx] || `Loc ${locIdx}`,
|
locName: locNameByIdx[locIdx] || `Loc ${locIdx}`,
|
||||||
time: s.time,
|
time: s.time,
|
||||||
id: s.short_id || (s.id || '').slice(0, 8),
|
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;
|
) : rows;
|
||||||
|
|
||||||
if (!filtered.length) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody.innerHTML = filtered.map(r => {
|
list.innerHTML = filtered.map(r => this._renderSnapshotRow(r)).join('');
|
||||||
// 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
|
// Render one global-Snapshots-tab backup as the same .task-item card
|
||||||
// since there's no app page to open.
|
// the per-app Backups tab uses, so the two surfaces look identical.
|
||||||
const hasApp = r.app && r.app !== '—';
|
// Extras vs the per-app card:
|
||||||
const deepLink = hasApp
|
// - An app-name chip (because the global list isn't scoped to one app)
|
||||||
? `/app/${encodeURIComponent(r.app)}/backups?snapshot=${encodeURIComponent(r.id)}`
|
// that doubles as a deep-link to /app/<name>/backups?snapshot=<id>
|
||||||
: null;
|
// - A Delete action alongside Restore (per-app card only offers
|
||||||
const appCell = hasApp
|
// Restore — delete lives in the global view)
|
||||||
? `<a class="backup-snapshot-link" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}">${this.escape(r.app)}</a>`
|
_renderSnapshotRow(r) {
|
||||||
: this.escape(r.app);
|
const hasApp = r.app && r.app !== '—';
|
||||||
const idCell = hasApp
|
const deepLink = hasApp
|
||||||
? `<a class="backup-snapshot-link backup-snapshot-id" href="${this.escape(deepLink)}" data-deep-link="${this.escape(deepLink)}">${this.escape(r.id)}</a>`
|
? `/app/${encodeURIComponent(r.app)}/backups?snapshot=${encodeURIComponent(r.id)}`
|
||||||
: `<span class="backup-snapshot-id">${this.escape(r.id)}</span>`;
|
: null;
|
||||||
return `
|
const iconUrl = hasApp ? `/icons/apps/${encodeURIComponent(r.app)}.svg` : '/icons/apps/libreportal.svg';
|
||||||
<tr>
|
const displayName = hasApp ? this.appMeta(r.app).displayName : 'System config';
|
||||||
<td>${appCell}</td>
|
const appChip = hasApp
|
||||||
<td>${this.escape(r.host)}</td>
|
? `<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>`
|
||||||
<td>${this.escape(r.locName)}</td>
|
: `<span class="backup-snapshot-app-chip">${this.escape(displayName)}</span>`;
|
||||||
<td>${this.formatRelative(r.time)}</td>
|
const sid = String(r.id);
|
||||||
<td>${idCell}</td>
|
return `
|
||||||
<td class="backup-col-actions">
|
<div class="task-item backup-snapshot-item" data-snapshot="${this.escape(sid)}" data-loc="${this.escape(String(r.locIdx))}">
|
||||||
<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>
|
<div class="task-header" data-action="toggle-snapshot-row">
|
||||||
<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>
|
<div class="task-info">
|
||||||
</td>
|
<img src="${this.escape(iconUrl)}" alt="" class="task-app-icon" onerror="this.style.display='none'">
|
||||||
</tr>
|
<span class="task-title">${this.escape(this.formatRelative(r.time))}</span>
|
||||||
`;
|
${appChip}
|
||||||
}).join('');
|
<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() {
|
renderConfiguration() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user