Files
BackItUp/app/templates/backup_edit.html
T

685 lines
44 KiB
HTML
Raw Normal View History

2026-07-29 17:36:22 +02:00
{% extends "base_admin.html" %}
{% block title %}StFV Backup - Backup bearbeiten{% endblock %}
{% block content %}
<h2 style="margin-bottom:4px;">Backup bearbeiten</h2>
2026-07-29 17:36:22 +02:00
<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>
2026-07-29 17:36:22 +02:00
<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>
2026-07-29 17:36:22 +02:00
</div>
</section>
2026-07-29 17:36:22 +02:00
<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>
2026-07-29 17:36:22 +02:00
</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>
2026-07-29 17:36:22 +02:00
</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>
2026-07-29 17:36:22 +02:00
</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>
2026-07-29 17:36:22 +02:00
</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>
2026-07-29 17:36:22 +02:00
</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>
2026-07-29 17:36:22 +02:00
<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>
2026-07-29 17:36:22 +02:00
</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='/'; }
2026-07-29 17:36:22 +02:00
}
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;
2026-07-29 17:36:22 +02:00
}
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;
}
}
}
});
2026-07-29 17:36:22 +02:00
</script>
{% endblock %}