naprawa błędów i nowe funkcje
This commit is contained in:
parent
31c898ba0c
commit
f39a4a9414
81
app.py
81
app.py
@ -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()
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
@ -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
73
templates/logs.html
Normal 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 %}
|
@ -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 %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user