diff --git a/dist/urbm-2026.07.13.r005-x86_64-1.txz.sha256 b/dist/urbm-2026.07.13.r005-x86_64-1.txz.sha256 deleted file mode 100644 index 005462c..0000000 --- a/dist/urbm-2026.07.13.r005-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -d8f84b5b02336673ea9816f589546a7f2b7f7bdc805f945561228f84504feac4 urbm-2026.07.13.r005-x86_64-1.txz diff --git a/dist/urbm-2026.07.13.r005-x86_64-1.txz b/dist/urbm-2026.07.13.r006-x86_64-1.txz similarity index 56% rename from dist/urbm-2026.07.13.r005-x86_64-1.txz rename to dist/urbm-2026.07.13.r006-x86_64-1.txz index 937bdf9..268f5cd 100644 Binary files a/dist/urbm-2026.07.13.r005-x86_64-1.txz and b/dist/urbm-2026.07.13.r006-x86_64-1.txz differ diff --git a/dist/urbm-2026.07.13.r006-x86_64-1.txz.sha256 b/dist/urbm-2026.07.13.r006-x86_64-1.txz.sha256 new file mode 100644 index 0000000..f6f4782 --- /dev/null +++ b/dist/urbm-2026.07.13.r006-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +fbefb46df55f79eb03e1b272295dddb02f3d38ed320eadb7e8bbe8ea3391ec53 urbm-2026.07.13.r006-x86_64-1.txz diff --git a/dist/urbm.plg b/dist/urbm.plg index bcacf0a..e5b0015 100644 --- a/dist/urbm.plg +++ b/dist/urbm.plg @@ -2,13 +2,18 @@ - + - + ]> +### 2026.07.13.r006 +- Show timestamp, job, absolute value, and signed change from the previous run when hovering or focusing chart points. +- Compare points only with the previous run of the same backup job, including a predecessor just outside the visible seven-day range. +- Avoid rebuilding an idle dashboard every two seconds so chart tooltips remain stable. + ### 2026.07.13.r005 - Restore the dashboard backup-size and file-count timelines using completed runs from the last seven days. - Plot runs at their actual time and add a backup-job selector that defaults to the most recently used job. diff --git a/plugin/urbm.plg b/plugin/urbm.plg index 54524cf..adbe177 100644 --- a/plugin/urbm.plg +++ b/plugin/urbm.plg @@ -2,13 +2,18 @@ - + ]> +### 2026.07.13.r006 +- Show timestamp, job, absolute value, and signed change from the previous run when hovering or focusing chart points. +- Compare points only with the previous run of the same backup job, including a predecessor just outside the visible seven-day range. +- Avoid rebuilding an idle dashboard every two seconds so chart tooltips remain stable. + ### 2026.07.13.r005 - Restore the dashboard backup-size and file-count timelines using completed runs from the last seven days. - Plot runs at their actual time and add a backup-job selector that defaults to the most recently used job. diff --git a/webgui/URBM.page b/webgui/URBM.page index 4c9b21a..ce45433 100644 --- a/webgui/URBM.page +++ b/webgui/URBM.page @@ -8,7 +8,7 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore" --- diff --git a/webgui/assets/urbm.css b/webgui/assets/urbm.css index 1141fcf..f501101 100644 --- a/webgui/assets/urbm.css +++ b/webgui/assets/urbm.css @@ -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; } diff --git a/webgui/assets/urbm.js b/webgui/assets/urbm.js index e0ed71c..2f0f48d 100644 --- a/webgui/assets/urbm.js +++ b/webgui/assets/urbm.js @@ -305,10 +305,16 @@ return `
Backup-Zeitplan
Dieser Job wird nicht automatisch gestartet. Er kann jederzeit in der Job-Liste über Starten ausgeführt werden.
Wochentage${weekDays.map(day=>``).join('')}
${schedule.mode==='custom'?``:''}
`; } - 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 `
${esc(emptyText)}
`; 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 `${esc(formatter(max*ratio))}`;}).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 `${esc(`${date} · ${job} · ${formatter(point.value)}`)}`;}).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 `${esc(hint)}`;}).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 `${esc(value)}`;}).join(''); return `${grid}${dots}${dateLabels}`; } @@ -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=`${backupJobs.map(job=>``).join('')}`; const needsSetup=!state.config.jobs.length||!state.config.repositories.length; const latestFailed=orderedRuns[0]?.status==='failed'; @@ -471,8 +476,8 @@
Wartend / laufend
${active}
Jüngste Fehler
${failures.length}
Letzte 7 Tage

Backup-Verlauf

Jeder Punkt entspricht einem abgeschlossenen Backup-Lauf.

-

Backup-Größe

Verarbeitete Quelldaten pro Backup
${lineChart(backups,'bytesProcessed',formatBytes,'In den letzten sieben Tagen liegen keine abgeschlossenen Backups vor.')}
-

Gesicherte Dateien

Verarbeitete Dateien pro Backup
${lineChart(backups,'filesProcessed',value=>Math.round(value).toLocaleString(),'In den letzten sieben Tagen liegen keine abgeschlossenen Backups vor.')}
+

Backup-Größe

Verarbeitete Quelldaten pro Backup
${lineChart(backups,'bytesProcessed',formatBytes,'Größe','In den letzten sieben Tagen liegen keine abgeschlossenen Backups vor.')}
+

Gesicherte Dateien

Verarbeitete Dateien pro Backup
${lineChart(backups,'filesProcessed',value=>Math.round(value).toLocaleString(),'Dateien','In den letzten sieben Tagen liegen keine abgeschlossenen Backups vor.')}

Letzte Aktivitäten

Die sechs jüngsten Vorgänge auf einen Blick.
${button('Alle anzeigen','go-view:activity')}
${runsTable(last)}
`; } @@ -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(); })();