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 shutil
|
||||
import socket
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from ftplib import FTP
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email import encoders
|
||||
from flask import jsonify
|
||||
from flask import Flask
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from difflib import HtmlDiff
|
||||
import difflib
|
||||
from datetime import datetime, timedelta
|
||||
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
|
||||
backup_type = db.Column(db.String(50), default='export') # 'export' lub 'binary'
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
checksum = db.Column(db.String(64), nullable=True)
|
||||
|
||||
class OperationLog(db.Model):
|
||||
__tablename__ = 'operation_logs'
|
||||
__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_password = db.Column(db.String(255), nullable=True)
|
||||
smtp_notifications_enabled = db.Column(db.Boolean, default=False)
|
||||
log_retention_days = db.Column(db.Integer, default=7)
|
||||
|
||||
###############################################################################
|
||||
# Inicjalizacja bazy
|
||||
@ -166,6 +167,13 @@ def load_pkey(ssh_key_str: str):
|
||||
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
|
||||
|
||||
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
|
||||
###############################################################################
|
||||
@ -229,12 +237,17 @@ def ssh_backup(router: Router, backup_name: str) -> str:
|
||||
print(f"[DEBUG] ssh_backup -> local_path={local_path}")
|
||||
return local_path
|
||||
|
||||
def ssh_upload_backup(router: Router, local_backup_path: str):
|
||||
print(f"[DEBUG] ssh_upload_backup -> router id={router.id}, local_backup_path={local_backup_path}")
|
||||
def ssh_upload_backup(router: Router, local_backup_path: str, expected_checksum: str = None):
|
||||
# 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.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
|
||||
if key_source and key_source.strip():
|
||||
try:
|
||||
@ -245,8 +258,10 @@ def ssh_upload_backup(router: Router, local_backup_path: str):
|
||||
raise e
|
||||
else:
|
||||
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()
|
||||
remote_file = os.path.basename(local_backup_path)
|
||||
sftp.put(local_backup_path, remote_file)
|
||||
@ -254,6 +269,7 @@ def ssh_upload_backup(router: Router, local_backup_path: str):
|
||||
client.close()
|
||||
print(f"[DEBUG] ssh_upload_backup -> przesłano {local_backup_path} do routera")
|
||||
|
||||
|
||||
def ssh_test_connection(router: Router) -> dict:
|
||||
"""Testuje połączenie z routerem i zwraca informacje: model, uptime, hostname."""
|
||||
client = paramiko.SSHClient()
|
||||
@ -504,7 +520,8 @@ def scheduled_auto_binary_backup():
|
||||
try:
|
||||
backup_name = f"{r.name}_{r.id}_{datetime.now():%Y%m%d_%H%M%S}"
|
||||
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.commit()
|
||||
notify(s, f"Auto-binary backup dla routera {r.name} OK", True)
|
||||
@ -720,12 +737,12 @@ def advanced_schedule():
|
||||
s = get_settings()
|
||||
if request.method == 'POST':
|
||||
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()
|
||||
# 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
|
||||
db.session.commit()
|
||||
reschedule_jobs() # Aktualizuje harmonogram zadań
|
||||
reschedule_jobs() # Aktualizacja harmonogramu zadań
|
||||
flash("Zaawansowane ustawienia harmonogramu zostały zapisane.")
|
||||
return redirect(url_for('advanced_schedule'))
|
||||
return render_template('advanced_schedule.html', settings=s)
|
||||
@ -866,7 +883,8 @@ def router_backup(router_id):
|
||||
try:
|
||||
backup_name = f"{router.name}_{router.id}_{datetime.now():%Y%m%d_%H%M%S}"
|
||||
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.commit()
|
||||
notify(get_settings(), f"Backup {router.name} OK", True)
|
||||
@ -890,13 +908,19 @@ def upload_backup(router_id, backup_id):
|
||||
if not b:
|
||||
flash("Nie znaleziono backupu binarnego.")
|
||||
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:
|
||||
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()}")
|
||||
flash("Plik backupu wgrany do routera.")
|
||||
except Exception as 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')
|
||||
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.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__':
|
||||
with app.app_context():
|
||||
scheduler = BackgroundScheduler()
|
||||
|
@ -8,6 +8,15 @@
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('advanced_schedule') }}" method="POST">
|
||||
<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>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="retention_cron" name="retention_cron" value="{{ settings.retention_cron }}">
|
||||
@ -80,18 +89,15 @@
|
||||
</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();
|
||||
}
|
||||
@ -102,10 +108,8 @@
|
||||
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();
|
||||
|
@ -21,6 +21,9 @@
|
||||
.diff-add { color: green; }
|
||||
.diff-rem { color: red; }
|
||||
</style>
|
||||
|
||||
<!-- Blok head umożliwiający dołączenie dodatkowych stylów -->
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<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('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('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('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 %}
|
||||
@ -112,5 +118,7 @@
|
||||
});
|
||||
}
|
||||
</script>
|
||||
<!-- Blok scripts umożliwiający dołączenie dodatkowych skryptów -->
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -91,7 +91,10 @@
|
||||
<!-- Log operacji -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<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">
|
||||
<thead>
|
||||
<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">
|
||||
<!-- Mniejsze przyciski górne -->
|
||||
<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 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>
|
||||
<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>
|
||||
@ -50,12 +50,12 @@
|
||||
<!-- Formularz masowych akcji dla eksportów -->
|
||||
<form id="export_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
|
||||
<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)
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Tabela z eksportami z podzielonymi kolumnami akcji -->
|
||||
<!-- Tabela z eksportami -->
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
@ -87,19 +87,19 @@
|
||||
{% endif %}
|
||||
</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>
|
||||
</a>
|
||||
</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>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<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) }}">
|
||||
<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>
|
||||
</button>
|
||||
</form>
|
||||
@ -107,7 +107,7 @@
|
||||
<td>
|
||||
<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-lg btn-danger" title="Usuń">
|
||||
<button type="submit" class="btn btn-danger btn-sm" title="Usuń">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@ -130,12 +130,12 @@
|
||||
<!-- Formularz masowych akcji dla backupów binarnych -->
|
||||
<form id="binary_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
|
||||
<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)
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Tabela z backupami binarnymi z podzielonymi kolumnami akcji -->
|
||||
<!-- Tabela z backupami binarnymi -->
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
@ -155,24 +155,27 @@
|
||||
<td>
|
||||
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form">
|
||||
</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.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</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>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
</button>
|
||||
</form>
|
||||
@ -180,7 +183,7 @@
|
||||
<td>
|
||||
<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-lg btn-danger" title="Usuń">
|
||||
<button type="submit" class="btn btn-danger btn-sm" title="Usuń">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@ -202,15 +205,11 @@
|
||||
<script>
|
||||
document.getElementById('select_all_export').addEventListener('change', function(e) {
|
||||
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="export_mass_actions_form"]');
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
}
|
||||
checkboxes.forEach(cb => cb.checked = e.target.checked);
|
||||
});
|
||||
document.getElementById('select_all_binary').addEventListener('change', function(e) {
|
||||
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="binary_mass_actions_form"]');
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
}
|
||||
checkboxes.forEach(cb => cb.checked = e.target.checked);
|
||||
});
|
||||
|
||||
// Inicjalizacja zakładek Bootstrap (jeśli nie są już inicjowane globalnie)
|
||||
@ -222,5 +221,11 @@ triggerTabList.forEach(function (triggerEl) {
|
||||
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>
|
||||
{% endblock %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user