diff --git a/dist/urbm-2026.07.13.r009-x86_64-1.txz.sha256 b/dist/urbm-2026.07.13.r009-x86_64-1.txz.sha256 deleted file mode 100644 index 540c94c..0000000 --- a/dist/urbm-2026.07.13.r009-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -11efad3fbbe8f71092177ef912353d0c0f733b66974af18aba83a0275c2c22f9 urbm-2026.07.13.r009-x86_64-1.txz diff --git a/dist/urbm-2026.07.13.r009-x86_64-1.txz b/dist/urbm-2026.07.13.r010-x86_64-1.txz similarity index 56% rename from dist/urbm-2026.07.13.r009-x86_64-1.txz rename to dist/urbm-2026.07.13.r010-x86_64-1.txz index 22118ce..3ac4416 100644 Binary files a/dist/urbm-2026.07.13.r009-x86_64-1.txz and b/dist/urbm-2026.07.13.r010-x86_64-1.txz differ diff --git a/dist/urbm-2026.07.13.r010-x86_64-1.txz.sha256 b/dist/urbm-2026.07.13.r010-x86_64-1.txz.sha256 new file mode 100644 index 0000000..e90fe19 --- /dev/null +++ b/dist/urbm-2026.07.13.r010-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +3f655152c8e0875711c8b02036b05b1a163227a658fe6b2e4bf91397270a7f48 urbm-2026.07.13.r010-x86_64-1.txz diff --git a/dist/urbm.plg b/dist/urbm.plg index 57b6ba4..9c0389e 100644 --- a/dist/urbm.plg +++ b/dist/urbm.plg @@ -2,13 +2,17 @@ - + - + ]> +### 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 - 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. diff --git a/internal/service/safety_test.go b/internal/service/safety_test.go index d776c7d..6135754 100644 --- a/internal/service/safety_test.go +++ b/internal/service/safety_test.go @@ -2,6 +2,7 @@ package service import ( "context" + "encoding/json" "os" "path/filepath" "testing" @@ -11,6 +12,17 @@ import ( "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) { s := &Service{repositoryLocks: map[string]chan struct{}{}} repo := model.Repository{Type: model.RepositoryLocal, Location: "/repo"} diff --git a/internal/service/service.go b/internal/service/service.go index d174b4f..f256752 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -134,7 +134,9 @@ func (s *Service) Runs() []model.Run { return s.queue.Snapshot() } func (s *Service) BackupMetrics() []model.BackupMetric { s.metricsMu.RLock() 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 { return map[string]any{ diff --git a/plugin/urbm.plg b/plugin/urbm.plg index 4eff816..cfb5e42 100644 --- a/plugin/urbm.plg +++ b/plugin/urbm.plg @@ -2,13 +2,17 @@ - + ]> +### 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 - 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. diff --git a/webgui/URBM.page b/webgui/URBM.page index 3f03172..84cf0b1 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.js b/webgui/assets/urbm.js index 3331fea..73c4c77 100644 --- a/webgui/assets/urbm.js +++ b/webgui/assets/urbm.js @@ -402,7 +402,7 @@ async function load() { try { 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(); } catch (error) { health.textContent = 'Daemon nicht erreichbar'; health.className = 'bu-health bad'; @@ -1105,6 +1105,6 @@ window.addEventListener('scroll', hideTooltip, true); window.addEventListener('resize', hideTooltip); 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(); })();