Add backup chart deltas
This commit is contained in:
@@ -87,7 +87,8 @@
|
||||
.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-dots circle { cursor: pointer; fill: var(--bu-panel); stroke: var(--bu-accent); stroke-width: 3; transition: fill .15s ease, stroke-width .15s ease; }
|
||||
.bu-chart-dots circle:hover, .bu-chart-dots circle:focus { fill: var(--bu-accent); outline: none; stroke-width: 5; }
|
||||
.bu-chart-bar { fill: var(--bu-accent); opacity: .82; }
|
||||
.bu-chart-bar:hover { opacity: 1; }
|
||||
.bu-chart-empty { align-items: center; color: var(--bu-muted); display: flex; height: 240px; justify-content: center; text-align: center; }
|
||||
|
||||
+14
-9
@@ -305,10 +305,16 @@
|
||||
return `<fieldset class="wide bu-fieldset bu-schedule"><legend>Backup-Zeitplan</legend><div class="bu-schedule-grid"><label>Ausführung<select name="scheduleMode">${modes.map(([value,label])=>`<option value="${value}" ${value===schedule.mode?'selected':''}>${label}</option>`).join('')}</select></label><label class="bu-schedule-time" ${schedule.mode==='manual'?'hidden':''}>Uhrzeit<input name="scheduleTime" type="time" required value="${esc(schedule.time)}"></label></div><div class="bu-schedule-manual bu-inline-info" ${schedule.mode==='manual'?'':'hidden'}>Dieser Job wird nicht automatisch gestartet. Er kann jederzeit in der Job-Liste über <strong>Starten</strong> ausgeführt werden.</div><div class="bu-weekdays" ${schedule.mode==='selected'?'':'hidden'}><span>Wochentage</span>${weekDays.map(day=>`<label><input type="checkbox" name="scheduleDays" value="${day.value}" ${schedule.days.includes(day.value)?'checked':''}><span>${day.short}</span></label>`).join('')}</div>${schedule.mode==='custom'?`<label class="bu-custom-cron">Bestehender Zeitplan<input name="customCron" readonly value="${esc(schedule.cron)}"></label>`:''}</fieldset>`;
|
||||
}
|
||||
|
||||
function lineChart(runs, field, formatter, emptyText, days=7) {
|
||||
function lineChart(runs, field, formatter, valueLabel, emptyText, days=7) {
|
||||
const end=Date.now();
|
||||
const start=end-days*24*60*60*1000;
|
||||
const points = runs.map(run=>({run,time:new Date(run.finishedAt||run.createdAt).getTime(),value:Number(run[field] || (field==='bytesProcessed'?run.bytesAdded:run.filesNew) || 0)})).filter(point=>point.time>=start&&point.time<=end).sort((a,b)=>a.time-b.time);
|
||||
const previousByJob=new Map();
|
||||
const allPoints=runs.map(run=>({run,time:new Date(run.finishedAt||run.createdAt).getTime(),value:Number(run[field] || (field==='bytesProcessed'?run.bytesAdded:run.filesNew) || 0)})).sort((a,b)=>a.time-b.time).map(point=>{
|
||||
const previous=previousByJob.get(point.run.jobId);
|
||||
previousByJob.set(point.run.jobId,point);
|
||||
return {...point,delta:previous?point.value-previous.value:null};
|
||||
});
|
||||
const points=allPoints.filter(point=>point.time>=start&&point.time<=end);
|
||||
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);
|
||||
@@ -316,7 +322,7 @@
|
||||
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 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();const delta=point.delta===null?'kein vorheriger Lauf':point.delta===0?'±0':`${point.delta>0?'+':'−'}${formatter(Math.abs(point.delta))}`;const hint=`Zeitpunkt: ${date} · Job: ${job} · ${valueLabel}: ${formatter(point.value)} · Veränderung: ${delta}`;return `<circle cx="${point.x}" cy="${point.y}" r="5" tabindex="0" role="img" aria-label="${esc(hint)}" data-tooltip="${esc(hint)}" data-tooltip-delay="100"><title>${esc(hint)}</title></circle>`;}).join('');
|
||||
const dateLabels=[0,1/3,2/3,1].map((ratio,index)=>{const value=new Date(start+(end-start)*ratio).toLocaleDateString(undefined,{day:'2-digit',month:'2-digit'});return `<text class="bu-chart-date" x="${left+innerW*ratio}" y="${height-8}" text-anchor="${index===0?'start':index===3?'end':'middle'}">${esc(value)}</text>`;}).join('');
|
||||
return `<svg class="bu-chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="Verlauf der letzten ${days} Tage"><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>${dateLabels}</svg>`;
|
||||
}
|
||||
@@ -455,8 +461,7 @@
|
||||
const backupJobs=state.config.jobs.filter(job=>job.type!=='rsync');
|
||||
if(!state.dashboardJobId) state.dashboardJobId=orderedRuns.find(run=>run.taskType==='backup'&&backupJobs.some(job=>job.id===run.jobId))?.jobId||backupJobs[0]?.id||'all';
|
||||
if(state.dashboardJobId!=='all'&&!backupJobs.some(job=>job.id===state.dashboardJobId)) state.dashboardJobId='all';
|
||||
const sevenDaysAgo=Date.now()-7*24*60*60*1000;
|
||||
const backups=orderedRuns.filter(run=>run.taskType==='backup'&&['success','warning'].includes(run.status)&&runTime(run)>=sevenDaysAgo&&(state.dashboardJobId==='all'||run.jobId===state.dashboardJobId));
|
||||
const backups=orderedRuns.filter(run=>run.taskType==='backup'&&['success','warning'].includes(run.status)&&(state.dashboardJobId==='all'||run.jobId===state.dashboardJobId));
|
||||
const chartOptions=`<option value="all" ${state.dashboardJobId==='all'?'selected':''}>Alle Backup-Jobs</option>${backupJobs.map(job=>`<option value="${esc(job.id)}" ${state.dashboardJobId===job.id?'selected':''}>${esc(job.name)}</option>`).join('')}`;
|
||||
const needsSetup=!state.config.jobs.length||!state.config.repositories.length;
|
||||
const latestFailed=orderedRuns[0]?.status==='failed';
|
||||
@@ -471,8 +476,8 @@
|
||||
<section class="bu-card bu-stat-card"><div><div class="bu-muted">Wartend / laufend</div><div class="bu-stat">${active}</div></div><span class="bu-stat-icon" aria-hidden="true">↻</span></section>
|
||||
<section class="bu-card bu-stat-card ${failures.length?'has-alert':''}"><div><div class="bu-muted">Jüngste Fehler</div><div class="bu-stat">${failures.length}</div></div><span class="bu-stat-icon" aria-hidden="true">!</span></section>
|
||||
</div><div class="bu-chart-toolbar"><div><span class="bu-eyebrow">Letzte 7 Tage</span><h2>Backup-Verlauf</h2><p class="bu-muted">Jeder Punkt entspricht einem abgeschlossenen Backup-Lauf.</p></div><label>Backup-Job<select id="bu-dashboard-job">${chartOptions}</select></label></div><div class="bu-chart-grid-layout">
|
||||
<section class="bu-card"><h2>Backup-Größe</h2><div class="bu-muted">Verarbeitete Quelldaten pro Backup</div>${lineChart(backups,'bytesProcessed',formatBytes,'In den letzten sieben Tagen liegen keine abgeschlossenen Backups vor.')}</section>
|
||||
<section class="bu-card"><h2>Gesicherte Dateien</h2><div class="bu-muted">Verarbeitete Dateien pro Backup</div>${lineChart(backups,'filesProcessed',value=>Math.round(value).toLocaleString(),'In den letzten sieben Tagen liegen keine abgeschlossenen Backups vor.')}</section>
|
||||
<section class="bu-card"><h2>Backup-Größe</h2><div class="bu-muted">Verarbeitete Quelldaten pro Backup</div>${lineChart(backups,'bytesProcessed',formatBytes,'Größe','In den letzten sieben Tagen liegen keine abgeschlossenen Backups vor.')}</section>
|
||||
<section class="bu-card"><h2>Gesicherte Dateien</h2><div class="bu-muted">Verarbeitete Dateien pro Backup</div>${lineChart(backups,'filesProcessed',value=>Math.round(value).toLocaleString(),'Dateien','In den letzten sieben Tagen liegen keine abgeschlossenen Backups vor.')}</section>
|
||||
</div><section class="bu-card bu-section-gap"><div class="bu-toolbar"><div><h2>Letzte Aktivitäten</h2><div class="bu-muted">Die sechs jüngsten Vorgänge auf einen Blick.</div></div>${button('Alle anzeigen','go-view:activity')}</div>${runsTable(last)}</section>`;
|
||||
}
|
||||
|
||||
@@ -1083,7 +1088,7 @@
|
||||
root.addEventListener('mouseover', e => {
|
||||
const target = e.target.closest('[data-tooltip]');
|
||||
if (!target || target.contains(e.relatedTarget)) return;
|
||||
showTooltip(target, 1200);
|
||||
showTooltip(target,Number(target.dataset.tooltipDelay||1200));
|
||||
});
|
||||
root.addEventListener('mouseout', e => {
|
||||
const target = e.target.closest('[data-tooltip]');
|
||||
@@ -1099,6 +1104,6 @@
|
||||
window.addEventListener('scroll', hideTooltip, true);
|
||||
window.addEventListener('resize', hideTooltip);
|
||||
enhanceTooltips(root);
|
||||
setInterval(async()=>{ if(!state.config)return; try { state.runs=newestRuns(await api('/v1/runs')); if(['dashboard','activity'].includes(state.view))render(); }catch(_){ } },2000);
|
||||
setInterval(async()=>{ if(!state.config)return; try { const runs=newestRuns(await api('/v1/runs')); const changed=JSON.stringify(runs)!==JSON.stringify(state.runs); state.runs=runs; if(changed&&['dashboard','activity'].includes(state.view))render(); }catch(_){ } },2000);
|
||||
load();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user