diff --git a/dist/urbm-2026.06.15.r011-x86_64-1.txz.sha256 b/dist/urbm-2026.06.15.r011-x86_64-1.txz.sha256 deleted file mode 100644 index 50c9ad0..0000000 --- a/dist/urbm-2026.06.15.r011-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -84579aa8c702a442494daa340631dc71ef485fa05bbbfdd2980a68611e226bb4 urbm-2026.06.15.r011-x86_64-1.txz diff --git a/dist/urbm-2026.06.15.r011-x86_64-1.txz b/dist/urbm-2026.06.15.r012-x86_64-1.txz similarity index 55% rename from dist/urbm-2026.06.15.r011-x86_64-1.txz rename to dist/urbm-2026.06.15.r012-x86_64-1.txz index 06358d3..a51b8cc 100644 Binary files a/dist/urbm-2026.06.15.r011-x86_64-1.txz and b/dist/urbm-2026.06.15.r012-x86_64-1.txz differ diff --git a/dist/urbm-2026.06.15.r012-x86_64-1.txz.sha256 b/dist/urbm-2026.06.15.r012-x86_64-1.txz.sha256 new file mode 100644 index 0000000..bd35dd6 --- /dev/null +++ b/dist/urbm-2026.06.15.r012-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +5503027a5ce2c20162e16786a1892d13046fa8002fca2f156fc13b2ef66bf89f urbm-2026.06.15.r012-x86_64-1.txz diff --git a/dist/urbm.plg b/dist/urbm.plg index 306fa48..ee5a58a 100644 --- a/dist/urbm.plg +++ b/dist/urbm.plg @@ -2,13 +2,18 @@ - + - + ]> +### 2026.06.15.r012 +- Allow multiple backup jobs to share one Restic repository while retaining per-job snapshot tags and retention policies. +- Catch asynchronous form submission errors so save failures are shown in the WebGUI instead of only in the browser console. +- Clarify shared-repository behavior in backup job help text. + ### 2026.06.15.r011 - Parse Restic repository statistics correctly when progress output precedes the JSON result. - Support compact and pretty-printed multi-line statistics JSON. diff --git a/internal/model/validate.go b/internal/model/validate.go index 2e36906..95fa02d 100644 --- a/internal/model/validate.go +++ b/internal/model/validate.go @@ -26,7 +26,6 @@ func ValidateConfig(c Config) error { repos[repo.ID] = repo } jobs := make(map[string]struct{}, len(c.Jobs)) - repositoryOwners := make(map[string]string, len(c.Jobs)) for _, job := range c.Jobs { if err := ValidateJob(job); err != nil { return fmt.Errorf("job %q: %w", job.ID, err) @@ -38,10 +37,6 @@ func ValidateConfig(c Config) error { if _, exists := repos[job.RepositoryID]; !exists { return fmt.Errorf("job %q references unknown repository %q", job.ID, job.RepositoryID) } - if owner, exists := repositoryOwners[job.RepositoryID]; exists { - return fmt.Errorf("repository %q is already assigned to job %q", job.RepositoryID, owner) - } - repositoryOwners[job.RepositoryID] = job.ID } notifications := make(map[string]struct{}, len(c.Notifications)) for _, target := range c.Notifications { diff --git a/internal/model/validate_test.go b/internal/model/validate_test.go index 075a80f..2afe12b 100644 --- a/internal/model/validate_test.go +++ b/internal/model/validate_test.go @@ -46,15 +46,15 @@ func TestRepositoryIsolation(t *testing.T) { } } -func TestRepositoryCannotBeSharedByJobs(t *testing.T) { +func TestRepositoryCanBeSharedByJobs(t *testing.T) { c := DefaultConfig() c.Repositories = []Repository{{SchemaVersion: 1, ID: "repo", Name: "Local", Type: RepositoryLocal, Location: "/tmp/repo", PasswordRef: "password"}} - base := Job{SchemaVersion: 1, Name: "Flash", Type: JobFlash, RepositoryID: "repo", Consistency: Consistency{Mode: "live"}, Compression: "auto"} + base := Job{SchemaVersion: 1, Name: "Flash", Type: JobFlash, RepositoryID: "repo", Consistency: Consistency{Mode: "live"}, Compression: "auto", Retention: Retention{KeepWithinDays: 30}} first, second := base, base first.ID, second.ID = "first", "second" c.Jobs = []Job{first, second} - if err := ValidateConfig(c); err == nil { - t.Fatal("shared repository accepted") + if err := ValidateConfig(c); err != nil { + t.Fatalf("shared repository rejected: %v", err) } } diff --git a/plugin/urbm.plg b/plugin/urbm.plg index 1faaf2d..7ec0cef 100644 --- a/plugin/urbm.plg +++ b/plugin/urbm.plg @@ -2,13 +2,18 @@ - + ]> +### 2026.06.15.r012 +- Allow multiple backup jobs to share one Restic repository while retaining per-job snapshot tags and retention policies. +- Catch asynchronous form submission errors so save failures are shown in the WebGUI instead of only in the browser console. +- Clarify shared-repository behavior in backup job help text. + ### 2026.06.15.r011 - Parse Restic repository statistics correctly when progress output precedes the JSON result. - Support compact and pretty-printed multi-line statistics JSON. diff --git a/webgui/URBM.page b/webgui/URBM.page index 1281902..9059da8 100644 --- a/webgui/URBM.page +++ b/webgui/URBM.page @@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore" - +
- -

URBM 2026.06.15.r011

Restic Backup Manager für Unraid

+ +

URBM 2026.06.15.r012

Restic Backup Manager für Unraid

Verbindung wird hergestellt...
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
- + diff --git a/webgui/assets/urbm.js b/webgui/assets/urbm.js index 22d894b..4d6e43e 100644 --- a/webgui/assets/urbm.js +++ b/webgui/assets/urbm.js @@ -13,7 +13,7 @@ const fieldHelp = { name: 'Frei wählbarer Anzeigename. Beispiel: Appdata täglich oder VM Home Assistant.', type: 'Art der zu sichernden Daten. Beispiel: appdata für Containerdaten, vm für virtuelle Maschinen oder flash für den Unraid-USB-Stick.', - repositoryId: 'Speicherziel für diesen Vorgang. Jeder Backup-Job verwendet ein eigenes Restic-Repository.', + repositoryId: 'Speicherziel für diesen Vorgang. Mehrere Backup-Jobs können dasselbe Restic-Repository verwenden; Aufbewahrungsregeln bleiben über Job-Tags getrennt.', scheduleMode: 'Legt fest, an welchen Tagen das Backup startet. Beispiel: täglich, Montag bis Freitag oder ausgewählte Wochentage.', scheduleTime: 'Uhrzeit für den automatischen Start. Beispiel: 02:30 startet das Backup nachts um halb drei.', scheduleDays: 'Wähle mindestens einen Wochentag. Beispiel: Montag, Mittwoch und Sonntag jeweils zur eingestellten Uhrzeit.', @@ -251,7 +251,7 @@ }); const dashboardHelp = { 'Aktive Jobs': 'Anzahl aktivierter Backup-Jobs, die nach Zeitplan laufen dürfen. Beispiel: 3 bedeutet drei aktive Sicherungspläne.', - 'Repositorys': 'Anzahl konfigurierter Restic-Speicherziele. Im MVP gehört jedes Repository genau zu einem Job.', + 'Repositorys': 'Anzahl konfigurierter Restic-Speicherziele. Mehrere Jobs dürfen dasselbe Repository verwenden.', 'Wartend / laufend': 'Aufgaben in der Warteschlange oder in Ausführung. URBM führt immer nur eine Aufgabe gleichzeitig aus.', 'Letzte Fehler': 'Fehlgeschlagene Läufe in der jüngsten Historie. Details stehen unter Aktivitäten.', }; @@ -352,7 +352,7 @@ function renderJobs() { const rows = state.config.jobs.map(j => `${esc(j.name)}
${esc(j.id)}${esc(typeLabels[j.type]||j.type)}${esc(repoName(j.repositoryId))}${esc(scheduleLabel(j.schedule?.cron))}${j.enabled ? status('success') : status('cancelled')}${actionGroup(button('Starten',`run-job:${j.id}`,'primary')+button('Bearbeiten',`edit-job:${j.id}`)+button('Duplizieren',`duplicate-job:${j.id}`)+button('Löschen',`delete-job:${j.id}`,'danger'))}`).join(''); - return `

Backup-Jobs

Jeder Job verwendet ein eigenes Restic-Repository.
${button('Neuer Job','new-job','primary')}
+ return `

Backup-Jobs

Mehrere Jobs können dasselbe Restic-Repository verwenden; Snapshots und Aufbewahrung werden per Job-Tag getrennt.
${button('Neuer Job','new-job','primary')}
${rows ? `${rows}
JobTypRepositoryZeitplanAktivAktionen
` : '
Keine Backup-Jobs konfiguriert.
'}
`; } @@ -599,7 +599,7 @@ if (name === 'delete-job') { if(confirm('Diesen Job löschen?')) { state.config.jobs=state.config.jobs.filter(j=>j.id!==a); await saveConfig(); render(); } return; } if (name === 'duplicate-job') { const source=state.config.jobs.find(j=>j.id===a); const copy=structuredClone(source); copy.id=id('job'); copy.name += ' Kopie'; copy.enabled=false; state.config.jobs.push(copy); await saveConfig(); return render(); } if (name === 'delete-repo') { if(state.config.jobs.some(j=>j.repositoryId===a)) throw new Error('Das Repository wird noch von einem Job verwendet.'); if(confirm('Dieses Repository aus der URBM-Konfiguration löschen? Die Backup-Daten am Ziel bleiben erhalten.')) { state.config.repositories=state.config.repositories.filter(r=>r.id!==a); await saveConfig(); render(); } return; } - if (name.startsWith('submit-')) return submit(name); + if (name.startsWith('submit-')) { await submit(name); return; } } catch (error) { notify(error.message, true); } }