This commit is contained in:
root 2025-02-22 22:33:36 +01:00
commit 7807157052
23 changed files with 2510 additions and 0 deletions

1308
app.py Normal file

File diff suppressed because it is too large Load Diff

7
gunicorn_config.py Normal file
View File

@ -0,0 +1,7 @@
bind = "0.0.0.0:81"
workers = 4
timeout = 120
server_header = False
def on_starting(server):
server.cfg.server_header = False
server.log.info("Server header disabled")

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
Flask
flask_sqlalchemy
passlib
paramiko
APScheduler
requests
gunicorn
flask_wtf
gevent
#croniter

43
templates/add_router.html Normal file
View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card shadow-sm">
<div class="card-header">
<h2 class="mb-0">Dodaj nowe urządzenie</h2>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="name" class="form-label"><b>Nazwa</b></label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="host" class="form-label"><b>Host/IP</b></label>
<input type="text" class="form-control" id="host" name="host" required>
</div>
<div class="mb-3">
<label for="port" class="form-label"><b>Port SSH</b></label>
<input type="number" class="form-control" id="port" name="port" value="22" required>
</div>
<div class="mb-3">
<label for="ssh_user" class="form-label"><b>Użytkownik SSH</b></label>
<input type="text" class="form-control" id="ssh_user" name="ssh_user" required>
</div>
<div class="mb-3">
<label for="ssh_key" class="form-label">
<b>Klucz prywatny</b> | Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code><br>
Pozostaw puste jeśli ten RouterOS będzie używał <a href="{{ url_for('settings_view') }}">klucza globalnego</a>
</label>
<textarea class="form-control" id="ssh_key" name="ssh_key" rows="4"></textarea>
</div>
<div class="mb-3">
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
Jeśli podajesz klucz SSH lub zdefiniowany jest <a href="{{ url_for('settings_view') }}">klucz globalny</a>, to logowanie hasłem jest nieaktywne.
<input type="password" class="form-control" id="ssh_password" name="ssh_password">
</div>
<button type="submit" class="btn btn-primary">Dodaj</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block content %}
<h2>Dodaj nowy router</h2>
<form method="POST">
<label>Nazwa: <input type="text" name="name" required></label><br>
<label>Adres IP/Host: <input type="text" name="host" required></label><br>
<label>Port SSH: <input type="number" name="port" value="22"></label><br>
<label>Użytkownik SSH: <input type="text" name="ssh_user" value="admin"></label><br>
<label>Klucz prywatny (string/ścieżka): <textarea name="ssh_key"></textarea></label><br>
<label>Hasło SSH: <input type="password" name="ssh_password"></label><br><br>
<button type="submit">Zapisz</button>
</form>
{% endblock %}

View File

@ -0,0 +1,114 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card shadow-sm">
<div class="card-header">
<h2 class="mb-0">Zaawansowane ustawienia harmonogramu</h2>
</div>
<div class="card-body">
<form action="{{ url_for('advanced_schedule') }}" method="POST">
<div class="mb-3">
<label for="retention_cron" class="form-label">Harmonogram retencji (cron)</label>
<div class="input-group">
<input type="text" class="form-control" id="retention_cron" name="retention_cron" value="{{ settings.retention_cron }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('retention_cron')">Generuj cron</button>
</div>
<div class="form-text">Np. <code>0 */12 * * *</code> co 12 godzin</div>
</div>
<div class="mb-3">
<label for="binary_cron" class="form-label">Harmonogram kopii zapasowych binarnych (cron)</label>
<div class="input-group">
<input type="text" class="form-control" id="binary_cron" name="binary_cron" value="{{ settings.binary_cron|default('') }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('binary_cron')">Generuj cron</button>
</div>
<div class="form-text">Np. <code>15 2 * * *</code> codziennie o 2:15</div>
</div>
<div class="mb-3">
<label for="export_cron" class="form-label">Harmonogram eksportów (cron)</label>
<div class="input-group">
<input type="text" class="form-control" id="export_cron" name="export_cron" value="{{ settings.export_cron }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('export_cron')">Generuj cron</button>
</div>
<div class="form-text">Np. <code>0 */12 * * *</code> co 12 godzin</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enable_auto_export" name="enable_auto_export" {% if settings.enable_auto_export %}checked{% endif %}>
<label class="form-check-label" for="enable_auto_export">Włącz automatyczny eksport</label>
</div>
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
</form>
</div>
</div>
</div>
<!-- Modal do generowania wyrażenia CRON -->
<div class="modal fade" id="cronModal" tabindex="-1" aria-labelledby="cronModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="cronModalLabel">Generuj wyrażenie CRON</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="cron_minute" class="form-label">Minuta</label>
<input type="text" class="form-control" id="cron_minute" placeholder="0-59">
</div>
<div class="mb-3">
<label for="cron_hour" class="form-label">Godzina</label>
<input type="text" class="form-control" id="cron_hour" placeholder="0-23">
</div>
<div class="mb-3">
<label for="cron_day" class="form-label">Dzień miesiąca</label>
<input type="text" class="form-control" id="cron_day" placeholder="1-31 lub *">
</div>
<div class="mb-3">
<label for="cron_month" class="form-label">Miesiąc</label>
<input type="text" class="form-control" id="cron_month" placeholder="1-12 lub *">
</div>
<div class="mb-3">
<label for="cron_dow" class="form-label">Dzień tygodnia</label>
<input type="text" class="form-control" id="cron_dow" placeholder="0-6 (0 = niedziela) lub *">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button>
<button type="button" class="btn btn-primary" onclick="generateCronExpression()">Generuj</button>
</div>
</div>
</div>
</div>
<script>
// Zmienna przechowująca ID pola, do którego ma być wpisane wyrażenie cron
var targetCronField = '';
function openCronModal(fieldId) {
targetCronField = fieldId;
// Wyzeruj wartości w modalu
document.getElementById('cron_minute').value = '*';
document.getElementById('cron_hour').value = '*';
document.getElementById('cron_day').value = '*';
document.getElementById('cron_month').value = '*';
document.getElementById('cron_dow').value = '*';
// Otwórz modal (przy użyciu Bootstrap 5)
var cronModal = new bootstrap.Modal(document.getElementById('cronModal'));
cronModal.show();
}
function generateCronExpression() {
var minute = document.getElementById('cron_minute').value || '*';
var hour = document.getElementById('cron_hour').value || '*';
var day = document.getElementById('cron_day').value || '*';
var month = document.getElementById('cron_month').value || '*';
var dow = document.getElementById('cron_dow').value || '*';
var cronExpr = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + dow;
document.getElementById(targetCronField).value = cronExpr;
// Zamknij modal
var modalEl = document.getElementById('cronModal');
var modalInstance = bootstrap.Modal.getInstance(modalEl);
modalInstance.hide();
}
</script>
{% endblock %}

138
templates/all_files.html Normal file
View File

@ -0,0 +1,138 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-4">
<h2 class="text-center mb-4">Lista wszystkich backupów</h2>
<!-- Formularz filtrowania -->
<div class="card mb-4 shadow-sm">
<div class="card-body">
<form method="GET" action="{{ url_for('all_files') }}" class="row g-2">
<div class="col-md-4">
<input type="text" name="search" placeholder="Wyszukaj backupy" class="form-control" value="{{ search }}">
</div>
<div class="col-md-3">
<select name="sort_by" class="form-select">
<option value="created_at" {% if sort_by=='created_at' %}selected{% endif %}>Data</option>
<option value="file_path" {% if sort_by=='file_path' %}selected{% endif %}>Nazwa pliku</option>
</select>
</div>
<div class="col-md-3">
<select name="order" class="form-select">
<option value="desc" {% if order=='desc' %}selected{% endif %}>Malejąco</option>
<option value="asc" {% if order=='asc' %}selected{% endif %}>Rosnąco</option>
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">Filtruj</button>
</div>
</form>
</div>
</div>
<!-- Tabela z backupami -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover mb-0">
<thead class="table-dark">
<tr>
<th style="width: 2%;"><input type="checkbox" id="select_all"></th>
<th>Router</th>
<th>Typ</th>
<th>Nazwa pliku</th>
<th>Data</th>
<th>Rozmiar</th>
<th>Pobierz</th>
<th>Wyślij mailem</th>
<th>Wgraj</th>
<th>Podgląd</th>
<th>Usuń</th>
</tr>
</thead>
<tbody>
{% for file in files %}
<tr>
<td><input type="checkbox" name="backup_id" value="{{ file.id }}" form="mass_actions_form"></td>
<td>{{ file.router.name }}</td>
<td>
{% if file.backup_type == 'export' %}
<span class="badge bg-success">Export</span>
{% elif file.backup_type == 'binary' %}
<span class="badge bg-info">Binary</span>
{% else %}
<span class="badge bg-secondary">{{ file.backup_type }}</span>
{% endif %}
</td>
<td>{{ file.file_path|basename }}</td>
<td>{{ file.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td>{{ file.file_path|filesize }}</td>
<td>
<a href="{{ url_for('download_file', filename=file.file_path|basename) }}" class="btn btn-lg btn-info">
<i class="bi bi-download"></i>
</a>
</td>
<td>
<form action="{{ url_for('send_by_email', backup_id=file.id) }}" method="POST" class="d-inline">
<input type="hidden" name="next" value="{{ url_for('all_files') }}">
<button type="submit" class="btn btn-lg btn-warning">
<i class="bi bi-envelope"></i>
</button>
</form>
</td>
<td>
{% if file.backup_type == 'binary' %}
<form action="{{ url_for('upload_backup', router_id=file.router.id, backup_id=file.id) }}" method="POST" class="d-inline">
<input type="hidden" name="next" value="{{ url_for('all_files') }}">
<button type="submit" class="btn btn-lg btn-secondary">
<i class="bi bi-upload"></i>
</button>
</form>
{% else %}
<em>N/D</em>
{% endif %}
</td>
<td>
{% if file.backup_type == 'export' %}
<a href="{{ url_for('view_export', backup_id=file.id) }}" class="btn btn-lg btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
{% else %}
<em>N/D</em>
{% endif %}
</td>
<td>
<form action="{{ url_for('delete_backup', backup_id=file.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
<input type="hidden" name="next" value="{{ url_for('all_files') }}">
<button type="submit" class="btn btn-lg btn-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-3"><strong>Łączny rozmiar:</strong> {{ total_size|filesize }}</p>
</div>
</div>
<!-- Formularz dla masowych akcji (jeden formularz) -->
<form id="mass_actions_form" action="{{ url_for('mass_actions') }}" method="POST" class="d-flex justify-content-end mb-4">
<button type="submit" name="action" value="download" class="btn btn-lg btn-success me-2">
<i class="bi bi-file-earmark-zip"></i> Pobierz zip zaznaczonych
</button>
<button type="submit" name="action" value="delete" class="btn btn-lg btn-danger" onclick="return confirm('Na pewno usunąć zaznaczone backupy?');">
<i class="bi bi-trash"></i> Usuń zaznaczone backupy
</button>
</form>
</div>
<script>
document.getElementById('select_all').addEventListener('change', function(e) {
var checkboxes = document.querySelectorAll('input[name="backup_id"]');
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = e.target.checked;
}
});
</script>
{% endblock %}

116
templates/base.html Normal file
View File

@ -0,0 +1,116 @@
<!DOCTYPE html>
<html lang="pl" class="{% if session.dark_mode %}dark-mode{% endif %}">
<head>
<meta charset="UTF-8" />
<title>Backup RouterOS App</title>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<style>
.dark-mode body {
background-color: #222;
color: #ffffff;
}
.dark-mode a, .dark-mode a:hover {
color: #ddd;
}
.dark-mode .navbar, .dark-mode .table {
background-color: #333 !important;
color: #fff;
}
.diff-add { color: green; }
.diff-rem { color: red; }
</style>
</head>
<body>
<nav class="navbar navbar-expand navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a href="{{ url_for('index') }}" class="navbar-brand">Backup RouterOS</a>
<div>
{% if session.user_id %}
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary me-2">Dashboard</a>
<a href="{{ url_for('routers_list') }}" class="btn btn-secondary me-2">Urządzenia</a>
<a href="{{ url_for('diff_selector') }}" class="btn btn-secondary me-2">Diff selector</a>
<a href="{{ url_for('all_files') }}" class="btn btn-secondary me-2">Wszystkie pliki</a>
<a href="{{ url_for('settings_view') }}" class="btn btn-secondary me-2">Ustawienia</a>
<a href="{{ url_for('advanced_schedule') }}" class="btn btn-secondary me-2">Harmonogram</a>
<a href="{{ url_for('change_password') }}" class="btn btn-secondary me-2">Zmiana hasła</a>
<a href="{{ url_for('logout') }}" class="btn btn-secondary me-2">Wyloguj</a>
{% else %}
<a href="{{ url_for('login') }}" class="btn btn-secondary me-2">Zaloguj</a>
<a href="{{ url_for('register') }}" class="btn btn-secondary me-2">Utwórz konto</a>
{% endif %}
<!--<a href="{{ url_for('toggle_dark_mode') }}" class="btn btn-warning">Toggle Dark Mode</a>-->
</div>
</div>
</nav>
<div class="container">
{% with messages = get_flashed_messages() %}
{% if messages %}
<div class="alert alert-info">
{% for msg in messages %}
<div>{{ msg }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<!-- Modal Test Połączenia -->
<div class="modal fade" id="testConnectionModal" tabindex="-1" aria-labelledby="testConnectionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="testConnectionModalLabel">Test Połączenia</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body" id="testConnectionModalBody">
<!-- Zawartość zostanie załadowana przez AJAX -->
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function ajaxExport(router_id) {
fetch("/router/" + router_id + "/export", {
method: "POST",
headers: {"X-Requested-With": "XMLHttpRequest"}
})
.then(response => response.json())
.then(data => {
if(data.status === "success"){
alert("Eksport wykonany: " + data.message);
// Możesz też zaktualizować część strony dynamicznie
} else {
alert("Błąd eksportu: " + data.message);
}
})
.catch(error => {
console.error("Błąd AJAX:", error);
alert("Wystąpił błąd.");
});
}
</script>
<script>
function openTestConnectionModal(routerId) {
fetch('/router/' + routerId + '/test_connection?modal=1')
.then(response => response.text())
.then(html => {
document.getElementById('testConnectionModalBody').innerHTML = html;
var myModal = new bootstrap.Modal(document.getElementById('testConnectionModal'));
myModal.show();
})
.catch(error => {
console.error("Błąd ładowania modalu: ", error);
alert("Wystąpił błąd podczas ładowania danych.");
});
}
</script>
</body>
</html>

View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header text-center">
<h2>Zmień hasło</h2>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="current_password" class="form-label">Obecne hasło</label>
<input type="password" class="form-control" id="current_password" name="current_password" placeholder="Wpisz obecne hasło" required>
</div>
<div class="mb-3">
<label for="new_password" class="form-label">Nowe hasło</label>
<input type="password" class="form-control" id="new_password" name="new_password" placeholder="Wpisz nowe hasło" required>
</div>
<div class="mb-3">
<label for="confirm_password" class="form-label">Potwierdź nowe hasło</label>
<input type="password" class="form-control" id="confirm_password" name="confirm_password" placeholder="Powtórz nowe hasło" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Zmień hasło</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<a href="{{ url_for('dashboard') }}">Powrót do panelu</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

133
templates/dashboard.html Normal file
View File

@ -0,0 +1,133 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-4">
<h2 class="text-center mb-4">Panel administracyjny</h2>
<!-- Wiersz akcji ogólnych -->
<div class="row mb-4">
<div class="col-md-12 text-center">
<a href="{{ url_for('routers_list') }}" class="btn btn-lg btn-outline-primary">
<i class="bi bi-hdd-network"></i> Zobacz routery
</a>
</div>
</div>
<!-- Karty głównych statystyk -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card text-white bg-primary shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Routery</h5>
<p class="display-4">{{ routers_count }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-success shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Exporty</h5>
<p class="display-4">{{ export_count }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-info shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Backupy binarne</h5>
<p class="display-4">{{ binary_count }}</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card text-white bg-dark shadow-sm">
<div class="card-body text-center">
<h5 class="card-title">Łącznie</h5>
<p class="display-4">{{ total_backups }}</p>
</div>
</div>
</div>
</div>
<!-- Dodatkowe statystyki -->
<div class="card mb-4 shadow-sm">
<div class="card-body">
<h5 class="card-title">Dodatkowe statystyki</h5>
<div class="row">
<div class="col-md-6">
<p><strong>Czas działania:</strong> {{ uptime }}</p>
<p><strong>Aktualny czas:</strong> {{ current_time.strftime('%Y-%m-%d %H:%M:%S') }}</p>
</div>
<div class="col-md-6">
<p><strong>Całkowity rozmiar dysku:</strong> {{ disk_total|filesize }}</p>
<p><strong>Zajęte (/data):</strong> {{ disk_used|filesize }} ({{ disk_usage_percent|round(2) }}%)</p>
<p><strong>Wolne:</strong> {{ disk_free|filesize }}</p>
</div>
</div>
</div>
</div>
<!-- Przyciski akcji dla wszystkich routerów -->
<div class="row mb-4">
<div class="col-md-6 d-flex justify-content-center">
<form action="{{ url_for('export_all_routers') }}" method="POST">
<button type="submit" class="btn btn-lg btn-outline-success">
<i class="bi bi-arrow-down-circle"></i> Eksport dla wszystkich routerów
</button>
</form>
</div>
<div class="col-md-6 d-flex justify-content-center">
<form action="{{ url_for('backup_all_routers') }}" method="POST">
<button type="submit" class="btn btn-lg btn-outline-secondary">
<i class="bi bi-cloud-download"></i> Backup binarny dla wszystkich routerów
</button>
</form>
</div>
</div>
<!-- Statystyki operacji -->
{% set total_ops = success_ops + failure_ops %}
{% if total_ops > 0 %}
{% set success_percent = (success_ops * 100) // total_ops %}
{% else %}
{% set success_percent = 0 %}
{% endif %}
<div class="card mb-4 shadow-sm">
<div class="card-body">
<h5 class="card-title">Statystyki operacji</h5>
<p>Udane operacje: {{ success_ops }}, Nieudane operacje: {{ failure_ops }}</p>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ success_percent }}%;" aria-valuenow="{{ success_percent }}" aria-valuemin="0" aria-valuemax="100">
{{ success_percent }}%
</div>
<div class="progress-bar bg-danger" role="progressbar" style="width: {{ 100 - success_percent }}%;" aria-valuenow="{{ 100 - success_percent }}" aria-valuemin="0" aria-valuemax="100">
{{ 100 - success_percent }}%
</div>
</div>
</div>
</div>
<!-- Log operacji -->
<div class="card shadow-sm">
<div class="card-body">
<h5 class="card-title">Log operacji</h5>
<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Data</th>
<th>Wiadomość</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td>{{ log.timestamp.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td>{{ log.message }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endblock %}

27
templates/diff.html Normal file
View File

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-4">
<h2>Porównanie: {{ backup1.file_path|basename }} vs {{ backup2.file_path|basename }}</h2>
<div id="diffContainer"></div>
<a href="{{ url_for('router_details', router_id=backup1.router_id) }}" class="btn btn-secondary mt-3">Powrót</a>
</div>
<!-- Dodajemy diff2html -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
<script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Upewnij się, że diff_text jest poprawnie escapowany
var diffText = `{{ diff_text|e }}`;
var targetElement = document.getElementById("diffContainer");
var configuration = {
drawFileList: true,
matching: 'lines',
outputFormat: 'line-by-line'
};
var diffHtml = Diff2Html.html(diffText, configuration);
targetElement.innerHTML = diffHtml;
});
</script>
{% endblock %}

View File

@ -0,0 +1,50 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-4">
<h2 class="text-center mb-4">Porównanie backupów (Diff)</h2>
<div class="card shadow-sm">
<div class="card-body">
<form action="{{ url_for('diff_selector') }}" method="POST" id="diffForm">
<div class="row mb-3">
<div class="col-md-6">
<label for="backup1" class="form-label">Wybierz pierwszy backup:</label>
<select class="form-select" id="backup1" name="backup1" required>
<option value="" disabled selected>-- Wybierz backup --</option>
{% for backup in backups %}
<option value="{{ backup.id }}">
{{ backup.file_path|basename }} ({{ backup.created_at.strftime("%Y-%m-%d %H:%M:%S") }})
</option>
{% endfor %}
</select>
</div>
<div class="col-md-6">
<label for="backup2" class="form-label">Wybierz drugi backup:</label>
<select class="form-select" id="backup2" name="backup2" required>
<option value="" disabled selected>-- Wybierz backup --</option>
{% for backup in backups %}
<option value="{{ backup.id }}">
{{ backup.file_path|basename }} ({{ backup.created_at.strftime("%Y-%m-%d %H:%M:%S") }})
</option>
{% endfor %}
</select>
</div>
</div>
<div class="text-center">
<button type="submit" class="btn btn-primary btn-lg">Porównaj backupy</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.getElementById("diffForm").addEventListener("submit", function(event) {
var backup1 = document.getElementById("backup1").value;
var backup2 = document.getElementById("backup2").value;
if(backup1 === backup2) {
event.preventDefault();
alert("Wybierz dwa różne backupy do porównania.");
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card shadow-sm">
<div class="card-header">
<h2 class="mb-0">Edycja urządzenia</h2>
</div>
<div class="card-body">
<form method="POST">
<div class="mb-3">
<label for="name" class="form-label"><b>Nazwa</b></label>
<input type="text" class="form-control" id="name" name="name" value="{{ router.name }}" required>
</div>
<div class="mb-3">
<label for="host" class="form-label"><b>Host/IP</b></label>
<input type="text" class="form-control" id="host" name="host" value="{{ router.host }}" required>
</div>
<div class="mb-3">
<label for="port" class="form-label"><b>Port SSH</b></label>
<input type="number" class="form-control" id="port" name="port" value="{{ router.port }}" required>
</div>
<div class="mb-3">
<label for="ssh_user" class="form-label"><b>Użytkownik SSH</b></label>
<input type="text" class="form-control" id="ssh_user" name="ssh_user" value="{{ router.ssh_user }}">
</div>
<div class="mb-3">
<label for="ssh_key" class="form-label">
<label for="ssh_password" class="form-label"><b>Klucz prywatny</b></label> | Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code><br>
Pozostaw puste jeśli ten RouterOS będzie używał <a href="{{ url_for('settings_view') }}">klucza globalnego</a>
</label>
<textarea class="form-control" id="ssh_key" name="ssh_key" rows="4">{{ router.ssh_key }}</textarea>
</div>
<div class="mb-3">
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
Jeśli podajesz klucz SSH lub zdefiniowany jest <a href="{{ url_for('settings_view') }}">klucz globalny</a>, to logowanie hasłem jest nieaktywne.
<input type="password" class="form-control" id="ssh_password" name="ssh_password" value="{{ router.ssh_password }}">
</div>
<button type="submit" class="btn btn-success">Zapisz zmiany</button>
</form>
</div>
</div>
</div>
{% endblock %}

14
templates/index.html Normal file
View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% block content %}
<div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 80vh;">
<div class="text-center">
<img src="https://mikrotik.com/logo/assets/logo-colors-dark-ToiqSI6u.svg" alt="Mikrotik Logo" class="img-fluid" style="max-width: 200px;">
<h1 class="mt-3">Witamy w aplikacji Backup RouterOS</h1>
<p class="lead">Zarządzaj backupami swoich urządzeń RouterOS w prosty sposób.</p>
<div class="mt-4">
<a href="{{ url_for('login') }}" class="btn btn-primary btn-lg me-3">Login</a>
<a href="{{ url_for('register') }}" class="btn btn-success btn-lg">Rejestracja</a>
</div>
</div>
</div>
{% endblock %}

32
templates/login.html Normal file
View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header text-center">
<h2>Zaloguj się</h2>
</div>
<div class="card-body">
<form action="{{ url_for('login') }}" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Nazwa użytkownika</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika">
</div>
<div class="mb-3">
<label for="password" class="form-label">Hasło</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło">
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Zaloguj się</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<a href="{{ url_for('register') }}">Nie masz konta? Zarejestruj się</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

32
templates/register.html Normal file
View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block content %}
<div class="container">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header text-center">
<h2>Rejestracja</h2>
</div>
<div class="card-body">
<form action="{{ url_for('register') }}" method="POST">
<div class="mb-3">
<label for="username" class="form-label">Nazwa użytkownika</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika">
</div>
<div class="mb-3">
<label for="password" class="form-label">Hasło</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło">
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Zarejestruj się</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<a href="{{ url_for('login') }}">Masz już konto? Zaloguj się</a>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block content %}
<h2>Router: {{ router.name }}</h2>
<p>
<strong>Host:</strong> {{ router.host }} |
<strong>Port:</strong> {{ router.port }} |
<strong>SSH User:</strong> {{ router.ssh_user }}
</p>
<div class="mb-3">
<form action="{{ url_for('router_export', router_id=router.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-primary">Wykonaj /export</button>
</form>
<form action="{{ url_for('router_backup', router_id=router.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-secondary">Wykonaj backup binarny</button>
</form>
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-warning">Edytuj ustawienia</a>
</div>
<!-- Sekcja eksportów -->
<h3>Pliki z /export</h3>
{% if export_backups %}
<!-- Tabela z indywidualnymi akcjami -->
<table class="table table-bordered">
<thead>
<tr>
<th>Nazwa pliku</th>
<th>Rozmiar</th>
<th>Data</th>
<th>Diff</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for b in export_backups %}
<tr>
<td>{{ b.file_path|basename }}</td>
<td>{{ b.file_path|filesize }}</td>
<td>{{ b.created_at }}</td>
<td>
{% if loop.index0 > 0 %}
<a href="{{ url_for('diff_view', backup_id1=b.id, backup_id2=export_backups[0].id) }}" class="btn btn-sm btn-info">Diff</a>
{% else %}
<small>Brak nowszego</small>
{% endif %}
</td>
<td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a>
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-sm btn-outline-primary">Podgląd</a>
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" style="display: inline;">
<button type="submit" class="btn btn-sm btn-primary">Wyślij mailem</button>
</form>
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<button type="submit" class="btn btn-sm btn-danger">Usuń</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Formularz do pobierania ZIP zaznaczonych eksportów -->
<h4>Pobierz wybrane pliki z /export jako zip</h4>
<form action="{{ url_for('download_zip') }}" method="POST">
<table class="table table-bordered">
<thead>
<tr>
<th><input type="checkbox" id="select_all_export_zip"></th>
<th>Nazwa pliku</th>
<th>Rozmiar</th>
<th>Data</th>
</tr>
</thead>
<tbody>
{% for b in export_backups %}
<tr>
<td><input type="checkbox" name="backup_id" value="{{ b.id }}"></td>
<td>{{ b.file_path|basename }}</td>
<td>{{ b.file_path|filesize }}</td>
<td>{{ b.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button>
</form>
{% else %}
<p class="text-muted">Pusto</p>
{% endif %}
<br>
<hr>
<br>
<!-- Sekcja backupów binarnych -->
<h3>Pliki binarne (.backup)</h3>
{% if binary_backups %}
<table class="table table-bordered">
<thead>
<tr>
<th>Nazwa pliku</th>
<th>Rozmiar</th>
<th>Data</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for b in binary_backups %}
<tr>
<td>{{ b.file_path|basename }}</td>
<td>{{ b.file_path|filesize }}</td>
<td>{{ b.created_at }}</td>
<td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a>
<form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-sm btn-secondary">Wgraj do routera</button>
</form>
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" style="display:inline;">
<button type="submit" class="btn btn-sm btn-primary">Wyślij mailem</button>
</form>
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<button type="submit" class="btn btn-sm btn-danger">Usuń</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Formularz do pobierania ZIP zaznaczonych backupów binarnych -->
<h4>Pobierz wybrane backupy binarne jako zip</h4>
<form action="{{ url_for('download_zip') }}" method="POST">
<table class="table table-bordered">
<thead>
<tr>
<th><input type="checkbox" id="select_all_binary_zip"></th>
<th>Nazwa pliku</th>
<th>Rozmiar</th>
<th>Data</th>
</tr>
</thead>
<tbody>
{% for b in binary_backups %}
<tr>
<td><input type="checkbox" name="backup_id" value="{{ b.id }}"></td>
<td>{{ b.file_path|basename }}</td>
<td>{{ b.file_path|filesize }}</td>
<td>{{ b.created_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button>
</form>
{% else %}
<p class="text-muted">Pusto</p>
{% endif %}
{% endblock %}

24
templates/routeros.html Normal file
View File

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% block content %}
<h2>Moje Routery</h2>
<a href="{{ url_for('add_router') }}">+ Dodaj nowy router</a>
<table border="1" cellpadding="5" cellspacing="0">
<tr>
<th>Nazwa</th>
<th>Host</th>
<th>Port</th>
<th>Akcje</th>
</tr>
{% for r in routers %}
<tr>
<td>{{ r.name }}</td>
<td>{{ r.host }}</td>
<td>{{ r.port }}</td>
<td>
<a href="{{ url_for('router_details', router_id=r.id) }}">Szczegóły</a>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block content %}
<h2>Router: {{ router.name }}</h2>
<p>Host: {{ router.host }} | Port: {{ router.port }} | SSH User: {{ router.ssh_user }}</p>
<!-- Akcje: Wykonaj export, Wykonaj backup binarny -->
<form action="{{ url_for('router_export', router_id=router.id) }}" method="POST" style="display:inline;">
<button type="submit">Wykonaj export (/export)</button>
</form>
<form action="{{ url_for('router_backup', router_id=router.id) }}" method="POST" style="display:inline;">
<button type="submit">Wykonaj backup binarny</button>
</form>
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-warning mb-3">
Edytuj ustawienia
</a>
<h3>Lista Backupów</h3>
<table border="1" cellpadding="5" cellspacing="0">
<tr>
<th>Data</th>
<th>Plik</th>
<th>Typ</th>
<th>Diff</th>
</tr>
{% for b in backups %}
<tr>
<td>{{ b.created_at }}</td>
<td>{{ b.file_path | basename }}</td>
<td>{{ b.backup_type }}</td>
<td>
{# Przy diff potrzebujemy wybrać, do którego backupu porównać #}
{# Można przygotować prosty select lub link do innej podstrony #}
{# Dla uproszczenia link do b1=b.id, b2=ostatni? #}
{# Lub w widoku trzeba by rozwinąć logikę #}
<!-- Tu tylko pokazujemy, że jest taka opcja: -->
<small>Diff z innym exportem: np.
<a href="{{ url_for('diff_view', backup_id1=b.id, backup_id2=backups[0].id if backups|length > 0 else b.id) }}">porównaj z najnowszym</a>
</small>
</td>
</tr>
{% endfor %}
</table>
{% endblock %}

63
templates/routers.html Normal file
View File

@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2>Moje Routery</h2>
<a href="{{ url_for('add_router') }}" class="btn btn-success">
<i class="bi bi-plus-lg"></i> Dodaj nowe urządzenie
</a>
</div>
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="table-primary">
<tr>
<th>Nazwa</th>
<th>Host</th>
<th>Port</th>
<th>Exporty</th>
<th>Backupy binarne</th>
<th>Test Połączenia</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
{% for router in routers %}
<tr>
<td>{{ router.name }}</td>
<td>{{ router.host }}</td>
<td>{{ router.port }}</td>
<td>
<span class="badge bg-success">
{{ router.backups|selectattr("backup_type", "equalto", "export")|list|length }}
</span>
</td>
<td>
<span class="badge bg-info">
{{ router.backups|selectattr("backup_type", "equalto", "binary")|list|length }}
</span>
</td>
<td>
<button type="button" class="btn btn-sm btn-info" onclick="openTestConnectionModal({{ router.id }})">
<i class="bi bi-wifi"></i> Test
</button>
</td>
<td>
<a href="{{ url_for('router_details', router_id=router.id) }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> Szczegóły
</a>
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-sm btn-warning">
<i class="bi bi-pencil"></i> Edytuj
</a>
<form action="{{ url_for('delete_router', router_id=router.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Na pewno usunąć urządzenie?');">
<i class="bi bi-trash"></i> Usuń
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

68
templates/settings.html Normal file
View File

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-5">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h2 class="mb-0">Ustawienia globalne</h2>
</div>
<div class="card-body">
<form method="POST">
<!-- Sekcja Pushover -->
<div class="mb-4">
<h4 class="mb-3">Powiadomienia - Pushover</h4>
<div class="mb-3">
<label for="pushover_token" class="form-label">Pushover Token</label>
<input type="text" class="form-control" id="pushover_token" name="pushover_token" value="{{ settings.pushover_token }}">
</div>
<div class="mb-3">
<label for="pushover_userkey" class="form-label">Pushover User Key</label>
<input type="text" class="form-control" id="pushover_userkey" name="pushover_userkey" value="{{ settings.pushover_userkey }}">
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="notify_failures_only" name="notify_failures_only" value="True" {% if settings.notify_failures_only %}checked{% endif %}>
<label class="form-check-label" for="notify_failures_only">Wysyłaj powiadomienia tylko o błędach</label>
</div>
</div>
<hr>
<!-- Sekcja SMTP -->
<div class="mb-4">
<h4 class="mb-3">Powiadomienia - SMTP (e-mail)</h4>
<div class="mb-3">
<label for="smtp_host" class="form-label">SMTP Host</label>
<input type="text" class="form-control" id="smtp_host" name="smtp_host" value="{{ settings.smtp_host }}">
</div>
<div class="mb-3">
<label for="smtp_port" class="form-label">SMTP Port</label>
<input type="number" class="form-control" id="smtp_port" name="smtp_port" value="{{ settings.smtp_port }}">
</div>
<div class="mb-3">
<label for="smtp_login" class="form-label">SMTP Login / Adres</label>
<input type="text" class="form-control" id="smtp_login" name="smtp_login" value="{{ settings.smtp_login }}">
</div>
<div class="mb-3">
<label for="smtp_password" class="form-label">SMTP Hasło</label>
<input type="password" class="form-control" id="smtp_password" name="smtp_password" value="{{ settings.smtp_password }}">
</div>
</div>
<hr>
<!-- Sekcja globalnego klucza SSH -->
<div class="mb-4">
<h4 class="mb-3">Globalny klucz SSH</h4>
<div class="mb-3">
<label for="global_ssh_key" class="form-label">
Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code>
</label>
<textarea class="form-control" id="global_ssh_key" name="global_ssh_key" rows="4">{{ settings.global_ssh_key }}</textarea>
</div>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Zapisz ustawienia</button>
</div>
</form>
</div>
<div class="card-footer text-center">
<p>Ustawienia dotyczące backupu oraz harmonogramu CRON znajdują się na <a href="{{ url_for('advanced_schedule') }}">zaawansowanych ustawieniach harmonogramu</a>.</p>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,8 @@
<div>
<h4>Test połączenia: {{ router.name }}</h4>
<ul>
<li><strong>Model:</strong> {{ result.model }}</li>
<li><strong>Uptime:</strong> {{ result.uptime }}</li>
<li><strong>Hostname:</strong> {{ result.hostname }}</li>
</ul>
</div>

View File

@ -0,0 +1,29 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-4">
<h2>Podgląd eksportu: {{ backup.file_path|basename }}</h2>
<textarea id="exportEditor" readonly>{{ content|e }}</textarea>
<a href="{{ next_url }}" class="btn btn-secondary">Powrót</a>
</div>
<!-- CodeMirror CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/neo.min.css">
<!-- CodeMirror JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script>
<!-- Dodajemy tryb dla plików shell, który dobrze radzi sobie z konfiguracjami RouterOS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/shell/shell.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
var editor = CodeMirror.fromTextArea(document.getElementById("exportEditor"), {
mode: "text/x-sh",
theme: "neo",
lineNumbers: true,
readOnly: true
});
// Dopasowanie rozmiaru edytora do zawartości
editor.setSize("100%", "800px");
});
</script>
{% endblock %}