naprawa bledow i poprawki ux

This commit is contained in:
Mateusz Gruszczyński 2025-02-23 00:24:42 +01:00
parent ad6771c290
commit 31c898ba0c
11 changed files with 331 additions and 144 deletions

6
.gitignore vendored
View File

@ -1,4 +1,4 @@
__pycache__ __pycache__
data data/
instance instance/
venv venv/

110
app.py
View File

@ -5,6 +5,7 @@ import atexit
import io import io
import zipfile import zipfile
import requests import requests
import re
import smtplib import smtplib
import shutil import shutil
import socket import socket
@ -16,24 +17,27 @@ 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 difflib import shutil
from datetime import datetime
from difflib import HtmlDiff from difflib import HtmlDiff
import difflib import difflib
#from flask_wtf.csrf import CSRFProtect
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy import text
from flask import ( from flask import (
Flask, render_template, request, redirect, Flask, render_template, request, redirect,
url_for, session, flash, send_file url_for, session, flash, send_file
) )
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from passlib.hash import bcrypt from passlib.hash import bcrypt
#from flask_wtf.csrf import CSRFProtect
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
# REGEX dla nazwy urzadzenia
ALLOWED_NAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$')
############################################################################### ###############################################################################
# Konfiguracja Flask # Konfiguracja Flask
############################################################################### ###############################################################################
@ -63,7 +67,6 @@ class User(db.Model):
def check_password(self, password): def check_password(self, password):
return bcrypt.verify(password, self.password_hash) return bcrypt.verify(password, self.password_hash)
class Router(db.Model): class Router(db.Model):
__tablename__ = 'routers' __tablename__ = 'routers'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -84,15 +87,12 @@ 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)
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
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
message = db.Column(db.Text, nullable=False) message = db.Column(db.Text, nullable=False)
timestamp = db.Column(db.DateTime, default=datetime.utcnow) timestamp = db.Column(db.DateTime, default=datetime.utcnow)
class GlobalSettings(db.Model): class GlobalSettings(db.Model):
__tablename__ = 'global_settings' __tablename__ = 'global_settings'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@ -111,6 +111,7 @@ class GlobalSettings(db.Model):
smtp_port = db.Column(db.Integer, default=587) smtp_port = db.Column(db.Integer, default=587)
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)
############################################################################### ###############################################################################
# Inicjalizacja bazy # Inicjalizacja bazy
@ -371,6 +372,9 @@ def send_pushover(token, userkey, message, title="RouterOS Backup"):
return False return False
def send_mail_with_attachment(smtp_host, smtp_port, smtp_user, smtp_pass, to_address, subject, plain_body, attachment_path="", html_body=None): def send_mail_with_attachment(smtp_host, smtp_port, smtp_user, smtp_pass, to_address, subject, plain_body, attachment_path="", html_body=None):
if not (smtp_host and smtp_host.strip() and smtp_user and smtp_user.strip() and smtp_pass and smtp_pass.strip()):
print("SMTP not properly configured, skipping email sending.")
return False
try: try:
# Utwórz wiadomość typu alternative (obsługuje plain text i HTML) # Utwórz wiadomość typu alternative (obsługuje plain text i HTML)
msg = MIMEMultipart("alternative") msg = MIMEMultipart("alternative")
@ -403,10 +407,13 @@ def send_mail_with_attachment(smtp_host, smtp_port, smtp_user, smtp_pass, to_add
server.send_message(msg) server.send_message(msg)
return True return True
except Exception as e: except Exception as e:
print("Mail error:", e) print("Mail error_send_mail_with_attachment:", e)
return False return False
def send_mail(smtp_host, smtp_port, smtp_user, smtp_pass, to_address, subject, body): def send_mail(smtp_host, smtp_port, smtp_user, smtp_pass, to_address, subject, body):
if not (smtp_host and smtp_user and smtp_pass):
print("SMTP not configured, skipping email")
return False
try: try:
msg = MIMEMultipart() msg = MIMEMultipart()
msg["From"] = smtp_user msg["From"] = smtp_user
@ -420,7 +427,7 @@ def send_mail(smtp_host, smtp_port, smtp_user, smtp_pass, to_address, subject, b
server.send_message(msg) server.send_message(msg)
return True return True
except Exception as e: except Exception as e:
print("Mail error:", e) print("Mail error_send_mail:", e)
return False return False
def notify(settings: GlobalSettings, message: str, success: bool): def notify(settings: GlobalSettings, message: str, success: bool):
@ -428,16 +435,25 @@ def notify(settings: GlobalSettings, message: str, success: bool):
return return
if settings.pushover_token and settings.pushover_userkey: if settings.pushover_token and settings.pushover_userkey:
send_pushover(settings.pushover_token, settings.pushover_userkey, message) send_pushover(settings.pushover_token, settings.pushover_userkey, message)
if settings.smtp_host and settings.smtp_login and settings.smtp_password: # Wysyłka maila tylko jeśli SMTP Notifications są włączone
send_mail_with_attachment( if settings.smtp_notifications_enabled:
smtp_host=settings.smtp_host, if (settings.smtp_host and settings.smtp_host.strip() and
smtp_port=settings.smtp_port, settings.smtp_login and settings.smtp_login.strip() and
smtp_user=settings.smtp_login, settings.smtp_password and settings.smtp_password.strip()):
smtp_pass=settings.smtp_password, try:
to_address=settings.smtp_login, send_mail_with_attachment(
subject="RouterOS Backup Notification", smtp_host=settings.smtp_host.strip(),
body=message smtp_port=settings.smtp_port,
) smtp_user=settings.smtp_login.strip(),
smtp_pass=settings.smtp_password.strip(),
to_address=settings.smtp_login.strip(),
subject="RouterOS Backup Notification",
plain_body=message
)
except Exception as e:
print("SMTP send error:", e)
else:
print("SMTP configuration is incomplete. Skipping email notification.")
############################################################################### ###############################################################################
# Zadania cykliczne # Zadania cykliczne
@ -642,8 +658,6 @@ def index():
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
return render_template('index.html') return render_template('index.html')
import shutil
from datetime import datetime
# Globalna zmienna z czasem uruchomienia aplikacji # Globalna zmienna z czasem uruchomienia aplikacji
app_start_time = datetime.now() app_start_time = datetime.now()
@ -765,17 +779,23 @@ def routers_list():
routers = Router.query.filter_by(owner_id=user.id).order_by(Router.created_at.desc()).all() routers = Router.query.filter_by(owner_id=user.id).order_by(Router.created_at.desc()).all()
return render_template('routers.html', user=user, routers=routers) return render_template('routers.html', user=user, routers=routers)
@app.route('/routers/add', methods=['GET','POST']) @app.route('/routers/add', methods=['GET','POST'])
@login_required @login_required
def add_router(): def add_router():
if request.method=='POST': if request.method=='POST':
user = get_current_user() user = get_current_user()
name = request.form['name'] name = request.form['name'].strip()
host = request.form['host'] # Walidacja nazwy: tylko litery, cyfry, - i _
port = request.form.get('port','22') if not ALLOWED_NAME_REGEX.match(name):
ssh_user = request.form['ssh_user'] flash("Nazwa urządzenia może zawierać wyłącznie litery, cyfry, myślniki (-) oraz podkreślenia (_).")
ssh_key = request.form['ssh_key'] return redirect(url_for('add_router'))
ssh_password = request.form['ssh_password'] host = request.form['host'].strip()
port = request.form.get('port','22').strip()
ssh_user = request.form['ssh_user'].strip()
ssh_key = request.form['ssh_key'].strip()
ssh_password = request.form['ssh_password'].strip()
r = Router(owner_id=user.id, name=name, host=host, port=int(port), r = Router(owner_id=user.id, name=name, host=host, port=int(port),
ssh_user=ssh_user, ssh_key=ssh_key, ssh_password=ssh_password) ssh_user=ssh_user, ssh_key=ssh_key, ssh_password=ssh_password)
db.session.add(r) db.session.add(r)
@ -795,7 +815,12 @@ def router_details(router_id):
all_b = Backup.query.filter_by(router_id=router.id).order_by(Backup.created_at.desc()).all() all_b = Backup.query.filter_by(router_id=router.id).order_by(Backup.created_at.desc()).all()
export_b = [x for x in all_b if x.backup_type=='export'] export_b = [x for x in all_b if x.backup_type=='export']
bin_b = [x for x in all_b if x.backup_type=='binary'] bin_b = [x for x in all_b if x.backup_type=='binary']
return render_template('router_details.html', router=router, export_backups=export_b, binary_backups=bin_b) #return render_template('router_details_v2.html', router=router, export_backups=export_b, binary_backups=bin_b)
view = request.args.get('view', 'v2')
if view == 'v2':
return render_template('router_details_v2.html', router=router, export_backups=export_b, binary_backups=bin_b, current_view='v2')
else:
return render_template('router_details.html', router=router, export_backups=export_b, binary_backups=bin_b, current_view='v1')
@app.route('/router/<int:router_id>/export', methods=['POST']) @app.route('/router/<int:router_id>/export', methods=['POST'])
@login_required @login_required
@ -1063,6 +1088,7 @@ def settings_view():
s.pushover_token = request.form.get('pushover_token', '') s.pushover_token = request.form.get('pushover_token', '')
s.pushover_userkey = request.form.get('pushover_userkey', '') s.pushover_userkey = request.form.get('pushover_userkey', '')
s.notify_failures_only = bool(request.form.get('notify_failures_only', False)) s.notify_failures_only = bool(request.form.get('notify_failures_only', False))
s.smtp_notifications_enabled = True if request.form.get('smtp_notifications_enabled') == 'on' else False
s.smtp_host = request.form.get('smtp_host', '') s.smtp_host = request.form.get('smtp_host', '')
s.smtp_port = int(request.form.get('smtp_port', '587')) s.smtp_port = int(request.form.get('smtp_port', '587'))
s.smtp_login = request.form.get('smtp_login', '') s.smtp_login = request.form.get('smtp_login', '')
@ -1073,7 +1099,6 @@ def settings_view():
return redirect(url_for('settings_view')) return redirect(url_for('settings_view'))
return render_template('settings.html', settings=s) return render_template('settings.html', settings=s)
# Nowa zakładka: edycja routera
@app.route('/router/<int:router_id>/edit', methods=['GET','POST']) @app.route('/router/<int:router_id>/edit', methods=['GET','POST'])
@login_required @login_required
def edit_router(router_id): def edit_router(router_id):
@ -1083,15 +1108,19 @@ def edit_router(router_id):
flash("Brak routera.") flash("Brak routera.")
return redirect(url_for('routers_list')) return redirect(url_for('routers_list'))
if request.method == 'POST': if request.method == 'POST':
router.name = request.form['name'] # Przytnij wejścia i waliduj nazwę
router.host = request.form['host'] new_name = request.form['name'].strip()
router.port = int(request.form.get('port', '22')) if not ALLOWED_NAME_REGEX.match(new_name):
router.ssh_user = request.form['ssh_user'] flash("Nazwa urządzenia może zawierać wyłącznie litery, cyfry, myślniki (-) oraz podkreślenia (_).")
router.ssh_key = request.form['ssh_key'] return redirect(url_for('edit_router', router_id=router_id))
router.ssh_password = request.form['ssh_password'] router.name = new_name
router.host = request.form['host'].strip()
router.port = int(request.form.get('port', '22').strip())
router.ssh_user = request.form['ssh_user'].strip()
router.ssh_key = request.form['ssh_key'].strip()
router.ssh_password = request.form['ssh_password'].strip()
db.session.commit() db.session.commit()
flash("Zapisano zmiany w routerze.") flash("Zapisano zmiany w routerze.")
#return redirect(url_for('router_details', router_id=router.id))
return redirect(url_for('routers_list')) return redirect(url_for('routers_list'))
return render_template('edit_router.html', router=router) return render_template('edit_router.html', router=router)
@ -1247,7 +1276,7 @@ def mass_actions():
@app.route('/health', methods=['GET']) @app.route('/health', methods=['GET'])
def healthcheck(): def healthcheck():
try: try:
db.session.execute('SELECT 1') db.session.execute(text('SELECT 1'))
status = 'ok' status = 'ok'
except Exception as e: except Exception as e:
status = 'error' status = 'error'
@ -1296,7 +1325,6 @@ 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)
if __name__ == '__main__': if __name__ == '__main__':
with app.app_context(): with app.app_context():
scheduler = BackgroundScheduler() scheduler = BackgroundScheduler()
@ -1305,4 +1333,4 @@ if __name__ == '__main__':
schedule_auto_binary_backup_job() schedule_auto_binary_backup_job()
scheduler.start() scheduler.start()
atexit.register(lambda: scheduler.shutdown()) atexit.register(lambda: scheduler.shutdown())
app.run(host='0.0.0.0', port=81, use_reloader=False, debug=True) app.run(host='0.0.0.0', port=5581, use_reloader=False, debug=True)

View File

@ -1,13 +0,0 @@
{% 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

@ -48,24 +48,6 @@
</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 --> <!-- Przyciski akcji dla wszystkich routerów -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-6 d-flex justify-content-center"> <div class="col-md-6 d-flex justify-content-center">
@ -107,7 +89,7 @@
</div> </div>
<!-- Log operacji --> <!-- Log operacji -->
<div class="card shadow-sm"> <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</h5>
<table class="table table-sm table-bordered"> <table class="table table-sm table-bordered">
@ -129,5 +111,23 @@
</div> </div>
</div> </div>
<!-- Dodatkowe statystyki przeniesione na sam dół, pod logami -->
<div class="card 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>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -2,6 +2,7 @@
{% block content %} {% block content %}
<div class="container my-4"> <div class="container my-4">
<h2>Porównanie: {{ backup1.file_path|basename }} vs {{ backup2.file_path|basename }}</h2> <h2>Porównanie: {{ backup1.file_path|basename }} vs {{ backup2.file_path|basename }}</h2>
<hr>
<div id="diffContainer"></div> <div id="diffContainer"></div>
<a href="{{ url_for('router_details', router_id=backup1.router_id) }}" class="btn btn-secondary mt-3">Powrót</a> <a href="{{ url_for('router_details', router_id=backup1.router_id) }}" class="btn btn-secondary mt-3">Powrót</a>
</div> </div>

View File

@ -1,5 +1,12 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="text-end mb-3">
{% if current_view == 'v1' %}
<a href="{{ url_for('router_details', router_id=router.id, view='v2') }}" class="btn btn-outline-secondary">Przełącz na widok v2</a>
{% else %}
<a href="{{ url_for('router_details', router_id=router.id, view='v1') }}" class="btn btn-outline-secondary">Przełącz na widok v1</a>
{% endif %}
</div>
<h2>Router: {{ router.name }}</h2> <h2>Router: {{ router.name }}</h2>
<p> <p>
<strong>Host:</strong> {{ router.host }} | <strong>Host:</strong> {{ router.host }} |

View File

@ -0,0 +1,226 @@
{% extends "base.html" %}
{% block content %}
<div class="text-end mb-3">
{% if current_view == 'v1' %}
<a href="{{ url_for('router_details', router_id=router.id, view='v2') }}" class="btn btn-outline-secondary">Przełącz na widok v2</a>
{% else %}
<a href="{{ url_for('router_details', router_id=router.id, view='v1') }}" class="btn btn-outline-secondary">Przełącz na widok v1</a>
{% endif %}
</div>
<div class="container my-4">
<!-- Informacje o routerze -->
<div class="card shadow-sm mb-4">
<div class="card-header">
<h2 class="mb-0">Router: {{ router.name }}</h2>
</div>
<div class="card-body">
<p>
<strong>Host:</strong> {{ router.host }} &nbsp;|&nbsp;
<strong>Port:</strong> {{ router.port }} &nbsp;|&nbsp;
<strong>SSH User:</strong> {{ router.ssh_user }}
</p>
<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>
</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>
</div>
</div>
<!-- Zakładki z backupami -->
<ul class="nav nav-tabs" id="routerTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="export-tab" data-bs-toggle="tab" data-bs-target="#export" type="button" role="tab" aria-controls="export" aria-selected="true">Pliki /export</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="binary-tab" data-bs-toggle="tab" data-bs-target="#binary" type="button" role="tab" aria-controls="binary" aria-selected="false">Pliki binarne</button>
</li>
</ul>
<div class="tab-content" id="routerTabContent">
<!-- Zakładka /export -->
<div class="tab-pane fade show active" id="export" role="tabpanel" aria-labelledby="export-tab">
<div class="card mt-3 shadow-sm">
<div class="card-body">
{% if export_backups %}
<!-- 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">
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
</button>
</div>
</form>
<!-- Tabela z eksportami z podzielonymi kolumnami akcji -->
<table class="table table-bordered table-striped">
<thead class="table-dark">
<tr>
<th style="width: 3%;"><input type="checkbox" id="select_all_export"></th>
<th>Nazwa pliku</th>
<th>Rozmiar</th>
<th>Data</th>
<th>Diff</th>
<th>Pobierz</th>
<th>Podgląd</th>
<th>Wyślij mailem</th>
<th>Usuń</th>
</tr>
</thead>
<tbody>
{% for b in export_backups %}
<tr>
<td>
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="export_mass_actions_form">
</td>
<td>{{ b.file_path|basename }}</td>
<td>{{ b.file_path|filesize }}</td>
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</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-lg btn-info" 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">
<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">
<i class="bi bi-envelope"></i>
</button>
</form>
</td>
<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ń">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">Brak plików /export.</p>
{% endif %}
</div>
</div>
</div>
<!-- Zakładka backupów binarnych -->
<div class="tab-pane fade" id="binary" role="tabpanel" aria-labelledby="binary-tab">
<div class="card mt-3 shadow-sm">
<div class="card-body">
{% if binary_backups %}
<!-- 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">
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
</button>
</div>
</form>
<!-- Tabela z backupami binarnymi z podzielonymi kolumnami akcji -->
<table class="table table-bordered table-striped">
<thead class="table-dark">
<tr>
<th style="width: 3%;"><input type="checkbox" id="select_all_binary"></th>
<th>Nazwa pliku</th>
<th>Rozmiar</th>
<th>Data</th>
<th>Pobierz</th>
<th>Wgraj do routera</th>
<th>Wyślij mailem</th>
<th>Usuń</th>
</tr>
</thead>
<tbody>
{% for b in binary_backups %}
<tr>
<td>
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form">
</td>
<td>{{ b.file_path|basename }}</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">
<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">
<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">
<i class="bi bi-envelope"></i>
</button>
</form>
</td>
<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ń">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-muted">Brak plików binarnych.</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Skrypty do obsługi zaznaczania checkboxów w każdej zakładce -->
<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;
}
});
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;
}
});
// Inicjalizacja zakładek Bootstrap (jeśli nie są już inicjowane globalnie)
var triggerTabList = [].slice.call(document.querySelectorAll('#routerTab button'));
triggerTabList.forEach(function (triggerEl) {
var tabTrigger = new bootstrap.Tab(triggerEl);
triggerEl.addEventListener('click', function (event) {
event.preventDefault();
tabTrigger.show();
});
});
</script>
{% endblock %}

View File

@ -1,24 +0,0 @@
{% 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

@ -1,43 +0,0 @@
{% 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 %}

View File

@ -2,7 +2,7 @@
{% block content %} {% block content %}
<div class="container my-5"> <div class="container my-5">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header bg-primary text-white"> <div class="card-header">
<h2 class="mb-0">Ustawienia globalne</h2> <h2 class="mb-0">Ustawienia globalne</h2>
</div> </div>
<div class="card-body"> <div class="card-body">
@ -27,6 +27,10 @@
<!-- Sekcja SMTP --> <!-- Sekcja SMTP -->
<div class="mb-4"> <div class="mb-4">
<h4 class="mb-3">Powiadomienia - SMTP (e-mail)</h4> <h4 class="mb-3">Powiadomienia - SMTP (e-mail)</h4>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="smtp_notifications_enabled" name="smtp_notifications_enabled" {% if settings.smtp_notifications_enabled %}checked{% endif %}>
<label class="form-check-label" for="smtp_notifications_enabled">Włącz powiadomienia SMTP</label>
</div>
<div class="mb-3"> <div class="mb-3">
<label for="smtp_host" class="form-label">SMTP Host</label> <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 }}"> <input type="text" class="form-control" id="smtp_host" name="smtp_host" value="{{ settings.smtp_host }}">

View File

@ -2,6 +2,7 @@
{% block content %} {% block content %}
<div class="container my-4"> <div class="container my-4">
<h2>Podgląd eksportu: {{ backup.file_path|basename }}</h2> <h2>Podgląd eksportu: {{ backup.file_path|basename }}</h2>
<hr>
<textarea id="exportEditor" readonly>{{ content|e }}</textarea> <textarea id="exportEditor" readonly>{{ content|e }}</textarea>
<a href="{{ next_url }}" class="btn btn-secondary">Powrót</a> <a href="{{ next_url }}" class="btn btn-secondary">Powrót</a>
</div> </div>