Files
BackItUp/app/templates/backup_new.html
T

715 lines
25 KiB
HTML
Raw Normal View History

2026-07-29 17:36:22 +02:00
{% extends "base_admin.html" %}
{% block title %}StFV Backup - Neues Backup{% endblock %}
{% block content %}
<h2>Neues Backup einrichten</h2>
<p>Die Konfiguration erscheint Schritt fuer Schritt, damit du jedes Fenster nacheinander ausfuellen kannst.</p>
<style>
.wizard-step {
display: none;
}
.wizard-step.active {
display: block;
animation: fade-slide 240ms ease;
}
.wizard-nav {
margin-top: 10px;
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.wizard-pill {
border: 1px solid rgba(30, 42, 59, 0.2);
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
color: #5a6e87;
background: #fff;
}
.wizard-pill.active {
border-color: rgba(10, 127, 104, 0.35);
background: #e5fff7;
color: #0a7f68;
font-weight: 700;
}
.step-actions {
margin-top: 12px;
display: flex;
gap: 10px;
}
.btn-secondary {
border: 1px solid rgba(30, 42, 59, 0.2);
background: #fff;
color: #1e2a3b;
}
.tree {
list-style: none;
margin: 8px 0 0;
padding: 0 0 0 8px;
border-left: 1px solid rgba(30, 42, 59, 0.15);
}
.tree li {
margin: 4px 0;
padding-left: 10px;
position: relative;
}
.tree li::before {
content: "";
position: absolute;
left: -8px;
top: 12px;
width: 8px;
border-top: 1px solid rgba(30, 42, 59, 0.15);
}
.tree-node {
border: 0;
background: transparent;
color: #1e2a3b;
font-weight: 600;
cursor: pointer;
padding: 3px 4px;
border-radius: 6px;
}
.tree-node:hover {
background: #eef6ff;
}
.tree-node.active {
background: #e5fff7;
color: #0a7f68;
}
.tree-meta {
font-size: 13px;
color: #5a6e87;
margin-top: 6px;
}
.tree-empty {
color: #5a6e87;
font-size: 13px;
}
.mini-input {
margin-top: 8px;
display: flex;
gap: 8px;
}
.mini-input input {
margin: 0;
}
.inline-help {
color: #5a6e87;
font-size: 13px;
}
@keyframes fade-slide {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
<form method="post" action="{{ url_for('backup_new') }}" id="backupForm">
<input type="hidden" id="source_entry_type" name="source_entry_type" value="directory">
<input type="hidden" id="target_kind" name="target_kind" value="folder">
<div class="wizard-nav" id="wizardNav">
<span class="wizard-pill active" data-step-pill="0">Allgemein</span>
<span class="wizard-pill" data-step-pill="1">Source</span>
<span class="wizard-pill" data-step-pill="2">Destination</span>
<span class="wizard-pill" data-step-pill="3">Target</span>
<span class="wizard-pill" data-step-pill="4">Zeitplan</span>
</div>
<section class="card wizard-step active" data-step="0">
<h3>Allgemein</h3>
<label for="name">Backup Name</label>
<input id="name" name="name" type="text" placeholder="z. B. unraid-nextcloud-daily" required>
<div class="step-actions">
<button type="button" class="btn" onclick="nextStep(0)">Weiter</button>
</div>
</section>
<section class="card wizard-step" data-step="1">
<h3>Source</h3>
<label for="source_protocol">Typ</label>
<select id="source_protocol" name="source_protocol" required>
{% for protocol in target_protocols %}
<option value="{{ protocol }}">{{ protocol }}</option>
{% endfor %}
</select>
<div id="source_remote_fields">
<label for="source_host">Host/IP (optional mit :Port)</label>
<input id="source_host" name="source_host" type="text" placeholder="192.168.1.20 oder 192.168.1.20:445">
<div class="row">
<div>
<label for="source_username">Benutzer</label>
<input id="source_username" name="source_username" type="text">
</div>
<div>
<label for="source_password">Passwort</label>
<input id="source_password" name="source_password" type="password">
</div>
</div>
<div id="source_share_row">
<label for="source_share">Share / Root</label>
<input id="source_share" name="source_share" type="text" placeholder="source-share">
</div>
<label for="source_subpath">Unterpfad (SMB Browser)</label>
<input id="source_subpath" name="source_subpath" type="text" placeholder="/daten/projektA" value="/">
<p id="source_test_message"></p>
<div id="source_browser" class="card" style="display:none; margin-top:10px;">
<div id="source_browser_meta" class="tree-meta"></div>
<ul id="source_browser_tree" class="tree"></ul>
</div>
<p class="inline-help">Ordnerauswahl setzt Source als Ordner, Dateiauswahl setzt Source als Datei.</p>
</div>
<div id="source_local_fields" style="display:none;">
<label for="source_local_path">Lokaler Pfad</label>
<input id="source_local_path" type="text" placeholder="/mnt/storage/quellordner">
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Zurueck</button>
<button type="button" class="btn" onclick="nextStep(1)">Weiter</button>
</div>
</section>
<section class="card wizard-step" data-step="2">
<h3>Destination</h3>
<label for="destination_protocol">Typ</label>
<select id="destination_protocol" name="destination_protocol" required>
{% for protocol in target_protocols %}
<option value="{{ protocol }}">{{ protocol }}</option>
{% endfor %}
</select>
<div id="destination_remote_fields">
<label for="destination_host">Host/IP (optional mit :Port)</label>
<input id="destination_host" name="destination_host" type="text" placeholder="192.168.1.30 oder 192.168.1.30:445">
<div class="row">
<div>
<label for="destination_username">Benutzer</label>
<input id="destination_username" name="destination_username" type="text">
</div>
<div>
<label for="destination_password">Passwort</label>
<input id="destination_password" name="destination_password" type="password">
</div>
</div>
<div id="destination_share_row">
<label for="destination_share">Share / Root</label>
<input id="destination_share" name="destination_share" type="text" placeholder="backup-share">
</div>
<label for="destination_subpath">Unterpfad (SMB Browser)</label>
<input id="destination_subpath" name="destination_subpath" type="text" placeholder="/backups" value="/">
<div class="mini-input">
<input id="destination_new_folder" type="text" placeholder="Neuer Ordnername">
<button class="btn" type="button" onclick="createDestinationFolder()">Ordner erstellen</button>
</div>
<p id="destination_test_message"></p>
<div id="destination_browser" class="card" style="display:none; margin-top:10px;">
<div id="destination_browser_meta" class="tree-meta"></div>
<ul id="destination_browser_tree" class="tree"></ul>
</div>
</div>
<div id="destination_local_fields" style="display:none;">
<label for="destination_local_path">Lokaler Pfad</label>
<input id="destination_local_path" type="text" placeholder="/mnt/storage/zielordner">
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Zurueck</button>
<button type="button" class="btn" onclick="nextStep(2)">Weiter</button>
</div>
</section>
<section class="card wizard-step" data-step="3">
<h3>Target</h3>
<label for="target_pattern" id="target_pattern_label">Zielordner</label>
<input id="target_pattern" name="target_pattern" type="text" placeholder="backup_{date}_{time}" required>
<p class="inline-help">Wenn in Source eine Datei gewaehlt wurde, ist dies die Zieldatei. Bei Ordnerauswahl ist dies der Zielordner.</p>
<label for="compression_method">Kompression</label>
<select id="compression_method" name="compression_method" required>
{% for method in compression_methods %}
<option value="{{ method }}">{{ method }}</option>
{% endfor %}
</select>
<label for="encryption_mode">Verschluesselung</label>
<select id="encryption_mode" name="encryption_mode" required>
{% for mode in encryption_modes %}
<option value="{{ mode }}">{{ mode }}</option>
{% endfor %}
</select>
<div id="encryption_password_box">
<div class="row">
<div>
<label for="archive_password">Passwort</label>
<input id="archive_password" name="archive_password" type="password">
</div>
<div>
<label for="archive_password_confirm">Passwort bestaetigen</label>
<input id="archive_password_confirm" name="archive_password_confirm" type="password">
</div>
</div>
<p class="inline-help">Passwoerter werden verschluesselt gespeichert.</p>
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Zurueck</button>
<button type="button" class="btn" onclick="nextStep(3)">Weiter</button>
</div>
</section>
<section class="card wizard-step" data-step="4">
<h3>Zeitplan</h3>
<label for="schedule_mode">Ausfuehrung</label>
<select id="schedule_mode" name="schedule_mode" required>
<option value="daily">Taeglich</option>
<option value="weekly">Woechentlich</option>
<option value="monthly">Monatlich</option>
<option value="yearly">Jaehrlich</option>
<option value="every_n_days">Alle X Tage</option>
<option value="every_n_weeks">Alle X Wochen</option>
<option value="custom">Benutzerdefiniert (CRON)</option>
</select>
<div id="schedule_time_box">
<label for="schedule_time">Uhrzeit</label>
<input id="schedule_time" name="schedule_time" type="time" value="02:00">
</div>
<div id="schedule_weekday_box" style="display:none;">
<label for="schedule_weekday">Wochentag</label>
<select id="schedule_weekday" name="schedule_weekday">
<option value="1">Montag</option>
<option value="2">Dienstag</option>
<option value="3">Mittwoch</option>
<option value="4">Donnerstag</option>
<option value="5">Freitag</option>
<option value="6">Samstag</option>
<option value="0">Sonntag</option>
</select>
</div>
<div id="schedule_dom_box" style="display:none;">
<label for="schedule_day_of_month">Tag im Monat</label>
<input id="schedule_day_of_month" name="schedule_day_of_month" type="number" min="1" max="31" value="1">
</div>
<div id="schedule_interval_box" style="display:none;">
<label for="schedule_interval">Intervall (X)</label>
<input id="schedule_interval" name="schedule_interval" type="number" min="1" value="1">
</div>
<div id="schedule_cron_box" style="display:none;">
<label for="schedule_cron">CRON Ausdruck</label>
<input id="schedule_cron" name="schedule_cron" type="text" placeholder="0 2 * * *">
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(4)">Zurueck</button>
<button class="btn" type="submit">Backup speichern</button>
</div>
</section>
</form>
<script>
let currentStep = 0;
function htmlEscape(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function showStep(stepIdx) {
const steps = document.querySelectorAll('.wizard-step');
const pills = document.querySelectorAll('[data-step-pill]');
steps.forEach((el, idx) => el.classList.toggle('active', idx === stepIdx));
pills.forEach((el, idx) => el.classList.toggle('active', idx === stepIdx));
currentStep = stepIdx;
}
function requireValue(id, message) {
const el = document.getElementById(id);
if (!el || !el.value.trim()) {
alert(message);
el?.focus();
return false;
}
return true;
}
function validateStep(stepIdx) {
if (stepIdx === 0) {
return requireValue('name', 'Bitte Backup Name setzen.');
}
if (stepIdx === 1) {
const protocol = document.getElementById('source_protocol').value;
if (protocol === 'LOCAL') {
return requireValue('source_local_path', 'Bitte lokalen Source-Pfad angeben.');
}
if (!requireValue('source_host', 'Bitte Source Host/IP setzen.')) return false;
if (!requireValue('source_username', 'Bitte Source Benutzer setzen.')) return false;
if (!requireValue('source_password', 'Bitte Source Passwort setzen.')) return false;
if (protocol === 'SMB' && !requireValue('source_share', 'Bitte Source Share setzen.')) return false;
return requireValue('source_subpath', 'Bitte Source Unterpfad setzen.');
}
if (stepIdx === 2) {
const protocol = document.getElementById('destination_protocol').value;
if (protocol === 'LOCAL') {
return requireValue('destination_local_path', 'Bitte lokalen Destination-Pfad angeben.');
}
if (!requireValue('destination_host', 'Bitte Destination Host/IP setzen.')) return false;
if (!requireValue('destination_username', 'Bitte Destination Benutzer setzen.')) return false;
if (!requireValue('destination_password', 'Bitte Destination Passwort setzen.')) return false;
if (protocol === 'SMB' && !requireValue('destination_share', 'Bitte Destination Share setzen.')) return false;
return requireValue('destination_subpath', 'Bitte Destination Unterpfad setzen.');
}
if (stepIdx === 3) {
if (!requireValue('target_pattern', 'Bitte Zielordner oder Zieldatei setzen.')) return false;
const mode = document.getElementById('encryption_mode').value;
if (mode === 'password-aes256') {
if (!requireValue('archive_password', 'Bitte Verschluesselungs-Passwort setzen.')) return false;
if (!requireValue('archive_password_confirm', 'Bitte Passwort bestaetigen.')) return false;
}
return true;
}
return true;
}
function nextStep(stepIdx) {
if (!validateStep(stepIdx)) return;
showStep(Math.min(stepIdx + 1, 4));
}
function prevStep(stepIdx) {
showStep(Math.max(stepIdx - 1, 0));
}
function setRequired(el, enabled) {
if (!el) return;
if (enabled) {
el.setAttribute('required', 'required');
} else {
el.removeAttribute('required');
}
}
function syncProtocolFields(prefix) {
const protocol = document.getElementById(prefix + '_protocol').value;
const remoteFields = document.getElementById(prefix + '_remote_fields');
const localFields = document.getElementById(prefix + '_local_fields');
const shareRow = document.getElementById(prefix + '_share_row');
const host = document.getElementById(prefix + '_host');
const share = document.getElementById(prefix + '_share');
const subpath = document.getElementById(prefix + '_subpath');
const user = document.getElementById(prefix + '_username');
const pass = document.getElementById(prefix + '_password');
const localPath = document.getElementById(prefix + '_local_path');
if (protocol === 'LOCAL') {
remoteFields.style.display = 'none';
localFields.style.display = 'block';
shareRow.style.display = 'none';
setRequired(host, false);
setRequired(share, false);
setRequired(user, false);
setRequired(pass, false);
setRequired(localPath, true);
subpath.value = localPath.value || '';
share.value = '';
user.value = '';
pass.value = '';
host.value = 'local';
if (prefix === 'source') {
setSourceEntryType('directory');
}
return;
}
remoteFields.style.display = 'block';
localFields.style.display = 'none';
shareRow.style.display = protocol === 'SMB' ? 'block' : 'none';
setRequired(host, true);
setRequired(share, protocol === 'SMB');
setRequired(user, true);
setRequired(pass, true);
setRequired(localPath, false);
if (protocol === 'FTP' || protocol === 'SFTP') {
if (!share.value) share.value = '/';
if (!subpath.value) subpath.value = '/';
}
}
function collectPayload(prefix) {
const protocol = document.getElementById(prefix + '_protocol').value;
const payload = {
prefix: prefix,
[prefix + '_protocol']: protocol,
[prefix + '_host']: document.getElementById(prefix + '_host').value,
[prefix + '_share']: document.getElementById(prefix + '_share').value,
[prefix + '_subpath']: document.getElementById(prefix + '_subpath').value,
[prefix + '_username']: document.getElementById(prefix + '_username').value,
[prefix + '_password']: document.getElementById(prefix + '_password').value
};
if (protocol === 'LOCAL') {
payload[prefix + '_host'] = 'local';
payload[prefix + '_share'] = '';
payload[prefix + '_username'] = '';
payload[prefix + '_password'] = '';
payload[prefix + '_subpath'] = document.getElementById(prefix + '_local_path').value;
document.getElementById(prefix + '_subpath').value = payload[prefix + '_subpath'];
}
return payload;
}
function clearActiveTreeNodes(prefix) {
document
.querySelectorAll('#' + prefix + '_browser_tree .tree-node')
.forEach((node) => node.classList.remove('active'));
}
function setSourceEntryType(entryType) {
const t = entryType === 'file' ? 'file' : 'directory';
document.getElementById('source_entry_type').value = t;
const targetKind = t === 'file' ? 'file' : 'folder';
document.getElementById('target_kind').value = targetKind;
const label = document.getElementById('target_pattern_label');
label.textContent = targetKind === 'file' ? 'Zieldatei' : 'Zielordner';
}
function makeTreeItem(prefix, entry) {
const li = document.createElement('li');
const button = document.createElement('button');
button.type = 'button';
button.className = 'tree-node';
let icon = '📁';
if (entry.entry_type === 'up') icon = '↩';
if (entry.entry_type === 'file') icon = '📄';
button.textContent = icon + ' ' + entry.name;
button.dataset.path = entry.path;
button.dataset.entryType = entry.entry_type;
const childList = document.createElement('ul');
childList.className = 'tree';
childList.style.display = 'none';
button.addEventListener('click', async () => {
clearActiveTreeNodes(prefix);
button.classList.add('active');
document.getElementById(prefix + '_subpath').value = entry.path;
if (prefix === 'source') {
setSourceEntryType(entry.entry_type === 'file' ? 'file' : 'directory');
}
if (entry.entry_type === 'file') {
document.getElementById(prefix + '_test_message').textContent = 'Datei ausgewaehlt: ' + entry.path;
document.getElementById(prefix + '_test_message').style.color = '#0a7f68';
return;
}
await loadTreeLevel(prefix, entry.path, childList);
});
li.appendChild(button);
li.appendChild(childList);
return li;
}
async function loadTreeLevel(prefix, path, targetList) {
const payload = collectPayload(prefix);
payload[prefix + '_subpath'] = path;
const response = await fetch('{{ url_for("api_target_browse_smb") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
const msg = document.getElementById(prefix + '_test_message');
if (!data.ok) {
msg.textContent = data.message || 'SMB-Browsing fehlgeschlagen.';
msg.style.color = '#b9382f';
return;
}
msg.textContent = 'SMB-Verbindung erfolgreich.';
msg.style.color = '#0a7f68';
document.getElementById(prefix + '_subpath').value = data.path;
document.getElementById(prefix + '_browser_meta').textContent = 'Aktueller Pfad: ' + data.path;
targetList.innerHTML = '';
const entries = data.entries || [];
if (!entries.length) {
const empty = document.createElement('li');
empty.className = 'tree-empty';
empty.textContent = 'Keine Eintraege';
targetList.appendChild(empty);
targetList.style.display = 'block';
return;
}
entries.forEach((entry) => {
targetList.appendChild(makeTreeItem(prefix, entry));
});
targetList.style.display = 'block';
}
async function renderRootTree(prefix) {
const rootList = document.getElementById(prefix + '_browser_tree');
rootList.innerHTML = '';
await loadTreeLevel(prefix, document.getElementById(prefix + '_subpath').value || '/', rootList);
}
async function browseSmb(prefix) {
if (document.getElementById(prefix + '_protocol').value !== 'SMB') {
const msg = document.getElementById(prefix + '_test_message');
msg.textContent = 'SMB Browser nur bei SMB aktiv.';
msg.style.color = '#b9382f';
return;
}
const container = document.getElementById(prefix + '_browser');
container.style.display = 'block';
await renderRootTree(prefix);
}
async function createDestinationFolder() {
const folderInput = document.getElementById('destination_new_folder');
const name = folderInput.value.trim();
const msg = document.getElementById('destination_test_message');
if (!name) {
msg.textContent = 'Bitte zuerst einen neuen Ordnernamen eingeben.';
msg.style.color = '#b9382f';
return;
}
const payload = collectPayload('destination');
payload.folder_name = name;
const response = await fetch('{{ url_for("api_target_smb_mkdir") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await response.json();
if (!data.ok) {
msg.textContent = data.message || 'Ordner konnte nicht erstellt werden.';
msg.style.color = '#b9382f';
return;
}
folderInput.value = '';
document.getElementById('destination_subpath').value = data.path;
msg.textContent = 'Ordner erstellt: ' + data.path;
msg.style.color = '#0a7f68';
await browseSmb('destination');
}
function syncEncryptionFields() {
const mode = document.getElementById('encryption_mode').value;
const box = document.getElementById('encryption_password_box');
const pwd = document.getElementById('archive_password');
const pwd2 = document.getElementById('archive_password_confirm');
const visible = mode === 'password-aes256';
box.style.display = visible ? 'block' : 'none';
setRequired(pwd, visible);
setRequired(pwd2, visible);
if (!visible) {
pwd.value = '';
pwd2.value = '';
}
}
function syncScheduleFields() {
const mode = document.getElementById('schedule_mode').value;
document.getElementById('schedule_weekday_box').style.display = mode === 'weekly' || mode === 'every_n_weeks' ? 'block' : 'none';
document.getElementById('schedule_dom_box').style.display = mode === 'monthly' || mode === 'yearly' ? 'block' : 'none';
document.getElementById('schedule_interval_box').style.display = mode === 'every_n_days' || mode === 'every_n_weeks' ? 'block' : 'none';
document.getElementById('schedule_cron_box').style.display = mode === 'custom' ? 'block' : 'none';
}
document.getElementById('source_protocol').addEventListener('change', () => syncProtocolFields('source'));
document.getElementById('destination_protocol').addEventListener('change', () => syncProtocolFields('destination'));
document.getElementById('source_local_path').addEventListener('input', (ev) => {
document.getElementById('source_subpath').value = ev.target.value;
});
document.getElementById('destination_local_path').addEventListener('input', (ev) => {
document.getElementById('destination_subpath').value = ev.target.value;
});
document.getElementById('source_subpath').addEventListener('focus', () => {
if (document.getElementById('source_protocol').value === 'SMB') {
browseSmb('source');
}
});
document.getElementById('destination_subpath').addEventListener('focus', () => {
if (document.getElementById('destination_protocol').value === 'SMB') {
browseSmb('destination');
}
});
document.getElementById('source_subpath').addEventListener('click', () => {
if (document.getElementById('source_protocol').value === 'SMB') {
browseSmb('source');
}
});
document.getElementById('destination_subpath').addEventListener('click', () => {
if (document.getElementById('destination_protocol').value === 'SMB') {
browseSmb('destination');
}
});
document.getElementById('encryption_mode').addEventListener('change', syncEncryptionFields);
document.getElementById('schedule_mode').addEventListener('change', syncScheduleFields);
syncProtocolFields('source');
syncProtocolFields('destination');
syncEncryptionFields();
syncScheduleFields();
setSourceEntryType('directory');
showStep(0);
</script>
{% endblock %}