v0.0.1 first commit
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
{% extends "base_admin.html" %}
|
||||
|
||||
{% block title %}StFV Backup - Admin Konto{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Admin Konto</h2>
|
||||
<p>Hier kannst du deinen Benutzernamen und dein Passwort komplett aendern.</p>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ url_for('account_settings') }}">
|
||||
<label for="new_username">Neuer Benutzername</label>
|
||||
<input id="new_username" name="new_username" type="text" value="{{ adminuser }}" required>
|
||||
|
||||
<label for="current_password">Aktuelles Passwort</label>
|
||||
<input id="current_password" name="current_password" type="password" autocomplete="current-password" required>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="new_password">Neues Passwort</label>
|
||||
<input id="new_password" name="new_password" type="password" autocomplete="new-password" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirm_password">Neues Passwort bestaetigen</label>
|
||||
<input id="confirm_password" name="confirm_password" type="password" autocomplete="new-password" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn" type="submit">Admin Konto aktualisieren</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,109 @@
|
||||
{% extends "base_admin.html" %}
|
||||
|
||||
{% 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>
|
||||
|
||||
<form method="post" action="{{ url_for('backup_edit', backup_id=backup.id) }}">
|
||||
<div class="card">
|
||||
<label for="name">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>
|
||||
</div>
|
||||
|
||||
<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' }}">
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
document.getElementById('encryption_mode').addEventListener('change', syncEncryptionFields);
|
||||
document.getElementById('schedule_mode').addEventListener('change', syncScheduleFields);
|
||||
syncEncryptionFields();
|
||||
syncScheduleFields();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,714 @@
|
||||
{% extends "base_admin.html" %}
|
||||
|
||||
{% block title %}StFV Backup - Neues Backup{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Neues Backup einrichten</h2>
|
||||
<p>Die Konfiguration erscheint Schritt fuer Schritt, damit du jedes Fenster nacheinander ausfuellen kannst.</p>
|
||||
|
||||
<style>
|
||||
.wizard-step {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wizard-step.active {
|
||||
display: block;
|
||||
animation: fade-slide 240ms ease;
|
||||
}
|
||||
|
||||
.wizard-nav {
|
||||
margin-top: 10px;
|
||||
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;
|
||||
}
|
||||
|
||||
.wizard-pill.active {
|
||||
border-color: rgba(10, 127, 104, 0.35);
|
||||
background: #e5fff7;
|
||||
color: #0a7f68;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.step-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
border: 1px solid rgba(30, 42, 59, 0.2);
|
||||
background: #fff;
|
||||
color: #1e2a3b;
|
||||
}
|
||||
|
||||
.tree {
|
||||
list-style: none;
|
||||
margin: 8px 0 0;
|
||||
padding: 0 0 0 8px;
|
||||
border-left: 1px solid rgba(30, 42, 59, 0.15);
|
||||
}
|
||||
|
||||
.tree li {
|
||||
margin: 4px 0;
|
||||
padding-left: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tree li::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 12px;
|
||||
width: 8px;
|
||||
border-top: 1px solid rgba(30, 42, 59, 0.15);
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #1e2a3b;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
padding: 3px 4px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.tree-node:hover {
|
||||
background: #eef6ff;
|
||||
}
|
||||
|
||||
.tree-node.active {
|
||||
background: #e5fff7;
|
||||
color: #0a7f68;
|
||||
}
|
||||
|
||||
.tree-meta {
|
||||
font-size: 13px;
|
||||
color: #5a6e87;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.tree-empty {
|
||||
color: #5a6e87;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.mini-input {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mini-input input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.inline-help {
|
||||
color: #5a6e87;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@keyframes fade-slide {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
</style>
|
||||
|
||||
<form method="post" action="{{ url_for('backup_new') }}" id="backupForm">
|
||||
<input type="hidden" id="source_entry_type" name="source_entry_type" value="directory">
|
||||
<input type="hidden" id="target_kind" name="target_kind" value="folder">
|
||||
|
||||
<div class="wizard-nav" id="wizardNav">
|
||||
<span class="wizard-pill active" data-step-pill="0">Allgemein</span>
|
||||
<span class="wizard-pill" data-step-pill="1">Source</span>
|
||||
<span class="wizard-pill" data-step-pill="2">Destination</span>
|
||||
<span class="wizard-pill" data-step-pill="3">Target</span>
|
||||
<span class="wizard-pill" data-step-pill="4">Zeitplan</span>
|
||||
</div>
|
||||
|
||||
<section class="card wizard-step active" data-step="0">
|
||||
<h3>Allgemein</h3>
|
||||
<label for="name">Backup Name</label>
|
||||
<input id="name" name="name" type="text" placeholder="z. B. unraid-nextcloud-daily" required>
|
||||
<div class="step-actions">
|
||||
<button type="button" class="btn" onclick="nextStep(0)">Weiter</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card wizard-step" data-step="1">
|
||||
<h3>Source</h3>
|
||||
<label for="source_protocol">Typ</label>
|
||||
<select id="source_protocol" name="source_protocol" required>
|
||||
{% for protocol in target_protocols %}
|
||||
<option value="{{ protocol }}">{{ protocol }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<div id="source_remote_fields">
|
||||
<label for="source_host">Host/IP (optional mit :Port)</label>
|
||||
<input id="source_host" name="source_host" type="text" placeholder="192.168.1.20 oder 192.168.1.20:445">
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="source_username">Benutzer</label>
|
||||
<input id="source_username" name="source_username" type="text">
|
||||
</div>
|
||||
<div>
|
||||
<label for="source_password">Passwort</label>
|
||||
<input id="source_password" name="source_password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="source_share_row">
|
||||
<label for="source_share">Share / Root</label>
|
||||
<input id="source_share" name="source_share" type="text" placeholder="source-share">
|
||||
</div>
|
||||
|
||||
<label for="source_subpath">Unterpfad (SMB Browser)</label>
|
||||
<input id="source_subpath" name="source_subpath" type="text" placeholder="/daten/projektA" value="/">
|
||||
<p id="source_test_message"></p>
|
||||
<div id="source_browser" class="card" style="display:none; margin-top:10px;">
|
||||
<div id="source_browser_meta" class="tree-meta"></div>
|
||||
<ul id="source_browser_tree" class="tree"></ul>
|
||||
</div>
|
||||
<p class="inline-help">Ordnerauswahl setzt Source als Ordner, Dateiauswahl setzt Source als Datei.</p>
|
||||
</div>
|
||||
|
||||
<div id="source_local_fields" style="display:none;">
|
||||
<label for="source_local_path">Lokaler Pfad</label>
|
||||
<input id="source_local_path" type="text" placeholder="/mnt/storage/quellordner">
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="prevStep(1)">Zurueck</button>
|
||||
<button type="button" class="btn" onclick="nextStep(1)">Weiter</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card wizard-step" data-step="2">
|
||||
<h3>Destination</h3>
|
||||
<label for="destination_protocol">Typ</label>
|
||||
<select id="destination_protocol" name="destination_protocol" required>
|
||||
{% for protocol in target_protocols %}
|
||||
<option value="{{ protocol }}">{{ protocol }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<div id="destination_remote_fields">
|
||||
<label for="destination_host">Host/IP (optional mit :Port)</label>
|
||||
<input id="destination_host" name="destination_host" type="text" placeholder="192.168.1.30 oder 192.168.1.30:445">
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="destination_username">Benutzer</label>
|
||||
<input id="destination_username" name="destination_username" type="text">
|
||||
</div>
|
||||
<div>
|
||||
<label for="destination_password">Passwort</label>
|
||||
<input id="destination_password" name="destination_password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="destination_share_row">
|
||||
<label for="destination_share">Share / Root</label>
|
||||
<input id="destination_share" name="destination_share" type="text" placeholder="backup-share">
|
||||
</div>
|
||||
|
||||
<label for="destination_subpath">Unterpfad (SMB Browser)</label>
|
||||
<input id="destination_subpath" name="destination_subpath" type="text" placeholder="/backups" value="/">
|
||||
<div class="mini-input">
|
||||
<input id="destination_new_folder" type="text" placeholder="Neuer Ordnername">
|
||||
<button class="btn" type="button" onclick="createDestinationFolder()">Ordner erstellen</button>
|
||||
</div>
|
||||
<p id="destination_test_message"></p>
|
||||
<div id="destination_browser" class="card" style="display:none; margin-top:10px;">
|
||||
<div id="destination_browser_meta" class="tree-meta"></div>
|
||||
<ul id="destination_browser_tree" class="tree"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="destination_local_fields" style="display:none;">
|
||||
<label for="destination_local_path">Lokaler Pfad</label>
|
||||
<input id="destination_local_path" type="text" placeholder="/mnt/storage/zielordner">
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="prevStep(2)">Zurueck</button>
|
||||
<button type="button" class="btn" onclick="nextStep(2)">Weiter</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card wizard-step" data-step="3">
|
||||
<h3>Target</h3>
|
||||
<label for="target_pattern" id="target_pattern_label">Zielordner</label>
|
||||
<input id="target_pattern" name="target_pattern" type="text" placeholder="backup_{date}_{time}" required>
|
||||
<p class="inline-help">Wenn in Source eine Datei gewaehlt wurde, ist dies die Zieldatei. Bei Ordnerauswahl ist dies der Zielordner.</p>
|
||||
|
||||
<label for="compression_method">Kompression</label>
|
||||
<select id="compression_method" name="compression_method" required>
|
||||
{% for method in compression_methods %}
|
||||
<option value="{{ method }}">{{ method }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<label for="encryption_mode">Verschluesselung</label>
|
||||
<select id="encryption_mode" name="encryption_mode" required>
|
||||
{% for mode in encryption_modes %}
|
||||
<option value="{{ mode }}">{{ mode }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<div id="encryption_password_box">
|
||||
<div class="row">
|
||||
<div>
|
||||
<label for="archive_password">Passwort</label>
|
||||
<input id="archive_password" name="archive_password" type="password">
|
||||
</div>
|
||||
<div>
|
||||
<label for="archive_password_confirm">Passwort bestaetigen</label>
|
||||
<input id="archive_password_confirm" name="archive_password_confirm" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<p class="inline-help">Passwoerter werden verschluesselt gespeichert.</p>
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="prevStep(3)">Zurueck</button>
|
||||
<button type="button" class="btn" onclick="nextStep(3)">Weiter</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card wizard-step" data-step="4">
|
||||
<h3>Zeitplan</h3>
|
||||
<label for="schedule_mode">Ausfuehrung</label>
|
||||
<select id="schedule_mode" name="schedule_mode" required>
|
||||
<option value="daily">Taeglich</option>
|
||||
<option value="weekly">Woechentlich</option>
|
||||
<option value="monthly">Monatlich</option>
|
||||
<option value="yearly">Jaehrlich</option>
|
||||
<option value="every_n_days">Alle X Tage</option>
|
||||
<option value="every_n_weeks">Alle X Wochen</option>
|
||||
<option value="custom">Benutzerdefiniert (CRON)</option>
|
||||
</select>
|
||||
|
||||
<div id="schedule_time_box">
|
||||
<label for="schedule_time">Uhrzeit</label>
|
||||
<input id="schedule_time" name="schedule_time" type="time" value="02:00">
|
||||
</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">Montag</option>
|
||||
<option value="2">Dienstag</option>
|
||||
<option value="3">Mittwoch</option>
|
||||
<option value="4">Donnerstag</option>
|
||||
<option value="5">Freitag</option>
|
||||
<option value="6">Samstag</option>
|
||||
<option value="0">Sonntag</option>
|
||||
</select>
|
||||
</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="1">
|
||||
</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="1">
|
||||
</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" placeholder="0 2 * * *">
|
||||
</div>
|
||||
|
||||
<div class="step-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="prevStep(4)">Zurueck</button>
|
||||
<button class="btn" type="submit">Backup speichern</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
let currentStep = 0;
|
||||
|
||||
function htmlEscape(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function showStep(stepIdx) {
|
||||
const steps = document.querySelectorAll('.wizard-step');
|
||||
const pills = document.querySelectorAll('[data-step-pill]');
|
||||
steps.forEach((el, idx) => el.classList.toggle('active', idx === stepIdx));
|
||||
pills.forEach((el, idx) => el.classList.toggle('active', idx === stepIdx));
|
||||
currentStep = stepIdx;
|
||||
}
|
||||
|
||||
function requireValue(id, message) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el || !el.value.trim()) {
|
||||
alert(message);
|
||||
el?.focus();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validateStep(stepIdx) {
|
||||
if (stepIdx === 0) {
|
||||
return requireValue('name', 'Bitte Backup Name setzen.');
|
||||
}
|
||||
|
||||
if (stepIdx === 1) {
|
||||
const protocol = document.getElementById('source_protocol').value;
|
||||
if (protocol === 'LOCAL') {
|
||||
return requireValue('source_local_path', 'Bitte lokalen Source-Pfad angeben.');
|
||||
}
|
||||
if (!requireValue('source_host', 'Bitte Source Host/IP setzen.')) return false;
|
||||
if (!requireValue('source_username', 'Bitte Source Benutzer setzen.')) return false;
|
||||
if (!requireValue('source_password', 'Bitte Source Passwort setzen.')) return false;
|
||||
if (protocol === 'SMB' && !requireValue('source_share', 'Bitte Source Share setzen.')) return false;
|
||||
return requireValue('source_subpath', 'Bitte Source Unterpfad setzen.');
|
||||
}
|
||||
|
||||
if (stepIdx === 2) {
|
||||
const protocol = document.getElementById('destination_protocol').value;
|
||||
if (protocol === 'LOCAL') {
|
||||
return requireValue('destination_local_path', 'Bitte lokalen Destination-Pfad angeben.');
|
||||
}
|
||||
if (!requireValue('destination_host', 'Bitte Destination Host/IP setzen.')) return false;
|
||||
if (!requireValue('destination_username', 'Bitte Destination Benutzer setzen.')) return false;
|
||||
if (!requireValue('destination_password', 'Bitte Destination Passwort setzen.')) return false;
|
||||
if (protocol === 'SMB' && !requireValue('destination_share', 'Bitte Destination Share setzen.')) return false;
|
||||
return requireValue('destination_subpath', 'Bitte Destination Unterpfad setzen.');
|
||||
}
|
||||
|
||||
if (stepIdx === 3) {
|
||||
if (!requireValue('target_pattern', 'Bitte Zielordner oder Zieldatei setzen.')) return false;
|
||||
const mode = document.getElementById('encryption_mode').value;
|
||||
if (mode === 'password-aes256') {
|
||||
if (!requireValue('archive_password', 'Bitte Verschluesselungs-Passwort setzen.')) return false;
|
||||
if (!requireValue('archive_password_confirm', 'Bitte Passwort bestaetigen.')) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function nextStep(stepIdx) {
|
||||
if (!validateStep(stepIdx)) return;
|
||||
showStep(Math.min(stepIdx + 1, 4));
|
||||
}
|
||||
|
||||
function prevStep(stepIdx) {
|
||||
showStep(Math.max(stepIdx - 1, 0));
|
||||
}
|
||||
|
||||
function setRequired(el, enabled) {
|
||||
if (!el) return;
|
||||
if (enabled) {
|
||||
el.setAttribute('required', 'required');
|
||||
} else {
|
||||
el.removeAttribute('required');
|
||||
}
|
||||
}
|
||||
|
||||
function syncProtocolFields(prefix) {
|
||||
const protocol = document.getElementById(prefix + '_protocol').value;
|
||||
const remoteFields = document.getElementById(prefix + '_remote_fields');
|
||||
const localFields = document.getElementById(prefix + '_local_fields');
|
||||
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';
|
||||
localFields.style.display = 'block';
|
||||
shareRow.style.display = 'none';
|
||||
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';
|
||||
localFields.style.display = 'none';
|
||||
shareRow.style.display = protocol === 'SMB' ? 'block' : 'none';
|
||||
setRequired(host, true);
|
||||
setRequired(share, protocol === 'SMB');
|
||||
setRequired(user, true);
|
||||
setRequired(pass, true);
|
||||
setRequired(localPath, false);
|
||||
|
||||
if (protocol === 'FTP' || protocol === 'SFTP') {
|
||||
if (!share.value) share.value = '/';
|
||||
if (!subpath.value) subpath.value = '/';
|
||||
}
|
||||
}
|
||||
|
||||
function collectPayload(prefix) {
|
||||
const protocol = document.getElementById(prefix + '_protocol').value;
|
||||
const payload = {
|
||||
prefix: prefix,
|
||||
[prefix + '_protocol']: protocol,
|
||||
[prefix + '_host']: document.getElementById(prefix + '_host').value,
|
||||
[prefix + '_share']: document.getElementById(prefix + '_share').value,
|
||||
[prefix + '_subpath']: document.getElementById(prefix + '_subpath').value,
|
||||
[prefix + '_username']: document.getElementById(prefix + '_username').value,
|
||||
[prefix + '_password']: document.getElementById(prefix + '_password').value
|
||||
};
|
||||
|
||||
if (protocol === 'LOCAL') {
|
||||
payload[prefix + '_host'] = 'local';
|
||||
payload[prefix + '_share'] = '';
|
||||
payload[prefix + '_username'] = '';
|
||||
payload[prefix + '_password'] = '';
|
||||
payload[prefix + '_subpath'] = document.getElementById(prefix + '_local_path').value;
|
||||
document.getElementById(prefix + '_subpath').value = payload[prefix + '_subpath'];
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
function clearActiveTreeNodes(prefix) {
|
||||
document
|
||||
.querySelectorAll('#' + prefix + '_browser_tree .tree-node')
|
||||
.forEach((node) => node.classList.remove('active'));
|
||||
}
|
||||
|
||||
function setSourceEntryType(entryType) {
|
||||
const t = entryType === 'file' ? 'file' : 'directory';
|
||||
document.getElementById('source_entry_type').value = t;
|
||||
const targetKind = t === 'file' ? 'file' : 'folder';
|
||||
document.getElementById('target_kind').value = targetKind;
|
||||
const label = document.getElementById('target_pattern_label');
|
||||
label.textContent = targetKind === 'file' ? 'Zieldatei' : 'Zielordner';
|
||||
}
|
||||
|
||||
function makeTreeItem(prefix, entry) {
|
||||
const li = document.createElement('li');
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'tree-node';
|
||||
|
||||
let icon = '📁';
|
||||
if (entry.entry_type === 'up') icon = '↩';
|
||||
if (entry.entry_type === 'file') icon = '📄';
|
||||
button.textContent = icon + ' ' + entry.name;
|
||||
button.dataset.path = entry.path;
|
||||
button.dataset.entryType = entry.entry_type;
|
||||
|
||||
const childList = document.createElement('ul');
|
||||
childList.className = 'tree';
|
||||
childList.style.display = 'none';
|
||||
|
||||
button.addEventListener('click', async () => {
|
||||
clearActiveTreeNodes(prefix);
|
||||
button.classList.add('active');
|
||||
document.getElementById(prefix + '_subpath').value = entry.path;
|
||||
|
||||
if (prefix === 'source') {
|
||||
setSourceEntryType(entry.entry_type === 'file' ? 'file' : 'directory');
|
||||
}
|
||||
|
||||
if (entry.entry_type === 'file') {
|
||||
document.getElementById(prefix + '_test_message').textContent = 'Datei ausgewaehlt: ' + entry.path;
|
||||
document.getElementById(prefix + '_test_message').style.color = '#0a7f68';
|
||||
return;
|
||||
}
|
||||
|
||||
await loadTreeLevel(prefix, entry.path, childList);
|
||||
});
|
||||
|
||||
li.appendChild(button);
|
||||
li.appendChild(childList);
|
||||
return li;
|
||||
}
|
||||
|
||||
async function loadTreeLevel(prefix, path, targetList) {
|
||||
const payload = collectPayload(prefix);
|
||||
payload[prefix + '_subpath'] = path;
|
||||
|
||||
const response = await fetch('{{ url_for("api_target_browse_smb") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
const msg = document.getElementById(prefix + '_test_message');
|
||||
if (!data.ok) {
|
||||
msg.textContent = data.message || 'SMB-Browsing fehlgeschlagen.';
|
||||
msg.style.color = '#b9382f';
|
||||
return;
|
||||
}
|
||||
|
||||
msg.textContent = 'SMB-Verbindung erfolgreich.';
|
||||
msg.style.color = '#0a7f68';
|
||||
document.getElementById(prefix + '_subpath').value = data.path;
|
||||
document.getElementById(prefix + '_browser_meta').textContent = 'Aktueller Pfad: ' + data.path;
|
||||
|
||||
targetList.innerHTML = '';
|
||||
const entries = data.entries || [];
|
||||
if (!entries.length) {
|
||||
const empty = document.createElement('li');
|
||||
empty.className = 'tree-empty';
|
||||
empty.textContent = 'Keine Eintraege';
|
||||
targetList.appendChild(empty);
|
||||
targetList.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
entries.forEach((entry) => {
|
||||
targetList.appendChild(makeTreeItem(prefix, entry));
|
||||
});
|
||||
targetList.style.display = 'block';
|
||||
}
|
||||
|
||||
async function renderRootTree(prefix) {
|
||||
const rootList = document.getElementById(prefix + '_browser_tree');
|
||||
rootList.innerHTML = '';
|
||||
await loadTreeLevel(prefix, document.getElementById(prefix + '_subpath').value || '/', rootList);
|
||||
}
|
||||
|
||||
async function browseSmb(prefix) {
|
||||
if (document.getElementById(prefix + '_protocol').value !== 'SMB') {
|
||||
const msg = document.getElementById(prefix + '_test_message');
|
||||
msg.textContent = 'SMB Browser nur bei SMB aktiv.';
|
||||
msg.style.color = '#b9382f';
|
||||
return;
|
||||
}
|
||||
|
||||
const container = document.getElementById(prefix + '_browser');
|
||||
container.style.display = 'block';
|
||||
await renderRootTree(prefix);
|
||||
}
|
||||
|
||||
async function createDestinationFolder() {
|
||||
const folderInput = document.getElementById('destination_new_folder');
|
||||
const name = folderInput.value.trim();
|
||||
const msg = document.getElementById('destination_test_message');
|
||||
|
||||
if (!name) {
|
||||
msg.textContent = 'Bitte zuerst einen neuen Ordnernamen eingeben.';
|
||||
msg.style.color = '#b9382f';
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = collectPayload('destination');
|
||||
payload.folder_name = name;
|
||||
|
||||
const response = await fetch('{{ url_for("api_target_smb_mkdir") }}', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.ok) {
|
||||
msg.textContent = data.message || 'Ordner konnte nicht erstellt werden.';
|
||||
msg.style.color = '#b9382f';
|
||||
return;
|
||||
}
|
||||
|
||||
folderInput.value = '';
|
||||
document.getElementById('destination_subpath').value = data.path;
|
||||
msg.textContent = 'Ordner erstellt: ' + data.path;
|
||||
msg.style.color = '#0a7f68';
|
||||
await browseSmb('destination');
|
||||
}
|
||||
|
||||
function syncEncryptionFields() {
|
||||
const mode = document.getElementById('encryption_mode').value;
|
||||
const box = document.getElementById('encryption_password_box');
|
||||
const pwd = document.getElementById('archive_password');
|
||||
const pwd2 = document.getElementById('archive_password_confirm');
|
||||
const visible = mode === 'password-aes256';
|
||||
box.style.display = visible ? 'block' : 'none';
|
||||
setRequired(pwd, visible);
|
||||
setRequired(pwd2, visible);
|
||||
if (!visible) {
|
||||
pwd.value = '';
|
||||
pwd2.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';
|
||||
}
|
||||
|
||||
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('source_subpath').addEventListener('focus', () => {
|
||||
if (document.getElementById('source_protocol').value === 'SMB') {
|
||||
browseSmb('source');
|
||||
}
|
||||
});
|
||||
document.getElementById('destination_subpath').addEventListener('focus', () => {
|
||||
if (document.getElementById('destination_protocol').value === 'SMB') {
|
||||
browseSmb('destination');
|
||||
}
|
||||
});
|
||||
document.getElementById('source_subpath').addEventListener('click', () => {
|
||||
if (document.getElementById('source_protocol').value === 'SMB') {
|
||||
browseSmb('source');
|
||||
}
|
||||
});
|
||||
document.getElementById('destination_subpath').addEventListener('click', () => {
|
||||
if (document.getElementById('destination_protocol').value === 'SMB') {
|
||||
browseSmb('destination');
|
||||
}
|
||||
});
|
||||
document.getElementById('encryption_mode').addEventListener('change', syncEncryptionFields);
|
||||
document.getElementById('schedule_mode').addEventListener('change', syncScheduleFields);
|
||||
|
||||
syncProtocolFields('source');
|
||||
syncProtocolFields('destination');
|
||||
syncEncryptionFields();
|
||||
syncScheduleFields();
|
||||
setSourceEntryType('directory');
|
||||
showStep(0);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,256 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}StFV Backup{% endblock %}</title>
|
||||
<style>
|
||||
:root {
|
||||
--paper: #f8fbff;
|
||||
--ink: #1e2a3b;
|
||||
--muted: #5a6e87;
|
||||
--panel: #ffffff;
|
||||
--line: rgba(30, 42, 59, 0.12);
|
||||
--accent: #0a7f68;
|
||||
--accent-soft: #e5fff7;
|
||||
--danger: #b9382f;
|
||||
--danger-soft: #ffe9e7;
|
||||
--shadow: 0 24px 52px rgba(30, 42, 59, 0.13);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
color: var(--ink);
|
||||
font-family: "IBM Plex Sans", "Trebuchet MS", "Noto Sans", sans-serif;
|
||||
background:
|
||||
radial-gradient(1000px 550px at -5% -20%, rgba(102, 161, 255, 0.2), transparent 60%),
|
||||
radial-gradient(700px 450px at 105% 110%, rgba(10, 127, 104, 0.17), transparent 65%),
|
||||
linear-gradient(165deg, #f2f8ff, #fff9f1);
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.layout {
|
||||
max-width: 1050px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.top {
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
margin: 0;
|
||||
font-size: clamp(22px, 4vw, 32px);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sub {
|
||||
margin: 8px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
text-decoration: none;
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: 11px;
|
||||
padding: 8px 12px;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav-link.active {
|
||||
background: var(--accent-soft);
|
||||
border-color: rgba(10, 127, 104, 0.35);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.logout {
|
||||
border: 0;
|
||||
border-radius: 11px;
|
||||
padding: 9px 13px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #3e4f6f, #2a3851);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.messages { display: grid; gap: 10px; }
|
||||
|
||||
.msg {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.msg.success {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
border: 1px solid rgba(10, 127, 104, 0.3);
|
||||
}
|
||||
|
||||
.msg.error {
|
||||
background: var(--danger-soft);
|
||||
color: var(--danger);
|
||||
border: 1px solid rgba(185, 56, 47, 0.3);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 8px;
|
||||
font-size: clamp(20px, 3vw, 28px);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
h3 { margin: 0 0 6px; }
|
||||
|
||||
p { margin: 0; color: var(--muted); }
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
color: var(--ink);
|
||||
background: #fff;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--accent), #02a883);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.users-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.users-table th,
|
||||
.users-table td {
|
||||
border-bottom: 1px solid var(--line);
|
||||
padding: 10px 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
border-radius: 999px;
|
||||
padding: 4px 9px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
background: var(--accent-soft);
|
||||
border: 1px solid rgba(10, 127, 104, 0.35);
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.row { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="layout">
|
||||
<section class="panel top">
|
||||
<div>
|
||||
<h1 class="brand">StFV Backup</h1>
|
||||
<p class="sub">Angemeldet als {{ adminuser }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav>
|
||||
<a class="nav-link {% if active_menu == 'dashboard' %}active{% endif %}" href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||
<a class="nav-link {% if active_menu == 'backups' %}active{% endif %}" href="{{ url_for('backup_new') }}">Neues Backup</a>
|
||||
<a class="nav-link {% if active_menu == 'server' %}active{% endif %}" href="{{ url_for('server_settings') }}">Server Settings</a>
|
||||
<a class="nav-link {% if active_menu == 'account' %}active{% endif %}" href="{{ url_for('account_settings') }}">Admin Konto</a>
|
||||
<a class="nav-link {% if active_menu == 'users' %}active{% endif %}" href="{{ url_for('users') }}">Benutzer</a>
|
||||
</nav>
|
||||
</div>
|
||||
<form method="post" action="{{ url_for('logout') }}">
|
||||
<button class="logout" type="submit">Logout</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
<section class="panel content messages">
|
||||
{% for category, message in messages %}
|
||||
<p class="msg {{ category }}">{{ message }}</p>
|
||||
{% endfor %}
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<section class="panel content">
|
||||
{% block content %}{% endblock %}
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,82 @@
|
||||
{% extends "base_admin.html" %}
|
||||
|
||||
{% block title %}StFV Backup - Dashboard{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<style>
|
||||
.actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid rgba(30, 42, 59, 0.18);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #1e2a3b;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
margin-right: 5px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.icon-btn:hover {
|
||||
background: #f3f8ff;
|
||||
}
|
||||
|
||||
.icon-btn-danger {
|
||||
color: #b9382f;
|
||||
border-color: rgba(185, 56, 47, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
||||
<h2>Eingerichtete Backups</h2>
|
||||
<p>Hier siehst du alle Backup-Jobs mit Zeitplan, letztem Lauf und Größe.</p>
|
||||
|
||||
<div class="card">
|
||||
<a class="btn" href="{{ url_for('backup_new') }}">Neues Backup einrichten</a>
|
||||
</div>
|
||||
|
||||
{% if backups %}
|
||||
<table class="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Zeitplan</th>
|
||||
<th>Letzter Lauf</th>
|
||||
<th>Größe</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for backup in backups %}
|
||||
<tr>
|
||||
<td><strong>{{ backup.name }}</strong></td>
|
||||
<td>{{ backup.schedule_mode }}</td>
|
||||
<td>{{ backup.last_run_at | fmt_dt }}</td>
|
||||
<td>{{ backup.last_size_bytes | fmt_bytes }}</td>
|
||||
<td class="actions">
|
||||
<form method="post" action="{{ url_for('backup_run', backup_id=backup.backup_id) }}" style="display:inline;">
|
||||
<button class="icon-btn" type="submit" title="Manuell ausführen" aria-label="Manuell ausführen">▶</button>
|
||||
</form>
|
||||
<a class="icon-btn" href="{{ url_for('backup_edit', backup_id=backup.backup_id) }}" title="Bearbeiten" aria-label="Bearbeiten">✎</a>
|
||||
<form method="post" action="{{ url_for('backup_delete', backup_id=backup.backup_id) }}" style="display:inline;" onsubmit="return confirm('Backup wirklich löschen?');">
|
||||
<button class="icon-btn icon-btn-danger" type="submit" title="Löschen" aria-label="Löschen">🗑</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div class="card">
|
||||
<p>Noch keine Backups eingerichtet. Lege jetzt den ersten Job an.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,157 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>StFV Backup - Login</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-1: #f7f9fc;
|
||||
--bg-2: #d9e8ff;
|
||||
--bg-3: #ffe7c9;
|
||||
--text: #10223a;
|
||||
--muted: #3f5875;
|
||||
--surface: rgba(255, 255, 255, 0.85);
|
||||
--accent: #007d66;
|
||||
--accent-strong: #005f4e;
|
||||
--danger: #c2352a;
|
||||
--border: rgba(16, 34, 58, 0.12);
|
||||
--shadow: 0 20px 55px rgba(16, 34, 58, 0.2);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: "Fira Sans", "Avenir Next", "Noto Sans", sans-serif;
|
||||
color: var(--text);
|
||||
background:
|
||||
radial-gradient(1200px 700px at 10% -20%, var(--bg-2), transparent 65%),
|
||||
radial-gradient(900px 600px at 90% 110%, var(--bg-3), transparent 60%),
|
||||
linear-gradient(150deg, #eff5ff, var(--bg-1));
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
animation: fade-in 450ms ease-out;
|
||||
}
|
||||
|
||||
.shell {
|
||||
width: min(460px, 100%);
|
||||
background: var(--surface);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 22px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
animation: slide-up 500ms ease-out;
|
||||
}
|
||||
|
||||
.head {
|
||||
padding: 26px 26px 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(26px, 6vw, 34px);
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 18px 26px 28px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 16px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
border: 1px solid var(--border);
|
||||
background: #ffffff;
|
||||
border-radius: 14px;
|
||||
padding: 12px 14px;
|
||||
font-size: 15px;
|
||||
color: var(--text);
|
||||
transition: border-color 180ms ease, box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #5ca8ff;
|
||||
box-shadow: 0 0 0 4px rgba(92, 168, 255, 0.17);
|
||||
}
|
||||
|
||||
.error {
|
||||
margin: 0 0 14px;
|
||||
color: var(--danger);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 14px;
|
||||
padding: 13px 14px;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, var(--accent), #00a37f);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: transform 120ms ease, filter 180ms ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(1.05);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
background: linear-gradient(135deg, var(--accent-strong), #00795f);
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from { transform: translateY(20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<header class="head">
|
||||
<h1>StFV Backup</h1>
|
||||
</header>
|
||||
<section class="content">
|
||||
{% if error_message %}
|
||||
<p class="error">{{ error_message }}</p>
|
||||
{% endif %}
|
||||
<form method="post" action="{{ url_for('login') }}">
|
||||
<div class="field">
|
||||
<label for="username">Benutzername</label>
|
||||
<input id="username" name="username" type="text" autocomplete="username" required>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">Passwort</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required>
|
||||
</div>
|
||||
<button type="submit">Anmelden</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,26 @@
|
||||
{% extends "base_admin.html" %}
|
||||
|
||||
{% block title %}StFV Backup - Server Settings{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Server Settings</h2>
|
||||
<p>Diese Einstellungen gelten fuer den Flask-Webserver.</p>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="{{ url_for('server_settings') }}">
|
||||
<label for="ip">Server Host / IP</label>
|
||||
<input id="ip" name="ip" type="text" value="{{ config.ip }}" required>
|
||||
|
||||
<label for="port">Server Port</label>
|
||||
<input id="port" name="port" type="number" min="1" max="65535" value="{{ config.port }}" required>
|
||||
|
||||
<label for="debug">Debug Modus</label>
|
||||
<select id="debug" name="debug">
|
||||
<option value="false" {% if not config.debug %}selected{% endif %}>false</option>
|
||||
<option value="true" {% if config.debug %}selected{% endif %}>true</option>
|
||||
</select>
|
||||
|
||||
<button class="btn" type="submit">Server Settings speichern</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,56 @@
|
||||
{% extends "base_admin.html" %}
|
||||
|
||||
{% block title %}StFV Backup - Benutzer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Benutzerverwaltung</h2>
|
||||
<p>Lege neue Benutzer fuer den Browser-Login an und steuere Adminrechte.</p>
|
||||
|
||||
<div class="grid">
|
||||
<article class="card">
|
||||
<h3>Neuen Benutzer anlegen</h3>
|
||||
<form method="post" action="{{ url_for('users') }}">
|
||||
<label for="username">Benutzername</label>
|
||||
<input id="username" name="username" type="text" required>
|
||||
|
||||
<label for="password">Passwort</label>
|
||||
<input id="password" name="password" type="password" autocomplete="new-password" required>
|
||||
|
||||
<label for="password_confirm">Passwort bestaetigen</label>
|
||||
<input id="password_confirm" name="password_confirm" type="password" autocomplete="new-password" required>
|
||||
|
||||
<label>
|
||||
<input name="is_admin" type="checkbox" style="width:auto; margin-right:8px;"> Als Admin anlegen
|
||||
</label>
|
||||
|
||||
<button class="btn" type="submit">Benutzer erstellen</button>
|
||||
</form>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3>Bestehende Benutzer</h3>
|
||||
<table class="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Benutzername</th>
|
||||
<th>Rolle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>
|
||||
{% if user.is_admin %}
|
||||
<span class="badge">Admin</span>
|
||||
{% else %}
|
||||
Benutzer
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</article>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user