Add backup history charts
This commit is contained in:
@@ -9,10 +9,10 @@ Tag="backup restic snapshots restore"
|
||||
<?php
|
||||
$pluginRoot = '/plugins/backupper';
|
||||
?>
|
||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=202606143">
|
||||
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/backupper.css?v=202606144">
|
||||
<div id="backupper-app" data-api="<?= $pluginRoot ?>/api.php">
|
||||
<header class="bu-header">
|
||||
<div><h1>Backupper <span class="bu-version">2026.06.14.3</span></h1><p>Encrypted Restic backups for Unraid</p></div>
|
||||
<div><h1>Backupper <span class="bu-version">2026.06.14.4</span></h1><p>Encrypted Restic backups for Unraid</p></div>
|
||||
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die Backupper-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Connecting...</div>
|
||||
</header>
|
||||
<nav class="bu-tabs" aria-label="Backupper sections">
|
||||
@@ -29,4 +29,4 @@ $pluginRoot = '/plugins/backupper';
|
||||
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
|
||||
</div>
|
||||
<script>window.BACKUPPER_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
|
||||
<script src="<?= $pluginRoot ?>/assets/backupper-2026.06.14.3.js"></script>
|
||||
<script src="<?= $pluginRoot ?>/assets/backupper-2026.06.14.4.js"></script>
|
||||
|
||||
@@ -54,9 +54,17 @@
|
||||
#backupper-app .bu-tabs button.active { border-bottom-color: var(--bu-accent) !important; color: var(--bu-text) !important; opacity: 1; }
|
||||
|
||||
.bu-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
|
||||
.bu-chart-grid-layout { display: grid; gap: 16px; grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 16px; }
|
||||
.bu-card { background: var(--bu-panel); border: 1px solid var(--bu-border); border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.12); color: var(--bu-text); padding: 18px; }
|
||||
#backupper-app .bu-card h2, #backupper-app .bu-card h3 { color: var(--bu-text) !important; font-weight: 700; margin-top: 0; }
|
||||
.bu-stat { color: var(--bu-text); font-size: 32px; font-weight: 700; }
|
||||
.bu-chart { display: block; height: 240px; margin-top: 12px; overflow: visible; width: 100%; }
|
||||
.bu-chart-grid line { stroke: var(--bu-border); stroke-width: 1; }
|
||||
.bu-chart-grid text, .bu-chart-date { fill: var(--bu-muted); font-size: 11px; }
|
||||
.bu-chart-area { fill: var(--bu-accent); opacity: .10; }
|
||||
.bu-chart-line { fill: none; stroke: var(--bu-accent); stroke-linecap: round; stroke-linejoin: round; stroke-width: 3; }
|
||||
.bu-chart-dots circle { fill: var(--bu-panel); stroke: var(--bu-accent); stroke-width: 3; }
|
||||
.bu-chart-empty { align-items: center; color: var(--bu-muted); display: flex; height: 240px; justify-content: center; text-align: center; }
|
||||
#backupper-app .bu-muted { color: var(--bu-muted) !important; opacity: .72; }
|
||||
.bu-toolbar { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; margin-bottom: 14px; }
|
||||
.bu-actions { display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
@@ -173,6 +181,7 @@
|
||||
@media (max-width: 700px) {
|
||||
#backupper-app { padding-left: 8px; padding-right: 8px; }
|
||||
.bu-form { grid-template-columns: 1fr; }
|
||||
.bu-chart-grid-layout { grid-template-columns: 1fr; }
|
||||
.bu-header { align-items: flex-start; flex-direction: column; gap: 12px; }
|
||||
.bu-table { display: block; overflow-x: auto; }
|
||||
.bu-table-actions { min-width: 240px; }
|
||||
|
||||
@@ -110,6 +110,26 @@
|
||||
const button = (label, action, kind = '') => `<button class="bu-button ${kind}" type="button" data-action="${esc(action)}"${tipAttr(actionTooltip(action, label))}>${esc(label)}</button>`;
|
||||
const disabledButton = (label, help) => `<button class="bu-button" type="button" disabled${tipAttr(help)}>${esc(label)}</button>`;
|
||||
const actionGroup = (buttons) => `<div class="bu-actions bu-table-actions">${buttons}</div>`;
|
||||
const formatBytes = (value) => {
|
||||
const units = ['B','KiB','MiB','GiB','TiB']; let size = Number(value)||0; let unit = 0;
|
||||
while (size >= 1024 && unit < units.length-1) { size /= 1024; unit++; }
|
||||
return `${size >= 10 || unit === 0 ? size.toFixed(0) : size.toFixed(1)} ${units[unit]}`;
|
||||
};
|
||||
|
||||
function lineChart(runs, field, formatter, emptyText) {
|
||||
const points = runs.map(run=>({run,value:Number(run[field] || (field==='bytesProcessed'?run.bytesAdded:run.filesNew) || 0)}));
|
||||
if (!points.length || points.every(point=>point.value===0)) return `<div class="bu-chart-empty">${esc(emptyText)}</div>`;
|
||||
const width=720, height=220, left=54, right=18, top=18, bottom=38, innerW=width-left-right, innerH=height-top-bottom;
|
||||
const max=Math.max(...points.map(point=>point.value),1);
|
||||
const coords=points.map((point,index)=>({ ...point, x:left+(points.length===1?innerW/2:index*innerW/(points.length-1)), y:top+innerH-(point.value/max)*innerH }));
|
||||
const polyline=coords.map(point=>`${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ');
|
||||
const area=`${left},${top+innerH} ${polyline} ${left+innerW},${top+innerH}`;
|
||||
const grid=[0,.25,.5,.75,1].map(ratio=>{const y=top+innerH-ratio*innerH; return `<line x1="${left}" y1="${y}" x2="${left+innerW}" y2="${y}"/><text x="${left-8}" y="${y+4}" text-anchor="end">${esc(formatter(max*ratio))}</text>`;}).join('');
|
||||
const dots=coords.map(point=>{const job=state.config.jobs.find(item=>item.id===point.run.jobId)?.name||point.run.jobId; const date=new Date(point.run.finishedAt||point.run.createdAt).toLocaleString(); return `<circle cx="${point.x}" cy="${point.y}" r="4"><title>${esc(`${date} · ${job} · ${formatter(point.value)}`)}</title></circle>`;}).join('');
|
||||
const first=new Date(points[0].run.finishedAt||points[0].run.createdAt).toLocaleDateString();
|
||||
const last=new Date(points.at(-1).run.finishedAt||points.at(-1).run.createdAt).toLocaleDateString();
|
||||
return `<svg class="bu-chart" viewBox="0 0 ${width} ${height}" role="img"><g class="bu-chart-grid">${grid}</g><polygon class="bu-chart-area" points="${area}"/><polyline class="bu-chart-line" points="${polyline}"/><g class="bu-chart-dots">${dots}</g><text class="bu-chart-date" x="${left}" y="${height-8}">${esc(first)}</text><text class="bu-chart-date" x="${left+innerW}" y="${height-8}" text-anchor="end">${esc(last)}</text></svg>`;
|
||||
}
|
||||
|
||||
function setTooltip(element, text) {
|
||||
if (!element || !text || element.dataset.tooltip) return;
|
||||
@@ -203,11 +223,15 @@
|
||||
const active = state.runs.filter(r => ['queued','running'].includes(r.status)).length;
|
||||
const failures = state.runs.filter(r => r.status === 'failed').slice(-5);
|
||||
const last = [...state.runs].reverse().slice(0, 8);
|
||||
const backups = state.runs.filter(r=>r.taskType==='backup' && ['success','warning'].includes(r.status)).slice(-20);
|
||||
return `<div class="bu-grid">
|
||||
<section class="bu-card"><div class="bu-muted">Enabled jobs</div><div class="bu-stat">${state.config.jobs.filter(j => j.enabled).length}</div></section>
|
||||
<section class="bu-card"><div class="bu-muted">Repositories</div><div class="bu-stat">${state.config.repositories.length}</div></section>
|
||||
<section class="bu-card"><div class="bu-muted">Queued / running</div><div class="bu-stat">${active}</div></section>
|
||||
<section class="bu-card"><div class="bu-muted">Recent failures</div><div class="bu-stat">${failures.length}</div></section>
|
||||
</div><div class="bu-chart-grid-layout">
|
||||
<section class="bu-card"><h2>Backup size</h2><div class="bu-muted">Total source data processed per backup</div>${lineChart(backups,'bytesProcessed',formatBytes,'Run a backup to collect size history.')}</section>
|
||||
<section class="bu-card"><h2>Files backed up</h2><div class="bu-muted">Total files processed per backup</div>${lineChart(backups,'filesProcessed',value=>Math.round(value).toLocaleString(),'Run a backup to collect file history.')}</section>
|
||||
</div><section class="bu-card" style="margin-top:16px"><h2>Recent activity</h2>${runsTable(last)}</section>`;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user