Handle empty dashboard metrics safely
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
11efad3fbbe8f71092177ef912353d0c0f733b66974af18aba83a0275c2c22f9 urbm-2026.07.13.r009-x86_64-1.txz
|
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
3f655152c8e0875711c8b02036b05b1a163227a658fe6b2e4bf91397270a7f48 urbm-2026.07.13.r010-x86_64-1.txz
|
||||||
Vendored
+6
-2
@@ -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.
|
||||||
|
|||||||
@@ -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"}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 ?>">
|
||||||
|
|||||||
@@ -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();
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user