1299 lines
47 KiB
HTML
1299 lines
47 KiB
HTML
{% 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 ö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 ö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 ö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 ö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('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|
||
|
||
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) + ' · ' + 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 %}
|