naprawa błędów i nowe funkcje

This commit is contained in:
Mateusz Gruszczyński 2025-02-23 10:31:15 +01:00
parent 31c898ba0c
commit f39a4a9414
6 changed files with 188 additions and 42 deletions

81
app.py
View File

@ -9,18 +9,16 @@ import re
import smtplib import smtplib
import shutil import shutil
import socket import socket
import hashlib
from datetime import datetime from datetime import datetime
from ftplib import FTP
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email import encoders from email import encoders
from flask import jsonify from flask import jsonify
from flask import Flask from flask import Flask
import shutil
from datetime import datetime from datetime import datetime
from difflib import HtmlDiff from difflib import HtmlDiff
import difflib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy import text from sqlalchemy import text
@ -87,6 +85,8 @@ class Backup(db.Model):
file_path = db.Column(db.String(255), nullable=False) # Ścieżka do pliku file_path = db.Column(db.String(255), nullable=False) # Ścieżka do pliku
backup_type = db.Column(db.String(50), default='export') # 'export' lub 'binary' backup_type = db.Column(db.String(50), default='export') # 'export' lub 'binary'
created_at = db.Column(db.DateTime, default=datetime.utcnow) created_at = db.Column(db.DateTime, default=datetime.utcnow)
checksum = db.Column(db.String(64), nullable=True)
class OperationLog(db.Model): class OperationLog(db.Model):
__tablename__ = 'operation_logs' __tablename__ = 'operation_logs'
__table_args__ = {'extend_existing': True} # Zapobiega redefinicji tabeli __table_args__ = {'extend_existing': True} # Zapobiega redefinicji tabeli
@ -112,6 +112,7 @@ class GlobalSettings(db.Model):
smtp_login = db.Column(db.String(255), nullable=True) smtp_login = db.Column(db.String(255), nullable=True)
smtp_password = db.Column(db.String(255), nullable=True) smtp_password = db.Column(db.String(255), nullable=True)
smtp_notifications_enabled = db.Column(db.Boolean, default=False) smtp_notifications_enabled = db.Column(db.Boolean, default=False)
log_retention_days = db.Column(db.Integer, default=7)
############################################################################### ###############################################################################
# Inicjalizacja bazy # Inicjalizacja bazy
@ -166,6 +167,13 @@ def load_pkey(ssh_key_str: str):
except Exception as e: except Exception as e:
raise ValueError("Nie udało się załadować klucza SSH. Sprawdź, czy klucz jest poprawny i nie jest zaszyfrowany") from e raise ValueError("Nie udało się załadować klucza SSH. Sprawdź, czy klucz jest poprawny i nie jest zaszyfrowany") from e
def compute_checksum(file_path):
sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""):
sha256.update(chunk)
return sha256.hexdigest()
############################################################################### ###############################################################################
# Funkcje SSH # Funkcje SSH
############################################################################### ###############################################################################
@ -229,12 +237,17 @@ def ssh_backup(router: Router, backup_name: str) -> str:
print(f"[DEBUG] ssh_backup -> local_path={local_path}") print(f"[DEBUG] ssh_backup -> local_path={local_path}")
return local_path return local_path
def ssh_upload_backup(router: Router, local_backup_path: str): def ssh_upload_backup(router: Router, local_backup_path: str, expected_checksum: str = None):
print(f"[DEBUG] ssh_upload_backup -> router id={router.id}, local_backup_path={local_backup_path}") # Weryfikacja sumy kontrolnej, jeśli podana
if expected_checksum:
local_checksum = compute_checksum(local_backup_path)
if local_checksum != expected_checksum:
raise ValueError("Suma kontrolna pliku nie zgadza się plik może być uszkodzony.")
client = paramiko.SSHClient() client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Używamy indywidualnego klucza, a jeśli nie ma, to globalnego # Wybór klucza: indywidualny lub globalny
key_source = router.ssh_key if router.ssh_key and router.ssh_key.strip() else get_settings().global_ssh_key key_source = router.ssh_key if router.ssh_key and router.ssh_key.strip() else get_settings().global_ssh_key
if key_source and key_source.strip(): if key_source and key_source.strip():
try: try:
@ -245,8 +258,10 @@ def ssh_upload_backup(router: Router, local_backup_path: str):
raise e raise e
else: else:
client.connect(router.host, port=router.port, username=router.ssh_user, client.connect(router.host, port=router.port, username=router.ssh_user,
password=router.ssh_password, timeout=10, allow_agent=False, look_for_keys=False, banner_timeout=10) password=router.ssh_password, timeout=10,
allow_agent=False, look_for_keys=False, banner_timeout=10)
# Otwieramy sesję SFTP, przesyłamy plik, a następnie zamykamy połączenie
sftp = client.open_sftp() sftp = client.open_sftp()
remote_file = os.path.basename(local_backup_path) remote_file = os.path.basename(local_backup_path)
sftp.put(local_backup_path, remote_file) sftp.put(local_backup_path, remote_file)
@ -254,6 +269,7 @@ def ssh_upload_backup(router: Router, local_backup_path: str):
client.close() client.close()
print(f"[DEBUG] ssh_upload_backup -> przesłano {local_backup_path} do routera") print(f"[DEBUG] ssh_upload_backup -> przesłano {local_backup_path} do routera")
def ssh_test_connection(router: Router) -> dict: def ssh_test_connection(router: Router) -> dict:
"""Testuje połączenie z routerem i zwraca informacje: model, uptime, hostname.""" """Testuje połączenie z routerem i zwraca informacje: model, uptime, hostname."""
client = paramiko.SSHClient() client = paramiko.SSHClient()
@ -504,7 +520,8 @@ def scheduled_auto_binary_backup():
try: try:
backup_name = f"{r.name}_{r.id}_{datetime.now():%Y%m%d_%H%M%S}" backup_name = f"{r.name}_{r.id}_{datetime.now():%Y%m%d_%H%M%S}"
local_path = ssh_backup(r, backup_name) local_path = ssh_backup(r, backup_name)
b = Backup(router_id=r.id, file_path=local_path, backup_type='binary') checksum = compute_checksum(local_path)
b = Backup(router_id=r.id, file_path=local_path, backup_type='binary', checksum=checksum)
db.session.add(b) db.session.add(b)
db.session.commit() db.session.commit()
notify(s, f"Auto-binary backup dla routera {r.name} OK", True) notify(s, f"Auto-binary backup dla routera {r.name} OK", True)
@ -720,12 +737,12 @@ def advanced_schedule():
s = get_settings() s = get_settings()
if request.method == 'POST': if request.method == 'POST':
s.retention_cron = request.form.get('retention_cron', '').strip() s.retention_cron = request.form.get('retention_cron', '').strip()
s.binary_cron = request.form.get('binary_cron', '').strip() # nowe pole s.binary_cron = request.form.get('binary_cron', '').strip()
s.export_cron = request.form.get('export_cron', '').strip() s.export_cron = request.form.get('export_cron', '').strip()
# Checkbox: jeśli nie jest zaznaczony, nie pojawi się w formularzu, więc ustawiamy na False s.backup_retention_days = int(request.form.get('backup_retention_days', s.backup_retention_days))
s.enable_auto_export = True if request.form.get('enable_auto_export') == 'on' else False s.enable_auto_export = True if request.form.get('enable_auto_export') == 'on' else False
db.session.commit() db.session.commit()
reschedule_jobs() # Aktualizuje harmonogram zadań reschedule_jobs() # Aktualizacja harmonogramu zadań
flash("Zaawansowane ustawienia harmonogramu zostały zapisane.") flash("Zaawansowane ustawienia harmonogramu zostały zapisane.")
return redirect(url_for('advanced_schedule')) return redirect(url_for('advanced_schedule'))
return render_template('advanced_schedule.html', settings=s) return render_template('advanced_schedule.html', settings=s)
@ -866,7 +883,8 @@ def router_backup(router_id):
try: try:
backup_name = f"{router.name}_{router.id}_{datetime.now():%Y%m%d_%H%M%S}" backup_name = f"{router.name}_{router.id}_{datetime.now():%Y%m%d_%H%M%S}"
local_path = ssh_backup(router, backup_name) local_path = ssh_backup(router, backup_name)
b = Backup(router_id=router.id, file_path=local_path, backup_type='binary') checksum = compute_checksum(local_path)
b = Backup(router_id=router.id, file_path=local_path, backup_type='binary', checksum=checksum)
db.session.add(b) db.session.add(b)
db.session.commit() db.session.commit()
notify(get_settings(), f"Backup {router.name} OK", True) notify(get_settings(), f"Backup {router.name} OK", True)
@ -890,13 +908,19 @@ def upload_backup(router_id, backup_id):
if not b: if not b:
flash("Nie znaleziono backupu binarnego.") flash("Nie znaleziono backupu binarnego.")
return redirect(url_for('router_details', router_id=router.id)) return redirect(url_for('router_details', router_id=router.id))
# Sprawdź sumę kontrolną pliku przed wgraniem
local_checksum = compute_checksum(b.file_path)
if b.checksum != local_checksum:
flash("Błąd: suma kontrolna backupu nie zgadza się plik może być uszkodzony.")
return redirect(url_for('router_details', router_id=router.id))
try: try:
ssh_upload_backup(router, b.file_path) ssh_upload_backup(router, b.file_path, expected_checksum=b.checksum)
log_operation(f"Backup {os.path.basename(b.file_path)} wgrany do routera {router.name} at {datetime.utcnow()}") log_operation(f"Backup {os.path.basename(b.file_path)} wgrany do routera {router.name} at {datetime.utcnow()}")
flash("Plik backupu wgrany do routera.") flash("Plik backupu wgrany do routera.")
except Exception as e: except Exception as e:
flash(f"Błąd wgrywania: {e}") flash(f"Błąd wgrywania: {e}")
#return redirect(url_for('router_details', router_id=router.id))
next_url = request.form.get('next') or request.referrer or url_for('dashboard') next_url = request.form.get('next') or request.referrer or url_for('dashboard')
return redirect(next_url) return redirect(next_url)
@ -1325,6 +1349,35 @@ def test_connection(router_id):
return render_template("test_connection_modal.html", router=router, result=result) return render_template("test_connection_modal.html", router=router, result=result)
return render_template("test_connection.html", router=router, result=result) return render_template("test_connection.html", router=router, result=result)
@app.route('/logs')
@login_required
def logs_page():
logs = OperationLog.query.order_by(OperationLog.timestamp.desc()).all()
return render_template('logs.html', logs=logs)
@app.route('/logs/delete', methods=['POST'])
@login_required
def delete_old_logs():
try:
delete_days = int(request.form.get('delete_days', 0))
except ValueError:
flash("Podana wartość jest nieprawidłowa.")
return redirect(url_for('logs_page'))
if delete_days < 1:
flash("Podaj wartość większą lub równą 1.")
return redirect(url_for('logs_page'))
cutoff_date = datetime.utcnow() - timedelta(days=delete_days)
old_logs = OperationLog.query.filter(OperationLog.timestamp < cutoff_date).all()
deleted_count = 0
for log in old_logs:
db.session.delete(log)
deleted_count += 1
db.session.commit()
flash(f"Usunięto {deleted_count} logów starszych niż {delete_days} dni.")
return redirect(url_for('logs_page'))
if __name__ == '__main__': if __name__ == '__main__':
with app.app_context(): with app.app_context():
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()

View File

@ -8,6 +8,15 @@
<div class="card-body"> <div class="card-body">
<form action="{{ url_for('advanced_schedule') }}" method="POST"> <form action="{{ url_for('advanced_schedule') }}" method="POST">
<div class="mb-3"> <div class="mb-3">
<div class="mb-3">
<label for="backup_retention_days" class="form-label">Próg retencji backupów (dni)</label>
<small>Usuwanie danych starszych niż ustawione w progu</small>
<input type="number" class="form-control" id="backup_retention_days" name="backup_retention_days" value="{{ settings.backup_retention_days }}">
</div>
<div class="mb-3">
<label for="log_retention_days" class="form-label">Próg retencji logów (dni)</label>
<input type="number" class="form-control" id="log_retention_days" name="log_retention_days" value="{{ settings.log_retention_days }}">
</div>
<label for="retention_cron" class="form-label">Harmonogram retencji (cron)</label> <label for="retention_cron" class="form-label">Harmonogram retencji (cron)</label>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" id="retention_cron" name="retention_cron" value="{{ settings.retention_cron }}"> <input type="text" class="form-control" id="retention_cron" name="retention_cron" value="{{ settings.retention_cron }}">
@ -80,18 +89,15 @@
</div> </div>
<script> <script>
// Zmienna przechowująca ID pola, do którego ma być wpisane wyrażenie cron
var targetCronField = ''; var targetCronField = '';
function openCronModal(fieldId) { function openCronModal(fieldId) {
targetCronField = fieldId; targetCronField = fieldId;
// Wyzeruj wartości w modalu
document.getElementById('cron_minute').value = '*'; document.getElementById('cron_minute').value = '*';
document.getElementById('cron_hour').value = '*'; document.getElementById('cron_hour').value = '*';
document.getElementById('cron_day').value = '*'; document.getElementById('cron_day').value = '*';
document.getElementById('cron_month').value = '*'; document.getElementById('cron_month').value = '*';
document.getElementById('cron_dow').value = '*'; document.getElementById('cron_dow').value = '*';
// Otwórz modal (przy użyciu Bootstrap 5)
var cronModal = new bootstrap.Modal(document.getElementById('cronModal')); var cronModal = new bootstrap.Modal(document.getElementById('cronModal'));
cronModal.show(); cronModal.show();
} }
@ -102,10 +108,8 @@
var day = document.getElementById('cron_day').value || '*'; var day = document.getElementById('cron_day').value || '*';
var month = document.getElementById('cron_month').value || '*'; var month = document.getElementById('cron_month').value || '*';
var dow = document.getElementById('cron_dow').value || '*'; var dow = document.getElementById('cron_dow').value || '*';
var cronExpr = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + dow; var cronExpr = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + dow;
document.getElementById(targetCronField).value = cronExpr; document.getElementById(targetCronField).value = cronExpr;
// Zamknij modal
var modalEl = document.getElementById('cronModal'); var modalEl = document.getElementById('cronModal');
var modalInstance = bootstrap.Modal.getInstance(modalEl); var modalInstance = bootstrap.Modal.getInstance(modalEl);
modalInstance.hide(); modalInstance.hide();

View File

@ -21,6 +21,9 @@
.diff-add { color: green; } .diff-add { color: green; }
.diff-rem { color: red; } .diff-rem { color: red; }
</style> </style>
<!-- Blok head umożliwiający dołączenie dodatkowych stylów -->
{% block head %}{% endblock %}
</head> </head>
<body> <body>
<nav class="navbar navbar-expand navbar-dark bg-dark mb-4"> <nav class="navbar navbar-expand navbar-dark bg-dark mb-4">
@ -32,8 +35,11 @@
<a href="{{ url_for('routers_list') }}" class="btn btn-secondary me-2">Urządzenia</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('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('all_files') }}" class="btn btn-secondary me-2">Wszystkie pliki</a>
<a href="{{ url_for('logs_page') }}" class="btn btn-secondary me-2">Logi</a>
<a href="{{ url_for('settings_view') }}" class="btn btn-secondary me-2">Ustawienia</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('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('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> <a href="{{ url_for('logout') }}" class="btn btn-secondary me-2">Wyloguj</a>
{% else %} {% else %}
@ -112,5 +118,7 @@
}); });
} }
</script> </script>
<!-- Blok scripts umożliwiający dołączenie dodatkowych skryptów -->
{% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@ -91,7 +91,10 @@
<!-- Log operacji --> <!-- Log operacji -->
<div class="card shadow-sm mb-4"> <div class="card shadow-sm mb-4">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Log operacji</h5> <h5 class="card-title">
Log operacji
<a href="{{ url_for('logs_page') }}" class="btn btn-sm btn-outline-primary ms-2">Więcej logów</a>
</h5>
<table class="table table-sm table-bordered"> <table class="table table-sm table-bordered">
<thead> <thead>
<tr> <tr>

73
templates/logs.html Normal file
View File

@ -0,0 +1,73 @@
{% extends "base.html" %}
{% block head %}
{{ super() }}
<!-- DataTables CSS -->
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.4/css/dataTables.bootstrap5.min.css">
{% endblock %}
{% block content %}
<div class="container my-4">
<h2 class="text-center mb-4">Historia logów operacji</h2>
<!-- Formularz usuwania logów starszych od podanej liczby dni -->
<div class="card mb-4 shadow-sm">
<div class="card-body">
<form action="{{ url_for('delete_old_logs') }}" method="POST" class="row g-2 align-items-center">
<div class="col-auto">
<label for="delete_days" class="col-form-label">Usuń logi starsze niż:</label>
</div>
<div class="col-auto">
<input type="number" class="form-control" id="delete_days" name="delete_days" min="1" placeholder="Liczba dni" required>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-danger">Usuń logi</button>
</div>
</form>
</div>
</div>
<!-- Tabela logów -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<table id="logsTable" class="table table-striped table-bordered">
<thead class="table-dark">
<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 class="text-center mt-3">
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-primary">Powrót do Dashboard</a>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<!-- jQuery (jeśli nie jest już dołączone w base.html) -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<!-- DataTables JS -->
<script src="https://cdn.datatables.net/1.13.4/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.13.4/js/dataTables.bootstrap5.min.js"></script>
<script>
$(document).ready(function() {
$('#logsTable').DataTable({
responsive: true,
order: [[0, 'desc']]
});
});
</script>
{% endblock %}

View File

@ -22,12 +22,12 @@
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
<!-- Mniejsze przyciski górne --> <!-- Mniejsze przyciski górne -->
<form action="{{ url_for('router_export', router_id=router.id) }}" method="POST" class="d-inline"> <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> <button type="submit" class="btn btn-primary btn-sm">Wykonaj /export</button>
</form> </form>
<form action="{{ url_for('router_backup', router_id=router.id) }}" method="POST" class="d-inline"> <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> <button type="submit" class="btn btn-secondary btn-sm">Wykonaj backup binarny</button>
</form> </form>
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-warning">Edytuj ustawienia</a> <a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-warning btn-sm">Edytuj ustawienia</a>
</div> </div>
</div> </div>
</div> </div>
@ -50,12 +50,12 @@
<!-- Formularz masowych akcji dla eksportów --> <!-- Formularz masowych akcji dla eksportów -->
<form id="export_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3"> <form id="export_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button type="submit" name="action" value="download" class="btn btn-lg btn-success"> <button type="submit" name="action" value="download" class="btn btn-success btn-sm">
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip) <i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
</button> </button>
</div> </div>
</form> </form>
<!-- Tabela z eksportami z podzielonymi kolumnami akcji --> <!-- Tabela z eksportami -->
<table class="table table-bordered table-striped"> <table class="table table-bordered table-striped">
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
@ -87,19 +87,19 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz"> <a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-info btn-sm" title="Pobierz">
<i class="bi bi-download"></i> <i class="bi bi-download"></i>
</a> </a>
</td> </td>
<td> <td>
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-lg btn-outline-primary" title="Podgląd"> <a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-outline-primary btn-sm" title="Podgląd">
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i>
</a> </a>
</td> </td>
<td> <td>
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline"> <form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}"> <input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<button type="submit" class="btn btn-lg btn-primary" title="Wyślij mailem"> <button type="submit" class="btn btn-primary btn-sm" title="Wyślij mailem">
<i class="bi bi-envelope"></i> <i class="bi bi-envelope"></i>
</button> </button>
</form> </form>
@ -107,7 +107,7 @@
<td> <td>
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');"> <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) }}"> <input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<button type="submit" class="btn btn-lg btn-danger" title="Usuń"> <button type="submit" class="btn btn-danger btn-sm" title="Usuń">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</form> </form>
@ -130,12 +130,12 @@
<!-- Formularz masowych akcji dla backupów binarnych --> <!-- Formularz masowych akcji dla backupów binarnych -->
<form id="binary_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3"> <form id="binary_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<button type="submit" name="action" value="download" class="btn btn-lg btn-success"> <button type="submit" name="action" value="download" class="btn btn-success btn-sm">
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip) <i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
</button> </button>
</div> </div>
</form> </form>
<!-- Tabela z backupami binarnymi z podzielonymi kolumnami akcji --> <!-- Tabela z backupami binarnymi -->
<table class="table table-bordered table-striped"> <table class="table table-bordered table-striped">
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
@ -155,24 +155,27 @@
<td> <td>
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form"> <input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form">
</td> </td>
<td>{{ b.file_path|basename }}</td> <td>
<!-- Dodaj tooltip z sumą kontrolną -->
<span data-bs-toggle="tooltip" title="Checksum: {{ b.checksum }}">{{ b.file_path|basename }}</span>
</td>
<td>{{ b.file_path|filesize }}</td> <td>{{ b.file_path|filesize }}</td>
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td> <td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td> <td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz"> <a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-info btn-sm" title="Pobierz">
<i class="bi bi-download"></i> <i class="bi bi-download"></i>
</a> </a>
</td> </td>
<td> <td>
<form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline"> <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-lg btn-secondary" title="Wgraj do routera"> <button type="submit" class="btn btn-secondary btn-sm" title="Wgraj do routera">
<i class="bi bi-upload"></i> <i class="bi bi-upload"></i>
</button> </button>
</form> </form>
</td> </td>
<td> <td>
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline"> <form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-lg btn-primary" title="Wyślij mailem"> <button type="submit" class="btn btn-primary btn-sm" title="Wyślij mailem">
<i class="bi bi-envelope"></i> <i class="bi bi-envelope"></i>
</button> </button>
</form> </form>
@ -180,7 +183,7 @@
<td> <td>
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');"> <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) }}"> <input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<button type="submit" class="btn btn-lg btn-danger" title="Usuń"> <button type="submit" class="btn btn-danger btn-sm" title="Usuń">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</form> </form>
@ -202,15 +205,11 @@
<script> <script>
document.getElementById('select_all_export').addEventListener('change', function(e) { document.getElementById('select_all_export').addEventListener('change', function(e) {
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="export_mass_actions_form"]'); var checkboxes = document.querySelectorAll('input[name="backup_id"][form="export_mass_actions_form"]');
for (var i = 0; i < checkboxes.length; i++) { checkboxes.forEach(cb => cb.checked = e.target.checked);
checkboxes[i].checked = e.target.checked;
}
}); });
document.getElementById('select_all_binary').addEventListener('change', function(e) { document.getElementById('select_all_binary').addEventListener('change', function(e) {
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="binary_mass_actions_form"]'); var checkboxes = document.querySelectorAll('input[name="backup_id"][form="binary_mass_actions_form"]');
for (var i = 0; i < checkboxes.length; i++) { checkboxes.forEach(cb => cb.checked = e.target.checked);
checkboxes[i].checked = e.target.checked;
}
}); });
// Inicjalizacja zakładek Bootstrap (jeśli nie są już inicjowane globalnie) // Inicjalizacja zakładek Bootstrap (jeśli nie są już inicjowane globalnie)
@ -222,5 +221,11 @@ triggerTabList.forEach(function (triggerEl) {
tabTrigger.show(); tabTrigger.show();
}); });
}); });
// Inicjalizacja tooltipów Bootstrap
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
</script> </script>
{% endblock %} {% endblock %}