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:
- clone (to ex. /opt/routeros_backup)
- create venv
- install requirements via pip
- copy systemd service (routeros_backup.service)
## 🔧 Instalacja
2. Start
- systemctl start routeros_backup.service
- go to http://<IP>:81
### 1. Klonowanie repozytorium
```sh
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 shutil
import socket
import hashlib
from datetime import datetime
from ftplib import FTP
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email import encoders
from flask import jsonify
from flask import Flask
import shutil
from datetime import datetime
from difflib import HtmlDiff
import difflib
from datetime import datetime, timedelta
from sqlalchemy import text
@@ -29,7 +27,7 @@ from flask import (
url_for, session, flash, send_file
)
from flask_sqlalchemy import SQLAlchemy
from passlib.hash import bcrypt
from passlib.context import CryptContext
#from flask_wtf.csrf import CSRFProtect
from apscheduler.schedulers.background import BackgroundScheduler
@@ -58,14 +56,18 @@ os.makedirs(DATA_DIR, exist_ok=True)
###############################################################################
# Modele bazy danych
###############################################################################
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(120), unique=True, 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):
return bcrypt.verify(password, self.password_hash)
return pwd_context.verify(password, self.password_hash)
class Router(db.Model):
__tablename__ = 'routers'
@@ -87,6 +89,8 @@ class Backup(db.Model):
file_path = db.Column(db.String(255), nullable=False) # Ścieżka do pliku
backup_type = db.Column(db.String(50), default='export') # 'export' lub 'binary'
created_at = db.Column(db.DateTime, default=datetime.utcnow)
checksum = db.Column(db.String(64), nullable=True)
class OperationLog(db.Model):
__tablename__ = 'operation_logs'
__table_args__ = {'extend_existing': True} # Zapobiega redefinicji tabeli
@@ -112,6 +116,8 @@ class GlobalSettings(db.Model):
smtp_login = db.Column(db.String(255), nullable=True)
smtp_password = db.Column(db.String(255), nullable=True)
smtp_notifications_enabled = db.Column(db.Boolean, default=False)
log_retention_days = db.Column(db.Integer, default=7)
recipient_email = db.Column(db.String(255), nullable=True)
###############################################################################
# Inicjalizacja bazy
@@ -166,6 +172,13 @@ def load_pkey(ssh_key_str: str):
except Exception as e:
raise ValueError("Nie udało się załadować klucza SSH. Sprawdź, czy klucz jest poprawny i nie jest zaszyfrowany") from e
def compute_checksum(file_path):
sha256 = hashlib.sha256()
with open(file_path, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b""):
sha256.update(chunk)
return sha256.hexdigest()
###############################################################################
# Funkcje SSH
###############################################################################
@@ -229,12 +242,17 @@ def ssh_backup(router: Router, backup_name: str) -> str:
print(f"[DEBUG] ssh_backup -> local_path={local_path}")
return local_path
def ssh_upload_backup(router: Router, local_backup_path: str):
print(f"[DEBUG] ssh_upload_backup -> router id={router.id}, local_backup_path={local_backup_path}")
def ssh_upload_backup(router: Router, local_backup_path: str, expected_checksum: str = None):
# Weryfikacja sumy kontrolnej, jeśli podana
if expected_checksum:
local_checksum = compute_checksum(local_backup_path)
if local_checksum != expected_checksum:
raise ValueError("Suma kontrolna pliku nie zgadza się plik może być uszkodzony.")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# Używamy indywidualnego klucza, a jeśli nie ma, to globalnego
# Wybór klucza: indywidualny lub globalny
key_source = router.ssh_key if router.ssh_key and router.ssh_key.strip() else get_settings().global_ssh_key
if key_source and key_source.strip():
try:
@@ -245,8 +263,10 @@ def ssh_upload_backup(router: Router, local_backup_path: str):
raise e
else:
client.connect(router.host, port=router.port, username=router.ssh_user,
password=router.ssh_password, timeout=10, allow_agent=False, look_for_keys=False, banner_timeout=10)
password=router.ssh_password, timeout=10,
allow_agent=False, look_for_keys=False, banner_timeout=10)
# Otwieramy sesję SFTP, przesyłamy plik, a następnie zamykamy połączenie
sftp = client.open_sftp()
remote_file = os.path.basename(local_backup_path)
sftp.put(local_backup_path, remote_file)
@@ -254,6 +274,7 @@ def ssh_upload_backup(router: Router, local_backup_path: str):
client.close()
print(f"[DEBUG] ssh_upload_backup -> przesłano {local_backup_path} do routera")
def ssh_test_connection(router: Router) -> dict:
"""Testuje połączenie z routerem i zwraca informacje: model, uptime, hostname."""
client = paramiko.SSHClient()
@@ -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.")
return False
try:
# Utwórz wiadomość typu alternative (obsługuje plain text i HTML)
msg = MIMEMultipart("alternative")
msg["From"] = smtp_user
msg["To"] = to_address
msg["Subject"] = subject
# Dodaj część tekstową
part1 = MIMEText(plain_body, "plain")
msg.attach(part1)
# Jeśli nie podano wersji HTML, wygeneruj domyślny szablon
if html_body is None:
html_body = get_email_template(subject, plain_body)
part2 = MIMEText(html_body, "html")
msg.attach(part2)
# Dodaj załącznik, jeśli podany i istnieje
if attachment_path and os.path.isfile(attachment_path):
with open(attachment_path, "rb") as attachment:
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)}")
msg.attach(part)
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
if smtp_port == 465:
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
server.login(smtp_user, smtp_pass)
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
except Exception as 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_password and settings.smtp_password.strip()):
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(
smtp_host=settings.smtp_host.strip(),
smtp_port=settings.smtp_port,
smtp_user=settings.smtp_login.strip(),
smtp_pass=settings.smtp_password.strip(),
to_address=settings.smtp_login.strip(),
to_address=to_address,
subject="RouterOS Backup Notification",
plain_body=message
)
@@ -491,10 +518,10 @@ def scheduled_auto_backup():
db.session.add(b)
db.session.commit()
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:
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():
with app.app_context():
@@ -504,7 +531,8 @@ def scheduled_auto_binary_backup():
try:
backup_name = f"{r.name}_{r.id}_{datetime.now():%Y%m%d_%H%M%S}"
local_path = ssh_backup(r, backup_name)
b = Backup(router_id=r.id, file_path=local_path, backup_type='binary')
checksum = compute_checksum(local_path)
b = Backup(router_id=r.id, file_path=local_path, backup_type='binary', checksum=checksum)
db.session.add(b)
db.session.commit()
notify(s, f"Auto-binary backup dla routera {r.name} OK", True)
@@ -568,6 +596,19 @@ def schedule_auto_export_job():
cron_used = s.export_cron if s.export_cron else "0 */12 * * *"
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ń
###############################################################################
@@ -580,8 +621,8 @@ scheduler = BackgroundScheduler()
# 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=scheduled_auto_backup, trigger='interval', days=1, id="auto_backup_job")
scheduler.start()
# Sprzątanie przy zamykaniu
atexit.register(lambda: scheduler.shutdown())
@@ -602,6 +643,7 @@ def reschedule_jobs():
schedule_auto_export_job()
schedule_retention_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
@@ -649,6 +691,19 @@ def get_data_folder_size():
pass
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
###############################################################################
@@ -720,19 +775,20 @@ def advanced_schedule():
s = get_settings()
if request.method == 'POST':
s.retention_cron = request.form.get('retention_cron', '').strip()
s.binary_cron = request.form.get('binary_cron', '').strip() # nowe pole
s.binary_cron = request.form.get('binary_cron', '').strip()
s.export_cron = request.form.get('export_cron', '').strip()
# Checkbox: jeśli nie jest zaznaczony, nie pojawi się w formularzu, więc ustawiamy na False
s.backup_retention_days = int(request.form.get('backup_retention_days', s.backup_retention_days))
s.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
db.session.commit()
reschedule_jobs() # Aktualizuje harmonogram zadań
flash("Zaawansowane ustawienia harmonogramu zostały zapisane.")
reschedule_jobs() # Aktualizacja harmonogramu zadań
flash("Ustawienia harmonogramu zostały zapisane.")
return redirect(url_for('advanced_schedule'))
return render_template('advanced_schedule.html', settings=s)
@app.route('/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
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()
return render_template('routers.html', user=user, routers=routers)
@app.route('/routers/add', methods=['GET','POST'])
@login_required
def add_router():
@@ -845,8 +899,8 @@ def router_export(router_id):
notify(get_settings(), f"Export {router.name} OK", True)
log_operation(f"Export wykonany dla routera {router.name} at {datetime.utcnow()}")
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return {"status": "success", "message": "Eksport zakończony."}
flash("Eksport zakończony.")
return {"status": "success", "message": "export zakończony."}
flash("Export zakończony.")
except Exception as e:
notify(get_settings(), f"Export {router.name} FAIL: {e}", False)
log_operation(f"Export dla routera {router.name} FAILED: {e}")
@@ -866,7 +920,8 @@ def router_backup(router_id):
try:
backup_name = f"{router.name}_{router.id}_{datetime.now():%Y%m%d_%H%M%S}"
local_path = ssh_backup(router, backup_name)
b = Backup(router_id=router.id, file_path=local_path, backup_type='binary')
checksum = compute_checksum(local_path)
b = Backup(router_id=router.id, file_path=local_path, backup_type='binary', checksum=checksum)
db.session.add(b)
db.session.commit()
notify(get_settings(), f"Backup {router.name} OK", True)
@@ -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()
if not b:
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:
ssh_upload_backup(router, b.file_path)
ssh_upload_backup(router, b.file_path, expected_checksum=b.checksum)
log_operation(f"Backup {os.path.basename(b.file_path)} wgrany do routera {router.name} at {datetime.utcnow()}")
flash("Plik backupu wgrany do routera.")
except Exception as e:
flash(f"Błąd wgrywania: {e}")
#return redirect(url_for('router_details', router_id=router.id))
next_url = request.form.get('next') or request.referrer or url_for('dashboard')
return redirect(next_url)
@@ -964,7 +1028,6 @@ def export_all_routers():
flash(" | ".join(messages))
return redirect(url_for('dashboard'))
# Nowa podstrona: diff selector
@app.route('/diff_selector', methods=['GET', 'POST'])
@login_required
def diff_selector():
@@ -984,13 +1047,11 @@ def diff_selector():
def all_files():
user = get_current_user()
query = Backup.query.join(Router).filter(Router.owner_id == user.id)
# Filtrowanie wyszukiwanie po nazwie pliku (zastosowanie filtru "basename")
search = request.args.get('search', '')
if search:
query = query.filter(Backup.file_path.ilike(f"%{search}%"))
# Sortowanie sort_by i order
sort_by = request.args.get('sort_by', 'created_at')
order = request.args.get('order', 'desc')
if sort_by not in ['created_at', 'file_path']:
@@ -1020,7 +1081,7 @@ def view_export(backup_id):
flash("Brak dostępu do backupu.")
return redirect(url_for('all_files'))
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'))
try:
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.")
return redirect(url_for('settings_view'))
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,
s.smtp_login, subject, body, b.file_path):
flash("Wysłano eksport mailem.")
flash("Wysłano export mailem.")
else:
flash("Błąd wysyłki mailowej.")
#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_login = request.form.get('smtp_login', '')
s.smtp_password = request.form.get('smtp_password', '')
s.recipient_email = request.form.get('recipient_email', '')
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.")
return redirect(url_for('settings_view'))
return render_template('settings.html', settings=s)
@@ -1208,7 +1270,6 @@ def diff_view(backup_id1, backup_id2):
lineterm=''
))
diff_text = "\n".join(diff_lines)
return render_template('diff.html', diff_text=diff_text, backup1=b1, backup2=b2)
@app.route('/routers/all_backup', methods=['POST'])
@@ -1301,7 +1362,7 @@ def change_password():
flash("Nowe hasło i potwierdzenie nie są zgodne.")
return redirect(url_for('change_password'))
user.password_hash = bcrypt.hash(new_password)
user.password_hash = pwd_context.hash(new_password)
db.session.commit()
flash("Hasło zostało zmienione pomyślnie.")
return redirect(url_for('dashboard'))
@@ -1320,17 +1381,84 @@ def test_connection(router_id):
except Exception as e:
flash(f"Błąd testu połączenia: {e}")
return redirect(url_for('routers_list'))
# Jeśli wywołanie zawiera parametr modal=1, zwracamy widok dla modalu
if request.args.get("modal") == "1":
return render_template("test_connection_modal.html", router=router, result=result)
return render_template("test_connection.html", router=router, result=result)
@app.route('/logs')
@login_required
def logs_page():
logs = OperationLog.query.order_by(OperationLog.timestamp.desc()).all()
return render_template('logs.html', logs=logs)
@app.route('/logs/delete', methods=['POST'])
@login_required
def delete_old_logs():
try:
delete_days = int(request.form.get('delete_days', 0))
except ValueError:
flash("Podana wartość jest nieprawidłowa.")
return redirect(url_for('logs_page'))
if delete_days < 1:
flash("Podaj wartość większą lub równą 1.")
return redirect(url_for('logs_page'))
cutoff_date = datetime.utcnow() - timedelta(days=delete_days)
old_logs = OperationLog.query.filter(OperationLog.timestamp < cutoff_date).all()
deleted_count = 0
for log in old_logs:
db.session.delete(log)
deleted_count += 1
db.session.commit()
flash(f"Usunięto {deleted_count} logów starszych niż {delete_days} dni.")
return redirect(url_for('logs_page'))
@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__':
with app.app_context():
scheduler = BackgroundScheduler()
schedule_retention_job()
schedule_auto_export_job()
schedule_auto_binary_backup_job()
scheduler.start()
reschedule_jobs()
atexit.register(lambda: scheduler.shutdown())
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
APScheduler
requests
gunicorn
#gunicorn
flask_wtf
gevent
#croniter
waitress

View File

@@ -1,18 +1,14 @@
[Unit]
Description=RouterOS Backup Application
Description=RouterOS Backup Waitress Service
After=network.target
[Service]
#User=www-data # Zmień na odpowiedniego użytkownika
#Group=www-data
#User=routeros
#Group=routeros
WorkingDirectory=/opt/routeros_backup
Environment="PATH=/opt/hosts_app/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
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
ExecStart=/opt/routeros_backup/venv/bin/python3 /opt/routeros_backup/run_waitress.py
Restart=always
RestartSec=5
Environment=PYTHONUNBUFFERED=1
[Install]
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" %}
{% block content %}
<div class="container mt-5">
<div class="card shadow-sm">
<div class="card-header">
<h2 class="mb-0">Dodaj nowe urządzenie</h2>
<div class="container my-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light">
<h4 class="mb-0">Dodaj nowe urządzenie</h4>
</div>
<div class="card-body">
<form method="POST">
@@ -24,18 +24,16 @@
<input type="text" class="form-control" id="ssh_user" name="ssh_user" required>
</div>
<div class="mb-3">
<label for="ssh_key" class="form-label">
<b>Klucz prywatny</b> | Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code><br>
Pozostaw puste jeśli ten RouterOS będzie używał <a href="{{ url_for('settings_view') }}">klucza globalnego</a>
</label>
<textarea class="form-control" id="ssh_key" name="ssh_key" rows="4"></textarea>
<label for="ssh_key" class="form-label"><b>Klucz prywatny</b></label><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>
<textarea class="form-control mt-2" id="ssh_key" name="ssh_key" rows="4"></textarea>
</div>
<div class="mb-3">
<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.
<input type="password" class="form-control" id="ssh_password" name="ssh_password">
<small>Jeśli jest klucz SSH lub klucz globalny, hasło może być ignorowane.</small>
<input type="password" class="form-control mt-2" id="ssh_password" name="ssh_password">
</div>
<button type="submit" class="btn btn-primary">Dodaj</button>
<button type="submit" class="btn btn-primary">Dodaj urządzenie</button>
</form>
</div>
</div>

View File

@@ -1,43 +1,61 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card shadow-sm">
<div class="card-header">
<h2 class="mb-0">Zaawansowane ustawienia harmonogramu</h2>
<div class="container my-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light">
<h4 class="mb-0">Zaawansowane ustawienia harmonogramu</h4>
</div>
<div class="card-body">
<form action="{{ url_for('advanced_schedule') }}" method="POST">
<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">
<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>
</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 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">
<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>
</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 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">
<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>
</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 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 %}>
<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>
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
</form>
</div>
<div class="card-footer text-center">
<small class="text-muted">Ustawienia zostaną zapisane i użyte przez cron.</small>
</div>
</div>
</div>
@@ -80,18 +98,15 @@
</div>
<script>
// Zmienna przechowująca ID pola, do którego ma być wpisane wyrażenie cron
var targetCronField = '';
function openCronModal(fieldId) {
targetCronField = fieldId;
// Wyzeruj wartości w modalu
document.getElementById('cron_minute').value = '*';
document.getElementById('cron_hour').value = '*';
document.getElementById('cron_day').value = '*';
document.getElementById('cron_month').value = '*';
document.getElementById('cron_dow').value = '*';
// Otwórz modal (przy użyciu Bootstrap 5)
var cronModal = new bootstrap.Modal(document.getElementById('cronModal'));
cronModal.show();
}
@@ -102,10 +117,8 @@
var day = document.getElementById('cron_day').value || '*';
var month = document.getElementById('cron_month').value || '*';
var dow = document.getElementById('cron_dow').value || '*';
var cronExpr = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + dow;
document.getElementById(targetCronField).value = cronExpr;
// Zamknij modal
var modalEl = document.getElementById('cronModal');
var modalInstance = bootstrap.Modal.getInstance(modalEl);
modalInstance.hide();

View File

@@ -3,8 +3,8 @@
<div class="container my-4">
<h2 class="text-center mb-4">Lista wszystkich backupów</h2>
<!-- Formularz filtrowania -->
<div class="card mb-4 shadow-sm">
<!-- Karta filtra -->
<div class="card mb-4 shadow-sm border-0">
<div class="card-body">
<form method="GET" action="{{ url_for('all_files') }}" class="row g-2">
<div class="col-md-4">
@@ -29,11 +29,11 @@
</div>
</div>
<!-- Tabela z backupami -->
<div class="card shadow-sm mb-4">
<!-- Karta tabeli backupów -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<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">
<tr>
<th style="width: 2%;"><input type="checkbox" id="select_all"></th>
@@ -63,18 +63,23 @@
<span class="badge bg-secondary">{{ file.backup_type }}</span>
{% endif %}
</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.file_path|filesize }}</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>
</a>
</td>
<td>
<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-lg btn-warning">
<button type="submit" class="btn btn-sm btn-warning">
<i class="bi bi-envelope"></i>
</button>
</form>
@@ -82,8 +87,7 @@
<td>
{% 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">
<input type="hidden" name="next" value="{{ url_for('all_files') }}">
<button type="submit" class="btn btn-lg btn-secondary">
<button type="submit" class="btn btn-sm btn-secondary">
<i class="bi bi-upload"></i>
</button>
</form>
@@ -93,7 +97,7 @@
</td>
<td>
{% 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>
</a>
{% else %}
@@ -102,8 +106,7 @@
</td>
<td>
<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-lg btn-danger">
<button type="submit" class="btn btn-sm btn-danger">
<i class="bi bi-trash"></i>
</button>
</form>
@@ -117,22 +120,21 @@
</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">
<button type="submit" name="action" value="download" class="btn btn-lg btn-success me-2">
<i class="bi bi-file-earmark-zip"></i> Pobierz zip zaznaczonych
<button type="submit" name="action" value="download" class="btn btn-success me-2">
<i class="bi bi-file-earmark-zip"></i> Pobierz zip
</button>
<button type="submit" name="action" value="delete" class="btn btn-lg btn-danger" onclick="return confirm('Na pewno usunąć zaznaczone backupy?');">
<i class="bi bi-trash"></i> Usuń 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
</button>
</form>
</div>
<script>
document.getElementById('select_all').addEventListener('change', function(e) {
var checkboxes = document.querySelectorAll('input[name="backup_id"]');
for (var i = 0; i < checkboxes.length; i++) {
checkboxes[i].checked = e.target.checked;
}
checkboxes.forEach(cb => cb.checked = e.target.checked);
});
</script>
{% endblock %}

View File

@@ -1,116 +1,392 @@
<!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>
<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 -->
<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>
/* 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 {
background-color: #222;
background-color: #121212;
color: #ffffff;
}
.dark-mode a, .dark-mode a:hover {
.dark-mode a,
.dark-mode a:hover {
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;
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-rem { color: red; }
.dark-mode .text-muted {
color: #aaa !important; /* zamiast #aaa możesz wybrać #bbb, #ccc itp. */
}
</style>
{% block head %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand navbar-dark bg-dark mb-4">
<body class="d-flex flex-column">
<!-- 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">
<a href="{{ url_for('index') }}" class="navbar-brand">Backup RouterOS</a>
<div>
{% if session.user_id %}
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary me-2">Dashboard</a>
<a href="{{ url_for('routers_list') }}" class="btn btn-secondary me-2">Urządzenia</a>
<a href="{{ url_for('diff_selector') }}" class="btn btn-secondary me-2">Diff selector</a>
<a href="{{ url_for('all_files') }}" class="btn btn-secondary me-2">Wszystkie pliki</a>
<a href="{{ url_for('settings_view') }}" class="btn btn-secondary me-2">Ustawienia</a>
<a href="{{ url_for('advanced_schedule') }}" class="btn btn-secondary me-2">Harmonogram</a>
<a href="{{ url_for('change_password') }}" class="btn btn-secondary me-2">Zmiana hasła</a>
<a href="{{ url_for('logout') }}" class="btn btn-secondary me-2">Wyloguj</a>
{% else %}
<a href="{{ url_for('login') }}" class="btn btn-secondary me-2">Zaloguj</a>
<a href="{{ url_for('register') }}" class="btn btn-secondary me-2">Utwórz konto</a>
<a href="/" class="navbar-brand">Backup RouterOS</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown"
aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavDropdown">
{% if session.get('user_id') %}
<ul class="navbar-nav me-auto">
<!-- Dashboard -->
<li class="nav-item">
<a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
</li>
<!-- Urządzenia dropdown -->
<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 %}
<!--<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>
</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 %}
<div class="alert alert-info">
{% for msg in messages %}
<div>{{ msg }}</div>
{% endfor %}
</div>
{% for category, msg in messages %}
{% set bs_cat = bootstrap_alert_category(category) %}
<div class="alert alert-{{ bs_cat }} alert-dismissible fade show" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
{% block content %}{% endblock %}
</main>
<!-- Modal Test Połączenia -->
<div class="modal fade" id="testConnectionModal" tabindex="-1" aria-labelledby="testConnectionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="testConnectionModalLabel">Test Połączenia</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body" id="testConnectionModalBody">
<!-- Zawartość zostanie załadowana przez AJAX -->
<div class="modal fade" id="testConnectionModal" tabindex="-1" aria-labelledby="testConnectionModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="testConnectionModalLabel">Test Połączenia</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
</div>
<div class="modal-body" id="testConnectionModalBody">
<!-- Zawartość ładowana przez AJAX -->
</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>
function ajaxExport(router_id) {
fetch("/router/" + router_id + "/export", {
method: "POST",
headers: {"X-Requested-With": "XMLHttpRequest"}
})
.then(response => response.json())
.then(data => {
if(data.status === "success"){
alert("Eksport wykonany: " + data.message);
// Możesz też zaktualizować część strony dynamicznie
} else {
alert("Błąd eksportu: " + data.message);
}
})
.catch(error => {
console.error("Błąd AJAX:", error);
alert("Wystąpił błąd.");
});
// Funkcja do wczytywania modalu testu połączenia
function openTestConnectionModal(routerId) {
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>
<script>
function openTestConnectionModal(routerId) {
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>
</script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

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

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %}
{% block content %}
<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 -->
<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">
<i class="bi bi-hdd-network"></i> Zobacz routery
</a>
@@ -53,14 +53,14 @@
<div class="col-md-6 d-flex justify-content-center">
<form action="{{ url_for('export_all_routers') }}" method="POST">
<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>
</form>
</div>
<div class="col-md-6 d-flex justify-content-center">
<form action="{{ url_for('backup_all_routers') }}" method="POST">
<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>
</form>
</div>
@@ -73,11 +73,11 @@
{% else %}
{% set success_percent = 0 %}
{% endif %}
<div class="card mb-4 shadow-sm">
<div class="card mb-4 shadow-sm border-0">
<div class="card-body">
<h5 class="card-title">Statystyki operacji</h5>
<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">
{{ success_percent }}%
</div>
@@ -89,9 +89,12 @@
</div>
<!-- Log operacji -->
<div class="card shadow-sm mb-4">
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<h5 class="card-title">Log operacji</h5>
<h5 class="card-title">
Log operacji
<a href="{{ url_for('logs_page') }}" class="btn btn-sm btn-outline-primary ms-2">Więcej logów</a>
</h5>
<table class="table table-sm table-bordered">
<thead>
<tr>
@@ -111,8 +114,8 @@
</div>
</div>
<!-- Dodatkowe statystyki przeniesione na sam dół, pod logami -->
<div class="card shadow-sm">
<!-- Dodatkowe statystyki -->
<div class="card shadow-sm border-0">
<div class="card-body">
<h5 class="card-title">Dodatkowe statystyki</h5>
<div class="row">
@@ -128,6 +131,5 @@
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,28 +1,47 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-4">
<h2>Porównanie: {{ backup1.file_path|basename }} vs {{ backup2.file_path|basename }}</h2>
<hr>
<div id="diffContainer"></div>
<a href="{{ url_for('router_details', router_id=backup1.router_id) }}" class="btn btn-secondary mt-3">Powrót</a>
<div class="card border-0 shadow-sm">
<div class="card-header bg-light">
<h4 class="mb-0">
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>
<!-- Dodajemy diff2html -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
<script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html.min.js"></script>
<!-- diff2html resources -->
<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@3.4.4/bundles/js/diff2html.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
// Upewnij się, że diff_text jest poprawnie escapowany
var diffText = `{{ diff_text|e }}`;
var targetElement = document.getElementById("diffContainer");
var configuration = {
drawFileList: true,
matching: 'lines',
outputFormat: 'line-by-line'
};
var diffHtml = Diff2Html.html(diffText, configuration);
targetElement.innerHTML = diffHtml;
var diffText = `{{ diff_text|e }}`;
var targetElement = document.getElementById("diffContainer");
var configuration = {
drawFileList: true,
matching: 'lines',
outputFormat: 'line-by-line'
};
var diffHtml = Diff2Html.html(diffText, configuration);
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>
{% endblock %}

View File

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

View File

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

View File

@@ -2,7 +2,11 @@
{% block content %}
<div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 80vh;">
<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;">
{% endif %}
<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>
<div class="mt-4">
@@ -12,3 +16,4 @@
</div>
</div>
{% endblock %}

View File

@@ -1,21 +1,23 @@
{% extends "base.html" %}
{% block head %}{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
<div class="container my-5">
<div class="row justify-content-center align-items-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header text-center">
<h2>Zaloguj się</h2>
<div class="card shadow border-0">
<div class="card-header bg-light text-center">
<h4 class="mb-0">Zaloguj się</h4>
</div>
<div class="card-body">
<form action="{{ url_for('login') }}" method="POST">
<div class="mb-3">
<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 class="mb-3">
<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 class="d-grid">
<button type="submit" class="btn btn-primary">Zaloguj się</button>
@@ -23,7 +25,10 @@
</form>
</div>
<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>

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" %}
{% block head %}{% endblock %}
{% block content %}
<div class="container">
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
<div class="container my-5">
<div class="row justify-content-center align-items-center">
<div class="col-md-6">
<div class="card shadow">
<div class="card-header text-center">
<h2>Rejestracja</h2>
<div class="card shadow border-0">
<div class="card-header bg-light text-center">
<h4 class="mb-0">Rejestracja</h4>
</div>
<div class="card-body">
<form action="{{ url_for('register') }}" method="POST">
<div class="mb-3">
<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 class="mb-3">
<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 class="d-grid">
<button type="submit" class="btn btn-primary">Zarejestruj się</button>
@@ -23,7 +25,10 @@
</form>
</div>
<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>

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,16 @@
{% extends "base.html" %}
{% block content %}
<div class="container my-5">
<div class="card shadow-sm">
<div class="card-header">
<h2 class="mb-0">Ustawienia globalne</h2>
<div class="container my-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light">
<h4 class="mb-0">Ustawienia globalne</h4>
</div>
<div class="card-body">
<form method="POST">
<!-- Sekcja Pushover -->
<div class="mb-4">
<h4 class="mb-3">Powiadomienia - Pushover</h4>
<h5 class="mb-3">Powiadomienia - Pushover</h5>
<div class="mb-3">
<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 }}">
@@ -24,10 +25,11 @@
</div>
</div>
<hr>
<!-- Sekcja SMTP -->
<div class="mb-4">
<h4 class="mb-3">Powiadomienia - SMTP (e-mail)</h4>
<div class="mb-3 form-check">
<h5 class="mb-3">Powiadomienia - SMTP (e-mail)</h5>
<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 %}>
<label class="form-check-label" for="smtp_notifications_enabled">Włącz powiadomienia SMTP</label>
</div>
@@ -47,25 +49,43 @@
<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 }}">
</div>
</div>
<hr>
<!-- Sekcja globalnego klucza SSH -->
<div class="mb-4">
<h4 class="mb-3">Globalny klucz SSH</h4>
<div class="mb-3">
<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>
<label for="recipient_email" class="form-label">Adres e-mail docelowy</label>
<input type="email" class="form-control" id="recipient_email" name="recipient_email" value="{{ settings.recipient_email }}">
</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">
<button type="submit" class="btn btn-primary btn-lg">Zapisz ustawienia</button>
</div>
</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 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>

View File

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