Add manual-only backup schedules

This commit is contained in:
Mikei386
2026-06-15 17:22:09 +02:00
parent e66d89598d
commit 3579756cc9
8 changed files with 30 additions and 12 deletions
-1
View File
@@ -1 +0,0 @@
56f07ac0a18bf9067c8cb38ef6e5cbf69d52a2313e37f32f87b619268a47d6e8 urbm-2026.06.15.r018-x86_64-1.txz
+1
View File
@@ -0,0 +1 @@
89f13497e0503b91602af00343fb15cd897e775189e63fbd6af4e0c302ff06d4 urbm-2026.06.15.r019-x86_64-1.txz
+6 -2
View File
@@ -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.06.15.r018"> <!ENTITY version "2026.06.15.r019">
<!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 "56f07ac0a18bf9067c8cb38ef6e5cbf69d52a2313e37f32f87b619268a47d6e8"> <!ENTITY packageSHA256 "89f13497e0503b91602af00343fb15cd897e775189e63fbd6af4e0c302ff06d4">
]> ]>
<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.06.15.r019
- Add a clearly labeled manual-only schedule mode for every backup and Rsync job.
- Keep manual-only jobs enabled for one-click starts while excluding them from scheduled and catch-up runs.
### 2026.06.15.r018 ### 2026.06.15.r018
- Run a write-free Rsync preflight to estimate the actual transfer size before copying. - Run a write-free Rsync preflight to estimate the actual transfer size before copying.
- Abort Rsync jobs when the estimated transfer exceeds the free destination capacity. - Abort Rsync jobs when the estimated transfer exceeds the free destination capacity.
+5 -1
View File
@@ -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.06.15.r018"> <!ENTITY version "2026.06.15.r019">
<!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.06.15.r019
- Add a clearly labeled manual-only schedule mode for every backup and Rsync job.
- Keep manual-only jobs enabled for one-click starts while excluding them from scheduled and catch-up runs.
### 2026.06.15.r018 ### 2026.06.15.r018
- Run a write-free Rsync preflight to estimate the actual transfer size before copying. - Run a write-free Rsync preflight to estimate the actual transfer size before copying.
- Abort Rsync jobs when the estimated transfer exceeds the free destination capacity. - Abort Rsync jobs when the estimated transfer exceeds the free destination capacity.
+4 -4
View File
@@ -9,12 +9,12 @@ Tag="URBM Unraid Restic Backup Manager backup snapshots restore"
<?php <?php
$pluginRoot = '/plugins/urbm'; $pluginRoot = '/plugins/urbm';
?> ?>
<link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260615r018"> <link rel="stylesheet" href="<?= $pluginRoot ?>/assets/urbm.css?v=20260615r019">
<div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php"> <div id="urbm-app" data-api="<?= $pluginRoot ?>/api.php">
<header class="bu-header"> <header class="bu-header">
<div class="bu-brand"> <div class="bu-brand">
<img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260615r018" alt="URBM-Logo"> <img class="bu-logo" src="<?= $pluginRoot ?>/images/urbm.png?v=20260615r019" alt="URBM-Logo">
<div><h1>URBM <span class="bu-version">2026.06.15.r018</span></h1><p>Restic Backup Manager für Unraid</p></div> <div><h1>URBM <span class="bu-version">2026.06.15.r019</span></h1><p>Restic Backup Manager für Unraid</p></div>
</div> </div>
<div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Verbindung wird hergestellt...</div> <div id="bu-health" class="bu-health" tabindex="0" data-tooltip="Zeigt, ob die URBM-Hintergrundkomponente erreichbar ist. Beispiel: 'Daemon online' bedeutet, dass Jobs gestartet werden können.">Verbindung wird hergestellt...</div>
</header> </header>
@@ -32,4 +32,4 @@ $pluginRoot = '/plugins/urbm';
<div id="bu-tooltip" role="tooltip" aria-hidden="true"></div> <div id="bu-tooltip" role="tooltip" aria-hidden="true"></div>
</div> </div>
<script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script> <script>window.URBM_CSRF = <?= json_encode($var['csrf_token'] ?? '') ?>;</script>
<script src="<?= $pluginRoot ?>/assets/urbm-2026.06.15.r018.js"></script> <script src="<?= $pluginRoot ?>/assets/urbm-2026.06.15.r019.js"></script>
+1
View File
@@ -92,6 +92,7 @@
.bu-schedule-grid { display: grid; gap: 15px; grid-template-columns: minmax(240px, 2fr) minmax(150px, 1fr); } .bu-schedule-grid { display: grid; gap: 15px; grid-template-columns: minmax(240px, 2fr) minmax(150px, 1fr); }
.bu-weekdays { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; } .bu-weekdays { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
.bu-weekdays[hidden] { display: none; } .bu-weekdays[hidden] { display: none; }
.bu-schedule-time[hidden], .bu-schedule-manual[hidden] { display: none !important; }
.bu-weekdays > span { font-weight: 700; margin-right: 4px; } .bu-weekdays > span { font-weight: 700; margin-right: 4px; }
#urbm-app .bu-weekdays label { display: block; } #urbm-app .bu-weekdays label { display: block; }
#urbm-app .bu-weekdays label span { background: var(--bu-panel-alt); border: 1px solid var(--bu-border-strong); border-radius: 5px; cursor: pointer; display: block; min-width: 46px; padding: 9px 11px; text-align: center; } #urbm-app .bu-weekdays label span { background: var(--bu-panel-alt); border: 1px solid var(--bu-border-strong); border-radius: 5px; cursor: pointer; display: block; min-width: 46px; padding: 9px 11px; text-align: center; }
+13 -4
View File
@@ -193,7 +193,9 @@
}; };
const weekDays = [{value:'1',short:'Mo',name:'Montag'},{value:'2',short:'Di',name:'Dienstag'},{value:'3',short:'Mi',name:'Mittwoch'},{value:'4',short:'Do',name:'Donnerstag'},{value:'5',short:'Fr',name:'Freitag'},{value:'6',short:'Sa',name:'Samstag'},{value:'0',short:'So',name:'Sonntag'}]; const weekDays = [{value:'1',short:'Mo',name:'Montag'},{value:'2',short:'Di',name:'Dienstag'},{value:'3',short:'Mi',name:'Mittwoch'},{value:'4',short:'Do',name:'Donnerstag'},{value:'5',short:'Fr',name:'Freitag'},{value:'6',short:'Sa',name:'Samstag'},{value:'0',short:'So',name:'Sonntag'}];
function parseSchedule(cron = '0 2 * * *') { function parseSchedule(cron = '0 2 * * *') {
const match = String(cron).trim().match(/^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+(.+)$/); const value=String(cron).trim();
if(!value) return {mode:'manual',time:'02:00',days:[],cron:''};
const match = value.match(/^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+(.+)$/);
if (!match || Number(match[1]) > 59 || Number(match[2]) > 23) return {mode:'custom',time:'02:00',days:[],cron}; if (!match || Number(match[1]) > 59 || Number(match[2]) > 23) return {mode:'custom',time:'02:00',days:[],cron};
const time = `${String(match[2]).padStart(2,'0')}:${String(match[1]).padStart(2,'0')}`; const time = `${String(match[2]).padStart(2,'0')}:${String(match[1]).padStart(2,'0')}`;
if (match[3] === '*') return {mode:'daily',time,days:[],cron}; if (match[3] === '*') return {mode:'daily',time,days:[],cron};
@@ -204,6 +206,7 @@
} }
function scheduleCron(form) { function scheduleCron(form) {
const mode = form.get('scheduleMode'); const mode = form.get('scheduleMode');
if (mode === 'manual') return '';
if (mode === 'custom') return form.get('customCron'); if (mode === 'custom') return form.get('customCron');
const [hour, minute] = String(form.get('scheduleTime') || '02:00').split(':'); const [hour, minute] = String(form.get('scheduleTime') || '02:00').split(':');
const dayField = mode === 'daily' ? '*' : mode === 'weekdays' ? '1-5' : form.getAll('scheduleDays').join(','); const dayField = mode === 'daily' ? '*' : mode === 'weekdays' ? '1-5' : form.getAll('scheduleDays').join(',');
@@ -212,6 +215,7 @@
} }
function scheduleLabel(cron) { function scheduleLabel(cron) {
const schedule = parseSchedule(cron); const schedule = parseSchedule(cron);
if (schedule.mode === 'manual') return 'Nur manuell';
if (schedule.mode === 'custom') return cron || 'Manuell'; if (schedule.mode === 'custom') return cron || 'Manuell';
if (schedule.mode === 'daily') return `Täglich um ${schedule.time} Uhr`; if (schedule.mode === 'daily') return `Täglich um ${schedule.time} Uhr`;
if (schedule.mode === 'weekdays') return `MoFr um ${schedule.time} Uhr`; if (schedule.mode === 'weekdays') return `MoFr um ${schedule.time} Uhr`;
@@ -219,10 +223,11 @@
return `${labels} um ${schedule.time} Uhr`; return `${labels} um ${schedule.time} Uhr`;
} }
function scheduleFields(job = {}) { function scheduleFields(job = {}) {
const schedule = parseSchedule(job.schedule?.cron || '0 2 * * *'); const cron=job.schedule&&Object.prototype.hasOwnProperty.call(job.schedule,'cron')?job.schedule.cron:'0 2 * * *';
const modes = [['daily','Täglich'],['weekdays','Montag bis Freitag'],['selected','Ausgewählte Wochentage']]; const schedule = parseSchedule(cron);
const modes = [['manual','Nur manuell starten'],['daily','Täglich'],['weekdays','Montag bis Freitag'],['selected','Ausgewählte Wochentage']];
if (schedule.mode === 'custom') modes.push(['custom','Bestehenden Cron-Zeitplan beibehalten']); if (schedule.mode === 'custom') modes.push(['custom','Bestehenden Cron-Zeitplan beibehalten']);
return `<fieldset class="wide bu-fieldset bu-schedule"><legend>Backup-Zeitplan</legend><div class="bu-schedule-grid"><label>Tage<select name="scheduleMode">${modes.map(([value,label])=>`<option value="${value}" ${value===schedule.mode?'selected':''}>${label}</option>`).join('')}</select></label><label>Uhrzeit<input name="scheduleTime" type="time" required value="${esc(schedule.time)}"></label></div><div class="bu-weekdays" ${schedule.mode==='selected'?'':'hidden'}><span>Wochentage</span>${weekDays.map(day=>`<label><input type="checkbox" name="scheduleDays" value="${day.value}" ${schedule.days.includes(day.value)?'checked':''}><span>${day.short}</span></label>`).join('')}</div>${schedule.mode==='custom'?`<label class="bu-custom-cron">Bestehender Zeitplan<input name="customCron" readonly value="${esc(schedule.cron)}"></label>`:''}</fieldset>`; return `<fieldset class="wide bu-fieldset bu-schedule"><legend>Backup-Zeitplan</legend><div class="bu-schedule-grid"><label>Ausführung<select name="scheduleMode">${modes.map(([value,label])=>`<option value="${value}" ${value===schedule.mode?'selected':''}>${label}</option>`).join('')}</select></label><label class="bu-schedule-time" ${schedule.mode==='manual'?'hidden':''}>Uhrzeit<input name="scheduleTime" type="time" required value="${esc(schedule.time)}"></label></div><div class="bu-schedule-manual bu-inline-info" ${schedule.mode==='manual'?'':'hidden'}>Dieser Job wird nicht automatisch gestartet. Er kann jederzeit in der Job-Liste über <strong>Starten</strong> ausgeführt werden.</div><div class="bu-weekdays" ${schedule.mode==='selected'?'':'hidden'}><span>Wochentage</span>${weekDays.map(day=>`<label><input type="checkbox" name="scheduleDays" value="${day.value}" ${schedule.days.includes(day.value)?'checked':''}><span>${day.short}</span></label>`).join('')}</div>${schedule.mode==='custom'?`<label class="bu-custom-cron">Bestehender Zeitplan<input name="customCron" readonly value="${esc(schedule.cron)}"></label>`:''}</fieldset>`;
} }
function lineChart(runs, field, formatter, emptyText) { function lineChart(runs, field, formatter, emptyText) {
@@ -723,6 +728,10 @@
if (e.target.name === 'scheduleMode') { if (e.target.name === 'scheduleMode') {
const weekdays=e.target.closest('.bu-schedule')?.querySelector('.bu-weekdays'); const weekdays=e.target.closest('.bu-schedule')?.querySelector('.bu-weekdays');
if (weekdays) weekdays.hidden=e.target.value!=='selected'; if (weekdays) weekdays.hidden=e.target.value!=='selected';
const time=e.target.closest('.bu-schedule')?.querySelector('.bu-schedule-time');
if(time) time.hidden=e.target.value==='manual';
const manual=e.target.closest('.bu-schedule')?.querySelector('.bu-schedule-manual');
if(manual) manual.hidden=e.target.value!=='manual';
} }
if (e.target.name === 'type' && e.target.closest('#bu-job-form')) { if (e.target.name === 'type' && e.target.closest('#bu-job-form')) {
state.backupSelections=[]; state.workloadSelections=[]; state.backupSelections=[]; state.workloadSelections=[];