Add type-aware repository form
This commit is contained in:
@@ -97,6 +97,11 @@
|
||||
#backupper-app .bu-weekdays input:checked + span { background: var(--bu-accent) !important; border-color: var(--bu-accent) !important; color: #fff !important; }
|
||||
.bu-custom-cron { margin-top: 14px; }
|
||||
.bu-event-options { display: flex; flex-wrap: wrap; gap: 10px; }
|
||||
.bu-form-grid { display: grid; gap: 15px; grid-template-columns: repeat(2, minmax(220px, 1fr)); }
|
||||
.bu-managed-mount, .bu-external-mount { margin-top: 14px; }
|
||||
.bu-managed-mount[hidden], .bu-external-mount[hidden] { display: none; }
|
||||
#backupper-app .bu-inline-check { align-items: center; display: flex; gap: 9px; grid-template-columns: auto 1fr; }
|
||||
#backupper-app .bu-inline-check input { margin: 0; }
|
||||
#backupper-app .bu-event-options label { align-items: center; display: flex; gap: 7px; grid-template-columns: auto auto; }
|
||||
#backupper-app .bu-event-options input[type="checkbox"] { margin: 0; min-height: 20px; width: 20px; }
|
||||
|
||||
@@ -205,6 +210,7 @@
|
||||
.bu-workload-options { grid-template-columns: 1fr; }
|
||||
.bu-retention-grid { grid-template-columns: 1fr; }
|
||||
.bu-schedule-grid { grid-template-columns: 1fr; }
|
||||
.bu-form-grid { grid-template-columns: 1fr; }
|
||||
.bu-chart-grid-layout { grid-template-columns: 1fr; }
|
||||
.bu-header { align-items: flex-start; flex-direction: column; gap: 12px; }
|
||||
.bu-table { display: block; overflow-x: auto; }
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
const toast = document.getElementById('bu-toast');
|
||||
const tooltip = document.getElementById('bu-tooltip');
|
||||
const sourceRoots = ['/mnt/user','/mnt/disks','/mnt/remotes','/boot'].map(path=>({name:path,path}));
|
||||
const state = { config: null, runs: [], view: 'dashboard', snapshots: [], selectedSnapshot: null, snapshotRepo: null, files: [], browserPath: '/', restoreIncludes: [], backupSelections: [], workloadSelections: [], workloads: [], workloadAvailable: true, workloadError: '', sourceBrowserPath: '/', sourceDirectories: sourceRoots, sourceBrowserError: '' };
|
||||
const repositoryRoots = ['/mnt/user','/mnt/disks','/mnt/remotes'].map(path=>({name:path,path}));
|
||||
const state = { config: null, runs: [], view: 'dashboard', snapshots: [], selectedSnapshot: null, snapshotRepo: null, files: [], browserPath: '/', restoreIncludes: [], backupSelections: [], workloadSelections: [], workloads: [], workloadAvailable: true, workloadError: '', sourceBrowserPath: '/', sourceDirectories: sourceRoots, sourceBrowserError: '', repositoryBrowserPath: '/', repositoryDirectories: repositoryRoots, repositoryBrowserError: '' };
|
||||
|
||||
const fieldHelp = {
|
||||
name: 'Frei wählbarer Anzeigename. Beispiel: Appdata täglich oder VM Home Assistant.',
|
||||
@@ -69,6 +70,8 @@
|
||||
'browser-up': 'Öffnet im Snapshot-Browser das übergeordnete Verzeichnis.',
|
||||
'source-up': 'Öffnet im Ordner-Browser das übergeordnete Verzeichnis.',
|
||||
'select-current-source': 'Wählt den aktuell geöffneten Ordner vollständig als Backup-Quelle aus.',
|
||||
'repository-up': 'Öffnet im lokalen Repository-Browser das übergeordnete Verzeichnis.',
|
||||
'select-repository-folder': 'Übernimmt den aktuell geöffneten Ordner als lokalen Repository-Pfad.',
|
||||
'select-current-restore': 'Wählt den aktuell geöffneten Snapshot-Ordner vollständig für die Wiederherstellung aus.',
|
||||
};
|
||||
|
||||
@@ -109,6 +112,7 @@
|
||||
'browse': 'Öffnet den Dateibaum dieses Snapshots, um einzelne Dateien oder Ordner auszuwählen.',
|
||||
'open-dir': 'Öffnet dieses Verzeichnis innerhalb des schreibgeschützten Snapshots.',
|
||||
'open-source-dir': 'Öffnet diesen Ordner, um darin weitere Unterordner auszuwählen.',
|
||||
'open-repository-dir': 'Öffnet diesen lokalen Ordner, um ihn oder einen Unterordner als Repository-Ziel auszuwählen.',
|
||||
'cancel-run': 'Bricht die laufende oder wartende Aufgabe ab. Cleanup und Neustart gestoppter Workloads werden trotzdem versucht.',
|
||||
};
|
||||
return dynamic[name] || `${label} ausführen.`;
|
||||
@@ -374,15 +378,55 @@
|
||||
}
|
||||
|
||||
function repositoryForm(repo = {}) {
|
||||
state.repositoryBrowserPath = '/'; state.repositoryDirectories = repositoryRoots; state.repositoryBrowserError = '';
|
||||
content.innerHTML = `<section class="bu-card"><h2>${repo.id?'Edit':'New'} repository</h2><form id="bu-repo-form" class="bu-form"><input type="hidden" name="id" value="${esc(repo.id||'')}">
|
||||
<label>Name<input name="name" required value="${esc(repo.name||'')}"></label><label>Type<select name="type">${['local','smb','nfs','sftp'].map(v=>`<option ${v===repo.type?'selected':''}>${v}</option>`).join('')}</select></label>
|
||||
<label class="wide">Restic location<input name="location" required value="${esc(repo.location||'')}"></label><label>Password secret ID<input name="passwordRef" required value="${esc(repo.passwordRef||id('repo-password'))}"></label>
|
||||
<label>Repository password<input name="password" type="password" placeholder="Leave blank to keep existing"></label><label>Managed mount<input name="managed" type="checkbox" ${repo.mount?.managed?'checked':''}></label>
|
||||
<label>Remote share<input name="remote" value="${esc(repo.mount?.remote||'')}"></label><label>Mount point<input name="mountPoint" value="${esc(repo.mount?.mountPoint||'')}"></label>
|
||||
<label>Credential secret ID<input name="credentialRef" value="${esc(repo.credentialRef||'')}"></label><label>SMB credentials or SFTP private key<textarea name="credential" placeholder="SMB: username=... / password=... SFTP: private key PEM"></textarea></label>
|
||||
<label class="wide">SFTP known_hosts path<input name="knownHostsPath" value="${esc(repo.options?.knownHostsPath||'/root/.ssh/known_hosts')}"></label>
|
||||
<div id="bu-repository-specific" class="wide"></div>
|
||||
<fieldset class="wide bu-fieldset"><legend>Restic encryption</legend><div class="bu-form-grid"><label>Password secret ID<input name="passwordRef" required value="${esc(repo.passwordRef||id('repo-password'))}"></label><label>Repository password<input name="password" type="password" placeholder="Leave blank to keep existing"></label></div></fieldset>
|
||||
<div class="wide bu-actions">${button('Save','submit-repo','primary')}${button('Cancel','cancel-form')}</div></form></section>`;
|
||||
enhanceTooltips(content);
|
||||
renderRepositorySpecific(repo.type || 'local', repo);
|
||||
}
|
||||
|
||||
function repositoryLocationField(label, value, placeholder) {
|
||||
return `<label class="wide">${label}<input name="location" required placeholder="${esc(placeholder)}" value="${esc(value||'')}"></label>`;
|
||||
}
|
||||
|
||||
function renderRepositorySpecific(type, repo = {}) {
|
||||
const host = document.getElementById('bu-repository-specific');
|
||||
if (!host) return;
|
||||
if (type === 'local') {
|
||||
host.innerHTML = `${repositoryLocationField('Local repository folder',repo.location,'/mnt/user/backups/urbm')}<div id="bu-repository-browser"></div>`;
|
||||
renderRepositoryBrowser();
|
||||
} else if (type === 'smb' || type === 'nfs') {
|
||||
const managed = repo.mount?.managed === true;
|
||||
host.innerHTML = `<fieldset class="bu-fieldset"><legend>${type.toUpperCase()} destination</legend><label class="bu-inline-check"><input name="managed" type="checkbox" ${managed?'checked':''}><span>URBM should mount this share</span></label><div class="bu-external-mount" ${managed?'hidden':''}>${repositoryLocationField('Existing mounted folder',repo.location,'/mnt/remotes/backup-nas')}</div><div class="bu-managed-mount" ${managed?'':'hidden'}><div class="bu-form-grid"><label>Remote share<input name="remote" placeholder="${type==='smb'?'//192.168.1.20/backup':'192.168.1.20:/volume1/backup'}" value="${esc(repo.mount?.remote||'')}"></label><label>Mount point<input name="mountPoint" placeholder="/mnt/remotes/urbm-backup" value="${esc(repo.mount?.mountPoint||repo.location||'')}"></label>${type==='smb'?`<label>Credential secret ID<input name="credentialRef" value="${esc(repo.credentialRef||'smb-credentials')}"></label><label class="wide">SMB credentials<textarea name="credential" placeholder="username=backup password=secret"></textarea></label>`:''}</div></div></fieldset>`;
|
||||
} else {
|
||||
host.innerHTML = `<fieldset class="bu-fieldset"><legend>SFTP destination</legend><div class="bu-form-grid">${repositoryLocationField('SFTP repository location',repo.location,'backup@192.168.1.20:/srv/backups/urbm')}<label>SSH key secret ID<input name="credentialRef" value="${esc(repo.credentialRef||'')}"></label><label>SFTP known_hosts path<input name="knownHostsPath" value="${esc(repo.options?.knownHostsPath||'/root/.ssh/known_hosts')}"></label><label class="wide">Private SSH key (optional)<textarea name="credential" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"></textarea></label></div></fieldset>`;
|
||||
}
|
||||
enhanceTooltips(host);
|
||||
}
|
||||
|
||||
function renderRepositoryBrowser() {
|
||||
const host = document.getElementById('bu-repository-browser');
|
||||
if (!host) return;
|
||||
const rows = state.repositoryDirectories.map(item=>`<tr><td>${esc(item.name)}</td><td>${esc(item.path)}</td><td>${button('Open',`open-repository-dir:${encodeURIComponent(item.path)}`)}</td></tr>`).join('');
|
||||
const error = state.repositoryBrowserError ? `<div class="bu-inline-error">${esc(state.repositoryBrowserError)}</div>` : '';
|
||||
host.innerHTML = `<section class="bu-browser-card"><div class="bu-toolbar"><div><h3>Local folder browser</h3><span class="bu-muted">${esc(state.repositoryBrowserPath)}</span></div><div class="bu-actions">${state.repositoryBrowserPath!=='/'?button('Use this folder','select-repository-folder','primary'):''}${state.repositoryBrowserPath!=='/'?button('Up','repository-up'):''}</div></div>${error}<div class="bu-browser-scroll"><table class="bu-table"><thead><tr><th>Name</th><th>Path</th><th></th></tr></thead><tbody>${rows||'<tr><td colspan="3" class="bu-empty">No subfolders found.</td></tr>'}</tbody></table></div></section>`;
|
||||
enhanceTooltips(host);
|
||||
}
|
||||
|
||||
async function loadRepositoryDirectories(path) {
|
||||
if (path === '/') {
|
||||
state.repositoryBrowserPath='/'; state.repositoryDirectories=repositoryRoots; state.repositoryBrowserError=''; renderRepositoryBrowser(); return;
|
||||
}
|
||||
try {
|
||||
state.repositoryDirectories=await api(`/v1/filesystem/directories?path=${encodeURIComponent(path)}`);
|
||||
state.repositoryBrowserPath=path; state.repositoryBrowserError='';
|
||||
} catch (error) {
|
||||
state.repositoryBrowserError=`Folder could not be loaded: ${error.message}`;
|
||||
}
|
||||
renderRepositoryBrowser();
|
||||
}
|
||||
|
||||
function renderSnapshots() {
|
||||
@@ -447,6 +491,9 @@
|
||||
if (name === 'open-source-dir') { return loadSourceDirectories(decodeURIComponent(a)); }
|
||||
if (name === 'source-up') { const parent=state.sourceBrowserPath==='/'?'/':state.sourceBrowserPath.split('/').slice(0,-1).join('/')||'/'; return loadSourceDirectories(parent==='/mnt'?'/':parent); }
|
||||
if (name === 'select-current-source') { if(!state.backupSelections.includes(state.sourceBrowserPath)) state.backupSelections.push(state.sourceBrowserPath); return renderSourceBrowser(); }
|
||||
if (name === 'open-repository-dir') { return loadRepositoryDirectories(decodeURIComponent(a)); }
|
||||
if (name === 'repository-up') { const parent=state.repositoryBrowserPath==='/'?'/':state.repositoryBrowserPath.split('/').slice(0,-1).join('/')||'/'; return loadRepositoryDirectories(parent==='/mnt'?'/':parent); }
|
||||
if (name === 'select-repository-folder') { const location=document.querySelector('#bu-repo-form [name="location"]'); if(location) location.value=state.repositoryBrowserPath; return; }
|
||||
if (name === 'open-dir') { state.browserPath=decodeURIComponent(a); state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${state.selectedSnapshot}/files?path=${encodeURIComponent(state.browserPath)}`); return render(); }
|
||||
if (name === 'browser-up') { state.browserPath=state.browserPath.replace(/\/$/,'').split('/').slice(0,-1).join('/')||'/'; state.files=await api(`/v1/repositories/${state.snapshotRepo}/snapshots/${state.selectedSnapshot}/files?path=${encodeURIComponent(state.browserPath)}`); return render(); }
|
||||
if (name === 'select-current-restore') { if(!state.restoreIncludes.includes(state.browserPath)) state.restoreIncludes.push(state.browserPath); return render(); }
|
||||
@@ -471,10 +518,16 @@
|
||||
state.config.jobs=state.config.jobs.filter(j=>j.id!==job.id).concat(job); await saveConfig(); render();
|
||||
}
|
||||
if (kind === 'submit-repo') {
|
||||
const f=new FormData(document.getElementById('bu-repo-form')); const repo={schemaVersion:1,id:f.get('id')||id('repo'),name:f.get('name').trim(),type:f.get('type'),location:f.get('location').trim(),passwordRef:f.get('passwordRef').trim(),credentialRef:f.get('credentialRef').trim(),mount:f.get('managed')==='on'?{managed:true,remote:f.get('remote').trim(),mountPoint:f.get('mountPoint').trim()}:null,options:f.get('type')==='sftp'?{knownHostsPath:f.get('knownHostsPath').trim()}:{}};
|
||||
const f=new FormData(document.getElementById('bu-repo-form')); const value=name=>String(f.get(name)||'').trim(); const type=value('type'); const managed=(type==='smb'||type==='nfs')&&f.get('managed')==='on';
|
||||
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 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 (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')) { if(!repo.credentialRef) repo.credentialRef=id('mount-credential'); await api(`/v1/secrets/${encodeURIComponent(repo.credentialRef)}`,{method:'PUT',body:JSON.stringify({type:'mount-credential',value:f.get('credential')})}); }
|
||||
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')})});
|
||||
state.config.repositories=state.config.repositories.filter(r=>r.id!==repo.id).concat(repo);
|
||||
await saveConfig();
|
||||
state.config = await api('/v1/config');
|
||||
@@ -491,6 +544,15 @@
|
||||
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(); });
|
||||
content.addEventListener('click', e => { const target=e.target.closest('[data-action]'); if(!target)return; e.preventDefault(); handle(target.dataset.action,target); });
|
||||
content.addEventListener('change', e => {
|
||||
if (e.target.name === 'type' && e.target.closest('#bu-repo-form')) {
|
||||
renderRepositorySpecific(e.target.value);
|
||||
}
|
||||
if (e.target.name === 'managed' && e.target.closest('#bu-repo-form')) {
|
||||
const fieldset=e.target.closest('.bu-fieldset');
|
||||
const managed=fieldset?.querySelector('.bu-managed-mount'); const external=fieldset?.querySelector('.bu-external-mount');
|
||||
if(managed) managed.hidden=!e.target.checked;
|
||||
if(external) external.hidden=e.target.checked;
|
||||
}
|
||||
if (e.target.name === 'scheduleMode') {
|
||||
const weekdays=e.target.closest('.bu-schedule')?.querySelector('.bu-weekdays');
|
||||
if (weekdays) weekdays.hidden=e.target.value!=='selected';
|
||||
|
||||
Reference in New Issue
Block a user