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