Compare commits
2 Commits
master
...
1c1e0552bf
Author | SHA1 | Date | |
---|---|---|---|
1c1e0552bf | |||
77b9bce9f8 |
11
Dockerfile
11
Dockerfile
@@ -1,11 +0,0 @@
|
||||
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
107
README.md
@@ -1,101 +1,20 @@
|
||||
# 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.
|
||||
# Instalation:
|
||||
- clone (to ex. /opt/routeros_backup)
|
||||
- create venv
|
||||
- install requirements via pip
|
||||
- copy systemd service (routeros_backup.service)
|
||||
|
||||
## 🔧 Instalacja
|
||||
# Start
|
||||
- systemctl start routeros_backup.service
|
||||
- go to http://IPADDRESS:5581
|
||||
|
||||
### 1. Klonowanie repozytorium
|
||||
```sh
|
||||
git clone https://gitea.linuxiarz.pl/gru/routeros_backup.git
|
||||
cd routeros_backup
|
||||
```
|
||||
# Register, Login
|
||||
|
||||
### 2. Tworzenie i aktywacja środowiska wirtualnego (opcjonalnie)
|
||||
```sh
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # Linux/macOS
|
||||
venv\Scripts\activate # Windows
|
||||
```
|
||||
# Configure devices, keys, backups, crons
|
||||
|
||||
### 3. Instalacja zależności
|
||||
```sh
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 4. Uruchomienie aplikacji lokalnie
|
||||
```sh
|
||||
python run_waitress.py
|
||||
```
|
||||
Aplikacja będzie dostępna pod adresem: `http://127.0.0.1:5581/`
|
||||
## Authors
|
||||
|
||||
---
|
||||
|
||||
## 📦 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.
|
||||
- [@linuxiarz.pl]
|
||||
|
224
app.py
224
app.py
@@ -9,16 +9,18 @@ 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
|
||||
|
||||
@@ -27,7 +29,7 @@ from flask import (
|
||||
url_for, session, flash, send_file
|
||||
)
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from passlib.context import CryptContext
|
||||
from passlib.hash import bcrypt
|
||||
#from flask_wtf.csrf import CSRFProtect
|
||||
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
@@ -56,18 +58,14 @@ 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 pwd_context.verify(password, self.password_hash)
|
||||
return bcrypt.verify(password, self.password_hash)
|
||||
|
||||
class Router(db.Model):
|
||||
__tablename__ = 'routers'
|
||||
@@ -89,8 +87,6 @@ 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
|
||||
@@ -116,8 +112,6 @@ 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
|
||||
@@ -172,13 +166,6 @@ 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
|
||||
###############################################################################
|
||||
@@ -242,17 +229,12 @@ 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, 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.")
|
||||
|
||||
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}")
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
# Wybór klucza: indywidualny lub globalny
|
||||
# Używamy indywidualnego klucza, a jeśli nie ma, to globalnego
|
||||
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:
|
||||
@@ -263,10 +245,8 @@ def ssh_upload_backup(router: Router, local_backup_path: str, expected_checksum:
|
||||
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)
|
||||
@@ -274,7 +254,6 @@ def ssh_upload_backup(router: Router, local_backup_path: str, expected_checksum:
|
||||
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()
|
||||
@@ -397,19 +376,23 @@ 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")
|
||||
@@ -418,19 +401,10 @@ 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)
|
||||
|
||||
if smtp_port == 465:
|
||||
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
||||
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
||||
server.starttls()
|
||||
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)
|
||||
@@ -467,13 +441,12 @@ 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=to_address,
|
||||
to_address=settings.smtp_login.strip(),
|
||||
subject="RouterOS Backup Notification",
|
||||
plain_body=message
|
||||
)
|
||||
@@ -518,10 +491,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 export dla routera {r.name} wykonany pomyślnie at {datetime.utcnow()}.")
|
||||
log_operation(f"Automatyczny eksport 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 export dla routera {r.name} FAILED at {datetime.utcnow()}: {e}")
|
||||
log_operation(f"Automatyczny eksport dla routera {r.name} FAILED at {datetime.utcnow()}: {e}")
|
||||
|
||||
def scheduled_auto_binary_backup():
|
||||
with app.app_context():
|
||||
@@ -531,8 +504,7 @@ 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)
|
||||
checksum = compute_checksum(local_path)
|
||||
b = Backup(router_id=r.id, file_path=local_path, backup_type='binary', checksum=checksum)
|
||||
b = Backup(router_id=r.id, file_path=local_path, backup_type='binary')
|
||||
db.session.add(b)
|
||||
db.session.commit()
|
||||
notify(s, f"Auto-binary backup dla routera {r.name} OK", True)
|
||||
@@ -596,19 +568,6 @@ 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ń
|
||||
###############################################################################
|
||||
@@ -621,8 +580,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()
|
||||
|
||||
scheduler.start()
|
||||
# Sprzątanie przy zamykaniu
|
||||
atexit.register(lambda: scheduler.shutdown())
|
||||
|
||||
@@ -643,7 +602,6 @@ 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
|
||||
@@ -691,19 +649,6 @@ 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
|
||||
###############################################################################
|
||||
@@ -775,20 +720,19 @@ 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()
|
||||
s.binary_cron = request.form.get('binary_cron', '').strip() # nowe pole
|
||||
s.export_cron = request.form.get('export_cron', '').strip()
|
||||
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))
|
||||
# Checkbox: jeśli nie jest zaznaczony, nie pojawi się w formularzu, więc ustawiamy na False
|
||||
s.enable_auto_export = True if request.form.get('enable_auto_export') == 'on' else False
|
||||
db.session.commit()
|
||||
reschedule_jobs() # Aktualizacja harmonogramu zadań
|
||||
flash("Ustawienia harmonogramu zostały zapisane.")
|
||||
reschedule_jobs() # Aktualizuje harmonogram zadań
|
||||
flash("Zaawansowane 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', True)
|
||||
current_mode = session.get('dark_mode', False)
|
||||
session['dark_mode'] = not current_mode
|
||||
return redirect(request.referrer or url_for('index'))
|
||||
|
||||
@@ -835,6 +779,8 @@ 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():
|
||||
@@ -899,8 +845,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": "export zakończony."}
|
||||
flash("Export zakończony.")
|
||||
return {"status": "success", "message": "Eksport zakończony."}
|
||||
flash("Eksport 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}")
|
||||
@@ -920,8 +866,7 @@ 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)
|
||||
checksum = compute_checksum(local_path)
|
||||
b = Backup(router_id=router.id, file_path=local_path, backup_type='binary', checksum=checksum)
|
||||
b = Backup(router_id=router.id, file_path=local_path, backup_type='binary')
|
||||
db.session.add(b)
|
||||
db.session.commit()
|
||||
notify(get_settings(), f"Backup {router.name} OK", True)
|
||||
@@ -944,23 +889,14 @@ 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))
|
||||
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)
|
||||
|
||||
return redirect(url_for('router_details', router_id=router.id))
|
||||
try:
|
||||
ssh_upload_backup(router, b.file_path, expected_checksum=b.checksum)
|
||||
ssh_upload_backup(router, b.file_path)
|
||||
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)
|
||||
|
||||
@@ -1028,6 +964,7 @@ 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():
|
||||
@@ -1047,11 +984,13 @@ 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']:
|
||||
@@ -1081,7 +1020,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 exportu.")
|
||||
flash("Wybrany backup nie jest plikiem eksportu.")
|
||||
return redirect(url_for('all_files'))
|
||||
try:
|
||||
with open(b.file_path, 'r', encoding='utf-8') as f:
|
||||
@@ -1105,10 +1044,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 export {os.path.basename(b.file_path)} z routera {b.router.name}."
|
||||
body = f"Przesyłam eksport {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 export mailem.")
|
||||
flash("Wysłano eksport mailem.")
|
||||
else:
|
||||
flash("Błąd wysyłki mailowej.")
|
||||
#return redirect(url_for('router_details', router_id=b.router_id))
|
||||
@@ -1154,9 +1093,8 @@ 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)
|
||||
@@ -1270,6 +1208,7 @@ 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'])
|
||||
@@ -1362,7 +1301,7 @@ def change_password():
|
||||
flash("Nowe hasło i potwierdzenie nie są zgodne.")
|
||||
return redirect(url_for('change_password'))
|
||||
|
||||
user.password_hash = pwd_context.hash(new_password)
|
||||
user.password_hash = bcrypt.hash(new_password)
|
||||
db.session.commit()
|
||||
flash("Hasło zostało zmienione pomyślnie.")
|
||||
return redirect(url_for('dashboard'))
|
||||
@@ -1381,84 +1320,17 @@ 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():
|
||||
reschedule_jobs()
|
||||
scheduler = BackgroundScheduler()
|
||||
schedule_retention_job()
|
||||
schedule_auto_export_job()
|
||||
schedule_auto_binary_backup_job()
|
||||
scheduler.start()
|
||||
atexit.register(lambda: scheduler.shutdown())
|
||||
app.run(host='0.0.0.0', port=5581, use_reloader=False, debug=True)
|
||||
|
@@ -1,15 +0,0 @@
|
||||
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
|
7
gunicorn_config.py
Normal file
7
gunicorn_config.py
Normal file
@@ -0,0 +1,7 @@
|
||||
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")
|
@@ -4,8 +4,7 @@ passlib
|
||||
paramiko
|
||||
APScheduler
|
||||
requests
|
||||
#gunicorn
|
||||
gunicorn
|
||||
flask_wtf
|
||||
gevent
|
||||
#croniter
|
||||
waitress
|
@@ -1,14 +1,18 @@
|
||||
[Unit]
|
||||
Description=RouterOS Backup Waitress Service
|
||||
Description=RouterOS Backup Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
#User=routeros
|
||||
#Group=routeros
|
||||
#User=www-data # Zmień na odpowiedniego użytkownika
|
||||
#Group=www-data
|
||||
WorkingDirectory=/opt/routeros_backup
|
||||
ExecStart=/opt/routeros_backup/venv/bin/python3 /opt/routeros_backup/run_waitress.py
|
||||
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
|
||||
|
||||
Restart=always
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
@@ -1,9 +0,0 @@
|
||||
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
21
start.sh
@@ -1,21 +0,0 @@
|
||||
#!/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!"
|
@@ -1,9 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<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 class="container mt-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0">Dodaj nowe urządzenie</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
@@ -24,16 +24,18 @@
|
||||
<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></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>
|
||||
<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>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
|
||||
<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">
|
||||
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">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Dodaj urządzenie</button>
|
||||
<button type="submit" class="btn btn-primary">Dodaj</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,61 +1,43 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<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 class="container mt-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0">Zaawansowane ustawienia harmonogramu</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('advanced_schedule') }}" method="POST">
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
<label for="retention_cron" class="form-label">Harmonogram retencji (cron)</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="retention_cron" name="retention_cron" value="{{ settings.retention_cron }}">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('retention_cron')">Generuj cron</button>
|
||||
</div>
|
||||
<small class="text-muted">Np. <code>0 */12 * * *</code> – co 12 godzin</small>
|
||||
<div class="form-text">Np. <code>0 */12 * * *</code> – co 12 godzin</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="binary_cron" class="form-label">Harmonogram kopii zapasowych binarnych <code>cron</code></label>
|
||||
<label for="binary_cron" class="form-label">Harmonogram kopii zapasowych binarnych (cron)</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>
|
||||
<small class="text-muted">Np. <code>15 2 * * *</code> – codziennie o 2:15</small>
|
||||
<div class="form-text">Np. <code>15 2 * * *</code> – codziennie o 2:15</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="export_cron" class="form-label">Harmonogram exportów <code>cron</code></label>
|
||||
<label for="export_cron" class="form-label">Harmonogram eksportów (cron)</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>
|
||||
<small class="text-muted">Np. <code>0 */12 * * *</code> – co 12 godzin</small>
|
||||
<div class="form-text">Np. <code>0 */12 * * *</code> – co 12 godzin</div>
|
||||
</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 export</label>
|
||||
<label class="form-check-label" for="enable_auto_export">Włącz automatyczny eksport</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>
|
||||
|
||||
@@ -98,15 +80,18 @@
|
||||
</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();
|
||||
}
|
||||
@@ -117,8 +102,10 @@
|
||||
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();
|
||||
|
@@ -3,8 +3,8 @@
|
||||
<div class="container my-4">
|
||||
<h2 class="text-center mb-4">Lista wszystkich backupów</h2>
|
||||
|
||||
<!-- Karta filtra -->
|
||||
<div class="card mb-4 shadow-sm border-0">
|
||||
<!-- Formularz filtrowania -->
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<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>
|
||||
|
||||
<!-- Karta tabeli backupów -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<!-- Tabela z backupami -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle">
|
||||
<table class="table table-striped table-hover mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 2%;"><input type="checkbox" id="select_all"></th>
|
||||
@@ -63,23 +63,18 @@
|
||||
<span class="badge bg-secondary">{{ file.backup_type }}</span>
|
||||
{% endif %}
|
||||
</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.file_path|basename }}</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-sm btn-info">
|
||||
<a href="{{ url_for('download_file', filename=file.file_path|basename) }}" class="btn btn-lg 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">
|
||||
<button type="submit" class="btn btn-sm btn-warning">
|
||||
<input type="hidden" name="next" value="{{ url_for('all_files') }}">
|
||||
<button type="submit" class="btn btn-lg btn-warning">
|
||||
<i class="bi bi-envelope"></i>
|
||||
</button>
|
||||
</form>
|
||||
@@ -87,7 +82,8 @@
|
||||
<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">
|
||||
<button type="submit" class="btn btn-sm btn-secondary">
|
||||
<input type="hidden" name="next" value="{{ url_for('all_files') }}">
|
||||
<button type="submit" class="btn btn-lg btn-secondary">
|
||||
<i class="bi bi-upload"></i>
|
||||
</button>
|
||||
</form>
|
||||
@@ -97,7 +93,7 @@
|
||||
</td>
|
||||
<td>
|
||||
{% if file.backup_type == 'export' %}
|
||||
<a href="{{ url_for('view_export', backup_id=file.id) }}" class="btn btn-sm btn-outline-primary">
|
||||
<a href="{{ url_for('view_export', backup_id=file.id) }}" class="btn btn-lg btn-outline-primary">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
@@ -106,7 +102,8 @@
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('delete_backup', backup_id=file.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
|
||||
<button type="submit" class="btn btn-sm btn-danger">
|
||||
<input type="hidden" name="next" value="{{ url_for('all_files') }}">
|
||||
<button type="submit" class="btn btn-lg btn-danger">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
@@ -120,21 +117,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formularz dla masowych akcji (zaznaczone pliki) -->
|
||||
<!-- Formularz dla masowych akcji (jeden formularz) -->
|
||||
<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-success me-2">
|
||||
<i class="bi bi-file-earmark-zip"></i> Pobierz zip
|
||||
<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>
|
||||
<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 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>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('select_all').addEventListener('change', function(e) {
|
||||
var checkboxes = document.querySelectorAll('input[name="backup_id"]');
|
||||
checkboxes.forEach(cb => cb.checked = e.target.checked);
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -1,392 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pl" class="{% if session.get('dark_mode', True) %}dark-mode{% endif %}">
|
||||
<html lang="pl" class="{% if session.dark_mode %}dark-mode{% endif %}">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Backup RouterOS</title>
|
||||
<title>Backup RouterOS App</title>
|
||||
<!-- Bootstrap 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;
|
||||
}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
|
||||
|
||||
/* 2) Ogólne style trybu ciemnego */
|
||||
<style>
|
||||
.dark-mode body {
|
||||
background-color: #121212;
|
||||
background-color: #222;
|
||||
color: #ffffff;
|
||||
}
|
||||
.dark-mode a,
|
||||
.dark-mode a:hover {
|
||||
.dark-mode a, .dark-mode a:hover {
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
/* 3) Nawigacja i menu w trybie ciemnym */
|
||||
.dark-mode .navbar,
|
||||
.dark-mode .navbar-nav,
|
||||
.dark-mode .dropdown-menu {
|
||||
.dark-mode .navbar, .dark-mode .table {
|
||||
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 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">
|
||||
<body>
|
||||
<nav class="navbar navbar-expand navbar-dark bg-dark mb-4">
|
||||
<div class="container-fluid">
|
||||
<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>
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
<!-- 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>
|
||||
<!--<a href="{{ url_for('toggle_dark_mode') }}" class="btn btn-warning">Toggle Dark Mode</a>-->
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- Główna zawartość -->
|
||||
<main class="container mb-5">
|
||||
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
<div class="container">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% 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 %}
|
||||
<div class="alert alert-info">
|
||||
{% for msg in messages %}
|
||||
<div>{{ msg }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 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ść ładowana przez AJAX -->
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stopka -->
|
||||
<footer class="footer py-3 mt-auto">
|
||||
<div class="container text-center">
|
||||
<span>© 2025 Mateusz Gruszczyński, linuxiarz.pl</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap Bundle JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
|
||||
<!-- Dodatkowe skrypty -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// 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.");
|
||||
});
|
||||
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.");
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-light text-center">
|
||||
<h4 class="mb-0">Zmień hasło</h4>
|
||||
<div class="card shadow">
|
||||
<div class="card-header text-center">
|
||||
<h2>Zmień hasło</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
|
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="container my-4">
|
||||
<h2 class="text-center mb-4">Dashboard</h2>
|
||||
<h2 class="text-center mb-4">Panel administracyjny</h2>
|
||||
|
||||
<!-- Wiersz akcji ogólnych -->
|
||||
<div class="row mb-4">
|
||||
<div class="col text-center">
|
||||
<div class="col-md-12 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 wszystkich routerów
|
||||
<i class="bi bi-arrow-down-circle"></i> Eksport dla 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 wszystkich routerów
|
||||
<i class="bi bi-cloud-download"></i> Backup binarny dla wszystkich routerów
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -73,11 +73,11 @@
|
||||
{% else %}
|
||||
{% set success_percent = 0 %}
|
||||
{% endif %}
|
||||
<div class="card mb-4 shadow-sm border-0">
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Statystyki operacji</h5>
|
||||
<p>Udane operacje: {{ success_ops }}, Nieudane operacje: {{ failure_ops }}</p>
|
||||
<div class="progress mb-2">
|
||||
<div class="progress">
|
||||
<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,12 +89,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Log operacji -->
|
||||
<div class="card shadow-sm border-0 mb-4">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<h5 class="card-title">Log operacji</h5>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -114,8 +111,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dodatkowe statystyki -->
|
||||
<div class="card shadow-sm border-0">
|
||||
<!-- Dodatkowe statystyki przeniesione na sam dół, pod logami -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Dodatkowe statystyki</h5>
|
||||
<div class="row">
|
||||
@@ -131,5 +128,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,47 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="container my-4">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
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);
|
||||
}
|
||||
// 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;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -1,10 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="container my-4">
|
||||
<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>
|
||||
<h2 class="text-center mb-4">Porównanie backupów (Diff)</h2>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('diff_selector') }}" method="POST" id="diffForm">
|
||||
<div class="row mb-3">
|
||||
@@ -31,10 +29,8 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-4">
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
Porównaj backupy
|
||||
</button>
|
||||
<div class="text-center">
|
||||
<button type="submit" class="btn btn-primary btn-lg">Porównaj backupy</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -43,12 +39,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 %}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<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 class="container mt-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0">Edycja urządzenia</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
@@ -25,15 +25,15 @@
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<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">{{ router.ssh_key }}</textarea>
|
||||
<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>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
|
||||
<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 }}">
|
||||
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 }}">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">Zapisz zmiany</button>
|
||||
</form>
|
||||
|
@@ -2,11 +2,7 @@
|
||||
{% 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">
|
||||
@@ -16,4 +12,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
|
@@ -1,23 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block head %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow border-0">
|
||||
<div class="card-header bg-light text-center">
|
||||
<h4 class="mb-0">Zaloguj się</h4>
|
||||
<div class="card shadow">
|
||||
<div class="card-header text-center">
|
||||
<h2>Zaloguj się</h2>
|
||||
</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" required>
|
||||
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika">
|
||||
</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" required>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Zaloguj się</button>
|
||||
@@ -25,10 +23,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<small>
|
||||
Nie masz konta?
|
||||
<a href="{{ url_for('register') }}">Zarejestruj się</a>
|
||||
</small>
|
||||
<a href="{{ url_for('register') }}">Nie masz konta? Zarejestruj się</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,76 +0,0 @@
|
||||
{% 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 %}
|
@@ -1,23 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block head %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<div class="row justify-content-center align-items-center">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
|
||||
<div class="col-md-6">
|
||||
<div class="card shadow border-0">
|
||||
<div class="card-header bg-light text-center">
|
||||
<h4 class="mb-0">Rejestracja</h4>
|
||||
<div class="card shadow">
|
||||
<div class="card-header text-center">
|
||||
<h2>Rejestracja</h2>
|
||||
</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" required>
|
||||
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika">
|
||||
</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" required>
|
||||
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło">
|
||||
</div>
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary">Zarejestruj się</button>
|
||||
@@ -25,10 +23,7 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<small>
|
||||
Masz już konto?
|
||||
<a href="{{ url_for('login') }}">Zaloguj się</a>
|
||||
</small>
|
||||
<a href="{{ url_for('login') }}">Masz już konto? Zaloguj się</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -28,7 +28,6 @@
|
||||
<h3>Pliki z /export</h3>
|
||||
{% if export_backups %}
|
||||
<!-- Tabela z indywidualnymi akcjami -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -55,6 +54,7 @@
|
||||
<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,11 +67,8 @@
|
||||
{% 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>
|
||||
@@ -89,13 +86,12 @@
|
||||
<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 %}
|
||||
@@ -105,7 +101,6 @@
|
||||
<!-- Sekcja backupów binarnych -->
|
||||
<h3>Pliki binarne (.backup)</h3>
|
||||
{% if binary_backups %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -123,6 +118,7 @@
|
||||
<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>
|
||||
@@ -138,11 +134,9 @@
|
||||
{% 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>
|
||||
@@ -166,7 +160,6 @@
|
||||
</table>
|
||||
<button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">Pusto</p>
|
||||
{% endif %}
|
||||
|
@@ -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">
|
||||
<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>
|
||||
<!-- Informacje o routerze -->
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0">Router: {{ router.name }}</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
@@ -20,13 +20,14 @@
|
||||
<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 btn-sm">Wykonaj /export</button>
|
||||
<button type="submit" class="btn btn-primary">Wykonaj /export</button>
|
||||
</form>
|
||||
<form action="{{ url_for('router_backup', router_id=router.id) }}" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-secondary btn-sm">Wykonaj backup binarny</button>
|
||||
<button type="submit" class="btn btn-secondary">Wykonaj backup binarny</button>
|
||||
</form>
|
||||
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-warning btn-sm">Edytuj ustawienia</a>
|
||||
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-warning">Edytuj ustawienia</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -34,172 +35,160 @@
|
||||
<!-- 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 border-0">
|
||||
<div class="card mt-3 shadow-sm">
|
||||
<div class="card-body">
|
||||
{% if export_backups %}
|
||||
<!-- Formularz masowych akcji dla eksportów -->
|
||||
<form id="export_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" name="action" value="download" class="btn btn-success btn-sm">
|
||||
<button type="submit" name="action" value="download" class="btn btn-lg btn-success">
|
||||
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
<!-- Tabela z eksportami z podzielonymi kolumnami akcji -->
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 3%;"><input type="checkbox" id="select_all_export"></th>
|
||||
<th>Nazwa pliku</th>
|
||||
<th>Rozmiar</th>
|
||||
<th>Data</th>
|
||||
<th>Diff</th>
|
||||
<th>Pobierz</th>
|
||||
<th>Podgląd</th>
|
||||
<th>Wyślij mailem</th>
|
||||
<th>Usuń</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in export_backups %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="export_mass_actions_form">
|
||||
</td>
|
||||
<td>{{ b.file_path|basename }}</td>
|
||||
<td>{{ b.file_path|filesize }}</td>
|
||||
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
||||
<td>
|
||||
{% if loop.index0 > 0 %}
|
||||
<a href="{{ url_for('diff_view', backup_id1=b.id, backup_id2=export_backups[0].id) }}" class="btn btn-sm btn-info">Diff</a>
|
||||
{% else %}
|
||||
<small>Brak nowszego</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz">
|
||||
<i class="bi bi-download"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-lg btn-outline-primary" title="Podgląd">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
|
||||
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
|
||||
<button type="submit" class="btn btn-lg btn-primary" title="Wyślij mailem">
|
||||
<i class="bi bi-envelope"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
|
||||
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
|
||||
<button type="submit" class="btn btn-lg btn-danger" title="Usuń">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-muted">Brak plików /export.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zakładka backupów binarnych -->
|
||||
<div class="tab-pane fade" id="binary" role="tabpanel" aria-labelledby="binary-tab">
|
||||
<div class="card mt-3 shadow-sm border-0">
|
||||
<div class="card mt-3 shadow-sm">
|
||||
<div class="card-body">
|
||||
{% if binary_backups %}
|
||||
<!-- Formularz masowych akcji dla backupów binarnych -->
|
||||
<form id="binary_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
|
||||
<div class="d-flex justify-content-end">
|
||||
<button type="submit" name="action" value="download" class="btn btn-success btn-sm">
|
||||
<button type="submit" name="action" value="download" class="btn btn-lg btn-success">
|
||||
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<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>
|
||||
<!-- Tabela z backupami binarnymi z podzielonymi kolumnami akcji -->
|
||||
<table class="table table-bordered table-striped">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th style="width: 3%;"><input type="checkbox" id="select_all_binary"></th>
|
||||
<th>Nazwa pliku</th>
|
||||
<th>Rozmiar</th>
|
||||
<th>Data</th>
|
||||
<th>Pobierz</th>
|
||||
<th>Wgraj do routera</th>
|
||||
<th>Wyślij mailem</th>
|
||||
<th>Usuń</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in binary_backups %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form">
|
||||
</td>
|
||||
<td>{{ b.file_path|basename }}</td>
|
||||
<td>{{ b.file_path|filesize }}</td>
|
||||
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz">
|
||||
<i class="bi bi-download"></i>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-lg btn-secondary" title="Wgraj do routera">
|
||||
<i class="bi bi-upload"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
|
||||
<button type="submit" class="btn btn-lg btn-primary" title="Wyślij mailem">
|
||||
<i class="bi bi-envelope"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
|
||||
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
|
||||
<button type="submit" class="btn btn-lg btn-danger" title="Usuń">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p class="text-muted">Brak plików binarnych.</p>
|
||||
{% endif %}
|
||||
@@ -213,14 +202,18 @@
|
||||
<script>
|
||||
document.getElementById('select_all_export').addEventListener('change', function(e) {
|
||||
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="export_mass_actions_form"]');
|
||||
checkboxes.forEach(cb => cb.checked = e.target.checked);
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
}
|
||||
});
|
||||
document.getElementById('select_all_binary').addEventListener('change', function(e) {
|
||||
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="binary_mass_actions_form"]');
|
||||
checkboxes.forEach(cb => cb.checked = e.target.checked);
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
checkboxes[i].checked = e.target.checked;
|
||||
}
|
||||
});
|
||||
|
||||
// Inicjalizacja zakładek Bootstrap
|
||||
// Inicjalizacja zakładek Bootstrap (jeśli nie są już inicjowane globalnie)
|
||||
var triggerTabList = [].slice.call(document.querySelectorAll('#routerTab button'));
|
||||
triggerTabList.forEach(function (triggerEl) {
|
||||
var tabTrigger = new bootstrap.Tab(triggerEl);
|
||||
@@ -229,11 +222,5 @@ 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 %}
|
||||
|
@@ -1,67 +1,63 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="container my-4">
|
||||
<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 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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,16 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<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 class="container my-5">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h2 class="mb-0">Ustawienia globalne</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
|
||||
<!-- Sekcja Pushover -->
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">Powiadomienia - Pushover</h5>
|
||||
<h4 class="mb-3">Powiadomienia - Pushover</h4>
|
||||
<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 }}">
|
||||
@@ -25,11 +24,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<!-- Sekcja SMTP -->
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">Powiadomienia - SMTP (e-mail)</h5>
|
||||
<div class="form-check mb-3">
|
||||
<h4 class="mb-3">Powiadomienia - SMTP (e-mail)</h4>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="smtp_notifications_enabled" name="smtp_notifications_enabled" {% if settings.smtp_notifications_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="smtp_notifications_enabled">Włącz powiadomienia SMTP</label>
|
||||
</div>
|
||||
@@ -49,43 +47,25 @@
|
||||
<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 class="mb-3">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</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 class="mb-0">
|
||||
Ustawienia harmonogramu i retencji:
|
||||
<a href="{{ url_for('advanced_schedule') }}">Zaawansowane ustawienia</a>
|
||||
</p>
|
||||
<p>Ustawienia dotyczące backupu oraz harmonogramu CRON znajdują się na <a href="{{ url_for('advanced_schedule') }}">zaawansowanych ustawieniach harmonogramu</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,42 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="container my-4">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- CodeMirror 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">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/neo.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: "{{ 'darcula' if session.get('dark_mode', True) else 'neo' }}",
|
||||
theme: "neo",
|
||||
lineNumbers: true,
|
||||
readOnly: true
|
||||
});
|
||||
editor.setSize("100%", "600px");
|
||||
// Dopasowanie rozmiaru edytora do zawartości
|
||||
editor.setSize("100%", "800px");
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
Reference in New Issue
Block a user