Files
BackItUp/app/templates/backup_new.html
T

1299 lines
47 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 %}StFV Backup - Neues Backup{% endblock %}
{% block content %}
<h2 style="margin-bottom:4px;">Neues Backup einrichten</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;
}
.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;
}
@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>
</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" placeholder="" 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 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>
<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">
</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>
</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>
</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 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>
<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>
<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">
</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>
</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>
</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" placeholder="" 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 == '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>
<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">
</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" 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>
</div>
<div id="cron_error" style="display:none;margin-top:6px;font-size:13px;color:#b9382f;"></div>
<div class="step-actions">
<button type="button" class="btn btn-secondary" onclick="prevStep(4)">Zurück</button>
<button class="btn" type="submit">Backup speichern</button>
</div>
</section>
</form>
<script>
let currentStep = 0;
function htmlEscape(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
function showStep(stepIdx) {
const steps = document.querySelectorAll('.wizard-step');
const pills = document.querySelectorAll('[data-step-pill]');
steps.forEach((el, idx) => el.classList.toggle('active', idx === stepIdx));
pills.forEach((el, idx) => el.classList.toggle('active', idx === stepIdx));
currentStep = stepIdx;
}
function requireValue(id, message) {
const el = document.getElementById(id);
if (!el || !el.value.trim()) {
alert(message);
el?.focus();
return false;
}
return true;
}
function validateStep(stepIdx) {
if (stepIdx === 0) {
return requireValue('name', 'Bitte Backup Name setzen.');
}
if (stepIdx === 1) {
const protocol = document.getElementById('source_protocol').value;
if (protocol === 'LOCAL') {
return requireValue('source_local_path', 'Bitte lokalen Source-Pfad angeben.');
}
if (!requireValue('source_host', 'Bitte Source-Host/IP setzen.')) return false;
if (!requireValue('source_username', 'Bitte Source-Benutzer setzen.')) return false;
if (!requireValue('source_password', 'Bitte Source-Passwort setzen.')) return false;
if (protocol === 'SMB' && !requireValue('source_share', 'Bitte Source-Share setzen.')) return false;
return requireValue('source_subpath', 'Bitte Source-Unterpfad setzen.');
}
if (stepIdx === 2) {
const protocol = document.getElementById('destination_protocol').value;
if (protocol === 'LOCAL') {
return requireValue('destination_local_path', 'Bitte lokalen Destination-Pfad angeben.');
}
if (!requireValue('destination_host', 'Bitte Destination-Host/IP setzen.')) return false;
if (!requireValue('destination_username', 'Bitte Destination-Benutzer setzen.')) return false;
if (!requireValue('destination_password', 'Bitte Destination-Passwort setzen.')) return false;
if (protocol === 'SMB' && !requireValue('destination_share', 'Bitte Destination-Share setzen.')) return false;
return requireValue('destination_subpath', 'Bitte Destination-Unterpfad setzen.');
}
if (stepIdx === 3) {
if (!requireValue('target_pattern', 'Bitte Zielordner oder Zieldatei setzen.')) return false;
const mode = document.getElementById('encryption_mode').value;
if (mode === 'password-aes256') {
if (!requireValue('archive_password', 'Bitte Verschlüsselungs-Passwort setzen.')) return false;
if (!requireValue('archive_password_confirm', 'Bitte Passwort bestätigen.')) return false;
}
return true;
}
return true;
}
function nextStep(stepIdx) {
if (!validateStep(stepIdx)) return;
showStep(Math.min(stepIdx + 1, 4));
}
function prevStep(stepIdx) {
showStep(Math.max(stepIdx - 1, 0));
}
// 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); }
});
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');
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,
[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;
}
return payload;
}
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';
}
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);
});
}
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>';
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;
}
const msg = document.getElementById(prefix + '_test_message');
if (!data.ok) {
msg.textContent = data.message || 'SMB-Browsing fehlgeschlagen.';
msg.style.color = '#b9382f';
list.innerHTML = '';
return;
}
msg.textContent = '';
document.getElementById(prefix + '_subpath').value = data.path;
meta.textContent = 'Pfad: ' + data.path;
list.innerHTML = '';
const allEntries = data.entries || [];
const entries = filterEntries(allEntries, prefix + '_smb_show_hidden');
if (!entries.length) {
const empty = document.createElement('li');
empty.className = 'tree-empty';
empty.textContent = 'Keine Einträge';
list.appendChild(empty);
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);
});
}
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('.'));
}
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);
}
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);
}
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');
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;
}
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();
}
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';
}
}
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);
});
syncProtocolFields('source');
syncProtocolFields('destination');
syncBackupMode();
setSourceEntryType('directory');
buildTokenBar();
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 %}