diff --git a/dist/backupper-2026.06.14.9.5-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.5-x86_64-1.txz.sha256 deleted file mode 100644 index e3d6a54..0000000 --- a/dist/backupper-2026.06.14.9.5-x86_64-1.txz.sha256 +++ /dev/null @@ -1 +0,0 @@ -587fc8645d2a5271c4d40105e7209cda16636afd9be2613f83eb92c9208ad8b5 backupper-2026.06.14.9.5-x86_64-1.txz diff --git a/dist/backupper-2026.06.14.9.5-x86_64-1.txz b/dist/backupper-2026.06.14.9.6-x86_64-1.txz similarity index 55% rename from dist/backupper-2026.06.14.9.5-x86_64-1.txz rename to dist/backupper-2026.06.14.9.6-x86_64-1.txz index fe8bdd8..4a7ed23 100644 Binary files a/dist/backupper-2026.06.14.9.5-x86_64-1.txz and b/dist/backupper-2026.06.14.9.6-x86_64-1.txz differ diff --git a/dist/backupper-2026.06.14.9.6-x86_64-1.txz.sha256 b/dist/backupper-2026.06.14.9.6-x86_64-1.txz.sha256 new file mode 100644 index 0000000..56cb36d --- /dev/null +++ b/dist/backupper-2026.06.14.9.6-x86_64-1.txz.sha256 @@ -0,0 +1 @@ +6438688ae2884c69f23c7e2ec11eb7bab95d138b6f7d25c3c0335a6e319470a0 backupper-2026.06.14.9.6-x86_64-1.txz diff --git a/dist/backupper.plg b/dist/backupper.plg index c7b50af..1fc1926 100644 --- a/dist/backupper.plg +++ b/dist/backupper.plg @@ -2,13 +2,17 @@ - + - + ]> +### 2026.06.14.9.6 +- Hide technical secret IDs and manage them automatically for repositories, SMB, SFTP, and Gotify. +- Require a Restic password when creating a new repository while preserving existing secrets during edits. + ### 2026.06.14.9.5 - Add live backup progress with percentage, file and byte counters, transfer rate, ETA, and current file. - Add a sanitized in-memory live log for backup, restore, check, and prune activities. diff --git a/plugin/backupper.plg b/plugin/backupper.plg index 38af214..3532253 100644 --- a/plugin/backupper.plg +++ b/plugin/backupper.plg @@ -2,13 +2,17 @@ - + ]> +### 2026.06.14.9.6 +- Hide technical secret IDs and manage them automatically for repositories, SMB, SFTP, and Gotify. +- Require a Restic password when creating a new repository while preserving existing secrets during edits. + ### 2026.06.14.9.5 - Add live backup progress with percentage, file and byte counters, transfer rate, ETA, and current file. - Add a sanitized in-memory live log for backup, restore, check, and prune activities. diff --git a/webgui/Backupper.page b/webgui/Backupper.page index fcd4eae..410c3c6 100644 --- a/webgui/Backupper.page +++ b/webgui/Backupper.page @@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore" - +
- -

URBM 2026.06.14.9.5

Restic Backup Manager for Unraid

+ +

URBM 2026.06.14.9.6

Restic Backup Manager for Unraid

Connecting...
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/backupper';
- + diff --git a/webgui/assets/backupper.js b/webgui/assets/backupper.js index 1a12b91..9e0b938 100644 --- a/webgui/assets/backupper.js +++ b/webgui/assets/backupper.js @@ -28,12 +28,10 @@ keepMonthly: 'Zusätzliche monatliche Stände außerhalb der Altersfrist. 0 deaktiviert diese Zusatzregel.', enabled: 'Aktiviert automatische geplante Läufe. Deaktivierte Jobs können weiterhin gespeichert, aber nicht automatisch ausgeführt werden.', location: 'Restic-Ziel. Lokal: /mnt/user/backups/job1. SFTP: user@server:/backup/job1. Bei SMB/NFS mit Mount wird hier der Mount-Pfad verwendet.', - passwordRef: 'Interne ID des verschlüsselt gespeicherten Restic-Passworts. Beispiel: repo-password-appdata. Keine Leerzeichen verwenden.', password: 'Passwort zur Restic-Verschlüsselung. Beispiel: eine lange zufällige Passphrase. Leer lassen, wenn ein vorhandenes Passwort unverändert bleiben soll.', managed: 'Wenn aktiv, mountet URBM SMB/NFS vor dem Job selbst. Wenn aus, muss der angegebene Pfad bereits durch Unraid oder Unassigned Devices gemountet sein.', remote: 'Adresse der Netzwerkfreigabe. SMB-Beispiel: //192.168.1.20/backup. NFS-Beispiel: 192.168.1.20:/volume1/backup.', mountPoint: 'Lokaler Einhängepunkt auf Unraid. Beispiel: /mnt/remotes/backup-nas. Erlaubt sind Pfade unter /mnt/remotes oder /mnt/disks.', - credentialRef: 'Interne ID für SMB-Zugangsdaten oder einen SFTP-Schlüssel. Beispiel: nas-login oder sftp-backup-key.', credential: 'SMB-Beispiel: username=backup und password=geheim auf getrennten Zeilen. Bei SFTP hier den kompletten privaten SSH-Schlüssel einfügen.', knownHostsPath: 'Datei mit dem geprüften SSH-Hostschlüssel. Beispiel: /root/.ssh/known_hosts. StrictHostKeyChecking ist immer aktiv.', snapshotId: 'Eindeutige ID des Sicherungsstands. Beispiel: 7f3a91cd. Sie kann im Bereich Snapshots über Browse ausgewählt werden.', @@ -48,7 +46,6 @@ checkCron: 'Zeitplan für vollständige Repository-Prüfungen. Beispiel: 0 3 1 * * prüft am ersten Tag jedes Monats um 03:00 Uhr.', pruneCron: 'Zeitplan zum Freigeben nicht mehr benötigter Restic-Daten. Beispiel: 0 4 * * 0 startet sonntags um 04:00 Uhr.', url: 'Basisadresse deines Gotify-Servers. Beispiel: https://gotify.example.de oder http://192.168.1.20:8080.', - tokenRef: 'Interne ID für das verschlüsselt gespeicherte Gotify-App-Token. Beispiel: gotify-token-main.', token: 'App-Token aus Gotify unter Apps. Das Token wird verschlüsselt gespeichert und nicht erneut angezeigt.', events: 'Legt fest, bei welchen Ergebnissen dieser Kanal informiert. Mindestens ein Ereignis muss aktiv sein.', }; @@ -385,10 +382,12 @@ function repositoryForm(repo = {}) { state.repositoryBrowserPath = '/'; state.repositoryDirectories = repositoryRoots; state.repositoryBrowserError = ''; - content.innerHTML = `

${repo.id?'Edit':'New'} repository

+ const passwordRef=repo.passwordRef||id('repo-password'); + const credentialRef=repo.credentialRef||id('repo-credential'); + content.innerHTML = `

${repo.id?'Edit':'New'} repository

-
Restic encryption
+
Restic encryption

Encrypted by Restic and stored securely by URBM. ${repo.id?'Leave blank to keep the existing password.':'Keep this password safe; backups cannot be restored without it.'}

${button('Save','submit-repo','primary')}${button('Cancel','cancel-form')}
`; enhanceTooltips(content); renderRepositorySpecific(repo.type || 'local', repo); @@ -406,9 +405,9 @@ renderRepositoryBrowser(); } else if (type === 'smb' || type === 'nfs') { const managed = repo.mount?.managed === true; - host.innerHTML = `
${type.toUpperCase()} destination
${repositoryLocationField('Existing mounted folder',repo.location,'/mnt/remotes/backup-nas')}
${type==='smb'?``:''}
`; + host.innerHTML = `
${type.toUpperCase()} destination
${repositoryLocationField('Existing mounted folder',repo.location,'/mnt/remotes/backup-nas')}
${type==='smb'?``:''}
`; } else { - host.innerHTML = `
SFTP destination
${repositoryLocationField('SFTP repository location',repo.location,'backup@192.168.1.20:/srv/backups/urbm')}
`; + host.innerHTML = `
SFTP destination
${repositoryLocationField('SFTP repository location',repo.location,'backup@192.168.1.20:/srv/backups/urbm')}
`; } enhanceTooltips(host); } @@ -486,7 +485,7 @@ const eventBoxes = target => [['success','Success'],['warning','Warning'],['failed','Failed']].map(([value,label])=>``).join(''); return `

Settings

${button('Save settings','submit-settings','primary')}

Unraid notifications

Uses Unraid's notification system and all agents configured under Settings > Notification Settings.

Notify on
${eventBoxes(unraid)}
${button('Save Unraid notifications','submit-unraid-notify','primary')}
-

Gotify notification

Create an application in Gotify and enter its application token here.

Notify on
${eventBoxes(gotify)}
${button('Save Gotify notification','submit-gotify-notify','primary')}
`; +

Gotify notification

Create an application in Gotify and enter its application token here.

Notify on
${eventBoxes(gotify)}
${button('Save Gotify notification','submit-gotify-notify','primary')}
`; } async function saveConfig() { @@ -547,9 +546,9 @@ let location=managed?value('mountPoint'):value('location'); if(!location) throw new Error(managed?'Mount point is required':'Repository location is required'); if(managed&&!value('remote')) throw new Error('Remote share is required for a managed mount'); - let credentialRef=(type==='sftp'||(type==='smb'&&managed))?value('credentialRef'):''; - if(value('credential')&&!credentialRef) credentialRef=id(type==='sftp'?'sftp-key':'smb-credentials'); + const usesCredential=type==='sftp'||(type==='smb'&&managed); const credentialRef=usesCredential&&(value('hasCredential')==='true'||Boolean(f.get('credential')))?value('credentialRef'):''; const repo={schemaVersion:1,id:value('id')||id('repo'),name:value('name'),type,location,passwordRef:value('passwordRef'),credentialRef,mount:managed?{managed:true,remote:value('remote'),mountPoint:value('mountPoint')}:null,options:type==='sftp'?{knownHostsPath:value('knownHostsPath')}: {}}; + if(!value('id')&&!f.get('password')) throw new Error('A repository password is required for a new repository'); if (volatileLocation(repo) && !confirm('WARNING: This destination is under /tmp or /run and will be lost after an Unraid reboot. Save it anyway?')) return; if(f.get('password')) await api(`/v1/secrets/${encodeURIComponent(repo.passwordRef)}`,{method:'PUT',body:JSON.stringify({type:'restic-password',value:f.get('password')})}); if(f.get('credential')) await api(`/v1/secrets/${encodeURIComponent(repo.credentialRef)}`,{method:'PUT',body:JSON.stringify({type:type==='sftp'?'sftp-private-key':'smb-credential',value:f.get('credential')})}); @@ -563,7 +562,7 @@ if (kind === 'submit-restore') { const f=new FormData(document.getElementById('bu-restore-form')); await api('/v1/restores',{method:'POST',body:JSON.stringify({schemaVersion:1,id:'',repositoryId:f.get('repositoryId'),snapshotId:f.get('snapshotId'),includes:lines(f.get('includes')),target:f.get('target'),inPlace:f.get('inPlace')==='on',confirmed:f.get('confirmed')==='on'})}); notify('Restore queued'); state.view='activity'; render(); } if (kind === 'submit-settings') { const f=new FormData(document.getElementById('bu-settings-form')); state.config.settings={resticPath:f.get('resticPath'),restoreRoot:f.get('restoreRoot'),persistentLogDir:f.get('persistentLogDir'),catchUpWindowHours:Number(f.get('catchUp')),checkCron:f.get('checkCron'),pruneCron:f.get('pruneCron')}; await saveConfig(); } if (kind === 'submit-unraid-notify') { const f=new FormData(document.getElementById('bu-unraid-notify-form')); const target={schemaVersion:1,id:f.get('id')||'unraid',name:'Unraid',type:'unraid',enabled:f.get('enabled')==='on',events:f.getAll('events')}; if(target.enabled&&!target.events.length) throw new Error('Select at least one event for Unraid notifications'); state.config.notifications=state.config.notifications.filter(n=>n.type!=='unraid'&&n.type!=='ntfy').concat(target); await saveConfig(); render(); } - if (kind === 'submit-gotify-notify') { const f=new FormData(document.getElementById('bu-gotify-notify-form')); const target={schemaVersion:1,id:f.get('id')||'gotify',name:'Gotify',type:'gotify',enabled:f.get('enabled')==='on',url:String(f.get('url')||'').trim(),tokenRef:String(f.get('tokenRef')||'').trim(),events:f.getAll('events')}; if(target.enabled&&(!target.url||!target.tokenRef)) throw new Error('Gotify server URL and token secret ID are required'); if(target.enabled&&!target.events.length) throw new Error('Select at least one event for Gotify notifications'); if(f.get('token')) await api(`/v1/secrets/${encodeURIComponent(target.tokenRef)}`,{method:'PUT',body:JSON.stringify({type:'gotify-token',value:f.get('token')})}); state.config.notifications=state.config.notifications.filter(n=>n.type!=='gotify'&&n.type!=='ntfy'); if(target.enabled||target.url) state.config.notifications.push(target); await saveConfig(); render(); } + if (kind === 'submit-gotify-notify') { const f=new FormData(document.getElementById('bu-gotify-notify-form')); const existing=state.config.notifications.find(n=>n.type==='gotify'); const target={schemaVersion:1,id:f.get('id')||'gotify',name:'Gotify',type:'gotify',enabled:f.get('enabled')==='on',url:String(f.get('url')||'').trim(),tokenRef:String(f.get('tokenRef')||'').trim(),events:f.getAll('events')}; if(target.enabled&&!target.url) throw new Error('Gotify server URL is required'); if(target.enabled&&!target.events.length) throw new Error('Select at least one event for Gotify notifications'); if(target.enabled&&!existing&&!f.get('token')) throw new Error('A Gotify application token is required'); if(f.get('token')) await api(`/v1/secrets/${encodeURIComponent(target.tokenRef)}`,{method:'PUT',body:JSON.stringify({type:'gotify-token',value:f.get('token')})}); state.config.notifications=state.config.notifications.filter(n=>n.type!=='gotify'&&n.type!=='ntfy'); if(target.enabled||target.url) state.config.notifications.push(target); await saveConfig(); render(); } } document.querySelector('.bu-tabs').addEventListener('click', e => { const button=e.target.closest('[data-view]'); if(!button)return; state.view=button.dataset.view; document.querySelectorAll('.bu-tabs button').forEach(x=>x.classList.toggle('active',x===button)); render(); });