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