Compare commits

...

45 Commits

Author SHA1 Message Date
Mateusz Gruszczyński
911aaab005 git status! RFACTOR TEMPLATE git status! 2025-02-28 16:26:27 +01:00
Mateusz Gruszczyński
d40511823f zmiana w README 2025-02-26 08:15:19 +01:00
Mateusz Gruszczyński
26538f57f8 zmiana w README 2025-02-26 08:14:43 +01:00
Mateusz Gruszczyński
1d41d303f3 zmiana obslugi haseł, docker/podman 2025-02-26 08:10:56 +01:00
gru
e4b33cb0d3 Update templates/base.html 2025-02-25 23:42:55 +01:00
Mateusz Gruszczyński
6167cd7983 male zmiany w css 2025-02-25 23:01:37 +01:00
Mateusz Gruszczyński
77074d2789 male zmiany w css 2025-02-25 23:00:21 +01:00
Mateusz Gruszczyński
967e1bee27 male zmiany w css 2025-02-25 22:59:38 +01:00
Mateusz Gruszczyński
b7d3b0a7e1 male zmiany w css 2025-02-25 22:57:06 +01:00
Mateusz Gruszczyński
6ac006e355 male zmiany w css 2025-02-25 22:54:28 +01:00
Mateusz Gruszczyński
7e9ce9894f male zmiany w css 2025-02-25 22:53:57 +01:00
Mateusz Gruszczyński
68c93631ff male zmiany w css 2025-02-25 22:53:21 +01:00
Mateusz Gruszczyński
d4ff3ff635 male zmiany w css 2025-02-25 22:52:16 +01:00
Mateusz Gruszczyński
16c9a75253 male zmiany w css 2025-02-25 22:44:58 +01:00
Mateusz Gruszczyński
d66797312d male zmiany w css 2025-02-25 22:35:38 +01:00
Mateusz Gruszczyński
4bac59a118 male zmiany w css 2025-02-25 22:32:58 +01:00
Mateusz Gruszczyński
ab88b752f5 male zmiany w css 2025-02-25 22:20:32 +01:00
Mateusz Gruszczyński
a8d00cc807 male zmoiany w css 2025-02-25 22:14:23 +01:00
Mateusz Gruszczyński
ac2c9415c0 dark mode 2025-02-25 16:43:26 +01:00
Mateusz Gruszczyński
5705884e5c dark mode 2025-02-25 16:25:14 +01:00
Mateusz Gruszczyński
a9f954b7b3 dark mode 2025-02-25 12:42:46 +01:00
Mateusz Gruszczyński
d684c331bd dark mode 2025-02-25 12:37:57 +01:00
Mateusz Gruszczyński
e5b787213d new options and functions 2025-02-24 11:31:50 +01:00
Mateusz Gruszczyński
9189fe83bb 465/587 smtp 2025-02-24 08:57:32 +01:00
Mateusz Gruszczyński
8dd96a68f2 fix cron on start, move from nicorn to waitress 2025-02-24 08:53:06 +01:00
Mateusz Gruszczyński
972e7c61cd fix cron on start, move from nicorn to waitress 2025-02-24 08:42:51 +01:00
Mateusz Gruszczyński
0689e887ea fix cron on start 2025-02-24 08:35:40 +01:00
Mateusz Gruszczyński
05abc1c795 fix cron on start 2025-02-24 08:26:21 +01:00
Mateusz Gruszczyński
9b2d2ac7d9 fix cron on start 2025-02-24 08:19:24 +01:00
Mateusz Gruszczyński
0a9b0f22b8 fix cron on start 2025-02-23 23:19:33 +01:00
Mateusz Gruszczyński
c9a5bfaaf8 fix cron on start 2025-02-23 23:17:02 +01:00
Mateusz Gruszczyński
011590612e fix cron on start 2025-02-23 23:16:18 +01:00
Mateusz Gruszczyński
3456bd21ae fix cron on start 2025-02-23 23:14:10 +01:00
Mateusz Gruszczyński
27f969cc4b fix cron on start 2025-02-23 23:08:30 +01:00
Mateusz Gruszczyński
1e910e347c fix cron on start 2025-02-23 23:05:42 +01:00
Mateusz Gruszczyński
5ddd522b6f fix cron on start 2025-02-23 23:02:55 +01:00
Mateusz Gruszczyński
c80dbdaee1 fix cron on start 2025-02-23 22:50:44 +01:00
Mateusz Gruszczyński
b22134f15b poprawa eksport na export 2025-02-23 13:11:11 +01:00
Mateusz Gruszczyński
2b9a63a2b6 poprawa eksport na export 2025-02-23 13:10:35 +01:00
Mateusz Gruszczyński
7c8d3a0c86 poprawa eksport na export 2025-02-23 13:08:37 +01:00
Mateusz Gruszczyński
378fd7aade dodanie zadania 2025-02-23 12:49:32 +01:00
Mateusz Gruszczyński
92c8749bd8 dodanie zadania 2025-02-23 10:42:04 +01:00
Mateusz Gruszczyński
55d417708f naprawa redirectu 2025-02-23 10:36:27 +01:00
Mateusz Gruszczyński
94385e7bda naprawa błędów i nowe funkcje 2025-02-23 10:31:58 +01:00
Mateusz Gruszczyński
f39a4a9414 naprawa błędów i nowe funkcje 2025-02-23 10:31:15 +01:00
27 changed files with 1219 additions and 505 deletions

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "run_waitress.py"]

107
README.md
View File

@@ -1,22 +1,101 @@
# RouterOS Backup Manager
# RouterOS backup system RouterOS Backup Manager to aplikacja Flask umożliwiająca zarządzanie kopiami zapasowymi urządzeń Mikrotik RouterOS. Aplikacja pozwala na eksport konfiguracji, tworzenie backupów binarnych, ich przechowywanie, porównywanie oraz przywracanie.
1. Instalation: ## 🔧 Instalacja
- clone (to ex. /opt/routeros_backup)
- create venv
- install requirements via pip
- copy systemd service (routeros_backup.service)
2. Start ### 1. Klonowanie repozytorium
- systemctl start routeros_backup.service ```sh
- go to http://<IP>:81 git clone https://gitea.linuxiarz.pl/gru/routeros_backup.git
cd routeros_backup
```
3. Register, Login ### 2. Tworzenie i aktywacja środowiska wirtualnego (opcjonalnie)
```sh
python3 -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
```
4. Configure devices, keys, backups ### 3. Instalacja zależności
```sh
pip install -r requirements.txt
```
5. Done ### 4. Uruchomienie aplikacji lokalnie
```sh
python run_waitress.py
```
Aplikacja będzie dostępna pod adresem: `http://127.0.0.1:5581/`
## Authors ---
- [@linuxiarz.pl] ## 📦 Uruchamianie w Dockerze
1. **Zbudowanie obrazu Docker**
```sh
docker-compose build
```
2. **Uruchomienie kontenera**
```sh
docker-compose up -d
```
Aplikacja uruchomi się na porcie `5581`.
---
## 📚 Funkcjonalności
- 🔐 System użytkowników (rejestracja, logowanie, zmiana hasła)
- 📡 Połączenie SSH do routerów MikroTik
- 🛠 Eksport konfiguracji i tworzenie backupów binarnych
- 🕵️‍♂️ Porównywanie backupów (`diff`)
- 📩 Powiadomienia e-mail oraz Pushover
- 📅 Harmonogram automatycznych backupów (APScheduler)
- 🧹 Automatyczne czyszczenie starych backupów i logów
- 🚀 Obsługa przez interfejs webowy
---
## ⚙️ Konfiguracja
### Zmiana ustawień
Plik `app.py` zawiera konfigurację bazy danych oraz inne ustawienia aplikacji:
```python
app.config['SECRET_KEY'] = 'super-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///backup_routeros.db'
```
### 📬 Konfiguracja SMTP (E-mail)
Aby skonfigurować powiadomienia e-mail, wprowadź dane w sekcji ustawień:
- Serwer SMTP
- Login/hasło SMTP
- Port (587 dla TLS, 465 dla SSL)
### 📲 Powiadomienia Pushover
Aby włączyć powiadomienia Pushover, uzupełnij `pushover_token` oraz `pushover_userkey` w ustawieniach.
---
## 🔍 API & Health Check
Aplikacja zawiera endpoint `/health`, który zwraca status bazy danych:
```sh
curl http://127.0.0.1:5581/health
```
Przykładowa odpowiedź:
```json
{
"status": "ok",
"timestamp": "2024-02-26T12:34:56Z"
}
```
---
## 🚀 Autor i licencja
Projekt stworzony przez Mateusz Grusczyński @linuxiarz.pl - dostępny na licencji MIT.

224
app.py
View File

@@ -9,18 +9,16 @@ import re
import smtplib import smtplib
import shutil import shutil
import socket import socket
import hashlib
from datetime import datetime from datetime import datetime
from ftplib import FTP
from email.mime.base import MIMEBase from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email import encoders from email import encoders
from flask import jsonify from flask import jsonify
from flask import Flask from flask import Flask
import shutil
from datetime import datetime from datetime import datetime
from difflib import HtmlDiff from difflib import HtmlDiff
import difflib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy import text from sqlalchemy import text
@@ -29,7 +27,7 @@ from flask import (
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.context import CryptContext
#from flask_wtf.csrf import CSRFProtect #from flask_wtf.csrf import CSRFProtect
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
@@ -58,14 +56,18 @@ os.makedirs(DATA_DIR, exist_ok=True)
############################################################################### ###############################################################################
# Modele bazy danych # Modele bazy danych
############################################################################### ###############################################################################
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class User(db.Model): class User(db.Model):
__tablename__ = 'users' __tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(120), unique=True, nullable=False) username = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(255), nullable=False) password_hash = db.Column(db.String(255), nullable=False)
def set_password(self, password):
self.password_hash = pwd_context.hash(password)
def check_password(self, password): def check_password(self, password):
return bcrypt.verify(password, self.password_hash) return pwd_context.verify(password, self.password_hash)
class Router(db.Model): class Router(db.Model):
__tablename__ = 'routers' __tablename__ = 'routers'
@@ -87,6 +89,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 +116,8 @@ 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)
recipient_email = db.Column(db.String(255), nullable=True)
############################################################################### ###############################################################################
# Inicjalizacja bazy # Inicjalizacja bazy
@@ -166,6 +172,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 +242,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 +263,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 +274,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()
@@ -376,23 +397,19 @@ def send_mail_with_attachment(smtp_host, smtp_port, smtp_user, smtp_pass, to_add
print("SMTP not properly configured, skipping email sending.") print("SMTP not properly configured, skipping email sending.")
return False return False
try: try:
# Utwórz wiadomość typu alternative (obsługuje plain text i HTML)
msg = MIMEMultipart("alternative") msg = MIMEMultipart("alternative")
msg["From"] = smtp_user msg["From"] = smtp_user
msg["To"] = to_address msg["To"] = to_address
msg["Subject"] = subject msg["Subject"] = subject
# Dodaj część tekstową
part1 = MIMEText(plain_body, "plain") part1 = MIMEText(plain_body, "plain")
msg.attach(part1) msg.attach(part1)
# Jeśli nie podano wersji HTML, wygeneruj domyślny szablon
if html_body is None: if html_body is None:
html_body = get_email_template(subject, plain_body) html_body = get_email_template(subject, plain_body)
part2 = MIMEText(html_body, "html") part2 = MIMEText(html_body, "html")
msg.attach(part2) msg.attach(part2)
# Dodaj załącznik, jeśli podany i istnieje
if attachment_path and os.path.isfile(attachment_path): if attachment_path and os.path.isfile(attachment_path):
with open(attachment_path, "rb") as attachment: with open(attachment_path, "rb") as attachment:
part = MIMEBase("application", "octet-stream") part = MIMEBase("application", "octet-stream")
@@ -401,10 +418,19 @@ def send_mail_with_attachment(smtp_host, smtp_port, smtp_user, smtp_pass, to_add
part.add_header("Content-Disposition", f"attachment; filename={os.path.basename(attachment_path)}") part.add_header("Content-Disposition", f"attachment; filename={os.path.basename(attachment_path)}")
msg.attach(part) msg.attach(part)
with smtplib.SMTP(smtp_host, smtp_port) as server: if smtp_port == 465:
server.starttls() server = smtplib.SMTP_SSL(smtp_host, smtp_port)
server.login(smtp_user, smtp_pass) server.login(smtp_user, smtp_pass)
server.send_message(msg) server.send_message(msg)
server.quit()
else:
server = smtplib.SMTP(smtp_host, smtp_port)
server.ehlo()
if smtp_port == 587:
server.starttls()
server.login(smtp_user, smtp_pass)
server.send_message(msg)
server.quit()
return True return True
except Exception as e: except Exception as e:
print("Mail error_send_mail_with_attachment:", e) print("Mail error_send_mail_with_attachment:", e)
@@ -441,12 +467,13 @@ def notify(settings: GlobalSettings, message: str, success: bool):
settings.smtp_login and settings.smtp_login.strip() and settings.smtp_login and settings.smtp_login.strip() and
settings.smtp_password and settings.smtp_password.strip()): settings.smtp_password and settings.smtp_password.strip()):
try: try:
to_address = settings.recipient_email.strip() if settings.recipient_email and settings.recipient_email.strip() else settings.smtp_login.strip()
send_mail_with_attachment( send_mail_with_attachment(
smtp_host=settings.smtp_host.strip(), smtp_host=settings.smtp_host.strip(),
smtp_port=settings.smtp_port, smtp_port=settings.smtp_port,
smtp_user=settings.smtp_login.strip(), smtp_user=settings.smtp_login.strip(),
smtp_pass=settings.smtp_password.strip(), smtp_pass=settings.smtp_password.strip(),
to_address=settings.smtp_login.strip(), to_address=to_address,
subject="RouterOS Backup Notification", subject="RouterOS Backup Notification",
plain_body=message plain_body=message
) )
@@ -491,10 +518,10 @@ def scheduled_auto_backup():
db.session.add(b) db.session.add(b)
db.session.commit() db.session.commit()
notify(s, f"Auto-export dla routera {r.name} OK", True) notify(s, f"Auto-export dla routera {r.name} OK", True)
log_operation(f"Automatyczny eksport dla routera {r.name} wykonany pomyślnie at {datetime.utcnow()}.") log_operation(f"Automatyczny export dla routera {r.name} wykonany pomyślnie at {datetime.utcnow()}.")
except Exception as e: except Exception as e:
notify(s, f"Auto-export dla routera {r.name} FAILED: {e}", False) notify(s, f"Auto-export dla routera {r.name} FAILED: {e}", False)
log_operation(f"Automatyczny eksport dla routera {r.name} FAILED at {datetime.utcnow()}: {e}") log_operation(f"Automatyczny export dla routera {r.name} FAILED at {datetime.utcnow()}: {e}")
def scheduled_auto_binary_backup(): def scheduled_auto_binary_backup():
with app.app_context(): with app.app_context():
@@ -504,7 +531,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)
@@ -568,6 +596,19 @@ def schedule_auto_export_job():
cron_used = s.export_cron if s.export_cron else "0 */12 * * *" cron_used = s.export_cron if s.export_cron else "0 */12 * * *"
print(f"[DEBUG] schedule_auto_export_job -> cron_schedule={cron_used}") print(f"[DEBUG] schedule_auto_export_job -> cron_schedule={cron_used}")
def cleanup_old_logs():
with app.app_context():
s = get_settings()
cutoff_date = datetime.utcnow() - timedelta(days=s.log_retention_days)
old_logs = OperationLog.query.filter(OperationLog.timestamp < cutoff_date).all()
deleted_count = len(old_logs)
for log in old_logs:
db.session.delete(log)
db.session.commit()
log_operation(f"Automatyczna retencja logów: usunięto {deleted_count} logów starszych niż {s.log_retention_days} dni.")
############################################################################### ###############################################################################
# Konfiguracja APScheduler - harmonogram zadań # Konfiguracja APScheduler - harmonogram zadań
############################################################################### ###############################################################################
@@ -580,8 +621,8 @@ scheduler = BackgroundScheduler()
# Dodajemy z unikalnymi ID, co ułatwia re-schedulowanie # Dodajemy z unikalnymi ID, co ułatwia re-schedulowanie
scheduler.add_job(func=cleanup_old_backups, trigger='interval', days=1, id="cleanup_job") scheduler.add_job(func=cleanup_old_backups, trigger='interval', days=1, id="cleanup_job")
scheduler.add_job(func=scheduled_auto_backup, trigger='interval', days=1, id="auto_backup_job") scheduler.add_job(func=scheduled_auto_backup, trigger='interval', days=1, id="auto_backup_job")
scheduler.start() scheduler.start()
# Sprzątanie przy zamykaniu # Sprzątanie przy zamykaniu
atexit.register(lambda: scheduler.shutdown()) atexit.register(lambda: scheduler.shutdown())
@@ -602,6 +643,7 @@ def reschedule_jobs():
schedule_auto_export_job() schedule_auto_export_job()
schedule_retention_job() schedule_retention_job()
schedule_auto_binary_backup_job() schedule_auto_binary_backup_job()
scheduler.add_job(func=cleanup_old_logs, trigger='interval', days=1, id="cleanup_logs_job", replace_existing=True)
############################################################################### ###############################################################################
# Filtr Jinja2 - basename # Filtr Jinja2 - basename
@@ -649,6 +691,19 @@ def get_data_folder_size():
pass pass
return total_size return total_size
@app.template_global()
def bootstrap_alert_category(cat):
mapping = {
"error": "danger",
"fail": "danger",
"warn": "warning",
"warning": "warning",
"ok": "success",
"success": "success",
"info": "info"
}
return mapping.get(cat.lower(), "info")
############################################################################### ###############################################################################
# ROUTES # ROUTES
############################################################################### ###############################################################################
@@ -720,19 +775,20 @@ 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.log_retention_days = int(request.form.get('log_retention_days', s.log_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("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)
@app.route('/toggle_dark_mode') @app.route('/toggle_dark_mode')
def toggle_dark_mode(): def toggle_dark_mode():
current_mode = session.get('dark_mode', False) current_mode = session.get('dark_mode', True)
session['dark_mode'] = not current_mode session['dark_mode'] = not current_mode
return redirect(request.referrer or url_for('index')) return redirect(request.referrer or url_for('index'))
@@ -779,8 +835,6 @@ 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():
@@ -845,8 +899,8 @@ def router_export(router_id):
notify(get_settings(), f"Export {router.name} OK", True) notify(get_settings(), f"Export {router.name} OK", True)
log_operation(f"Export wykonany dla routera {router.name} at {datetime.utcnow()}") log_operation(f"Export wykonany dla routera {router.name} at {datetime.utcnow()}")
if request.headers.get("X-Requested-With") == "XMLHttpRequest": if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return {"status": "success", "message": "Eksport zakończony."} return {"status": "success", "message": "export zakończony."}
flash("Eksport zakończony.") flash("Export zakończony.")
except Exception as e: except Exception as e:
notify(get_settings(), f"Export {router.name} FAIL: {e}", False) notify(get_settings(), f"Export {router.name} FAIL: {e}", False)
log_operation(f"Export dla routera {router.name} FAILED: {e}") log_operation(f"Export dla routera {router.name} FAILED: {e}")
@@ -866,7 +920,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)
@@ -889,14 +944,23 @@ def upload_backup(router_id, backup_id):
b = Backup.query.filter_by(id=backup_id, router_id=router.id, backup_type='binary').first() b = Backup.query.filter_by(id=backup_id, router_id=router.id, backup_type='binary').first()
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))
next_url = request.form.get('next') or request.referrer or url_for('dashboard')
return redirect(next_url)
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))
next_url = request.form.get('next') or request.referrer or url_for('dashboard')
return redirect(next_url)
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)
@@ -964,7 +1028,6 @@ def export_all_routers():
flash(" | ".join(messages)) flash(" | ".join(messages))
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
# Nowa podstrona: diff selector
@app.route('/diff_selector', methods=['GET', 'POST']) @app.route('/diff_selector', methods=['GET', 'POST'])
@login_required @login_required
def diff_selector(): def diff_selector():
@@ -984,13 +1047,11 @@ def diff_selector():
def all_files(): def all_files():
user = get_current_user() user = get_current_user()
query = Backup.query.join(Router).filter(Router.owner_id == user.id) query = Backup.query.join(Router).filter(Router.owner_id == user.id)
# Filtrowanie wyszukiwanie po nazwie pliku (zastosowanie filtru "basename") # Filtrowanie wyszukiwanie po nazwie pliku (zastosowanie filtru "basename")
search = request.args.get('search', '') search = request.args.get('search', '')
if search: if search:
query = query.filter(Backup.file_path.ilike(f"%{search}%")) query = query.filter(Backup.file_path.ilike(f"%{search}%"))
# Sortowanie sort_by i order
sort_by = request.args.get('sort_by', 'created_at') sort_by = request.args.get('sort_by', 'created_at')
order = request.args.get('order', 'desc') order = request.args.get('order', 'desc')
if sort_by not in ['created_at', 'file_path']: if sort_by not in ['created_at', 'file_path']:
@@ -1020,7 +1081,7 @@ def view_export(backup_id):
flash("Brak dostępu do backupu.") flash("Brak dostępu do backupu.")
return redirect(url_for('all_files')) return redirect(url_for('all_files'))
if b.backup_type != 'export': if b.backup_type != 'export':
flash("Wybrany backup nie jest plikiem eksportu.") flash("Wybrany backup nie jest plikiem exportu.")
return redirect(url_for('all_files')) return redirect(url_for('all_files'))
try: try:
with open(b.file_path, 'r', encoding='utf-8') as f: with open(b.file_path, 'r', encoding='utf-8') as f:
@@ -1044,10 +1105,10 @@ def send_export_email(backup_id):
flash("Nie skonfigurowano ustawień SMTP w panelu.") flash("Nie skonfigurowano ustawień SMTP w panelu.")
return redirect(url_for('settings_view')) return redirect(url_for('settings_view'))
subject = f"RouterOS Export: {os.path.basename(b.file_path)}" subject = f"RouterOS Export: {os.path.basename(b.file_path)}"
body = f"Przesyłam eksport {os.path.basename(b.file_path)} z routera {b.router.name}." body = f"Przesyłam export {os.path.basename(b.file_path)} z routera {b.router.name}."
if send_mail_with_attachment(s.smtp_host, s.smtp_port, s.smtp_login, s.smtp_password, if send_mail_with_attachment(s.smtp_host, s.smtp_port, s.smtp_login, s.smtp_password,
s.smtp_login, subject, body, b.file_path): s.smtp_login, subject, body, b.file_path):
flash("Wysłano eksport mailem.") flash("Wysłano export mailem.")
else: else:
flash("Błąd wysyłki mailowej.") flash("Błąd wysyłki mailowej.")
#return redirect(url_for('router_details', router_id=b.router_id)) #return redirect(url_for('router_details', router_id=b.router_id))
@@ -1093,8 +1154,9 @@ def settings_view():
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', '')
s.smtp_password = request.form.get('smtp_password', '') s.smtp_password = request.form.get('smtp_password', '')
s.recipient_email = request.form.get('recipient_email', '')
db.session.commit() db.session.commit()
reschedule_jobs() # Aktualizacja zadań zadania dotyczące backupu/CRON zostaną teraz sterowane z /advanced_schedule #reschedule_jobs() # Aktualizacja zadań zadania dotyczące backupu/CRON zostaną teraz sterowane z /advanced_schedule
flash("Zapisano ustawienia.") flash("Zapisano ustawienia.")
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)
@@ -1208,7 +1270,6 @@ def diff_view(backup_id1, backup_id2):
lineterm='' lineterm=''
)) ))
diff_text = "\n".join(diff_lines) diff_text = "\n".join(diff_lines)
return render_template('diff.html', diff_text=diff_text, backup1=b1, backup2=b2) return render_template('diff.html', diff_text=diff_text, backup1=b1, backup2=b2)
@app.route('/routers/all_backup', methods=['POST']) @app.route('/routers/all_backup', methods=['POST'])
@@ -1301,7 +1362,7 @@ def change_password():
flash("Nowe hasło i potwierdzenie nie są zgodne.") flash("Nowe hasło i potwierdzenie nie są zgodne.")
return redirect(url_for('change_password')) return redirect(url_for('change_password'))
user.password_hash = bcrypt.hash(new_password) user.password_hash = pwd_context.hash(new_password)
db.session.commit() db.session.commit()
flash("Hasło zostało zmienione pomyślnie.") flash("Hasło zostało zmienione pomyślnie.")
return redirect(url_for('dashboard')) return redirect(url_for('dashboard'))
@@ -1320,17 +1381,84 @@ def test_connection(router_id):
except Exception as e: except Exception as e:
flash(f"Błąd testu połączenia: {e}") flash(f"Błąd testu połączenia: {e}")
return redirect(url_for('routers_list')) return redirect(url_for('routers_list'))
# Jeśli wywołanie zawiera parametr modal=1, zwracamy widok dla modalu
if request.args.get("modal") == "1": if request.args.get("modal") == "1":
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'))
@app.route('/test_email', methods=['POST'])
@login_required
def test_email():
s = get_settings()
# Sprawdzamy, czy ustawienia SMTP są skonfigurowane
if not (s.smtp_host and s.smtp_login and s.smtp_password):
flash("Brak skonfigurowanych ustawień SMTP.")
return redirect(url_for('settings_view'))
subject = "Testowy e-mail z RouterOS Backup"
body = "To jest testowa wiadomość e-mail wysłana z systemu RouterOS Backup."
# Używamy recipient_email, jeśli jest ustawiony, w przeciwnym razie smtp_login
to_address = s.recipient_email.strip() if s.recipient_email and s.recipient_email.strip() else s.smtp_login.strip()
success = send_mail_with_attachment(
smtp_host=s.smtp_host,
smtp_port=s.smtp_port,
smtp_user=s.smtp_login,
smtp_pass=s.smtp_password,
to_address=to_address,
subject=subject,
plain_body=body
)
if success:
flash("Testowy e-mail został wysłany.")
else:
flash("Wysyłka testowego e-maila nie powiodła się.")
return redirect(url_for('settings_view'))
@app.route('/test_pushover', methods=['POST'])
@login_required
def test_pushover():
s = get_settings()
# Sprawdzamy, czy ustawienia Pushover są skonfigurowane
if not (s.pushover_token and s.pushover_userkey):
flash("Brak skonfigurowanych ustawień Pushover.")
return redirect(url_for('settings_view'))
message = "Testowe powiadomienie Pushover z systemu RouterOS Backup."
success = send_pushover(s.pushover_token, s.pushover_userkey, message)
if success:
flash("Testowe powiadomienie Pushover zostało wysłane.")
else:
flash("Wysyłka testowego powiadomienia Pushover nie powiodła się.")
return redirect(url_for('settings_view'))
if __name__ == '__main__': if __name__ == '__main__':
with app.app_context(): with app.app_context():
scheduler = BackgroundScheduler() reschedule_jobs()
schedule_retention_job()
schedule_auto_export_job()
schedule_auto_binary_backup_job()
scheduler.start()
atexit.register(lambda: scheduler.shutdown()) atexit.register(lambda: scheduler.shutdown())
app.run(host='0.0.0.0', port=5581, use_reloader=False, debug=True) app.run(host='0.0.0.0', port=5581, use_reloader=False, debug=True)

15
docker-compose.yml Normal file
View File

@@ -0,0 +1,15 @@
version: '3.8'
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: routeros_backup
ports:
- "5581:5581"
environment:
- FLASK_ENV=production
restart: unless-stopped
volumes:
- ./data:/data

View File

@@ -1,7 +0,0 @@
bind = "0.0.0.0:81"
workers = 4
timeout = 120
server_header = False
def on_starting(server):
server.cfg.server_header = False
server.log.info("Server header disabled")

View File

@@ -4,7 +4,8 @@ passlib
paramiko paramiko
APScheduler APScheduler
requests requests
gunicorn #gunicorn
flask_wtf flask_wtf
gevent gevent
#croniter #croniter
waitress

View File

@@ -1,18 +1,14 @@
[Unit] [Unit]
Description=RouterOS Backup Application Description=RouterOS Backup Waitress Service
After=network.target After=network.target
[Service] [Service]
#User=www-data # Zmień na odpowiedniego użytkownika #User=routeros
#Group=www-data #Group=routeros
WorkingDirectory=/opt/routeros_backup WorkingDirectory=/opt/routeros_backup
Environment="PATH=/opt/hosts_app/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" ExecStart=/opt/routeros_backup/venv/bin/python3 /opt/routeros_backup/run_waitress.py
Environment="FLASK_APP=app.py"
Environment="FLASK_ENV=production"
ExecStart=/opt/routeros_backup/venv/bin/gunicorn -c /opt/routeros_backup/gunicorn_config.py --worker-class gevent --keep-alive 10 app:app
Restart=always Restart=always
RestartSec=5 Environment=PYTHONUNBUFFERED=1
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

9
run_waitress.py Normal file
View File

@@ -0,0 +1,9 @@
from waitress import serve
from app import app, reschedule_jobs, scheduler
import atexit
with app.app_context():
reschedule_jobs()
atexit.register(lambda: scheduler.shutdown())
serve(app, host='0.0.0.0', port=5581, ident='', threads=4)

21
start.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
# Sprawdzenie, czy podman jest dostępny
if command -v podman &> /dev/null; then
COMPOSE_CMD="podman-compose"
echo "🟢 Wykryto Podman, używam podman-compose..."
else
COMPOSE_CMD="docker-compose"
echo "🔵 Podman nie znaleziony, używam docker-compose..."
fi
# Zatrzymanie i usunięcie kontenerów
$COMPOSE_CMD down
# Odbudowa obrazu
$COMPOSE_CMD build
# Uruchomienie w tle
$COMPOSE_CMD up -d
echo "✅ Aplikacja uruchomiona!"

View File

@@ -1,9 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container mt-5"> <div class="container my-4">
<div class="card shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-header bg-light">
<h2 class="mb-0">Dodaj nowe urządzenie</h2> <h4 class="mb-0">Dodaj nowe urządzenie</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST"> <form method="POST">
@@ -24,18 +24,16 @@
<input type="text" class="form-control" id="ssh_user" name="ssh_user" required> <input type="text" class="form-control" id="ssh_user" name="ssh_user" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="ssh_key" class="form-label"> <label for="ssh_key" class="form-label"><b>Klucz prywatny</b></label><br>
<b>Klucz prywatny</b> | Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code><br> <small>Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code>. Jeśli pusty użyje klucza globalnego.</small>
Pozostaw puste jeśli ten RouterOS będzie używał <a href="{{ url_for('settings_view') }}">klucza globalnego</a> <textarea class="form-control mt-2" id="ssh_key" name="ssh_key" rows="4"></textarea>
</label>
<textarea class="form-control" id="ssh_key" name="ssh_key" rows="4"></textarea>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br> <label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
Jeśli podajesz klucz SSH lub zdefiniowany jest <a href="{{ url_for('settings_view') }}">klucz globalny</a>, to logowanie hasłem jest nieaktywne. <small>Jeśli jest klucz SSH lub klucz globalny, hasło może być ignorowane.</small>
<input type="password" class="form-control" id="ssh_password" name="ssh_password"> <input type="password" class="form-control mt-2" id="ssh_password" name="ssh_password">
</div> </div>
<button type="submit" class="btn btn-primary">Dodaj</button> <button type="submit" class="btn btn-primary">Dodaj urządzenie</button>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -1,43 +1,61 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container mt-5"> <div class="container my-4">
<div class="card shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-header bg-light">
<h2 class="mb-0">Zaawansowane ustawienia harmonogramu</h2> <h4 class="mb-0">Zaawansowane ustawienia harmonogramu</h4>
</div> </div>
<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">
<label for="retention_cron" class="form-label">Harmonogram retencji (cron)</label> <label for="backup_retention_days" class="form-label">Próg retencji backupów (dni)</label>
<small class="text-muted d-block mb-2">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>
<div class="mb-3">
<label for="retention_cron" class="form-label">Harmonogram retencji <code>cron</code></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 }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('retention_cron')">Generuj cron</button> <button type="button" class="btn btn-outline-secondary" onclick="openCronModal('retention_cron')">Generuj cron</button>
</div> </div>
<div class="form-text">Np. <code>0 */12 * * *</code> co 12 godzin</div> <small class="text-muted">Np. <code>0 */12 * * *</code> co 12 godzin</small>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="binary_cron" class="form-label">Harmonogram kopii zapasowych binarnych (cron)</label> <label for="binary_cron" class="form-label">Harmonogram kopii zapasowych binarnych <code>cron</code></label>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" id="binary_cron" name="binary_cron" value="{{ settings.binary_cron|default('') }}"> <input type="text" class="form-control" id="binary_cron" name="binary_cron" value="{{ settings.binary_cron|default('') }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('binary_cron')">Generuj cron</button> <button type="button" class="btn btn-outline-secondary" onclick="openCronModal('binary_cron')">Generuj cron</button>
</div> </div>
<div class="form-text">Np. <code>15 2 * * *</code> codziennie o 2:15</div> <small class="text-muted">Np. <code>15 2 * * *</code> codziennie o 2:15</small>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="export_cron" class="form-label">Harmonogram eksportów (cron)</label> <label for="export_cron" class="form-label">Harmonogram exportów <code>cron</code></label>
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" id="export_cron" name="export_cron" value="{{ settings.export_cron }}"> <input type="text" class="form-control" id="export_cron" name="export_cron" value="{{ settings.export_cron }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('export_cron')">Generuj cron</button> <button type="button" class="btn btn-outline-secondary" onclick="openCronModal('export_cron')">Generuj cron</button>
</div> </div>
<div class="form-text">Np. <code>0 */12 * * *</code> co 12 godzin</div> <small class="text-muted">Np. <code>0 */12 * * *</code> co 12 godzin</small>
</div> </div>
<div class="mb-3 form-check"> <div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enable_auto_export" name="enable_auto_export" {% if settings.enable_auto_export %}checked{% endif %}> <input type="checkbox" class="form-check-input" id="enable_auto_export" name="enable_auto_export" {% if settings.enable_auto_export %}checked{% endif %}>
<label class="form-check-label" for="enable_auto_export">Włącz automatyczny eksport</label> <label class="form-check-label" for="enable_auto_export">Włącz automatyczny export</label>
</div> </div>
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button> <button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
</form> </form>
</div> </div>
<div class="card-footer text-center">
<small class="text-muted">Ustawienia zostaną zapisane i użyte przez cron.</small>
</div>
</div> </div>
</div> </div>
@@ -80,18 +98,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 +117,8 @@
var day = document.getElementById('cron_day').value || '*'; var day = document.getElementById('cron_day').value || '*';
var month = document.getElementById('cron_month').value || '*'; var month = document.getElementById('cron_month').value || '*';
var dow = document.getElementById('cron_dow').value || '*'; var dow = document.getElementById('cron_dow').value || '*';
var cronExpr = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + dow; var cronExpr = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + dow;
document.getElementById(targetCronField).value = cronExpr; document.getElementById(targetCronField).value = cronExpr;
// Zamknij modal
var modalEl = document.getElementById('cronModal'); var modalEl = document.getElementById('cronModal');
var modalInstance = bootstrap.Modal.getInstance(modalEl); var modalInstance = bootstrap.Modal.getInstance(modalEl);
modalInstance.hide(); modalInstance.hide();

View File

@@ -3,8 +3,8 @@
<div class="container my-4"> <div class="container my-4">
<h2 class="text-center mb-4">Lista wszystkich backupów</h2> <h2 class="text-center mb-4">Lista wszystkich backupów</h2>
<!-- Formularz filtrowania --> <!-- Karta filtra -->
<div class="card mb-4 shadow-sm"> <div class="card mb-4 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<form method="GET" action="{{ url_for('all_files') }}" class="row g-2"> <form method="GET" action="{{ url_for('all_files') }}" class="row g-2">
<div class="col-md-4"> <div class="col-md-4">
@@ -29,11 +29,11 @@
</div> </div>
</div> </div>
<!-- Tabela z backupami --> <!-- Karta tabeli backupów -->
<div class="card shadow-sm mb-4"> <div class="card shadow-sm border-0 mb-4">
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover mb-0"> <table class="table table-striped table-hover align-middle">
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th style="width: 2%;"><input type="checkbox" id="select_all"></th> <th style="width: 2%;"><input type="checkbox" id="select_all"></th>
@@ -63,18 +63,23 @@
<span class="badge bg-secondary">{{ file.backup_type }}</span> <span class="badge bg-secondary">{{ file.backup_type }}</span>
{% endif %} {% endif %}
</td> </td>
<td>{{ file.file_path|basename }}</td> <td>
{% if file.backup_type == 'binary' %}
<span data-bs-toggle="tooltip" title="Checksum: {{ file.checksum }}">{{ file.file_path|basename }}</span>
{% else %}
{{ file.file_path|basename }}
{% endif %}
</td>
<td>{{ file.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td> <td>{{ file.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
<td>{{ file.file_path|filesize }}</td> <td>{{ file.file_path|filesize }}</td>
<td> <td>
<a href="{{ url_for('download_file', filename=file.file_path|basename) }}" class="btn btn-lg btn-info"> <a href="{{ url_for('download_file', filename=file.file_path|basename) }}" class="btn btn-sm btn-info">
<i class="bi bi-download"></i> <i class="bi bi-download"></i>
</a> </a>
</td> </td>
<td> <td>
<form action="{{ url_for('send_by_email', backup_id=file.id) }}" method="POST" class="d-inline"> <form action="{{ url_for('send_by_email', backup_id=file.id) }}" method="POST" class="d-inline">
<input type="hidden" name="next" value="{{ url_for('all_files') }}"> <button type="submit" class="btn btn-sm btn-warning">
<button type="submit" class="btn btn-lg btn-warning">
<i class="bi bi-envelope"></i> <i class="bi bi-envelope"></i>
</button> </button>
</form> </form>
@@ -82,8 +87,7 @@
<td> <td>
{% if file.backup_type == 'binary' %} {% if file.backup_type == 'binary' %}
<form action="{{ url_for('upload_backup', router_id=file.router.id, backup_id=file.id) }}" method="POST" class="d-inline"> <form action="{{ url_for('upload_backup', router_id=file.router.id, backup_id=file.id) }}" method="POST" class="d-inline">
<input type="hidden" name="next" value="{{ url_for('all_files') }}"> <button type="submit" class="btn btn-sm btn-secondary">
<button type="submit" class="btn btn-lg btn-secondary">
<i class="bi bi-upload"></i> <i class="bi bi-upload"></i>
</button> </button>
</form> </form>
@@ -93,7 +97,7 @@
</td> </td>
<td> <td>
{% if file.backup_type == 'export' %} {% if file.backup_type == 'export' %}
<a href="{{ url_for('view_export', backup_id=file.id) }}" class="btn btn-lg btn-outline-primary"> <a href="{{ url_for('view_export', backup_id=file.id) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i> <i class="bi bi-eye"></i>
</a> </a>
{% else %} {% else %}
@@ -102,8 +106,7 @@
</td> </td>
<td> <td>
<form action="{{ url_for('delete_backup', backup_id=file.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');"> <form action="{{ url_for('delete_backup', backup_id=file.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
<input type="hidden" name="next" value="{{ url_for('all_files') }}"> <button type="submit" class="btn btn-sm btn-danger">
<button type="submit" class="btn btn-lg btn-danger">
<i class="bi bi-trash"></i> <i class="bi bi-trash"></i>
</button> </button>
</form> </form>
@@ -117,22 +120,21 @@
</div> </div>
</div> </div>
<!-- Formularz dla masowych akcji (jeden formularz) --> <!-- Formularz dla masowych akcji (zaznaczone pliki) -->
<form id="mass_actions_form" action="{{ url_for('mass_actions') }}" method="POST" class="d-flex justify-content-end mb-4"> <form id="mass_actions_form" action="{{ url_for('mass_actions') }}" method="POST" class="d-flex justify-content-end mb-4">
<button type="submit" name="action" value="download" class="btn btn-lg btn-success me-2"> <button type="submit" name="action" value="download" class="btn btn-success me-2">
<i class="bi bi-file-earmark-zip"></i> Pobierz zip zaznaczonych <i class="bi bi-file-earmark-zip"></i> Pobierz zip
</button> </button>
<button type="submit" name="action" value="delete" class="btn btn-lg btn-danger" onclick="return confirm('Na pewno usunąć zaznaczone backupy?');"> <button type="submit" name="action" value="delete" class="btn btn-danger" onclick="return confirm('Na pewno usunąć zaznaczone pliki?');">
<i class="bi bi-trash"></i> Usuń zaznaczone backupy <i class="bi bi-trash"></i> Usuń zaznaczone
</button> </button>
</form> </form>
</div> </div>
<script> <script>
document.getElementById('select_all').addEventListener('change', function(e) { document.getElementById('select_all').addEventListener('change', function(e) {
var checkboxes = document.querySelectorAll('input[name="backup_id"]'); var checkboxes = document.querySelectorAll('input[name="backup_id"]');
for (var i = 0; i < checkboxes.length; i++) { checkboxes.forEach(cb => cb.checked = e.target.checked);
checkboxes[i].checked = e.target.checked;
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,116 +1,392 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="pl" class="{% if session.dark_mode %}dark-mode{% endif %}"> <html lang="pl" class="{% if session.get('dark_mode', True) %}dark-mode{% endif %}">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Backup RouterOS App</title> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Backup RouterOS</title>
<!-- Bootstrap CSS --> <!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style> <style>
/* 1) Poprawa kontrastu dla form-text w trybie ciemnym */
.dark-mode .form-text {
color: #ccc !important;
}
/* 2) Ogólne style trybu ciemnego */
.dark-mode body { .dark-mode body {
background-color: #222; background-color: #121212;
color: #ffffff; color: #ffffff;
} }
.dark-mode a, .dark-mode a:hover { .dark-mode a,
.dark-mode a:hover {
color: #ddd; color: #ddd;
} }
.dark-mode .navbar, .dark-mode .table {
/* 3) Nawigacja i menu w trybie ciemnym */
.dark-mode .navbar,
.dark-mode .navbar-nav,
.dark-mode .dropdown-menu {
background-color: #333 !important; background-color: #333 !important;
color: #fff; color: #fff;
} }
.dark-mode .navbar-nav .nav-link {
color: #ddd !important;
}
.dark-mode .navbar-nav .nav-link:hover {
color: #fff !important;
}
/* Dropdown :hover, :focus, active */
.dark-mode .dropdown-item:hover,
.dark-mode .dropdown-item:focus,
.dark-mode .dropdown-item.active {
background-color: #444 !important;
color: #fff !important;
}
/* 4) Tabele w trybie ciemnym */
.dark-mode .table {
background-color: #333 !important;
border-color: #444;
}
.dark-mode .table thead th {
background-color: #444;
color: #fff;
border: 1px solid #555;
}
.dark-mode .table tbody td {
color: #ddd;
border: 1px solid #555;
}
html.dark-mode table.table thead th {
background-color: #444 !important;
color: #fff !important;
border: 1px solid #555 !important;
}
html.dark-mode table.table tbody td {
background-color: #333 !important;
color: #ddd !important;
border: 1px solid #555 !important;
}
/* 5) Pola formularzy (input, select, textarea) w trybie ciemnym */
.dark-mode input,
.dark-mode textarea,
.dark-mode select {
background-color: #333;
color: #fff;
border: 1px solid #555;
}
.dark-mode ::placeholder {
color: #ccc;
}
/* 6) Przyciski w trybie ciemnym: .btn-warning, .btn-secondary, .btn-outline-dark */
.dark-mode .btn-warning {
background-color: #d39e00;
border-color: #b38600;
color: #fff;
}
.dark-mode .btn-warning:hover {
background-color: #e6aa00 !important;
border-color: #c98f00 !important;
color: #fff !important;
}
.dark-mode .btn-secondary {
background-color: #444;
border-color: #555;
color: #fff;
}
.dark-mode .btn-secondary:hover {
background-color: #555 !important;
border-color: #888888 !important;
color: #fff !important;
}
.dark-mode .btn-outline-dark:hover {
background-color: #444 !important;
border-color: #888888 !important;
color: #fff !important;
}
/* 7) Karty i bloki w trybie ciemnym */
.dark-mode .block,
.dark-mode .card {
background-color: #171717;
color: #fff;
}
/* 8) Stopka */
.dark-mode footer {
background-color: #1e1e1e !important;
color: #fff !important;
}
footer {
background-color: #f8f9fa;
color: #212529;
}
/* 9) Nadpisanie .card-header.bg-light w trybie ciemnym */
.dark-mode .card-header.bg-light {
background-color: #333 !important;
color: #fff !important;
}
/* 10) Style diff2html w trybie ciemnym */
.dark-mode .d2h-wrapper,
.dark-mode .d2h-file-header,
.dark-mode .d2h-file-info,
.dark-mode .d2h-file-diff,
.dark-mode .d2h-diff-table,
.dark-mode .d2h-code-line,
.dark-mode .d2h-code-line-ctn,
.dark-mode .d2h-code-side-linenumber {
background-color: #333 !important;
color: #ddd !important;
border-color: #444 !important;
}
/* 11) Modal w trybie ciemnym */
.dark-mode .modal-content {
background-color: #333;
color: #ddd;
border: none;
}
.dark-mode .modal-header,
.dark-mode .modal-footer {
border-color: #444;
}
.dark-mode .modal-title {
color: #fff;
}
.dark-mode .btn-close {
filter: invert(1);
}
/* 12) Niestandardowy styl trybu jasnego navbar */
.navbar-light.bg-custom-light {
background-color: #dcdcdc !important;
}
/* 13) DataTables w trybie ciemnym */
.dark-mode .dataTables_wrapper .dataTables_paginate .paginate_button {
background-color: #333 !important;
color: #fff !important;
border: 1px solid #555 !important;
}
.dark-mode .dataTables_wrapper .dataTables_paginate .paginate_button.current,
.dark-mode .dataTables_wrapper .dataTables_paginate .paginate_button:hover {
background-color: #444 !important;
color: #fff !important;
}
html.dark-mode .dataTables_wrapper .pagination .page-link {
background-color: #333 !important;
color: #fff !important;
border: 1px solid #555 !important;
}
html.dark-mode .dataTables_wrapper .pagination .page-item.active .page-link,
html.dark-mode .dataTables_wrapper .pagination .page-link:hover {
background-color: #444 !important;
color: #fff !important;
}
html.dark-mode .dataTables_wrapper .dataTables_info,
html.dark-mode .dataTables_wrapper .dataTables_length,
html.dark-mode .dataTables_wrapper .dataTables_filter {
color: #fff !important;
}
.dark-mode input:focus,
.dark-mode textarea:focus,
.dark-mode select:focus {
background-color: #333 !important;
color: #fff !important;
border-color: #555 !important;
box-shadow: none !important;
}
/* 14) Drobne poprawki przycisków wylogowania */
.btn-logout {
color: #fff;
}
/* 15) Główne ustawienia, flex layout */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
main.container {
flex: 1;
}
/* 16) Alerty (diff-add, diff-rem) pozostawione bez zmian */
.diff-add { color: green; } .diff-add { color: green; }
.diff-rem { color: red; } .diff-rem { color: red; }
.dark-mode .text-muted {
color: #aaa !important; /* zamiast #aaa możesz wybrać #bbb, #ccc itp. */
}
</style> </style>
{% block head %}{% endblock %}
</head> </head>
<body> <body class="d-flex flex-column">
<nav class="navbar navbar-expand navbar-dark bg-dark mb-4"> <!-- Navbar -->
<nav class="navbar navbar-expand-lg
{% if session.get('dark_mode', True) %}navbar-dark bg-dark{% else %}navbar-light bg-custom-light{% endif %}
mb-4">
<div class="container-fluid"> <div class="container-fluid">
<a href="{{ url_for('index') }}" class="navbar-brand">Backup RouterOS</a> <a href="/" class="navbar-brand">Backup RouterOS</a>
<div> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown"
{% if session.user_id %} aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary me-2">Dashboard</a> <span class="navbar-toggler-icon"></span>
<a href="{{ url_for('routers_list') }}" class="btn btn-secondary me-2">Urządzenia</a> </button>
<a href="{{ url_for('diff_selector') }}" class="btn btn-secondary me-2">Diff selector</a> <div class="collapse navbar-collapse" id="navbarNavDropdown">
<a href="{{ url_for('all_files') }}" class="btn btn-secondary me-2">Wszystkie pliki</a> {% if session.get('user_id') %}
<a href="{{ url_for('settings_view') }}" class="btn btn-secondary me-2">Ustawienia</a> <ul class="navbar-nav me-auto">
<a href="{{ url_for('advanced_schedule') }}" class="btn btn-secondary me-2">Harmonogram</a> <!-- Dashboard -->
<a href="{{ url_for('change_password') }}" class="btn btn-secondary me-2">Zmiana hasła</a> <li class="nav-item">
<a href="{{ url_for('logout') }}" class="btn btn-secondary me-2">Wyloguj</a> <a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
{% else %} </li>
<a href="{{ url_for('login') }}" class="btn btn-secondary me-2">Zaloguj</a> <!-- Urządzenia dropdown -->
<a href="{{ url_for('register') }}" class="btn btn-secondary me-2">Utwórz konto</a> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="devicesDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Urządzenia
</a>
<ul class="dropdown-menu" aria-labelledby="devicesDropdown">
<li><a class="dropdown-item" href="/routers">Lista</a></li>
<li><a class="dropdown-item" href="/routers/add">Dodaj nowe</a></li>
</ul>
</li>
<!-- Diff -->
<li class="nav-item">
<a class="nav-link" href="/diff_selector">Diff</a>
</li>
<!-- Wszystkie pliki -->
<li class="nav-item">
<a class="nav-link" href="/all_files">Wszystkie pliki</a>
</li>
<!-- Logi -->
<li class="nav-item">
<a class="nav-link" href="/logs">Logi</a>
</li>
<!-- Ustawienia dropdown -->
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="settingsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Ustawienia
</a>
<ul class="dropdown-menu" aria-labelledby="settingsDropdown">
<li><a class="dropdown-item" href="/settings">Główne</a></li>
<li><a class="dropdown-item" href="/advanced_schedule">Harmonogram</a></li>
</ul>
</li>
</ul>
{% endif %} {% endif %}
<!--<a href="{{ url_for('toggle_dark_mode') }}" class="btn btn-warning">Toggle Dark Mode</a>-->
<!-- Prawa strona navbaru -->
<ul class="navbar-nav ms-auto align-items-center">
<!-- Przełącznik trybu ciemnego -->
<li class="nav-item me-2">
<form action="{{ url_for('toggle_dark_mode') }}" method="GET" class="d-flex align-items-center">
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="darkModeSwitch"
onchange="this.form.submit()"
{% if session.get('dark_mode', True) %}checked{% endif %}>
<label class="form-check-label ms-1" for="darkModeSwitch" style="user-select:none;">Ciemny</label>
</div>
</form>
</li>
{% if session.get('user_id') %}
<li class="nav-item">
<a class="nav-link btn btn-alert ms-2 btn-logout" href="{{ url_for('change_password') }}">Zmień hasło</a>
</li>
<li class="nav-item">
<a class="nav-link btn btn-danger ms-2 btn-logout" href="{{ url_for('logout') }}">Wyloguj</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link btn btn-success ms-2" href="{{ url_for('login') }}">Zaloguj się</a>
</li>
<li class="nav-item">
<a class="nav-link btn btn-primary ms-2" href="{{ url_for('register') }}">Zarejestruj się</a>
</li>
{% endif %}
</ul>
</div> </div>
</div> </div>
</nav> </nav>
<div class="container">
{% with messages = get_flashed_messages() %}
<!-- Główna zawartość -->
<main class="container mb-5">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %} {% if messages %}
<div class="alert alert-info"> {% for category, msg in messages %}
{% for msg in messages %} {% set bs_cat = bootstrap_alert_category(category) %}
<div>{{ msg }}</div> <div class="alert alert-{{ bs_cat }} alert-dismissible fade show" role="alert">
{% endfor %} {{ msg }}
</div> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %} {% endif %}
{% endwith %} {% endwith %}
{% block content %}{% endblock %}
</div>
{% block content %}{% endblock %}
</main>
<!-- Modal Test Połączenia --> <!-- Modal Test Połączenia -->
<div class="modal fade" id="testConnectionModal" tabindex="-1" aria-labelledby="testConnectionModalLabel" aria-hidden="true"> <div class="modal fade" id="testConnectionModal" tabindex="-1" aria-labelledby="testConnectionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="testConnectionModalLabel">Test Połączenia</h5> <h5 class="modal-title" id="testConnectionModalLabel">Test Połączenia</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div> </div>
<div class="modal-body" id="testConnectionModalBody"> <div class="modal-body" id="testConnectionModalBody">
<!-- Zawartość zostanie załadowana przez AJAX --> <!-- Zawartość ładowana przez AJAX -->
</div>
</div> </div>
</div> </div>
</div> </div>
</div>
<!-- Stopka -->
<footer class="footer py-3 mt-auto">
<div class="container text-center">
<span>&copy; 2025 Mateusz Gruszczyński, linuxiarz.pl</span>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script> <!-- Bootstrap Bundle JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- Dodatkowe skrypty -->
<script> <script>
function ajaxExport(router_id) { // Funkcja do wczytywania modalu testu połączenia
fetch("/router/" + router_id + "/export", { function openTestConnectionModal(routerId) {
method: "POST", fetch('/router/' + routerId + '/test_connection?modal=1')
headers: {"X-Requested-With": "XMLHttpRequest"} .then(response => response.text())
}) .then(html => {
.then(response => response.json()) document.getElementById('testConnectionModalBody').innerHTML = html;
.then(data => { var myModal = new bootstrap.Modal(document.getElementById('testConnectionModal'));
if(data.status === "success"){ myModal.show();
alert("Eksport wykonany: " + data.message); })
// Możesz też zaktualizować część strony dynamicznie .catch(error => {
} else { console.error("Błąd ładowania modalu: ", error);
alert("Błąd eksportu: " + data.message); alert("Wystąpił błąd podczas ładowania danych.");
} });
})
.catch(error => {
console.error("Błąd AJAX:", error);
alert("Wystąpił błąd.");
});
} }
</script> </script>
<script>
function openTestConnectionModal(routerId) { {% block scripts %}{% endblock %}
fetch('/router/' + routerId + '/test_connection?modal=1')
.then(response => response.text())
.then(html => {
document.getElementById('testConnectionModalBody').innerHTML = html;
var myModal = new bootstrap.Modal(document.getElementById('testConnectionModal'));
myModal.show();
})
.catch(error => {
console.error("Błąd ładowania modalu: ", error);
alert("Wystąpił błąd podczas ładowania danych.");
});
}
</script>
</body> </body>
</html> </html>

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container"> <div class="container my-5">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;"> <div class="row justify-content-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="card shadow"> <div class="card border-0 shadow-sm">
<div class="card-header text-center"> <div class="card-header bg-light text-center">
<h2>Zmień hasło</h2> <h4 class="mb-0">Zmień hasło</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST"> <form method="POST">

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container my-4"> <div class="container my-4">
<h2 class="text-center mb-4">Panel administracyjny</h2> <h2 class="text-center mb-4">Dashboard</h2>
<!-- Wiersz akcji ogólnych --> <!-- Wiersz akcji ogólnych -->
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-12 text-center"> <div class="col text-center">
<a href="{{ url_for('routers_list') }}" class="btn btn-lg btn-outline-primary"> <a href="{{ url_for('routers_list') }}" class="btn btn-lg btn-outline-primary">
<i class="bi bi-hdd-network"></i> Zobacz routery <i class="bi bi-hdd-network"></i> Zobacz routery
</a> </a>
@@ -53,14 +53,14 @@
<div class="col-md-6 d-flex justify-content-center"> <div class="col-md-6 d-flex justify-content-center">
<form action="{{ url_for('export_all_routers') }}" method="POST"> <form action="{{ url_for('export_all_routers') }}" method="POST">
<button type="submit" class="btn btn-lg btn-outline-success"> <button type="submit" class="btn btn-lg btn-outline-success">
<i class="bi bi-arrow-down-circle"></i> Eksport dla wszystkich routerów <i class="bi bi-arrow-down-circle"></i> Eksport wszystkich routerów
</button> </button>
</form> </form>
</div> </div>
<div class="col-md-6 d-flex justify-content-center"> <div class="col-md-6 d-flex justify-content-center">
<form action="{{ url_for('backup_all_routers') }}" method="POST"> <form action="{{ url_for('backup_all_routers') }}" method="POST">
<button type="submit" class="btn btn-lg btn-outline-secondary"> <button type="submit" class="btn btn-lg btn-outline-secondary">
<i class="bi bi-cloud-download"></i> Backup binarny dla wszystkich routerów <i class="bi bi-cloud-download"></i> Backup binarny wszystkich routerów
</button> </button>
</form> </form>
</div> </div>
@@ -73,11 +73,11 @@
{% else %} {% else %}
{% set success_percent = 0 %} {% set success_percent = 0 %}
{% endif %} {% endif %}
<div class="card mb-4 shadow-sm"> <div class="card mb-4 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Statystyki operacji</h5> <h5 class="card-title">Statystyki operacji</h5>
<p>Udane operacje: {{ success_ops }}, Nieudane operacje: {{ failure_ops }}</p> <p>Udane operacje: {{ success_ops }}, Nieudane operacje: {{ failure_ops }}</p>
<div class="progress"> <div class="progress mb-2">
<div class="progress-bar bg-success" role="progressbar" style="width: {{ success_percent }}%;" aria-valuenow="{{ success_percent }}" aria-valuemin="0" aria-valuemax="100"> <div class="progress-bar bg-success" role="progressbar" style="width: {{ success_percent }}%;" aria-valuenow="{{ success_percent }}" aria-valuemin="0" aria-valuemax="100">
{{ success_percent }}% {{ success_percent }}%
</div> </div>
@@ -89,9 +89,12 @@
</div> </div>
<!-- Log operacji --> <!-- Log operacji -->
<div class="card shadow-sm mb-4"> <div class="card shadow-sm border-0 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>
@@ -111,8 +114,8 @@
</div> </div>
</div> </div>
<!-- Dodatkowe statystyki przeniesione na sam dół, pod logami --> <!-- Dodatkowe statystyki -->
<div class="card shadow-sm"> <div class="card shadow-sm border-0">
<div class="card-body"> <div class="card-body">
<h5 class="card-title">Dodatkowe statystyki</h5> <h5 class="card-title">Dodatkowe statystyki</h5>
<div class="row"> <div class="row">
@@ -128,6 +131,5 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,28 +1,47 @@
{% extends "base.html" %} {% extends "base.html" %}
{% 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> <div class="card border-0 shadow-sm">
<hr> <div class="card-header bg-light">
<div id="diffContainer"></div> <h4 class="mb-0">
<a href="{{ url_for('router_details', router_id=backup1.router_id) }}" class="btn btn-secondary mt-3">Powrót</a> Porównanie: {{ backup1.file_path|basename }} vs {{ backup2.file_path|basename }}
</h4>
</div>
<div class="card-body">
<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>
</div>
</div> </div>
<!-- Dodajemy diff2html --> <!-- diff2html resources -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html@3.4.4/bundles/css/diff2html.min.css" />
<script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.4/bundles/js/diff2html.min.js"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
// Upewnij się, że diff_text jest poprawnie escapowany var diffText = `{{ diff_text|e }}`;
var diffText = `{{ diff_text|e }}`; var targetElement = document.getElementById("diffContainer");
var targetElement = document.getElementById("diffContainer"); var configuration = {
var configuration = { drawFileList: true,
drawFileList: true, matching: 'lines',
matching: 'lines', outputFormat: 'line-by-line'
outputFormat: 'line-by-line' };
}; var diffHtml = Diff2Html.html(diffText, configuration);
var diffHtml = Diff2Html.html(diffText, configuration); targetElement.innerHTML = diffHtml;
targetElement.innerHTML = diffHtml;
// Dark mode styl dla diff2html, jeśli potrzebne
if(document.body.classList.contains('dark-mode')) {
var darkStyle = document.createElement('style');
darkStyle.textContent = `
.d2h-wrapper { background-color: #1e1e1e; color: #fff; }
.d2h-file-header { background-color: #2e2e2e; color: #fff; }
.d2h-diff-table { background-color: #1e1e1e; color: #fff; }
.d2h-code-line { background-color: #1e1e1e; color: #fff; }
.d2h-code-line-ctn { color: #fff; }
`;
document.head.appendChild(darkStyle);
}
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,8 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container my-4"> <div class="container my-4">
<h2 class="text-center mb-4">Porównanie backupów (Diff)</h2> <div class="card shadow-sm border-0">
<div class="card shadow-sm"> <div class="card-header bg-light d-flex align-items-center">
<h4 class="mb-0">Porównanie backupów (Diff)</h4>
</div>
<div class="card-body"> <div class="card-body">
<form action="{{ url_for('diff_selector') }}" method="POST" id="diffForm"> <form action="{{ url_for('diff_selector') }}" method="POST" id="diffForm">
<div class="row mb-3"> <div class="row mb-3">
@@ -29,8 +31,10 @@
</select> </select>
</div> </div>
</div> </div>
<div class="text-center"> <div class="text-center mt-4">
<button type="submit" class="btn btn-primary btn-lg">Porównaj backupy</button> <button type="submit" class="btn btn-primary btn-lg">
Porównaj backupy
</button>
</div> </div>
</form> </form>
</div> </div>
@@ -39,12 +43,12 @@
<script> <script>
document.getElementById("diffForm").addEventListener("submit", function(event) { document.getElementById("diffForm").addEventListener("submit", function(event) {
var backup1 = document.getElementById("backup1").value; var backup1 = document.getElementById("backup1").value;
var backup2 = document.getElementById("backup2").value; var backup2 = document.getElementById("backup2").value;
if(backup1 === backup2) { if (backup1 === backup2) {
event.preventDefault(); event.preventDefault();
alert("Wybierz dwa różne backupy do porównania."); alert("Wybierz dwa różne backupy do porównania.");
} }
}); });
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,9 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container mt-5"> <div class="container my-4">
<div class="card shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-header bg-light">
<h2 class="mb-0">Edycja urządzenia</h2> <h4 class="mb-0">Edycja urządzenia</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST"> <form method="POST">
@@ -25,15 +25,15 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="ssh_key" class="form-label"> <label for="ssh_key" class="form-label">
<label for="ssh_password" class="form-label"><b>Klucz prywatny</b></label> | Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code><br> <b>Klucz prywatny</b>
Pozostaw puste jeśli ten RouterOS będzie używał <a href="{{ url_for('settings_view') }}">klucza globalnego</a> </label><br>
</label> <small>Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code>. Jeśli pusty użyje klucza globalnego.</small>
<textarea class="form-control" id="ssh_key" name="ssh_key" rows="4">{{ router.ssh_key }}</textarea> <textarea class="form-control mt-2" id="ssh_key" name="ssh_key" rows="4">{{ router.ssh_key }}</textarea>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br> <label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
Jeśli podajesz klucz SSH lub zdefiniowany jest <a href="{{ url_for('settings_view') }}">klucz globalny</a>, to logowanie hasłem jest nieaktywne. <small>Jeśli jest klucz SSH lub klucz globalny, hasło może być ignorowane.</small>
<input type="password" class="form-control" id="ssh_password" name="ssh_password" value="{{ router.ssh_password }}"> <input type="password" class="form-control mt-2" id="ssh_password" name="ssh_password" value="{{ router.ssh_password }}">
</div> </div>
<button type="submit" class="btn btn-success">Zapisz zmiany</button> <button type="submit" class="btn btn-success">Zapisz zmiany</button>
</form> </form>

View File

@@ -2,7 +2,11 @@
{% block content %} {% block content %}
<div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 80vh;"> <div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 80vh;">
<div class="text-center"> <div class="text-center">
{% if session.get('dark_mode', True) %}
<img src="https://mikrotik.com/logo/assets/logo-colors-white-E8duxH7y.svg" alt="Mikrotik Logo" class="img-fluid" style="max-width: 200px;">
{% else %}
<img src="https://mikrotik.com/logo/assets/logo-colors-dark-ToiqSI6u.svg" alt="Mikrotik Logo" class="img-fluid" style="max-width: 200px;"> <img src="https://mikrotik.com/logo/assets/logo-colors-dark-ToiqSI6u.svg" alt="Mikrotik Logo" class="img-fluid" style="max-width: 200px;">
{% endif %}
<h1 class="mt-3">Witamy w aplikacji Backup RouterOS</h1> <h1 class="mt-3">Witamy w aplikacji Backup RouterOS</h1>
<p class="lead">Zarządzaj backupami swoich urządzeń RouterOS w prosty sposób.</p> <p class="lead">Zarządzaj backupami swoich urządzeń RouterOS w prosty sposób.</p>
<div class="mt-4"> <div class="mt-4">
@@ -12,3 +16,4 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,21 +1,23 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block head %}{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container my-5">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;"> <div class="row justify-content-center align-items-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="card shadow"> <div class="card shadow border-0">
<div class="card-header text-center"> <div class="card-header bg-light text-center">
<h2>Zaloguj się</h2> <h4 class="mb-0">Zaloguj się</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="{{ url_for('login') }}" method="POST"> <form action="{{ url_for('login') }}" method="POST">
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">Nazwa użytkownika</label> <label for="username" class="form-label">Nazwa użytkownika</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika"> <input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Hasło</label> <label for="password" class="form-label">Hasło</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło"> <input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło" required>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="submit" class="btn btn-primary">Zaloguj się</button> <button type="submit" class="btn btn-primary">Zaloguj się</button>
@@ -23,7 +25,10 @@
</form> </form>
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
<a href="{{ url_for('register') }}">Nie masz konta? Zarejestruj się</a> <small>
Nie masz konta?
<a href="{{ url_for('register') }}">Zarejestruj się</a>
</small>
</div> </div>
</div> </div>
</div> </div>

76
templates/logs.html Normal file
View File

@@ -0,0 +1,76 @@
{% 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>
<!-- Karta z usuwaniem logów -->
<div class="card mb-4 shadow-sm border-0">
<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ż (dni):</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 border-0 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 Dashboardu</a>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<!-- jQuery -->
<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']],
language: {
url: '//cdn.datatables.net/plug-ins/1.13.4/i18n/pl.json'
}
});
});
</script>
{% endblock %}

View File

@@ -1,21 +1,23 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block head %}{% endblock %}
{% block content %} {% block content %}
<div class="container"> <div class="container my-5">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;"> <div class="row justify-content-center align-items-center">
<div class="col-md-6"> <div class="col-md-6">
<div class="card shadow"> <div class="card shadow border-0">
<div class="card-header text-center"> <div class="card-header bg-light text-center">
<h2>Rejestracja</h2> <h4 class="mb-0">Rejestracja</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form action="{{ url_for('register') }}" method="POST"> <form action="{{ url_for('register') }}" method="POST">
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">Nazwa użytkownika</label> <label for="username" class="form-label">Nazwa użytkownika</label>
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika"> <input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika" required>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label">Hasło</label> <label for="password" class="form-label">Hasło</label>
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło"> <input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło" required>
</div> </div>
<div class="d-grid"> <div class="d-grid">
<button type="submit" class="btn btn-primary">Zarejestruj się</button> <button type="submit" class="btn btn-primary">Zarejestruj się</button>
@@ -23,7 +25,10 @@
</form> </form>
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
<a href="{{ url_for('login') }}">Masz już konto? Zaloguj się</a> <small>
Masz już konto?
<a href="{{ url_for('login') }}">Zaloguj się</a>
</small>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -28,6 +28,7 @@
<h3>Pliki z /export</h3> <h3>Pliki z /export</h3>
{% if export_backups %} {% if export_backups %}
<!-- Tabela z indywidualnymi akcjami --> <!-- Tabela z indywidualnymi akcjami -->
<div class="table-responsive">
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
@@ -54,7 +55,6 @@
<td> <td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a> <a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a>
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-sm btn-outline-primary">Podgląd</a> <a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-sm btn-outline-primary">Podgląd</a>
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" style="display: inline;"> <form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" style="display: inline;">
<button type="submit" class="btn btn-sm btn-primary">Wyślij mailem</button> <button type="submit" class="btn btn-sm btn-primary">Wyślij mailem</button>
</form> </form>
@@ -67,8 +67,11 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
<!-- Formularz do pobierania ZIP zaznaczonych eksportów --> <!-- Formularz do pobierania ZIP zaznaczonych eksportów -->
<h4>Pobierz wybrane pliki z /export jako zip</h4> <h4>Pobierz wybrane pliki z /export jako zip</h4>
<div class="table-responsive">
<form action="{{ url_for('download_zip') }}" method="POST"> <form action="{{ url_for('download_zip') }}" method="POST">
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
@@ -86,12 +89,13 @@
<td>{{ b.file_path|basename }}</td> <td>{{ b.file_path|basename }}</td>
<td>{{ b.file_path|filesize }}</td> <td>{{ b.file_path|filesize }}</td>
<td>{{ b.created_at }}</td> <td>{{ b.created_at }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
<button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button> <button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button>
</form> </form>
</div>
{% else %} {% else %}
<p class="text-muted">Pusto</p> <p class="text-muted">Pusto</p>
{% endif %} {% endif %}
@@ -101,6 +105,7 @@
<!-- Sekcja backupów binarnych --> <!-- Sekcja backupów binarnych -->
<h3>Pliki binarne (.backup)</h3> <h3>Pliki binarne (.backup)</h3>
{% if binary_backups %} {% if binary_backups %}
<div class="table-responsive">
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
<tr> <tr>
@@ -118,7 +123,6 @@
<td>{{ b.created_at }}</td> <td>{{ b.created_at }}</td>
<td> <td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a> <a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a>
<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-sm btn-secondary">Wgraj do routera</button> <button type="submit" class="btn btn-sm btn-secondary">Wgraj do routera</button>
</form> </form>
@@ -134,9 +138,11 @@
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div>
<!-- Formularz do pobierania ZIP zaznaczonych backupów binarnych --> <!-- Formularz do pobierania ZIP zaznaczonych backupów binarnych -->
<h4>Pobierz wybrane backupy binarne jako zip</h4> <h4>Pobierz wybrane backupy binarne jako zip</h4>
<div class="table-responsive">
<form action="{{ url_for('download_zip') }}" method="POST"> <form action="{{ url_for('download_zip') }}" method="POST">
<table class="table table-bordered"> <table class="table table-bordered">
<thead> <thead>
@@ -160,6 +166,7 @@
</table> </table>
<button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button> <button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button>
</form> </form>
</div>
{% else %} {% else %}
<p class="text-muted">Pusto</p> <p class="text-muted">Pusto</p>
{% endif %} {% endif %}

View File

@@ -1,17 +1,17 @@
{% 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>
<div class="container my-4"> <div class="container my-4">
<!-- Informacje o routerze --> <div class="mb-3 text-end">
<div class="card shadow-sm mb-4"> {% if current_view == 'v1' %}
<div class="card-header"> <a href="{{ url_for('router_details', router_id=router.id, view='v2') }}" class="btn btn-outline-secondary">Przełącz na widok v2</a>
<h2 class="mb-0">Router: {{ router.name }}</h2> {% 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="card border-0 shadow-sm mb-4">
<div class="card-header bg-light">
<h4 class="mb-0">Router: {{ router.name }}</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<p> <p>
@@ -20,14 +20,13 @@
<strong>SSH User:</strong> {{ router.ssh_user }} <strong>SSH User:</strong> {{ router.ssh_user }}
</p> </p>
<div class="d-flex flex-wrap gap-2"> <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"> <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>
@@ -35,160 +34,172 @@
<!-- Zakładki z backupami --> <!-- Zakładki z backupami -->
<ul class="nav nav-tabs" id="routerTab" role="tablist"> <ul class="nav nav-tabs" id="routerTab" role="tablist">
<li class="nav-item" role="presentation"> <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> <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>
<li class="nav-item" role="presentation"> <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> <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> </li>
</ul> </ul>
<div class="tab-content" id="routerTabContent"> <div class="tab-content" id="routerTabContent">
<!-- Zakładka /export --> <!-- Zakładka /export -->
<div class="tab-pane fade show active" id="export" role="tabpanel" aria-labelledby="export-tab"> <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 mt-3 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
{% if export_backups %} {% if export_backups %}
<!-- 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 -->
<table class="table table-bordered table-striped"> <div class="table-responsive">
<thead class="table-dark"> <table class="table table-bordered table-striped align-middle">
<tr> <thead class="table-dark">
<th style="width: 3%;"><input type="checkbox" id="select_all_export"></th> <tr>
<th>Nazwa pliku</th> <th style="width: 3%;"><input type="checkbox" id="select_all_export"></th>
<th>Rozmiar</th> <th>Nazwa pliku</th>
<th>Data</th> <th>Rozmiar</th>
<th>Diff</th> <th>Data</th>
<th>Pobierz</th> <th>Diff</th>
<th>Podgląd</th> <th>Pobierz</th>
<th>Wyślij mailem</th> <th>Podgląd</th>
<th>Usuń</th> <th>Wyślij mailem</th>
</tr> <th>Usuń</th>
</thead> </tr>
<tbody> </thead>
{% for b in export_backups %} <tbody>
<tr> {% for b in export_backups %}
<td> <tr>
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="export_mass_actions_form"> <td>
</td> <input type="checkbox" name="backup_id" value="{{ b.id }}" form="export_mass_actions_form">
<td>{{ b.file_path|basename }}</td> </td>
<td>{{ b.file_path|filesize }}</td> <td>{{ b.file_path|basename }}</td>
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td> <td>{{ b.file_path|filesize }}</td>
<td> <td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
{% if loop.index0 > 0 %} <td>
<a href="{{ url_for('diff_view', backup_id1=b.id, backup_id2=export_backups[0].id) }}" class="btn btn-sm btn-info">Diff</a> {% if loop.index0 > 0 %}
{% else %} <a href="{{ url_for('diff_view', backup_id1=b.id, backup_id2=export_backups[0].id) }}" class="btn btn-sm btn-info">Diff</a>
<small>Brak nowszego</small> {% else %}
{% endif %} <small>Brak nowszego</small>
</td> {% endif %}
<td> </td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz"> <td>
<i class="bi bi-download"></i> <a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-info btn-sm" title="Pobierz">
</a> <i class="bi bi-download"></i>
</td> </a>
<td> </td>
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-lg btn-outline-primary" title="Podgląd"> <td>
<i class="bi bi-eye"></i> <a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-outline-primary btn-sm" title="Podgląd">
</a> <i class="bi bi-eye"></i>
</td> </a>
<td> </td>
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline"> <td>
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}"> <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"> <input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<i class="bi bi-envelope"></i> <button type="submit" class="btn btn-primary btn-sm" title="Wyślij mailem">
</button> <i class="bi bi-envelope"></i>
</form> </button>
</td> </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?');"> <td>
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}"> <form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
<button type="submit" class="btn btn-lg btn-danger" title="Usuń"> <input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
<i class="bi bi-trash"></i> <button type="submit" class="btn btn-danger btn-sm" title="Usuń">
</button> <i class="bi bi-trash"></i>
</form> </button>
</td> </form>
</tr> </td>
{% endfor %} </tr>
</tbody> {% endfor %}
</table> </tbody>
</table>
</div>
{% else %} {% else %}
<p class="text-muted">Brak plików /export.</p> <p class="text-muted">Brak plików /export.</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
<!-- Zakładka backupów binarnych --> <!-- Zakładka backupów binarnych -->
<div class="tab-pane fade" id="binary" role="tabpanel" aria-labelledby="binary-tab"> <div class="tab-pane fade" id="binary" role="tabpanel" aria-labelledby="binary-tab">
<div class="card mt-3 shadow-sm"> <div class="card mt-3 shadow-sm border-0">
<div class="card-body"> <div class="card-body">
{% if binary_backups %} {% if binary_backups %}
<!-- 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 -->
<table class="table table-bordered table-striped"> <div class="table-responsive">
<thead class="table-dark"> <table class="table table-bordered table-striped align-middle">
<tr> <thead class="table-dark">
<th style="width: 3%;"><input type="checkbox" id="select_all_binary"></th> <tr>
<th>Nazwa pliku</th> <th style="width: 3%;"><input type="checkbox" id="select_all_binary"></th>
<th>Rozmiar</th> <th>Nazwa pliku</th>
<th>Data</th> <th>Rozmiar</th>
<th>Pobierz</th> <th>Data</th>
<th>Wgraj do routera</th> <th>Pobierz</th>
<th>Wyślij mailem</th> <th>Wgraj do routera</th>
<th>Usuń</th> <th>Wyślij mailem</th>
</tr> <th>Usuń</th>
</thead> </tr>
<tbody> </thead>
{% for b in binary_backups %} <tbody>
<tr> {% for b in binary_backups %}
<td> <tr>
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form"> <td>
</td> <input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form">
<td>{{ b.file_path|basename }}</td> </td>
<td>{{ b.file_path|filesize }}</td> <td>
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td> <span data-bs-toggle="tooltip" title="Checksum: {{ b.checksum }}">{{ b.file_path|basename }}</span>
<td> </td>
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz"> <td>{{ b.file_path|filesize }}</td>
<i class="bi bi-download"></i> <td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
</a> <td>
</td> <a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-info btn-sm" title="Pobierz">
<td> <i class="bi bi-download"></i>
<form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline"> </a>
<button type="submit" class="btn btn-lg btn-secondary" title="Wgraj do routera"> </td>
<i class="bi bi-upload"></i> <td>
</button> <form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline">
</form> <button type="submit" class="btn btn-secondary btn-sm" title="Wgraj do routera">
</td> <i class="bi bi-upload"></i>
<td> </button>
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline"> </form>
<button type="submit" class="btn btn-lg btn-primary" title="Wyślij mailem"> </td>
<i class="bi bi-envelope"></i> <td>
</button> <form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
</form> <button type="submit" class="btn btn-primary btn-sm" title="Wyślij mailem">
</td> <i class="bi bi-envelope"></i>
<td> </button>
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');"> </form>
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}"> </td>
<button type="submit" class="btn btn-lg btn-danger" title="Usuń"> <td>
<i class="bi bi-trash"></i> <form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
</button> <input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
</form> <button type="submit" class="btn btn-danger btn-sm" title="Usuń">
</td> <i class="bi bi-trash"></i>
</tr> </button>
{% endfor %} </form>
</tbody> </td>
</table> </tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %} {% else %}
<p class="text-muted">Brak plików binarnych.</p> <p class="text-muted">Brak plików binarnych.</p>
{% endif %} {% endif %}
@@ -202,18 +213,14 @@
<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
var triggerTabList = [].slice.call(document.querySelectorAll('#routerTab button')); var triggerTabList = [].slice.call(document.querySelectorAll('#routerTab button'));
triggerTabList.forEach(function (triggerEl) { triggerTabList.forEach(function (triggerEl) {
var tabTrigger = new bootstrap.Tab(triggerEl); var tabTrigger = new bootstrap.Tab(triggerEl);
@@ -222,5 +229,11 @@ triggerTabList.forEach(function (triggerEl) {
tabTrigger.show(); tabTrigger.show();
}); });
}); });
// Inicjalizacja tooltipów
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
new bootstrap.Tooltip(tooltipTriggerEl);
});
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -1,63 +1,67 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container my-4"> <div class="container my-4">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="card border-0 shadow-sm">
<h2>Moje Routery</h2> <div class="card-header bg-light d-flex justify-content-between align-items-center">
<a href="{{ url_for('add_router') }}" class="btn btn-success"> <h4 class="mb-0">Lista urządzeń</h4>
<i class="bi bi-plus-lg"></i> Dodaj nowe urządzenie <a href="{{ url_for('add_router') }}" class="btn btn-success">
</a> <i class="bi bi-plus-lg"></i> Dodaj nowe urządzenie
</div> </a>
<div class="table-responsive"> </div>
<table class="table table-striped table-hover"> <div class="card-body">
<thead class="table-primary"> <div class="table-responsive-sm">
<tr> <table class="table table-striped table-hover align-middle">
<th>Nazwa</th> <thead class="table-primary">
<th>Host</th> <tr>
<th>Port</th> <th>Nazwa</th>
<th>Exporty</th> <th>Host</th>
<th>Backupy binarne</th> <th>Port</th>
<th>Test Połączenia</th> <th>Exporty</th>
<th>Akcje</th> <th>Backupy binarne</th>
</tr> <th>Test Połączenia</th>
</thead> <th>Akcje</th>
<tbody> </tr>
{% for router in routers %} </thead>
<tr> <tbody>
<td>{{ router.name }}</td> {% for router in routers %}
<td>{{ router.host }}</td> <tr>
<td>{{ router.port }}</td> <td>{{ router.name }}</td>
<td> <td>{{ router.host }}</td>
<span class="badge bg-success"> <td>{{ router.port }}</td>
{{ router.backups|selectattr("backup_type", "equalto", "export")|list|length }} <td>
</span> <span class="badge bg-success">
</td> {{ router.backups|selectattr("backup_type", "equalto", "export")|list|length }}
<td> </span>
<span class="badge bg-info"> </td>
{{ router.backups|selectattr("backup_type", "equalto", "binary")|list|length }} <td>
</span> <span class="badge bg-info">
</td> {{ router.backups|selectattr("backup_type", "equalto", "binary")|list|length }}
<td> </span>
<button type="button" class="btn btn-sm btn-info" onclick="openTestConnectionModal({{ router.id }})"> </td>
<i class="bi bi-wifi"></i> Test <td>
</button> <button type="button" class="btn btn-sm btn-info" onclick="openTestConnectionModal({{ router.id }})">
</td> <i class="bi bi-wifi"></i> Test
<td> </button>
<a href="{{ url_for('router_details', router_id=router.id) }}" class="btn btn-sm btn-primary"> </td>
<i class="bi bi-eye"></i> Szczegóły <td>
</a> <a href="{{ url_for('router_details', router_id=router.id) }}" class="btn btn-sm btn-primary">
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-sm btn-warning"> <i class="bi bi-eye"></i> Szczegóły
<i class="bi bi-pencil"></i> Edytuj </a>
</a> <a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-sm btn-warning">
<form action="{{ url_for('delete_router', router_id=router.id) }}" method="POST" class="d-inline"> <i class="bi bi-pencil"></i> Edytuj
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Na pewno usunąć urządzenie?');"> </a>
<i class="bi bi-trash"></i> Usuń <form action="{{ url_for('delete_router', router_id=router.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć urządzenie?');">
</button> <button type="submit" class="btn btn-sm btn-danger">
</form> <i class="bi bi-trash"></i> Usuń
</td> </button>
</tr> </form>
{% endfor %} </td>
</tbody> </tr>
</table> {% endfor %}
</tbody>
</table>
</div>
</div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -1,15 +1,16 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container my-5"> <div class="container my-4">
<div class="card shadow-sm"> <div class="card border-0 shadow-sm">
<div class="card-header"> <div class="card-header bg-light">
<h2 class="mb-0">Ustawienia globalne</h2> <h4 class="mb-0">Ustawienia globalne</h4>
</div> </div>
<div class="card-body"> <div class="card-body">
<form method="POST"> <form method="POST">
<!-- Sekcja Pushover --> <!-- Sekcja Pushover -->
<div class="mb-4"> <div class="mb-4">
<h4 class="mb-3">Powiadomienia - Pushover</h4> <h5 class="mb-3">Powiadomienia - Pushover</h5>
<div class="mb-3"> <div class="mb-3">
<label for="pushover_token" class="form-label">Pushover Token</label> <label for="pushover_token" class="form-label">Pushover Token</label>
<input type="text" class="form-control" id="pushover_token" name="pushover_token" value="{{ settings.pushover_token }}"> <input type="text" class="form-control" id="pushover_token" name="pushover_token" value="{{ settings.pushover_token }}">
@@ -24,10 +25,11 @@
</div> </div>
</div> </div>
<hr> <hr>
<!-- Sekcja SMTP --> <!-- Sekcja SMTP -->
<div class="mb-4"> <div class="mb-4">
<h4 class="mb-3">Powiadomienia - SMTP (e-mail)</h4> <h5 class="mb-3">Powiadomienia - SMTP (e-mail)</h5>
<div class="mb-3 form-check"> <div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="smtp_notifications_enabled" name="smtp_notifications_enabled" {% if settings.smtp_notifications_enabled %}checked{% endif %}> <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> <label class="form-check-label" for="smtp_notifications_enabled">Włącz powiadomienia SMTP</label>
</div> </div>
@@ -47,25 +49,43 @@
<label for="smtp_password" class="form-label">SMTP Hasło</label> <label for="smtp_password" class="form-label">SMTP Hasło</label>
<input type="password" class="form-control" id="smtp_password" name="smtp_password" value="{{ settings.smtp_password }}"> <input type="password" class="form-control" id="smtp_password" name="smtp_password" value="{{ settings.smtp_password }}">
</div> </div>
</div>
<hr>
<!-- Sekcja globalnego klucza SSH -->
<div class="mb-4">
<h4 class="mb-3">Globalny klucz SSH</h4>
<div class="mb-3"> <div class="mb-3">
<label for="global_ssh_key" class="form-label"> <label for="recipient_email" class="form-label">Adres e-mail docelowy</label>
Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code> <input type="email" class="form-control" id="recipient_email" name="recipient_email" value="{{ settings.recipient_email }}">
</label>
<textarea class="form-control" id="global_ssh_key" name="global_ssh_key" rows="4">{{ settings.global_ssh_key }}</textarea>
</div> </div>
</div> </div>
<hr>
<!-- Sekcja globalnego klucza SSH -->
<div class="mb-4">
<h5 class="mb-3">Globalny klucz SSH</h5>
<label for="global_ssh_key" class="form-label">
Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code>
</label>
<textarea class="form-control" id="global_ssh_key" name="global_ssh_key" rows="4">{{ settings.global_ssh_key }}</textarea>
</div>
<div class="d-grid"> <div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Zapisz ustawienia</button> <button type="submit" class="btn btn-primary btn-lg">Zapisz ustawienia</button>
</div> </div>
</form> </form>
<!-- Przycisk do testowania powiadomień -->
<div class="mt-4 text-center">
<form method="POST" action="{{ url_for('test_email') }}" class="d-inline">
<button type="submit" class="btn btn-info">Testuj wysyłkę e-mail</button>
</form>
<form method="POST" action="{{ url_for('test_pushover') }}" class="d-inline ms-2">
<button type="submit" class="btn btn-warning">Testuj powiadomienie Pushover</button>
</form>
</div>
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
<p>Ustawienia dotyczące backupu oraz harmonogramu CRON znajdują się na <a href="{{ url_for('advanced_schedule') }}">zaawansowanych ustawieniach harmonogramu</a>.</p> <p class="mb-0">
Ustawienia harmonogramu i retencji:
<a href="{{ url_for('advanced_schedule') }}">Zaawansowane ustawienia</a>
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,30 +1,42 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div class="container my-4"> <div class="container my-4">
<h2>Podgląd eksportu: {{ backup.file_path|basename }}</h2> <div class="card border-0 shadow">
<hr> <div class="card-header bg-light">
<textarea id="exportEditor" readonly>{{ content|e }}</textarea> <h4 class="mb-0">
<a href="{{ next_url }}" class="btn btn-secondary">Powrót</a> Podgląd eksportu: {{ backup.file_path|basename }}
</h4>
</div>
<div class="card-body">
<textarea id="exportEditor" readonly>{{ content|e }}</textarea>
<div class="mt-3">
<a href="{{ next_url }}" class="btn btn-secondary">Powrót</a>
</div>
</div>
</div>
</div> </div>
<!-- CodeMirror CSS --> <!-- CodeMirror CSS -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css"> {% if session.get('dark_mode', True) %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/darcula.min.css">
{% else %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/neo.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/neo.min.css">
{% endif %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.css">
<!-- CodeMirror JS --> <!-- CodeMirror JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script>
<!-- Dodajemy tryb dla plików shell, który dobrze radzi sobie z konfiguracjami RouterOS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/shell/shell.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/shell/shell.min.js"></script>
<script> <script>
document.addEventListener("DOMContentLoaded", function() { document.addEventListener("DOMContentLoaded", function() {
var editor = CodeMirror.fromTextArea(document.getElementById("exportEditor"), { var editor = CodeMirror.fromTextArea(document.getElementById("exportEditor"), {
mode: "text/x-sh", mode: "text/x-sh",
theme: "neo", theme: "{{ 'darcula' if session.get('dark_mode', True) else 'neo' }}",
lineNumbers: true, lineNumbers: true,
readOnly: true readOnly: true
}); });
// Dopasowanie rozmiaru edytora do zawartości editor.setSize("100%", "600px");
editor.setSize("100%", "800px");
}); });
</script> </script>
{% endblock %} {% endblock %}