Handle empty dashboard metrics safely

This commit is contained in:
Mikei386
2026-07-13 20:44:46 +02:00
parent b1fba3ed16
commit ea97d7dbc9
9 changed files with 30 additions and 8 deletions
-1
View File
@@ -1 +0,0 @@
11efad3fbbe8f71092177ef912353d0c0f733b66974af18aba83a0275c2c22f9 urbm-2026.07.13.r009-x86_64-1.txz
+1
View File
@@ -0,0 +1 @@
3f655152c8e0875711c8b02036b05b1a163227a658fe6b2e4bf91397270a7f48 urbm-2026.07.13.r010-x86_64-1.txz
+6 -2
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.07.13.r009"> <!ENTITY version "2026.07.13.r010">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "11efad3fbbe8f71092177ef912353d0c0f733b66974af18aba83a0275c2c22f9"> <!ENTITY packageSHA256 "3f655152c8e0875711c8b02036b05b1a163227a658fe6b2e4bf91397270a7f48">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES> <CHANGES>
### 2026.07.13.r010
- Return an empty JSON array instead of `null` when no dashboard backup measurements exist yet.
- Make the WebGUI tolerate empty or invalid metric responses so the rest of the interface remains available.
### 2026.07.13.r009 ### 2026.07.13.r009
- Store completed-backup chart measurements independently in `backup-metrics.json` so clearing activities no longer erases the dashboard history. - Store completed-backup chart measurements independently in `backup-metrics.json` so clearing activities no longer erases the dashboard history.
- Migrate existing successful backup runs into the new durable metric history automatically and expose them through a dedicated read-only API. - Migrate existing successful backup runs into the new durable metric history automatically and expose them through a dedicated read-only API.
+12
View File
@@ -2,6 +2,7 @@ package service
import ( import (
"context" "context"
"encoding/json"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -11,6 +12,17 @@ import (
"git.casaderoll.de/michael/urbm/internal/store" "git.casaderoll.de/michael/urbm/internal/store"
) )
func TestEmptyBackupMetricsEncodeAsJSONArray(t *testing.T) {
s := &Service{}
encoded, err := json.Marshal(s.BackupMetrics())
if err != nil {
t.Fatal(err)
}
if string(encoded) != "[]" {
t.Fatalf("empty backup metrics JSON = %s", encoded)
}
}
func TestRepositoryLockHonorsContextCancellation(t *testing.T) { func TestRepositoryLockHonorsContextCancellation(t *testing.T) {
s := &Service{repositoryLocks: map[string]chan struct{}{}} s := &Service{repositoryLocks: map[string]chan struct{}{}}
repo := model.Repository{Type: model.RepositoryLocal, Location: "/repo"} repo := model.Repository{Type: model.RepositoryLocal, Location: "/repo"}
+3 -1
View File
@@ -134,7 +134,9 @@ func (s *Service) Runs() []model.Run { return s.queue.Snapshot() }
func (s *Service) BackupMetrics() []model.BackupMetric { func (s *Service) BackupMetrics() []model.BackupMetric {
s.metricsMu.RLock() s.metricsMu.RLock()
defer s.metricsMu.RUnlock() defer s.metricsMu.RUnlock()
return append([]model.BackupMetric(nil), s.backupMetrics...) metrics := make([]model.BackupMetric, len(s.backupMetrics))
copy(metrics, s.backupMetrics)
return metrics
} }
func (s *Service) Logs() map[string]any { func (s *Service) Logs() map[string]any {
return map[string]any{ return map[string]any{
+5 -1
View File
@@ -2,13 +2,17 @@
<!DOCTYPE PLUGIN [ <!DOCTYPE PLUGIN [
<!ENTITY name "urbm"> <!ENTITY name "urbm">
<!ENTITY author "Michael Roll"> <!ENTITY author "Michael Roll">
<!ENTITY version "2026.07.13.r009"> <!ENTITY version "2026.07.13.r010">
<!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg"> <!ENTITY pluginURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm.plg">
<!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz"> <!ENTITY packageURL "https://git.casaderoll.de/michael/URBM/raw/branch/main/dist/urbm-&version;-x86_64-1.txz">
<!ENTITY packageSHA256 "REPLACE_DURING_RELEASE"> <!ENTITY packageSHA256 "REPLACE_DURING_RELEASE">
]> ]>
<PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png"> <PLUGIN name="&name;" author="&author;" version="&version;" pluginURL="&pluginURL;" min="7.0.0" support="https://git.casaderoll.de/michael/URBM/issues" icon="urbm.png">
<CHANGES> <CHANGES>
### 2026.07.13.r010
- Return an empty JSON array instead of `null` when no dashboard backup measurements exist yet.
- Make the WebGUI tolerate empty or invalid metric responses so the rest of the interface remains available.
### 2026.07.13.r009 ### 2026.07.13.r009
- Store completed-backup chart measurements independently in `backup-metrics.json` so clearing activities no longer erases the dashboard history. - Store completed-backup chart measurements independently in `backup-metrics.json` so clearing activities no longer erases the dashboard history.
- Migrate existing successful backup runs into the new durable metric history automatically and expose them through a dedicated read-only API. - Migrate existing successful backup runs into the new durable metric history automatically and expose them through a dedicated read-only API.
+1 -1
View File
@@ -8,7 +8,7 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
--- ---
<?php <?php
$pluginRoot = '/plugins/urbm'; $pluginRoot = '/plugins/urbm';
$urbmVersion = '2026.07.13.r009'; $urbmVersion = '2026.07.13.r010';
$urbmAssetVersion = preg_replace('/[^A-Za-z0-9]/', '', $urbmVersion); $urbmAssetVersion = preg_replace('/[^A-Za-z0-9]/', '', $urbmVersion);
?> ?>
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=<?= $urbmAssetVersion ?>"> <link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=<?= $urbmAssetVersion ?>">
+2 -2
View File
@@ -402,7 +402,7 @@
async function load() { async function load() {
try { try {
const [config, runs, backupMetrics, daemon] = await Promise.all([api('/v1/config'), api('/v1/runs'), api('/v1/backup-metrics'), api('/v1/health')]); const [config, runs, backupMetrics, daemon] = await Promise.all([api('/v1/config'), api('/v1/runs'), api('/v1/backup-metrics'), api('/v1/health')]);
state.config = config; state.runs = newestRuns(runs); state.backupMetrics = backupMetrics; state.config = config; state.runs = newestRuns(runs); state.backupMetrics = Array.isArray(backupMetrics) ? backupMetrics : [];
health.textContent = `Daemon online (${daemon.version || 'unbekannt'})`; health.className = 'bu-health ok'; render(); health.textContent = `Daemon online (${daemon.version || 'unbekannt'})`; health.className = 'bu-health ok'; render();
} catch (error) { } catch (error) {
health.textContent = 'Daemon nicht erreichbar'; health.className = 'bu-health bad'; health.textContent = 'Daemon nicht erreichbar'; health.className = 'bu-health bad';
@@ -1105,6 +1105,6 @@
window.addEventListener('scroll', hideTooltip, true); window.addEventListener('scroll', hideTooltip, true);
window.addEventListener('resize', hideTooltip); window.addEventListener('resize', hideTooltip);
enhanceTooltips(root); enhanceTooltips(root);
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){ state.backupMetrics=await api('/v1/backup-metrics'); 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){ const backupMetrics=await api('/v1/backup-metrics'); state.backupMetrics=Array.isArray(backupMetrics) ? backupMetrics : []; if(['dashboard','activity'].includes(state.view))render(); } }catch(_){ } },2000);
load(); load();
})(); })();