naprawa bledow i poprawki ux
This commit is contained in:
parent
ad6771c290
commit
31c898ba0c
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
__pycache__
|
||||
data
|
||||
instance
|
||||
venv
|
||||
data/
|
||||
instance/
|
||||
venv/
|
||||
|
102
app.py
102
app.py
@ -5,6 +5,7 @@ import atexit
|
||||
import io
|
||||
import zipfile
|
||||
import requests
|
||||
import re
|
||||
import smtplib
|
||||
import shutil
|
||||
import socket
|
||||
@ -16,24 +17,27 @@ from email.mime.text import MIMEText
|
||||
from email import encoders
|
||||
from flask import jsonify
|
||||
from flask import Flask
|
||||
#import difflib
|
||||
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from difflib import HtmlDiff
|
||||
import difflib
|
||||
|
||||
#from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import text
|
||||
|
||||
from flask import (
|
||||
Flask, render_template, request, redirect,
|
||||
url_for, session, flash, send_file
|
||||
)
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from passlib.hash import bcrypt
|
||||
#from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
# REGEX dla nazwy urzadzenia
|
||||
ALLOWED_NAME_REGEX = re.compile(r'^[A-Za-z0-9_-]+$')
|
||||
|
||||
###############################################################################
|
||||
# Konfiguracja Flask
|
||||
###############################################################################
|
||||
@ -63,7 +67,6 @@ class User(db.Model):
|
||||
def check_password(self, password):
|
||||
return bcrypt.verify(password, self.password_hash)
|
||||
|
||||
|
||||
class Router(db.Model):
|
||||
__tablename__ = 'routers'
|
||||
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
|
||||
backup_type = db.Column(db.String(50), default='export') # 'export' lub 'binary'
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
|
||||
class OperationLog(db.Model):
|
||||
__tablename__ = 'operation_logs'
|
||||
__table_args__ = {'extend_existing': True} # Zapobiega redefinicji tabeli
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
message = db.Column(db.Text, nullable=False)
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
class GlobalSettings(db.Model):
|
||||
__tablename__ = 'global_settings'
|
||||
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_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)
|
||||
|
||||
###############################################################################
|
||||
# Inicjalizacja bazy
|
||||
@ -371,6 +372,9 @@ def send_pushover(token, userkey, message, title="RouterOS Backup"):
|
||||
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):
|
||||
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:
|
||||
# Utwórz wiadomość typu alternative (obsługuje plain text i HTML)
|
||||
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)
|
||||
return True
|
||||
except Exception as e:
|
||||
print("Mail error:", e)
|
||||
print("Mail error_send_mail_with_attachment:", e)
|
||||
return False
|
||||
|
||||
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:
|
||||
msg = MIMEMultipart()
|
||||
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)
|
||||
return True
|
||||
except Exception as e:
|
||||
print("Mail error:", e)
|
||||
print("Mail error_send_mail:", e)
|
||||
return False
|
||||
|
||||
def notify(settings: GlobalSettings, message: str, success: bool):
|
||||
@ -428,16 +435,25 @@ def notify(settings: GlobalSettings, message: str, success: bool):
|
||||
return
|
||||
if settings.pushover_token and settings.pushover_userkey:
|
||||
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
|
||||
if settings.smtp_notifications_enabled:
|
||||
if (settings.smtp_host and settings.smtp_host.strip() and
|
||||
settings.smtp_login and settings.smtp_login.strip() and
|
||||
settings.smtp_password and settings.smtp_password.strip()):
|
||||
try:
|
||||
send_mail_with_attachment(
|
||||
smtp_host=settings.smtp_host,
|
||||
smtp_host=settings.smtp_host.strip(),
|
||||
smtp_port=settings.smtp_port,
|
||||
smtp_user=settings.smtp_login,
|
||||
smtp_pass=settings.smtp_password,
|
||||
to_address=settings.smtp_login,
|
||||
smtp_user=settings.smtp_login.strip(),
|
||||
smtp_pass=settings.smtp_password.strip(),
|
||||
to_address=settings.smtp_login.strip(),
|
||||
subject="RouterOS Backup Notification",
|
||||
body=message
|
||||
plain_body=message
|
||||
)
|
||||
except Exception as e:
|
||||
print("SMTP send error:", e)
|
||||
else:
|
||||
print("SMTP configuration is incomplete. Skipping email notification.")
|
||||
|
||||
###############################################################################
|
||||
# Zadania cykliczne
|
||||
@ -642,8 +658,6 @@ def index():
|
||||
return redirect(url_for('dashboard'))
|
||||
return render_template('index.html')
|
||||
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
# Globalna zmienna z czasem uruchomienia aplikacji
|
||||
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()
|
||||
return render_template('routers.html', user=user, routers=routers)
|
||||
|
||||
|
||||
|
||||
@app.route('/routers/add', methods=['GET','POST'])
|
||||
@login_required
|
||||
def add_router():
|
||||
if request.method=='POST':
|
||||
user = get_current_user()
|
||||
name = request.form['name']
|
||||
host = request.form['host']
|
||||
port = request.form.get('port','22')
|
||||
ssh_user = request.form['ssh_user']
|
||||
ssh_key = request.form['ssh_key']
|
||||
ssh_password = request.form['ssh_password']
|
||||
name = request.form['name'].strip()
|
||||
# Walidacja nazwy: tylko litery, cyfry, - i _
|
||||
if not ALLOWED_NAME_REGEX.match(name):
|
||||
flash("Nazwa urządzenia może zawierać wyłącznie litery, cyfry, myślniki (-) oraz podkreślenia (_).")
|
||||
return redirect(url_for('add_router'))
|
||||
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),
|
||||
ssh_user=ssh_user, ssh_key=ssh_key, ssh_password=ssh_password)
|
||||
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()
|
||||
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']
|
||||
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'])
|
||||
@login_required
|
||||
@ -1063,6 +1088,7 @@ def settings_view():
|
||||
s.pushover_token = request.form.get('pushover_token', '')
|
||||
s.pushover_userkey = request.form.get('pushover_userkey', '')
|
||||
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_port = int(request.form.get('smtp_port', '587'))
|
||||
s.smtp_login = request.form.get('smtp_login', '')
|
||||
@ -1073,7 +1099,6 @@ def settings_view():
|
||||
return redirect(url_for('settings_view'))
|
||||
return render_template('settings.html', settings=s)
|
||||
|
||||
# Nowa zakładka: edycja routera
|
||||
@app.route('/router/<int:router_id>/edit', methods=['GET','POST'])
|
||||
@login_required
|
||||
def edit_router(router_id):
|
||||
@ -1083,15 +1108,19 @@ def edit_router(router_id):
|
||||
flash("Brak routera.")
|
||||
return redirect(url_for('routers_list'))
|
||||
if request.method == 'POST':
|
||||
router.name = request.form['name']
|
||||
router.host = request.form['host']
|
||||
router.port = int(request.form.get('port', '22'))
|
||||
router.ssh_user = request.form['ssh_user']
|
||||
router.ssh_key = request.form['ssh_key']
|
||||
router.ssh_password = request.form['ssh_password']
|
||||
# Przytnij wejścia i waliduj nazwę
|
||||
new_name = request.form['name'].strip()
|
||||
if not ALLOWED_NAME_REGEX.match(new_name):
|
||||
flash("Nazwa urządzenia może zawierać wyłącznie litery, cyfry, myślniki (-) oraz podkreślenia (_).")
|
||||
return redirect(url_for('edit_router', router_id=router_id))
|
||||
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()
|
||||
flash("Zapisano zmiany w routerze.")
|
||||
#return redirect(url_for('router_details', router_id=router.id))
|
||||
return redirect(url_for('routers_list'))
|
||||
return render_template('edit_router.html', router=router)
|
||||
|
||||
@ -1247,7 +1276,7 @@ def mass_actions():
|
||||
@app.route('/health', methods=['GET'])
|
||||
def healthcheck():
|
||||
try:
|
||||
db.session.execute('SELECT 1')
|
||||
db.session.execute(text('SELECT 1'))
|
||||
status = 'ok'
|
||||
except Exception as e:
|
||||
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.html", router=router, result=result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
with app.app_context():
|
||||
scheduler = BackgroundScheduler()
|
||||
@ -1305,4 +1333,4 @@ if __name__ == '__main__':
|
||||
schedule_auto_binary_backup_job()
|
||||
scheduler.start()
|
||||
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)
|
||||
|
@ -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 %}
|
@ -48,24 +48,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dodatkowe statystyki -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Dodatkowe statystyki</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Czas działania:</strong> {{ uptime }}</p>
|
||||
<p><strong>Aktualny czas:</strong> {{ current_time.strftime('%Y-%m-%d %H:%M:%S') }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Całkowity rozmiar dysku:</strong> {{ disk_total|filesize }}</p>
|
||||
<p><strong>Zajęte (/data):</strong> {{ disk_used|filesize }} ({{ disk_usage_percent|round(2) }}%)</p>
|
||||
<p><strong>Wolne:</strong> {{ disk_free|filesize }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Przyciski akcji dla wszystkich routerów -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6 d-flex justify-content-center">
|
||||
@ -107,7 +89,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Log operacji -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Log operacji</h5>
|
||||
<table class="table table-sm table-bordered">
|
||||
@ -129,5 +111,23 @@
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% block content %}
|
||||
<div class="container my-4">
|
||||
<h2>Porównanie: {{ backup1.file_path|basename }} vs {{ backup2.file_path|basename }}</h2>
|
||||
<hr>
|
||||
<div id="diffContainer"></div>
|
||||
<a href="{{ url_for('router_details', router_id=backup1.router_id) }}" class="btn btn-secondary mt-3">Powrót</a>
|
||||
</div>
|
||||
|
@ -1,5 +1,12 @@
|
||||
{% 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>
|
||||
<h2>Router: {{ router.name }}</h2>
|
||||
<p>
|
||||
<strong>Host:</strong> {{ router.host }} |
|
||||
|
226
templates/router_details_v2.html
Normal file
226
templates/router_details_v2.html
Normal 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 }} |
|
||||
<strong>Port:</strong> {{ router.port }} |
|
||||
<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 %}
|
@ -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 %}
|
@ -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 %}
|
@ -2,7 +2,7 @@
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0">Ustawienia globalne</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@ -27,6 +27,10 @@
|
||||
<!-- Sekcja SMTP -->
|
||||
<div class="mb-4">
|
||||
<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">
|
||||
<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 }}">
|
||||
|
@ -2,6 +2,7 @@
|
||||
{% block content %}
|
||||
<div class="container my-4">
|
||||
<h2>Podgląd eksportu: {{ backup.file_path|basename }}</h2>
|
||||
<hr>
|
||||
<textarea id="exportEditor" readonly>{{ content|e }}</textarea>
|
||||
<a href="{{ next_url }}" class="btn btn-secondary">Powrót</a>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user