Files

1299 lines
47 KiB
HTML
Raw Permalink Normal View History

2026-07-29 17:36:22 +02:00
{% extends "base_admin.html" %}
2026-08-11 23:33:40 +02:00
{% block title %}BackItUp - Neues Backup{% endblock %}
2026-07-29 17:36:22 +02:00
{% block content %}
<h2 style="margin-bottom:4px;">Neues Backup einrichten</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;
2026-07-29 17:36:22 +02:00
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;
2026-07-29 17:36:22 +02:00
}
.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;
}
.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;
}
.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 {
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-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;
transition: background 120ms;
}
.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;
}
2026-07-29 17:36:22 +02:00
@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">
<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>
2026-07-29 17:36:22 +02:00
</div>
<section class="card wizard-step active" data-step="0">
<div class="step-header"><h3>Allgemein</h3></div>
2026-07-29 17:36:22 +02:00
<label for="name">Backup Name</label>
<input id="name" name="name" type="text" placeholder="" required>
2026-07-29 17:36:22 +02:00
<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 protocol in target_protocols %}
<option value="{{ protocol }}">{{ protocol }}</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" placeholder="" style="flex:1; margin-bottom:0;">
<input id="source_port_ui" type="number" min="1" max="65535" placeholder="Port"
title="Port" 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" placeholder="">
</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" placeholder="">
<button type="button" class="btn-browse" onclick="openLocalBrowserOverlay('source')" title="Browser &ouml;ffnen">🔍</button>
</div>
</div>
</div>
2026-07-29 17:36:22 +02:00
<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>
2026-07-29 17:36:22 +02:00
<div id="source_remote_fields" style="display:none;">
2026-07-29 17:36:22 +02:00
<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>
<label for="source_subpath">Pfad</label>
<div class="path-input-group">
<input id="source_subpath" name="source_subpath" type="text" placeholder="" value="/">
<button type="button" class="btn-browse" onclick="browseSmb('source')" title="Browser &ouml;ffnen">🔍</button>
2026-07-29 17:36:22 +02:00
</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">
<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>
2026-07-29 17:36:22 +02:00
</div>
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Zurück</button>
2026-07-29 17:36:22 +02:00
<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 protocol in target_protocols %}
<option value="{{ protocol }}">{{ protocol }}</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" placeholder="" style="flex:1; margin-bottom:0;">
<input id="destination_port_ui" type="number" min="1" max="65535" placeholder="Port"
title="Port" 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" placeholder="">
</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" placeholder="">
<button type="button" class="btn-browse" onclick="openLocalBrowserOverlay('destination')" title="Browser &ouml;ffnen">🔍</button>
</div>
</div>
</div>
2026-07-29 17:36:22 +02:00
<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()">✕</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>
2026-07-29 17:36:22 +02:00
<div id="destination_remote_fields" style="display:none;">
2026-07-29 17:36:22 +02:00
<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>
<label for="destination_subpath">Pfad</label>
<div class="path-input-group">
<input id="destination_subpath" name="destination_subpath" type="text" placeholder="" value="/">
<button type="button" class="btn-browse" onclick="browseSmb('destination')" title="Browser &ouml;ffnen">🔍</button>
2026-07-29 17:36:22 +02:00
</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()">✕</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>
2026-07-29 17:36:22 +02:00
</div>
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Zurück</button>
2026-07-29 17:36:22 +02:00
<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>
2026-07-29 17:36:22 +02:00
<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" placeholder="" required>
<div id="token_context_menu" class="token-menu" style="display:none;"></div>
</div>
2026-07-29 17:36:22 +02:00
<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 == '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="0" style="width:100%;">
</div>
<div id="compression_col">
<label for="compression_method">Kompression</label>
<select id="compression_method" name="compression_method" required>
{% for method in compression_methods %}
<option value="{{ method }}">{% if method == 'none' %}Keine (1:1){% else %}{{ method }}{% endif %}</option>
{% endfor %}
</select>
</div>
</div>
2026-07-29 17:36:22 +02:00
<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 mode in encryption_modes %}
<option value="{{ mode }}">{{ mode }}</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">
</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">
2026-07-29 17:36:22 +02:00
</div>
</div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Zurück</button>
2026-07-29 17:36:22 +02:00
<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" placeholder="">
<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>
2026-07-29 17:36:22 +02:00
</div>
<div id="cron_error" style="display:none;margin-top:6px;font-size:13px;color:#b9382f;"></div>
2026-07-29 17:36:22 +02:00
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(4)">Zurück</button>
2026-07-29 17:36:22 +02:00
<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.');
2026-07-29 17:36:22 +02:00
}
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.');
2026-07-29 17:36:22 +02:00
}
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 Verschlüsselungs-Passwort setzen.')) return false;
if (!requireValue('archive_password_confirm', 'Bitte Passwort bestätigen.')) return false;
2026-07-29 17:36:22 +02:00
}
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));
}
// token groups for Target drag-and-drop variables
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 input = document.getElementById('target_pattern');
const s = input.selectionStart ?? input.value.length;
const e = input.selectionEnd ?? input.value.length;
input.value = input.value.slice(0, s) + token + input.value.slice(e);
input.setSelectionRange(s + token.length, s + token.length);
input.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';
}
const _targetInput = document.getElementById('target_pattern');
_targetInput.addEventListener('dragover', (e) => e.preventDefault());
_targetInput.addEventListener('drop', (e) => {
e.preventDefault();
const token = e.dataTransfer.getData('text/plain');
if (!token) return;
const s = _targetInput.selectionStart ?? _targetInput.value.length;
const end = _targetInput.selectionEnd ?? _targetInput.value.length;
_targetInput.value = _targetInput.value.slice(0, s) + token + _targetInput.value.slice(end);
_targetInput.setSelectionRange(s + token.length, s + token.length);
_targetInput.focus();
});
document.addEventListener('click', (e) => {
if (!e.target.closest('.token-chip') && !e.target.closest('.token-menu')) closeTokenMenu();
});
function openCreateFolderOverlay() {
document.getElementById('destination_new_folder_btn').style.display = 'none';
const bar = document.getElementById('destination_new_folder_bar');
bar.style.display = 'flex';
const inp = document.getElementById('destination_new_folder');
inp.value = ''; inp.focus();
}
function closeCreateFolderOverlay() {
document.getElementById('destination_new_folder_bar').style.display = 'none';
document.getElementById('destination_new_folder_btn').style.display = '';
}
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); }
});
2026-07-29 17:36:22 +02:00
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 localPathCol = document.getElementById(prefix + '_local_path_col');
const hostCol = document.getElementById(prefix + '_host_col');
2026-07-29 17:36:22 +02:00
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';
2026-07-29 17:36:22 +02:00
shareRow.style.display = 'none';
const portUiL = document.getElementById(prefix + '_port_ui');
if (portUiL) { portUiL.style.display = 'none'; portUiL.value = ''; }
2026-07-29 17:36:22 +02:00
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';
2026-07-29 17:36:22 +02:00
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 = '';
}
}
2026-07-29 17:36:22 +02:00
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'];
}
if (protocol === 'FTP' || protocol === 'SFTP') {
const portUi = document.getElementById(prefix + '_port_ui');
const portVal = portUi?.value || (protocol === 'FTP' ? '21' : '22');
const baseHost = String(payload[prefix + '_host'] || '').split(':')[0];
payload[prefix + '_host'] = baseHost + ':' + portVal;
}
2026-07-29 17:36:22 +02:00
return payload;
2026-07-29 17:36:22 +02:00
}
function setSourceEntryType(entryType) {
const t = entryType === 'file' ? 'file' : 'directory';
document.getElementById('source_entry_type').value = t;
document.getElementById('target_kind').value = t === 'file' ? 'file' : 'folder';
2026-07-29 17:36:22 +02:00
}
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 resp = await fetch('{{ url_for("api_local_browse") }}?path=' + encodeURIComponent(path));
data = await resp.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);
2026-07-29 17:36:22 +02:00
});
}
function openLocalBrowserOverlay(prefix) {
document.getElementById(prefix + '_local_browser_overlay').style.display = 'flex';
const currentPath = document.getElementById(prefix + '_local_path').value || '/';
loadLocalBrowserLevel(prefix, currentPath);
}
function closeLocalBrowserOverlay(prefix) {
document.getElementById(prefix + '_local_browser_overlay').style.display = 'none';
}
function acceptLocalBrowserSelection(prefix) {
closeLocalBrowserOverlay(prefix);
}
function refreshLocalBrowserLevel(prefix) {
const path = document.getElementById(prefix + '_local_path').value || '/';
loadLocalBrowserLevel(prefix, path);
}
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 inp = document.getElementById('destination_local_new_folder');
inp.value = ''; inp.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 resp = 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 resp.json();
if (!data.ok) {
const meta = document.getElementById('destination_local_browser_meta');
meta.textContent = data.message || 'Fehler.';
meta.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>';
2026-07-29 17:36:22 +02:00
const payload = collectPayload(prefix);
payload[prefix + '_subpath'] = path;
let data;
try {
const resp = await fetch('{{ url_for("api_target_browse") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
data = await resp.json();
} catch (err) {
list.innerHTML = '<li class="tree-empty">Verbindungsfehler</li>';
return;
}
2026-07-29 17:36:22 +02:00
const msg = document.getElementById(prefix + '_test_message');
if (!data.ok) {
msg.textContent = data.message || 'SMB-Browsing fehlgeschlagen.';
msg.style.color = '#b9382f';
list.innerHTML = '';
2026-07-29 17:36:22 +02:00
return;
}
msg.textContent = '';
2026-07-29 17:36:22 +02:00
document.getElementById(prefix + '_subpath').value = data.path;
meta.textContent = 'Pfad: ' + data.path;
2026-07-29 17:36:22 +02:00
list.innerHTML = '';
const allEntries = data.entries || [];
const entries = filterEntries(allEntries, prefix + '_smb_show_hidden');
2026-07-29 17:36:22 +02:00
if (!entries.length) {
const empty = document.createElement('li');
empty.className = 'tree-empty';
empty.textContent = 'Keine Einträge';
list.appendChild(empty);
2026-07-29 17:36:22 +02:00
return;
}
entries.forEach((entry) => {
const li = document.createElement('li');
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'tree-node';
const icon = entry.entry_type === 'up' ? '↩' : (entry.entry_type === 'file' ? '📄' : '📁');
btn.textContent = icon + ' ' + 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);
2026-07-29 17:36:22 +02:00
});
}
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);
}
function refreshBrowserLevel(prefix) {
const path = document.getElementById(prefix + '_subpath').value || '/';
loadBrowserLevel(prefix, path);
}
function filterEntries(entries, cbId) {
if (document.getElementById(cbId)?.checked) return entries;
return entries.filter(e => e.entry_type === 'up' || !e.name.startsWith('.'));
2026-07-29 17:36:22 +02:00
}
async function browseSmb(prefix) {
const p = document.getElementById(prefix + '_protocol').value;
if (!['SMB', 'FTP', 'SFTP'].includes(p)) {
2026-07-29 17:36:22 +02:00
const msg = document.getElementById(prefix + '_test_message');
msg.textContent = 'Browser ist nur bei SMB, FTP und SFTP verfügbar.';
2026-07-29 17:36:22 +02:00
msg.style.color = '#b9382f';
return;
}
openBrowserOverlay(prefix);
2026-07-29 17:36:22 +02:00
}
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';
closeCreateFolderOverlay();
loadBrowserLevel('destination', data.path);
2026-07-29 17:36:22 +02:00
}
function syncCompressionFields() {
const compression = 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');
2026-07-29 17:36:22 +02:00
const pwd = document.getElementById('archive_password');
const pwd2 = document.getElementById('archive_password_confirm');
if (compression === 'none') {
encRow.style.display = 'none';
encSel.value = 'none';
setRequired(pwd, false); setRequired(pwd2, false);
pwd.value = ''; pwd2.value = ''; return;
2026-07-29 17:36:22 +02:00
}
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 = ''; }
2026-07-29 17:36:22 +02:00
}
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();
}
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 resp = await fetch('{{ url_for("api_cron_preview") }}', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cron: val })
});
const data = await resp.json();
if (!data.ok) {
preview.style.display = 'none';
errEl.textContent = data.message || 'Ungültiger CRON-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';
}
2026-07-29 17:36:22 +02:00
}
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();
2026-07-29 17:36:22 +02:00
});
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);
2026-07-29 17:36:22 +02:00
});
syncProtocolFields('source');
syncProtocolFields('destination');
syncBackupMode();
2026-07-29 17:36:22 +02:00
setSourceEntryType('directory');
buildTokenBar();
2026-07-29 17:36:22 +02:00
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 %}