Files
BackItUp/app/templates/backup_edit.html
T
2026-08-11 23:33:40 +02:00

685 lines
44 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base_admin.html" %}
{% block title %}BackItUp - Backup bearbeiten{% endblock %}
{% block content %}
<h2 style="margin-bottom:4px;">Backup bearbeiten</h2>
<style>
.wizard-step { display: none; }
.wizard-step.active { display: block; animation: fade-slide 240ms ease; }
.wizard-nav { margin-top: 0; margin-bottom: 8px; 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; cursor: pointer; }
.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; align-items: center; }
.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; }
.browser-overlay { position: fixed; inset: 0; background: rgba(10,20,34,0.52); display: none; align-items: center; justify-content: center; z-index: 1100; padding: 20px; }
.browser-dialog { background: #fff; border-radius: 16px; width: min(92vw,560px); max-height: 74vh; display: flex; flex-direction: column; box-shadow: 0 20px 48px rgba(30,42,59,0.24); overflow: hidden; position: relative; }
.browser-dialog-header { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; border-bottom: 1px solid rgba(30,42,59,0.12); }
.browser-dialog-body { padding: 12px 16px 16px; overflow: auto; }
.browser-dialog-actions { display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 10px 16px 14px; border-top: 1px solid rgba(30,42,59,0.12); }
.browser-dialog-actions .btn-secondary { background: #fff; }
.browser-dialog-right { display: flex; gap: 8px; }
.create-folder-overlay { position: absolute; inset: 0; background: rgba(255,255,255,0.97); backdrop-filter: blur(4px); z-index: 20; display: flex; align-items: center; justify-content: center; border-radius: 16px; }
.create-folder-box { padding: 20px; width: 100%; max-width: 320px; display: grid; gap: 10px; }
.create-folder-box strong { font-size: 15px; }
.create-folder-actions { display: flex; gap: 8px; justify-content: flex-end; }
.step-header { margin: -16px -16px 14px; padding: 12px 16px; border-bottom: 1px solid rgba(30,42,59,0.08); background: rgba(30,42,59,0.025); border-radius: 14px 14px 0 0; }
.step-header h3 { margin: 0; }
.token-bar { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 10px; }
.token-chip { display: inline-flex; align-items: center; padding: 4px 11px; border-radius: 999px; background: #e5fff7; border: 1px solid rgba(10,127,104,0.35); color: #0a7f68; font-size: 12px; font-weight: 700; cursor: grab; user-select: none; }
.token-chip:hover { background: #c8f5e8; }
.token-menu { position: fixed; z-index: 3000; background: #fff; border: 1px solid rgba(30,42,59,0.15); border-radius: 12px; box-shadow: 0 8px 24px rgba(30,42,59,0.18); padding: 6px; min-width: 190px; }
.token-menu-item { padding: 7px 12px; border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 600; }
.token-menu-item:hover { background: #f0f7ff; }
.token-menu-sub { font-size: 11px; color: #5a6e87; font-weight: 400; margin-top: 1px; }
.row-3 { grid-template-columns: minmax(90px,0.8fr) 2fr 1.4fr; }
@media (max-width: 700px) { .row-3 { grid-template-columns: 1fr; } }
.path-input-group { display: flex; gap: 6px; margin-bottom: 12px; }
.path-input-group input { flex: 1; margin-bottom: 0; }
.btn-browse { padding: 8px 11px; border-radius: 12px; border: 1px solid rgba(30,42,59,0.2); background: #fff; cursor: pointer; font-size: 15px; line-height: 1; color: #1e2a3b; flex-shrink: 0; }
.btn-browse:hover { background: #f0f7ff; }
.hidden-toggle { font-size: 12px; color: #5a6e87; display: flex; align-items: center; gap: 5px; cursor: pointer; font-weight: 600; }
.hidden-toggle input[type="checkbox"] { width: auto; margin: 0; cursor: pointer; }
@keyframes fade-slide { from { opacity:0; transform:translateY(8px); } to { opacity:1; transform:translateY(0); } }
</style>
<form method="post" action="{{ url_for('backup_edit', backup_id=backup.id) }}" id="backupForm">
<input type="hidden" id="source_entry_type" name="source_entry_type" value="{{ backup.source_entry_type }}">
<input type="hidden" id="target_kind" name="target_kind" value="{{ backup.target_kind }}">
<div class="wizard-nav">
<button type="button" class="wizard-pill active" data-step-pill="0" onclick="showStep(0)">Allgemein</button>
<button type="button" class="wizard-pill" data-step-pill="1" onclick="showStep(1)">Source</button>
<button type="button" class="wizard-pill" data-step-pill="2" onclick="showStep(2)">Destination</button>
<button type="button" class="wizard-pill" data-step-pill="3" onclick="showStep(3)">Target</button>
<button type="button" class="wizard-pill" data-step-pill="4" onclick="showStep(4)">Zeitplan</button>
</div>
<section class="card wizard-step active" data-step="0">
<div class="step-header"><h3>Allgemein</h3></div>
<label for="name">Backup Name</label>
<input id="name" name="name" type="text" value="{{ backup.name }}" 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">
<div class="step-header"><h3>Source</h3></div>
<div class="row row-3">
<div>
<label for="source_protocol">Typ</label>
<select id="source_protocol" name="source_protocol" required>
{% for p in target_protocols %}<option value="{{ p }}" {% if p == backup.source_protocol %}selected{% endif %}>{{ p }}</option>{% endfor %}
</select>
</div>
<div id="source_host_col" style="display:none;">
<label for="source_host">Host/IP</label>
<div style="display:flex; gap:6px;">
<input id="source_host" name="source_host" type="text" value="{{ backup.source_host }}" style="flex:1; margin-bottom:0;">
<input id="source_port_ui" type="number" min="1" max="65535" placeholder="Port"
title="Port" value="{{ backup.source_port or '' }}" style="width:72px; margin-bottom:0; display:none;">
</div>
</div>
<div id="source_share_row" style="display:none;">
<label for="source_share">Freigabe</label>
<input id="source_share" name="source_share" type="text" value="{{ backup.source_share }}">
</div>
<div id="source_local_path_col" style="display:none; grid-column: 2 / -1;">
<label for="source_local_path">Lokaler Pfad</label>
<div class="path-input-group">
<input id="source_local_path" type="text" value="{{ backup.source_local_path }}">
<button type="button" class="btn-browse" onclick="openLocalBrowserOverlay('source')" title="Browser öffnen">🔍</button>
</div>
</div>
</div>
<div id="source_local_browser_overlay" class="browser-overlay">
<div class="browser-dialog" role="dialog" aria-modal="true" aria-label="Lokaler Browser">
<div class="browser-dialog-header"><strong>Lokaler Browser</strong><label class="hidden-toggle"><input type="checkbox" id="source_local_show_hidden" onchange="refreshLocalBrowserLevel('source')"> Versteckte</label><button type="button" class="btn btn-secondary" onclick="closeLocalBrowserOverlay('source')">×</button></div>
<div class="browser-dialog-body"><div id="source_local_browser_meta" class="tree-meta"></div><ul id="source_local_browser_tree" class="tree"></ul></div>
<div class="browser-dialog-actions">
<div></div>
<div class="browser-dialog-right">
<button type="button" class="btn btn-secondary" onclick="closeLocalBrowserOverlay('source')">Abbrechen</button>
<button type="button" class="btn" onclick="acceptLocalBrowserSelection('source')">Auswählen</button>
</div>
</div>
</div>
</div>
<div id="source_remote_fields" style="display:none;">
<div class="row">
<div><label for="source_username">Benutzer</label><input id="source_username" name="source_username" type="text" value="{{ backup.source_username }}"></div>
<div><label for="source_password">Passwort</label><input id="source_password" name="source_password" type="password" value="{{ backup.source_password }}"></div>
</div>
<label for="source_subpath">Pfad</label>
<div class="path-input-group">
<input id="source_subpath" name="source_subpath" type="text" value="{{ backup.source_subpath }}">
<button type="button" class="btn-browse" onclick="browseSmb('source')" title="Browser öffnen">🔍</button>
</div>
<p id="source_test_message"></p>
<div id="source_browser_overlay" class="browser-overlay">
<div class="browser-dialog" role="dialog" aria-modal="true" aria-label="Browser">
<div class="browser-dialog-header"><strong>Browser</strong><label class="hidden-toggle"><input type="checkbox" id="source_smb_show_hidden" onchange="refreshBrowserLevel('source')"> Versteckte</label><button type="button" class="btn btn-secondary" onclick="closeBrowserOverlay('source')">×</button></div>
<div class="browser-dialog-body"><div id="source_browser_meta" class="tree-meta"></div><ul id="source_browser_tree" class="tree"></ul></div>
<div class="browser-dialog-actions">
<div></div>
<div class="browser-dialog-right">
<button type="button" class="btn btn-secondary" onclick="closeBrowserOverlay('source')">Abbrechen</button>
<button type="button" class="btn" onclick="acceptBrowserSelection('source')">Auswählen</button>
</div>
</div>
</div>
</div>
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Zurück</button>
<button type="button" class="btn" onclick="nextStep(1)">Weiter</button>
</div>
</section>
<section class="card wizard-step" data-step="2">
<div class="step-header"><h3>Destination</h3></div>
<div class="row row-3">
<div>
<label for="destination_protocol">Typ</label>
<select id="destination_protocol" name="destination_protocol" required>
{% for p in target_protocols %}<option value="{{ p }}" {% if p == backup.destination_protocol %}selected{% endif %}>{{ p }}</option>{% endfor %}
</select>
</div>
<div id="destination_host_col" style="display:none;">
<label for="destination_host">Host/IP</label>
<div style="display:flex; gap:6px;">
<input id="destination_host" name="destination_host" type="text" value="{{ backup.destination_host }}" style="flex:1; margin-bottom:0;">
<input id="destination_port_ui" type="number" min="1" max="65535" placeholder="Port"
title="Port" value="{{ backup.destination_port or '' }}" style="width:72px; margin-bottom:0; display:none;">
</div>
</div>
<div id="destination_share_row" style="display:none;">
<label for="destination_share">Freigabe</label>
<input id="destination_share" name="destination_share" type="text" value="{{ backup.destination_share }}">
</div>
<div id="destination_local_path_col" style="display:none; grid-column: 2 / -1;">
<label for="destination_local_path">Lokaler Pfad</label>
<div class="path-input-group">
<input id="destination_local_path" type="text" value="{{ backup.destination_local_path }}">
<button type="button" class="btn-browse" onclick="openLocalBrowserOverlay('destination')" title="Browser öffnen">🔍</button>
</div>
</div>
</div>
<div id="destination_local_browser_overlay" class="browser-overlay">
<div class="browser-dialog" role="dialog" aria-modal="true" aria-label="Lokaler Browser">
<div class="browser-dialog-header"><strong>Lokaler Browser</strong><label class="hidden-toggle"><input type="checkbox" id="destination_local_show_hidden" onchange="refreshLocalBrowserLevel('destination')"> Versteckte</label><button type="button" class="btn btn-secondary" onclick="closeLocalBrowserOverlay('destination')">×</button></div>
<div class="browser-dialog-body"><div id="destination_local_browser_meta" class="tree-meta"></div><ul id="destination_local_browser_tree" class="tree"></ul></div>
<div class="browser-dialog-actions">
<div id="destination_local_new_folder_bar" style="display:none; flex:1; align-items:center; gap:6px;">
<input id="destination_local_new_folder" type="text" placeholder="Ordnername" autocomplete="off" style="flex:1; min-width:0; margin:0;">
<button class="btn" type="button" onclick="createLocalDestinationFolder()">Erstellen</button>
<button class="btn btn-secondary" type="button" onclick="closeLocalCreateFolderOverlay()">&#x2715;</button>
</div>
<button id="destination_local_new_folder_btn" type="button" class="btn btn-secondary" onclick="openLocalCreateFolderOverlay()">Ordner erstellen</button>
<div class="browser-dialog-right">
<button type="button" class="btn btn-secondary" onclick="closeLocalBrowserOverlay('destination')">Abbrechen</button>
<button type="button" class="btn" onclick="acceptLocalBrowserSelection('destination')">Auswählen</button>
</div>
</div>
</div>
</div>
<div id="destination_remote_fields" style="display:none;">
<div class="row">
<div><label for="destination_username">Benutzer</label><input id="destination_username" name="destination_username" type="text" value="{{ backup.destination_username }}"></div>
<div><label for="destination_password">Passwort</label><input id="destination_password" name="destination_password" type="password" value="{{ backup.destination_password }}"></div>
</div>
<label for="destination_subpath">Pfad</label>
<div class="path-input-group">
<input id="destination_subpath" name="destination_subpath" type="text" value="{{ backup.destination_subpath }}">
<button type="button" class="btn-browse" onclick="browseSmb('destination')" title="Browser öffnen">🔍</button>
</div>
<p id="destination_test_message"></p>
<div id="destination_browser_overlay" class="browser-overlay">
<div class="browser-dialog" role="dialog" aria-modal="true" aria-label="Browser">
<div class="browser-dialog-header"><strong>Browser</strong><label class="hidden-toggle"><input type="checkbox" id="destination_smb_show_hidden" onchange="refreshBrowserLevel('destination')"> Versteckte</label><button type="button" class="btn btn-secondary" onclick="closeBrowserOverlay('destination')">×</button></div>
<div class="browser-dialog-body"><div id="destination_browser_meta" class="tree-meta"></div><ul id="destination_browser_tree" class="tree"></ul></div>
<div class="browser-dialog-actions">
<div id="destination_new_folder_bar" style="display:none; flex:1; align-items:center; gap:6px;">
<input id="destination_new_folder" type="text" placeholder="Ordnername" autocomplete="off" style="flex:1; min-width:0; margin:0;">
<button class="btn" type="button" onclick="createDestinationFolder()">Erstellen</button>
<button class="btn btn-secondary" type="button" onclick="closeCreateFolderOverlay()">&#x2715;</button>
</div>
<button id="destination_new_folder_btn" type="button" class="btn btn-secondary" onclick="openCreateFolderOverlay()">Ordner erstellen</button>
<div class="browser-dialog-right">
<button type="button" class="btn btn-secondary" onclick="closeBrowserOverlay('destination')">Abbrechen</button>
<button type="button" class="btn" onclick="acceptBrowserSelection('destination')">Auswählen</button>
</div>
</div>
</div>
</div>
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Zurück</button>
<button type="button" class="btn" onclick="nextStep(2)">Weiter</button>
</div>
</section>
<section class="card wizard-step" data-step="3">
<div class="step-header"><h3>Target</h3></div>
<div id="target_pattern_box">
<label for="target_pattern">Ziel</label>
<div class="token-bar" id="token_bar"></div>
<input id="target_pattern" name="target_pattern" type="text" value="{{ backup.target_pattern }}" required>
<div id="token_context_menu" class="token-menu" style="display:none;"></div>
</div>
<div class="row" style="grid-template-columns: 1fr 120px 1fr;">
<div>
<label for="backup_mode">Backup-Modus</label>
<select id="backup_mode" name="backup_mode" onchange="syncBackupMode()">
{% for mode in backup_modes %}
<option value="{{ mode }}" {% if mode == backup.backup_mode %}selected{% endif %}>{% if mode == 'full' %}Vollständig{% elif mode == 'sync' %}Sync{% else %}Clone{% endif %}</option>
{% endfor %}
</select>
</div>
<div id="rotation_col">
<label for="rotation_keep">Rotation (0=∞)</label>
<input id="rotation_keep" name="rotation_keep" type="number" min="0" value="{{ backup.rotation_keep }}" style="width:100%;">
</div>
<div id="compression_col">
<label for="compression_method">Kompression</label>
<select id="compression_method" name="compression_method" required>
{% for m in compression_methods %}<option value="{{ m }}" {% if m == backup.compression_method %}selected{% endif %}>{% if m == 'none' %}Keine (1:1){% else %}{{ m }}{% endif %}</option>{% endfor %}
</select>
</div>
</div>
<div id="encryption_row" class="row" style="display:none; grid-template-columns: 1fr 1fr 1fr;">
<div>
<label for="encryption_mode">Verschlüsselung</label>
<select id="encryption_mode" name="encryption_mode" required>
{% for m in encryption_modes %}<option value="{{ m }}" {% if m == backup.encryption_mode %}selected{% endif %}>{{ m }}</option>{% endfor %}
</select>
</div>
<div id="archive_pwd_col" style="display:none;">
<label for="archive_password">Passwort</label>
<input id="archive_password" name="archive_password" type="password" value="{{ backup.archive_password }}">
</div>
<div id="archive_pwd2_col" style="display:none;">
<label for="archive_password_confirm">Bestätigen</label>
<input id="archive_password_confirm" name="archive_password_confirm" type="password" value="{{ backup.archive_password }}">
</div>
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Zurück</button>
<button type="button" class="btn" onclick="nextStep(3)">Weiter</button>
</div>
</section>
<section class="card wizard-step" data-step="4">
<div class="step-header"><h3>Zeitplan</h3></div>
<label for="schedule_cron">CRON Ausdruck</label>
<input id="schedule_cron" name="schedule_cron" type="text" value="{{ backup.schedule_cron }}">
<div id="cron_preview" style="display:none;margin-top:6px;font-size:13px;color:#1e2a3b;">
<div id="cron_description" style="font-weight:600;"></div>
<div id="cron_next_run" style="color:#5a6e87;"></div>
</div>
<div id="cron_error" style="display:none;margin-top:6px;font-size:13px;color:#b9382f;"></div>
<div class="step-actions">
<a class="btn btn-secondary" href="{{ url_for('dashboard') }}" style="text-decoration:none;">Abbrechen</a>
<button type="button" class="btn btn-secondary" onclick="prevStep(4)">Zurück</button>
<button class="btn" type="submit">Speichern</button>
</div>
</section>
</form>
<script>
let currentStep = 0;
function htmlEscape(v) { return String(v).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;').replaceAll("'",'&#39;'); }
function showStep(i) {
document.querySelectorAll('.wizard-step').forEach((el, idx) => el.classList.toggle('active', idx === i));
document.querySelectorAll('[data-step-pill]').forEach((el, idx) => el.classList.toggle('active', idx === i));
currentStep = i;
}
function requireValue(id, msg) { const el = document.getElementById(id); if (!el || !el.value.trim()) { alert(msg); el?.focus(); return false; } return true; }
function validateStep(i) {
if (i === 0) return requireValue('name', 'Bitte Backup Name setzen.');
if (i === 1) {
const p = document.getElementById('source_protocol').value;
if (p === 'LOCAL') return requireValue('source_local_path', 'Bitte lokalen Source-Pfad angeben.');
if (!requireValue('source_host','Bitte Host/IP setzen.')) return false;
if (!requireValue('source_username','Bitte Benutzer setzen.')) return false;
if (!requireValue('source_password','Bitte Passwort setzen.')) return false;
if (p === 'SMB' && !requireValue('source_share','Bitte Share setzen.')) return false;
return requireValue('source_subpath','Bitte Pfad setzen.');
}
if (i === 2) {
const p = document.getElementById('destination_protocol').value;
if (p === 'LOCAL') return requireValue('destination_local_path', 'Bitte lokalen Destination-Pfad angeben.');
if (!requireValue('destination_host','Bitte Host/IP setzen.')) return false;
if (!requireValue('destination_username','Bitte Benutzer setzen.')) return false;
if (!requireValue('destination_password','Bitte Passwort setzen.')) return false;
if (p === 'SMB' && !requireValue('destination_share','Bitte Share setzen.')) return false;
return requireValue('destination_subpath','Bitte Pfad setzen.');
}
if (i === 3) {
if (!requireValue('target_pattern','Bitte Ziel setzen.')) return false;
if (document.getElementById('encryption_mode').value === 'password-aes256') {
if (!requireValue('archive_password','Bitte Passwort setzen.')) return false;
if (!requireValue('archive_password_confirm','Bitte Passwort bestätigen.')) return false;
}
return true;
}
return true;
}
function nextStep(i) { if (!validateStep(i)) return; showStep(Math.min(i+1,4)); }
function prevStep(i) { showStep(Math.max(i-1,0)); }
function setRequired(el, on) { if (!el) return; on ? el.setAttribute('required','required') : el.removeAttribute('required'); }
function syncProtocolFields(prefix) {
const protocol = document.getElementById(prefix+'_protocol').value;
const remoteFields = document.getElementById(prefix+'_remote_fields');
const localPathCol = document.getElementById(prefix+'_local_path_col');
const hostCol = document.getElementById(prefix+'_host_col');
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'; localPathCol.style.display='block';
hostCol.style.display='none'; shareRow.style.display='none';
const portUiL = document.getElementById(prefix+'_port_ui');
if (portUiL) { portUiL.style.display='none'; portUiL.value=''; }
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'; localPathCol.style.display='none';
hostCol.style.display='block'; shareRow.style.display = protocol==='SMB'?'block':'none';
setRequired(host,true); setRequired(share,protocol==='SMB'); setRequired(user,true); setRequired(pass,true); setRequired(localPath,false);
const portUi = document.getElementById(prefix+'_port_ui');
if (portUi) {
if (protocol==='FTP') { portUi.style.display='block'; if(!portUi.value) portUi.value='21'; }
else if (protocol==='SFTP') { portUi.style.display='block'; if(!portUi.value) portUi.value='22'; }
else { portUi.style.display='none'; portUi.value=''; }
}
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+'_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'];
}
if (protocol==='FTP'||protocol==='SFTP') {
const pUi=document.getElementById(prefix+'_port_ui');
const pVal=pUi?.value||(protocol==='FTP'?'21':'22');
payload[prefix+'_host']=String(payload[prefix+'_host']||'').split(':')[0]+':'+pVal;
}
return payload;
}
function setSourceEntryType(t) {
const type = t==='file'?'file':'directory';
document.getElementById('source_entry_type').value = type;
document.getElementById('target_kind').value = type==='file'?'file':'folder';
}
async function loadLocalBrowserLevel(prefix, path) {
const list = document.getElementById(prefix+'_local_browser_tree');
const meta = document.getElementById(prefix+'_local_browser_meta');
list.innerHTML='<li class="tree-empty">L\u00e4dt\u2026</li>';
let data;
try { const r=await fetch('{{ url_for("api_local_browse") }}?path='+encodeURIComponent(path)); data=await r.json(); }
catch(e) { list.innerHTML='<li class="tree-empty">Verbindungsfehler</li>'; return; }
if (!data.ok) { meta.textContent=''; list.innerHTML='<li class="tree-empty">'+htmlEscape(data.message)+'</li>'; return; }
meta.textContent='Pfad: '+data.path;
document.getElementById(prefix+'_local_path').value=data.path;
document.getElementById(prefix+'_subpath').value=data.path;
list.innerHTML='';
const allEntries=data.entries||[];
const entries=filterEntries(allEntries, prefix+'_local_show_hidden');
if (!entries.length) { const e=document.createElement('li'); e.className='tree-empty'; e.textContent='Keine Eintr\u00e4ge'; list.appendChild(e); return; }
entries.forEach(entry=>{
const li=document.createElement('li'); const btn=document.createElement('button');
btn.type='button'; btn.className='tree-node';
btn.textContent=(entry.entry_type==='up'?'\u21a9':entry.entry_type==='file'?'\ud83d\udcc4':'\ud83d\udcc1')+' '+entry.name;
btn.addEventListener('click',()=>{
if (entry.entry_type==='file') {
document.getElementById(prefix+'_local_path').value=entry.path;
document.getElementById(prefix+'_subpath').value=entry.path;
if(prefix==='source') setSourceEntryType('file');
list.querySelectorAll('.tree-node').forEach(n=>n.classList.remove('active')); btn.classList.add('active');
meta.textContent='Pfad: '+entry.path;
} else {
document.getElementById(prefix+'_local_path').value=entry.path;
document.getElementById(prefix+'_subpath').value=entry.path;
if(prefix==='source') setSourceEntryType('directory');
loadLocalBrowserLevel(prefix,entry.path);
}
});
li.appendChild(btn); list.appendChild(li);
});
}
function openLocalBrowserOverlay(prefix) {
document.getElementById(prefix+'_local_browser_overlay').style.display='flex';
loadLocalBrowserLevel(prefix, document.getElementById(prefix+'_local_path').value||'/');
}
function closeLocalBrowserOverlay(prefix) { document.getElementById(prefix+'_local_browser_overlay').style.display='none'; }
function acceptLocalBrowserSelection(prefix) { closeLocalBrowserOverlay(prefix); }
function refreshLocalBrowserLevel(prefix) {
loadLocalBrowserLevel(prefix, document.getElementById(prefix+'_local_path').value||'/');
}
function openLocalCreateFolderOverlay() {
document.getElementById('destination_local_new_folder_btn').style.display='none';
const bar=document.getElementById('destination_local_new_folder_bar');
bar.style.display='flex';
const i=document.getElementById('destination_local_new_folder'); i.value=''; i.focus();
}
function closeLocalCreateFolderOverlay() {
document.getElementById('destination_local_new_folder_bar').style.display='none';
document.getElementById('destination_local_new_folder_btn').style.display='';
}
async function createLocalDestinationFolder() {
const inp=document.getElementById('destination_local_new_folder'); const name=inp.value.trim();
if (!name) return;
const currentPath=document.getElementById('destination_local_path').value||'/';
const r=await fetch('{{ url_for("api_local_mkdir") }}',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({path:currentPath,folder_name:name})});
const data=await r.json();
if (!data.ok) { const m=document.getElementById('destination_local_browser_meta'); m.textContent=data.message||'Fehler.'; m.style.color='#b9382f'; return; }
inp.value=''; document.getElementById('destination_local_path').value=data.path; document.getElementById('destination_subpath').value=data.path;
closeLocalCreateFolderOverlay(); loadLocalBrowserLevel('destination',data.path);
}
async function loadBrowserLevel(prefix, path) {
const list = document.getElementById(prefix+'_browser_tree');
const meta = document.getElementById(prefix+'_browser_meta');
list.innerHTML = '<li class="tree-empty">Lädt…</li>';
const payload = collectPayload(prefix); payload[prefix+'_subpath'] = path;
let data;
try { const r = await fetch('{{ url_for("api_target_browse") }}',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)}); data=await r.json(); }
catch(e) { list.innerHTML='<li class="tree-empty">Verbindungsfehler</li>'; return; }
const msg = document.getElementById(prefix+'_test_message');
if (!data.ok) { msg.textContent=data.message||'Fehler.'; msg.style.color='#b9382f'; list.innerHTML=''; return; }
msg.textContent=''; document.getElementById(prefix+'_subpath').value=data.path; meta.textContent='Pfad: '+data.path;
list.innerHTML='';
const allEntries2=data.entries||[];
const entries=filterEntries(allEntries2, prefix+'_local_show_hidden');
if (!entries.length) { const e=document.createElement('li'); e.className='tree-empty'; e.textContent='Keine Einträge'; list.appendChild(e); return; }
entries.forEach(entry=>{
const li=document.createElement('li'); const btn=document.createElement('button');
btn.type='button'; btn.className='tree-node';
btn.textContent=(entry.entry_type==='up'?'↩':entry.entry_type==='file'?'📄':'📁')+' '+entry.name;
btn.addEventListener('click',()=>{
if (entry.entry_type==='file') {
document.getElementById(prefix+'_subpath').value=entry.path;
if (prefix==='source') setSourceEntryType('file');
list.querySelectorAll('.tree-node').forEach(n=>n.classList.remove('active')); btn.classList.add('active');
document.getElementById(prefix+'_test_message').textContent='Datei ausgewählt: '+entry.path;
document.getElementById(prefix+'_test_message').style.color='#0a7f68';
} else { if(prefix==='source') setSourceEntryType('directory'); document.getElementById(prefix+'_subpath').value=entry.path; loadBrowserLevel(prefix,entry.path); }
});
li.appendChild(btn); list.appendChild(li);
});
}
function openBrowserOverlay(prefix) {
const p=document.getElementById(prefix+'_protocol').value;
if (!['SMB','FTP','SFTP'].includes(p)) return;
document.getElementById(prefix+'_browser_overlay').style.display='flex';
loadBrowserLevel(prefix,document.getElementById(prefix+'_subpath').value||'/');
}
function closeBrowserOverlay(prefix) { document.getElementById(prefix+'_browser_overlay').style.display='none'; }
function acceptBrowserSelection(prefix) { closeBrowserOverlay(prefix); }
async function browseSmb(prefix) {
const p=document.getElementById(prefix+'_protocol').value;
if (!['SMB','FTP','SFTP'].includes(p)) {
const msg=document.getElementById(prefix+'_test_message');
msg.textContent='Browser ist nur bei SMB, FTP und SFTP verfügbar.';
msg.style.color='#b9382f'; return;
}
openBrowserOverlay(prefix);
}
function refreshBrowserLevel(prefix) { loadBrowserLevel(prefix, document.getElementById(prefix+'_subpath').value||'/'); }
function filterEntries(entries, cbId) {
if (document.getElementById(cbId)?.checked) return entries;
return entries.filter(e => e.entry_type==='up' || !e.name.startsWith('.'));
}
function openCreateFolderOverlay() {
document.getElementById('destination_new_folder_btn').style.display='none';
const bar=document.getElementById('destination_new_folder_bar');
bar.style.display='flex';
const i=document.getElementById('destination_new_folder'); i.value=''; i.focus();
}
function closeCreateFolderOverlay() {
document.getElementById('destination_new_folder_bar').style.display='none';
document.getElementById('destination_new_folder_btn').style.display='';
}
async function createDestinationFolder() {
const inp=document.getElementById('destination_new_folder'); const name=inp.value.trim();
const msg=document.getElementById('destination_test_message');
if (!name) { msg.textContent='Bitte Ordnernamen eingeben.'; msg.style.color='#b9382f'; return; }
const payload=collectPayload('destination'); payload.folder_name=name;
const r=await fetch('{{ url_for("api_target_smb_mkdir") }}',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const data=await r.json();
if (!data.ok) { msg.textContent=data.message||'Fehler.'; msg.style.color='#b9382f'; return; }
inp.value=''; document.getElementById('destination_subpath').value=data.path;
msg.textContent='Ordner erstellt: '+data.path; msg.style.color='#0a7f68';
closeCreateFolderOverlay(); loadBrowserLevel('destination',data.path);
}
function syncCompressionFields() {
const c=document.getElementById('compression_method').value;
const encRow=document.getElementById('encryption_row');
const encSel=document.getElementById('encryption_mode');
const pwdCol=document.getElementById('archive_pwd_col');
const pwd2Col=document.getElementById('archive_pwd2_col');
const pwd=document.getElementById('archive_password'); const pwd2=document.getElementById('archive_password_confirm');
if (c==='none') { encRow.style.display='none'; encSel.value='none'; setRequired(pwd,false); setRequired(pwd2,false); pwd.value=''; pwd2.value=''; return; }
encRow.style.display='';
const showPwd=encSel.value==='password-aes256';
pwdCol.style.display=showPwd?'':'none'; pwd2Col.style.display=showPwd?'':'none';
setRequired(pwd,showPwd); setRequired(pwd2,showPwd);
if (!showPwd) { pwd.value=''; pwd2.value=''; }
}
function syncBackupMode() {
const mode=document.getElementById('backup_mode').value;
const compCol=document.getElementById('compression_col');
const compSel=document.getElementById('compression_method');
const rotCol=document.getElementById('rotation_col');
const targetBox=document.getElementById('target_pattern_box');
const targetInput=document.getElementById('target_pattern');
if (mode==='sync'||mode==='clone') {
compSel.value='none'; compSel.disabled=true;
compCol.style.opacity='0.45';
compCol.title='Sync/Clone arbeitet immer ohne Kompression';
rotCol.style.display='none';
targetBox.style.display='none';
targetInput.removeAttribute('required');
} else {
compSel.disabled=false; compCol.style.opacity=''; compCol.title='';
rotCol.style.display='';
targetBox.style.display='';
targetInput.setAttribute('required', '');
}
syncCompressionFields();
}
const TOKEN_GROUPS = [
{label:'Datum',defaultToken:'{date}',formats:[{token:'{date}',label:'YYYY-MM-DD',sub:'2026-07-30'},{token:'{date_de}',label:'TT.MM.JJJJ',sub:'30.07.2026'},{token:'{date_compact}',label:'JJJJMMTT',sub:'20260730'}]},
{label:'Uhrzeit',defaultToken:'{time}',formats:[{token:'{time}',label:'HH-MM-SS',sub:'14-30-00'},{token:'{time_short}',label:'HHMM',sub:'1430'}]},
{label:'Datum+Zeit',defaultToken:'{datetime}',formats:[{token:'{datetime}',label:'JJJJ-MM-TT_HH-MM-SS',sub:'2026-07-30_14-30-00'}]},
];
const activeTokens = TOKEN_GROUPS.map(g=>g.defaultToken);
function buildTokenBar() {
const bar=document.getElementById('token_bar'); bar.innerHTML='';
TOKEN_GROUPS.forEach((group,idx)=>{
const chip=document.createElement('span'); chip.className='token-chip'; chip.draggable=true;
chip.textContent=group.label; chip.title=activeTokens[idx]+' · Rechtsklick für Formate';
chip.addEventListener('click',()=>insertToken(activeTokens[idx]));
chip.addEventListener('dragstart',e=>{e.dataTransfer.setData('text/plain',activeTokens[idx]);e.dataTransfer.effectAllowed='copy';});
chip.addEventListener('contextmenu',e=>{e.preventDefault();showTokenMenu(e,group,idx);});
bar.appendChild(chip);
});
}
function insertToken(token) {
const inp=document.getElementById('target_pattern'); const s=inp.selectionStart??inp.value.length; const e=inp.selectionEnd??inp.value.length;
inp.value=inp.value.slice(0,s)+token+inp.value.slice(e); inp.setSelectionRange(s+token.length,s+token.length); inp.focus();
}
function showTokenMenu(ev,group,idx) {
const menu=document.getElementById('token_context_menu'); menu.innerHTML='';
group.formats.forEach(fmt=>{
const item=document.createElement('div'); item.className='token-menu-item';
item.innerHTML='<div>'+htmlEscape(fmt.label)+'</div><div class="token-menu-sub">'+htmlEscape(fmt.token)+' &nbsp;·&nbsp; '+htmlEscape(fmt.sub)+'</div>';
item.addEventListener('click',()=>{activeTokens[idx]=fmt.token;buildTokenBar();closeTokenMenu();insertToken(fmt.token);});
menu.appendChild(item);
});
menu.style.left=ev.clientX+'px'; menu.style.top=ev.clientY+'px'; menu.style.display='block';
requestAnimationFrame(()=>{ const r=menu.getBoundingClientRect(); if(r.right>window.innerWidth) menu.style.left=(ev.clientX-r.width)+'px'; if(r.bottom>window.innerHeight) menu.style.top=(ev.clientY-r.height)+'px'; });
}
function closeTokenMenu() { document.getElementById('token_context_menu').style.display='none'; }
document.addEventListener('click',e=>{ if(!e.target.closest('.token-chip')&&!e.target.closest('.token-menu')) closeTokenMenu(); });
const _tp=document.getElementById('target_pattern');
_tp.addEventListener('dragover',e=>e.preventDefault());
_tp.addEventListener('drop',e=>{ e.preventDefault(); const t=e.dataTransfer.getData('text/plain'); if(!t) return; const s=_tp.selectionStart??_tp.value.length; const en=_tp.selectionEnd??_tp.value.length; _tp.value=_tp.value.slice(0,s)+t+_tp.value.slice(en); _tp.setSelectionRange(s+t.length,s+t.length); _tp.focus(); });
document.getElementById('destination_new_folder').addEventListener('keydown',e=>{ if(e.key==='Enter'){e.preventDefault();createDestinationFolder();} if(e.key==='Escape') closeCreateFolderOverlay(); });
document.getElementById('name').addEventListener('keydown',e=>{ if(e.key==='Enter'){e.preventDefault();nextStep(0);} });
let _cronTimer=null;
async function updateCronPreview() {
const val=document.getElementById('schedule_cron').value.trim();
const preview=document.getElementById('cron_preview'); const errEl=document.getElementById('cron_error');
if (!val) { preview.style.display='none'; errEl.style.display='none'; return; }
try {
const r=await fetch('{{ url_for("api_cron_preview") }}',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cron:val})});
const data=await r.json();
if (!data.ok) { preview.style.display='none'; errEl.textContent=data.message||'Ungültiger Ausdruck.'; errEl.style.display='block'; return; }
document.getElementById('cron_description').textContent=data.description;
document.getElementById('cron_next_run').textContent='Nächstes Backup: '+data.next_run;
preview.style.display='block'; errEl.style.display='none';
} catch(_) { preview.style.display='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('destination_local_new_folder').addEventListener('keydown',(e)=>{ if(e.key==='Enter'){e.preventDefault();createLocalDestinationFolder();} if(e.key==='Escape') closeLocalCreateFolderOverlay(); });
document.getElementById('compression_method').addEventListener('change',syncCompressionFields);
document.getElementById('encryption_mode').addEventListener('change',syncCompressionFields);
document.getElementById('schedule_cron').addEventListener('input',()=>{ clearTimeout(_cronTimer); _cronTimer=setTimeout(updateCronPreview,500); });
// Preserve pre-filled passwords: syncProtocolFields may clear pass for LOCAL,
// but for remote protocols the pre-filled "***" value stays untouched.
syncProtocolFields('source');
syncProtocolFields('destination');
// syncBackupMode calls syncCompressionFields internally
syncBackupMode();
buildTokenBar();
if (document.getElementById('schedule_cron').value.trim()) updateCronPreview();
showStep(0);
// Merge port into host field before form submission (FTP/SFTP)
document.getElementById('backupForm').addEventListener('submit', () => {
for (const prefix of ['source', 'destination']) {
const protocol = document.getElementById(prefix + '_protocol').value;
if (protocol === 'FTP' || protocol === 'SFTP') {
const hostEl = document.getElementById(prefix + '_host');
const portUi = document.getElementById(prefix + '_port_ui');
if (portUi?.value && hostEl) {
hostEl.value = hostEl.value.split(':')[0] + ':' + portUi.value;
}
}
}
});
</script>
{% endblock %}