adiciona funcionalidade para carregar e exibir todas as execuções de backup; atualiza estatísticas de sucesso e falha no painel
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
2eed853115
commit
ef9ffdbe71
|
|
@ -49,6 +49,9 @@ function navigateTo(viewName) {
|
||||||
if (viewName === 'servers') {
|
if (viewName === 'servers') {
|
||||||
renderServers();
|
renderServers();
|
||||||
}
|
}
|
||||||
|
if (viewName === 'runs') {
|
||||||
|
loadAllRuns();
|
||||||
|
}
|
||||||
if (viewName === 'backups') {
|
if (viewName === 'backups') {
|
||||||
renderBackupsView();
|
renderBackupsView();
|
||||||
}
|
}
|
||||||
|
|
@ -99,16 +102,27 @@ async function renderBackupsView() {
|
||||||
</div>
|
</div>
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead><tr><th>Data</th><th>Mode</th><th>Status</th><th>Containers</th></tr></thead>
|
<thead><tr><th>Data</th><th>Mode</th><th>Status</th><th>Containers</th><th>Actions</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
${backups.length ? backups.map((b) => `
|
${backups.length ? backups.map((b) => {
|
||||||
|
const hasRestorable = (b.containers || []).some((c) => c.status === 'ok');
|
||||||
|
return `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(new Date(b.createdAt).toLocaleString('pt-BR'))}</td>
|
<td>${escapeHtml(new Date(b.createdAt).toLocaleString('pt-BR'))}</td>
|
||||||
<td>${escapeHtml(b.mode || '—')}</td>
|
<td>${escapeHtml(b.mode || '—')}</td>
|
||||||
<td><span class="status-badge status-badge--${escapeHtml(b.status)}">${escapeHtml(b.status)}</span></td>
|
<td><span class="status-badge status-badge--${escapeHtml(b.status)}">${escapeHtml(b.status)}</span></td>
|
||||||
<td>${escapeHtml((b.containers || []).map((c) => c.containerName).join(', '))}</td>
|
<td>${escapeHtml((b.containers || []).map((c) => c.containerName).join(', '))}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="btn btn--secondary btn--sm"
|
||||||
|
data-action="restore"
|
||||||
|
data-profile-id="${escapeHtml(profile.id)}"
|
||||||
|
data-backup-id="${escapeHtml(b.id)}"
|
||||||
|
${hasRestorable ? '' : 'disabled title="Nenhum container restaurável"'}
|
||||||
|
>Restore</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
`).join('') : '<tr><td colspan="4" class="empty-row">Nenhum backup realizado.</td></tr>'}
|
`}).join('') : '<tr><td colspan="5" class="empty-row">Nenhum backup realizado.</td></tr>'}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -127,16 +141,11 @@ async function updateDashboard() {
|
||||||
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
const lastBackupEl = document.querySelector('#lastBackupTime');
|
const lastBackupEl = document.querySelector('#lastBackupTime');
|
||||||
if (lastBackupEl) {
|
if (lastBackupEl) {
|
||||||
lastBackupEl.textContent = successful.length
|
lastBackupEl.textContent = String(successful.length);
|
||||||
? new Date(successful[0].createdAt).toLocaleString('pt-BR')
|
|
||||||
: '—';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Failed in last 24h
|
// Failed total
|
||||||
const cutoff = Date.now() - 24 * 60 * 60 * 1000;
|
const failed = allBackups.filter((b) => b.status === 'error');
|
||||||
const failed = allBackups.filter(
|
|
||||||
(b) => b.status === 'error' && new Date(b.createdAt).getTime() >= cutoff,
|
|
||||||
);
|
|
||||||
const failedEl = document.querySelector('#failedCount');
|
const failedEl = document.querySelector('#failedCount');
|
||||||
if (failedEl) failedEl.textContent = String(failed.length);
|
if (failedEl) failedEl.textContent = String(failed.length);
|
||||||
|
|
||||||
|
|
@ -191,6 +200,54 @@ function formatBytes(bytes) {
|
||||||
return bytes + ' B';
|
return bytes + ' B';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAllRuns() {
|
||||||
|
const tbody = document.querySelector('#allRunsBody');
|
||||||
|
if (!tbody) return;
|
||||||
|
tbody.innerHTML = '<tr><td colspan="8" class="empty-row">Carregando...</td></tr>';
|
||||||
|
|
||||||
|
if (!state.profiles.length) {
|
||||||
|
await loadProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.profiles.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="8" class="empty-row">Nenhum profile encontrado.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allBackups = (await Promise.all(
|
||||||
|
state.profiles.map((p) => api(`/api/profiles/${p.id}/backups`)),
|
||||||
|
)).flat();
|
||||||
|
|
||||||
|
const sorted = [...allBackups].sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||||
|
|
||||||
|
if (!sorted.length) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="8" class="empty-row">Nenhum run encontrado.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = sorted.map((b, index) => {
|
||||||
|
const profile = state.profiles.find((p) => p.id === b.profileId);
|
||||||
|
const profileName = profile ? profile.name : (b.profileName || b.profileId || '—');
|
||||||
|
const containers = (b.containers || []).map((c) => escapeHtml(c.containerName || c.containerId?.slice(0, 12) || '?')).join(', ');
|
||||||
|
const fileCount = (b.containers || []).reduce((sum, c) => sum + (c.fileCount || 0), 0);
|
||||||
|
const size = (b.containers || []).reduce((sum, c) => sum + (c.archiveSize || 0), 0);
|
||||||
|
const sizeStr = size > 0 ? formatBytes(size) : '—';
|
||||||
|
const statusLabel = b.status === 'ok' ? 'Completed' : b.status === 'partial' ? 'Partial' : 'Error';
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${index + 1}</td>
|
||||||
|
<td>${escapeHtml(profileName)}</td>
|
||||||
|
<td>${escapeHtml(b.mode || '—')}</td>
|
||||||
|
<td><span class="status-badge status-badge--${escapeHtml(b.status)}">${escapeHtml(statusLabel)}</span></td>
|
||||||
|
<td>${containers || '—'}</td>
|
||||||
|
<td>${fileCount || '—'}</td>
|
||||||
|
<td>${sizeStr}</td>
|
||||||
|
<td>${escapeHtml(new Date(b.createdAt).toLocaleString('pt-BR'))}</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
async function api(path, options = {}) {
|
async function api(path, options = {}) {
|
||||||
const response = await fetch(path, {
|
const response = await fetch(path, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -925,5 +982,7 @@ document.querySelector('#refreshContainers').addEventListener('click', init);
|
||||||
document.querySelector('#reloadProfiles').addEventListener('click', loadProfiles);
|
document.querySelector('#reloadProfiles').addEventListener('click', loadProfiles);
|
||||||
document.querySelector('#clearForm').addEventListener('click', resetForm);
|
document.querySelector('#clearForm').addEventListener('click', resetForm);
|
||||||
elements.profilesList.addEventListener('click', handleProfileAction);
|
elements.profilesList.addEventListener('click', handleProfileAction);
|
||||||
|
document.querySelector('#backupsViewList').addEventListener('click', handleProfileAction);
|
||||||
|
document.querySelector('#refreshRuns')?.addEventListener('click', () => loadAllRuns());
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
@ -106,9 +106,9 @@
|
||||||
|
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-info">
|
<div class="stat-info">
|
||||||
<span class="stat-label">Last Backup</span>
|
<span class="stat-label">Successful</span>
|
||||||
<strong id="lastBackupTime" class="stat-value stat-value--sm">—</strong>
|
<strong id="lastBackupTime" class="stat-value">0</strong>
|
||||||
<span class="stat-sub">Latest successful run</span>
|
<span class="stat-sub">Total com sucesso</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon stat-icon--green">
|
<div class="stat-icon stat-icon--green">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||||
|
|
@ -117,9 +117,9 @@
|
||||||
|
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-info">
|
<div class="stat-info">
|
||||||
<span class="stat-label">Failed (24h)</span>
|
<span class="stat-label">Failed</span>
|
||||||
<strong id="failedCount" class="stat-value">0</strong>
|
<strong id="failedCount" class="stat-value">0</strong>
|
||||||
<span class="stat-sub">Needs attention</span>
|
<span class="stat-sub">Total com falha</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-icon stat-icon--red">
|
<div class="stat-icon stat-icon--red">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
||||||
|
|
@ -234,6 +234,7 @@
|
||||||
<div id="view-runs" class="view hidden">
|
<div id="view-runs" class="view hidden">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1 class="page-title">Backup Runs</h1>
|
<h1 class="page-title">Backup Runs</h1>
|
||||||
|
<button id="refreshRuns" class="btn btn--secondary">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-toolbar">
|
<div class="card-toolbar">
|
||||||
|
|
@ -243,14 +244,14 @@
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>#</th>
|
||||||
<th>Profile</th>
|
<th>Profile</th>
|
||||||
|
<th>Mode</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
<th>Containers</th>
|
||||||
<th>Files</th>
|
<th>Files</th>
|
||||||
<th>Size</th>
|
<th>Size</th>
|
||||||
<th>Started</th>
|
<th>Started</th>
|
||||||
<th>Duration</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="allRunsBody">
|
<tbody id="allRunsBody">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue