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
|
# Register, Login
|
||||||
```sh
|
|
||||||
git clone https://gitea.linuxiarz.pl/gru/routeros_backup.git
|
|
||||||
cd routeros_backup
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Tworzenie i aktywacja środowiska wirtualnego (opcjonalnie)
|
# Configure devices, keys, backups, crons
|
||||||
```sh
|
|
||||||
python3 -m venv venv
|
|
||||||
source venv/bin/activate # Linux/macOS
|
|
||||||
venv\Scripts\activate # Windows
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Instalacja zależności
|
|
||||||
```sh
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Uruchomienie aplikacji lokalnie
|
## Authors
|
||||||
```sh
|
|
||||||
python run_waitress.py
|
|
||||||
```
|
|
||||||
Aplikacja będzie dostępna pod adresem: `http://127.0.0.1:5581/`
|
|
||||||
|
|
||||||
---
|
- [@linuxiarz.pl]
|
||||||
|
|
||||||
## 📦 Uruchamianie w Dockerze
|
|
||||||
|
|
||||||
1. **Zbudowanie obrazu Docker**
|
|
||||||
```sh
|
|
||||||
docker-compose build
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Uruchomienie kontenera**
|
|
||||||
```sh
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
Aplikacja uruchomi się na porcie `5581`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 Funkcjonalności
|
|
||||||
|
|
||||||
- 🔐 System użytkowników (rejestracja, logowanie, zmiana hasła)
|
|
||||||
- 📡 Połączenie SSH do routerów MikroTik
|
|
||||||
- 🛠 Eksport konfiguracji i tworzenie backupów binarnych
|
|
||||||
- 🕵️♂️ Porównywanie backupów (`diff`)
|
|
||||||
- 📩 Powiadomienia e-mail oraz Pushover
|
|
||||||
- 📅 Harmonogram automatycznych backupów (APScheduler)
|
|
||||||
- 🧹 Automatyczne czyszczenie starych backupów i logów
|
|
||||||
- 🚀 Obsługa przez interfejs webowy
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ⚙️ Konfiguracja
|
|
||||||
|
|
||||||
### Zmiana ustawień
|
|
||||||
Plik `app.py` zawiera konfigurację bazy danych oraz inne ustawienia aplikacji:
|
|
||||||
|
|
||||||
```python
|
|
||||||
app.config['SECRET_KEY'] = 'super-secret-key'
|
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///backup_routeros.db'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 📬 Konfiguracja SMTP (E-mail)
|
|
||||||
Aby skonfigurować powiadomienia e-mail, wprowadź dane w sekcji ustawień:
|
|
||||||
|
|
||||||
- Serwer SMTP
|
|
||||||
- Login/hasło SMTP
|
|
||||||
- Port (587 dla TLS, 465 dla SSL)
|
|
||||||
|
|
||||||
### 📲 Powiadomienia Pushover
|
|
||||||
Aby włączyć powiadomienia Pushover, uzupełnij `pushover_token` oraz `pushover_userkey` w ustawieniach.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔍 API & Health Check
|
|
||||||
Aplikacja zawiera endpoint `/health`, który zwraca status bazy danych:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl http://127.0.0.1:5581/health
|
|
||||||
```
|
|
||||||
|
|
||||||
Przykładowa odpowiedź:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"status": "ok",
|
|
||||||
"timestamp": "2024-02-26T12:34:56Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Autor i licencja
|
|
||||||
Projekt stworzony przez Mateusz Grusczyński @linuxiarz.pl - dostępny na licencji MIT.
|
|
||||||
|
224
app.py
224
app.py
@@ -9,16 +9,18 @@ import re
|
|||||||
import smtplib
|
import smtplib
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import hashlib
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from ftplib import FTP
|
||||||
from email.mime.base import MIMEBase
|
from email.mime.base import MIMEBase
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email import encoders
|
from email import encoders
|
||||||
from flask import jsonify
|
from flask import jsonify
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
import shutil
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from difflib import HtmlDiff
|
from difflib import HtmlDiff
|
||||||
|
import difflib
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from sqlalchemy import text
|
from sqlalchemy import text
|
||||||
|
|
||||||
@@ -27,7 +29,7 @@ from flask import (
|
|||||||
url_for, session, flash, send_file
|
url_for, session, flash, send_file
|
||||||
)
|
)
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from passlib.context import CryptContext
|
from passlib.hash import bcrypt
|
||||||
#from flask_wtf.csrf import CSRFProtect
|
#from flask_wtf.csrf import CSRFProtect
|
||||||
|
|
||||||
from apscheduler.schedulers.background import BackgroundScheduler
|
from apscheduler.schedulers.background import BackgroundScheduler
|
||||||
@@ -56,18 +58,14 @@ os.makedirs(DATA_DIR, exist_ok=True)
|
|||||||
###############################################################################
|
###############################################################################
|
||||||
# Modele bazy danych
|
# Modele bazy danych
|
||||||
###############################################################################
|
###############################################################################
|
||||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
|
||||||
class User(db.Model):
|
class User(db.Model):
|
||||||
__tablename__ = 'users'
|
__tablename__ = 'users'
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(120), unique=True, nullable=False)
|
username = db.Column(db.String(120), unique=True, nullable=False)
|
||||||
password_hash = db.Column(db.String(255), nullable=False)
|
password_hash = db.Column(db.String(255), nullable=False)
|
||||||
|
|
||||||
def set_password(self, password):
|
|
||||||
self.password_hash = pwd_context.hash(password)
|
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return pwd_context.verify(password, self.password_hash)
|
return bcrypt.verify(password, self.password_hash)
|
||||||
|
|
||||||
class Router(db.Model):
|
class Router(db.Model):
|
||||||
__tablename__ = 'routers'
|
__tablename__ = 'routers'
|
||||||
@@ -89,8 +87,6 @@ class Backup(db.Model):
|
|||||||
file_path = db.Column(db.String(255), nullable=False) # Ścieżka do pliku
|
file_path = db.Column(db.String(255), nullable=False) # Ścieżka do pliku
|
||||||
backup_type = db.Column(db.String(50), default='export') # 'export' lub 'binary'
|
backup_type = db.Column(db.String(50), default='export') # 'export' lub 'binary'
|
||||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
checksum = db.Column(db.String(64), nullable=True)
|
|
||||||
|
|
||||||
class OperationLog(db.Model):
|
class OperationLog(db.Model):
|
||||||
__tablename__ = 'operation_logs'
|
__tablename__ = 'operation_logs'
|
||||||
__table_args__ = {'extend_existing': True} # Zapobiega redefinicji tabeli
|
__table_args__ = {'extend_existing': True} # Zapobiega redefinicji tabeli
|
||||||
@@ -116,8 +112,6 @@ class GlobalSettings(db.Model):
|
|||||||
smtp_login = db.Column(db.String(255), nullable=True)
|
smtp_login = db.Column(db.String(255), nullable=True)
|
||||||
smtp_password = db.Column(db.String(255), nullable=True)
|
smtp_password = db.Column(db.String(255), nullable=True)
|
||||||
smtp_notifications_enabled = db.Column(db.Boolean, default=False)
|
smtp_notifications_enabled = db.Column(db.Boolean, default=False)
|
||||||
log_retention_days = db.Column(db.Integer, default=7)
|
|
||||||
recipient_email = db.Column(db.String(255), nullable=True)
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Inicjalizacja bazy
|
# Inicjalizacja bazy
|
||||||
@@ -172,13 +166,6 @@ def load_pkey(ssh_key_str: str):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ValueError("Nie udało się załadować klucza SSH. Sprawdź, czy klucz jest poprawny i nie jest zaszyfrowany") from e
|
raise ValueError("Nie udało się załadować klucza SSH. Sprawdź, czy klucz jest poprawny i nie jest zaszyfrowany") from e
|
||||||
|
|
||||||
def compute_checksum(file_path):
|
|
||||||
sha256 = hashlib.sha256()
|
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
for chunk in iter(lambda: f.read(4096), b""):
|
|
||||||
sha256.update(chunk)
|
|
||||||
return sha256.hexdigest()
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Funkcje SSH
|
# Funkcje SSH
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@@ -242,17 +229,12 @@ def ssh_backup(router: Router, backup_name: str) -> str:
|
|||||||
print(f"[DEBUG] ssh_backup -> local_path={local_path}")
|
print(f"[DEBUG] ssh_backup -> local_path={local_path}")
|
||||||
return local_path
|
return local_path
|
||||||
|
|
||||||
def ssh_upload_backup(router: Router, local_backup_path: str, expected_checksum: str = None):
|
def ssh_upload_backup(router: Router, local_backup_path: str):
|
||||||
# Weryfikacja sumy kontrolnej, jeśli podana
|
print(f"[DEBUG] ssh_upload_backup -> router id={router.id}, local_backup_path={local_backup_path}")
|
||||||
if expected_checksum:
|
|
||||||
local_checksum = compute_checksum(local_backup_path)
|
|
||||||
if local_checksum != expected_checksum:
|
|
||||||
raise ValueError("Suma kontrolna pliku nie zgadza się – plik może być uszkodzony.")
|
|
||||||
|
|
||||||
client = paramiko.SSHClient()
|
client = paramiko.SSHClient()
|
||||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||||
|
|
||||||
# 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
|
key_source = router.ssh_key if router.ssh_key and router.ssh_key.strip() else get_settings().global_ssh_key
|
||||||
if key_source and key_source.strip():
|
if key_source and key_source.strip():
|
||||||
try:
|
try:
|
||||||
@@ -263,10 +245,8 @@ def ssh_upload_backup(router: Router, local_backup_path: str, expected_checksum:
|
|||||||
raise e
|
raise e
|
||||||
else:
|
else:
|
||||||
client.connect(router.host, port=router.port, username=router.ssh_user,
|
client.connect(router.host, port=router.port, username=router.ssh_user,
|
||||||
password=router.ssh_password, timeout=10,
|
password=router.ssh_password, timeout=10, allow_agent=False, look_for_keys=False, banner_timeout=10)
|
||||||
allow_agent=False, look_for_keys=False, banner_timeout=10)
|
|
||||||
|
|
||||||
# Otwieramy sesję SFTP, przesyłamy plik, a następnie zamykamy połączenie
|
|
||||||
sftp = client.open_sftp()
|
sftp = client.open_sftp()
|
||||||
remote_file = os.path.basename(local_backup_path)
|
remote_file = os.path.basename(local_backup_path)
|
||||||
sftp.put(local_backup_path, remote_file)
|
sftp.put(local_backup_path, remote_file)
|
||||||
@@ -274,7 +254,6 @@ def ssh_upload_backup(router: Router, local_backup_path: str, expected_checksum:
|
|||||||
client.close()
|
client.close()
|
||||||
print(f"[DEBUG] ssh_upload_backup -> przesłano {local_backup_path} do routera")
|
print(f"[DEBUG] ssh_upload_backup -> przesłano {local_backup_path} do routera")
|
||||||
|
|
||||||
|
|
||||||
def ssh_test_connection(router: Router) -> dict:
|
def ssh_test_connection(router: Router) -> dict:
|
||||||
"""Testuje połączenie z routerem i zwraca informacje: model, uptime, hostname."""
|
"""Testuje połączenie z routerem i zwraca informacje: model, uptime, hostname."""
|
||||||
client = paramiko.SSHClient()
|
client = paramiko.SSHClient()
|
||||||
@@ -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.")
|
print("SMTP not properly configured, skipping email sending.")
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
|
# Utwórz wiadomość typu alternative (obsługuje plain text i HTML)
|
||||||
msg = MIMEMultipart("alternative")
|
msg = MIMEMultipart("alternative")
|
||||||
msg["From"] = smtp_user
|
msg["From"] = smtp_user
|
||||||
msg["To"] = to_address
|
msg["To"] = to_address
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
|
|
||||||
|
# Dodaj część tekstową
|
||||||
part1 = MIMEText(plain_body, "plain")
|
part1 = MIMEText(plain_body, "plain")
|
||||||
msg.attach(part1)
|
msg.attach(part1)
|
||||||
|
|
||||||
|
# Jeśli nie podano wersji HTML, wygeneruj domyślny szablon
|
||||||
if html_body is None:
|
if html_body is None:
|
||||||
html_body = get_email_template(subject, plain_body)
|
html_body = get_email_template(subject, plain_body)
|
||||||
part2 = MIMEText(html_body, "html")
|
part2 = MIMEText(html_body, "html")
|
||||||
msg.attach(part2)
|
msg.attach(part2)
|
||||||
|
|
||||||
|
# Dodaj załącznik, jeśli podany i istnieje
|
||||||
if attachment_path and os.path.isfile(attachment_path):
|
if attachment_path and os.path.isfile(attachment_path):
|
||||||
with open(attachment_path, "rb") as attachment:
|
with open(attachment_path, "rb") as attachment:
|
||||||
part = MIMEBase("application", "octet-stream")
|
part = MIMEBase("application", "octet-stream")
|
||||||
@@ -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)}")
|
part.add_header("Content-Disposition", f"attachment; filename={os.path.basename(attachment_path)}")
|
||||||
msg.attach(part)
|
msg.attach(part)
|
||||||
|
|
||||||
if smtp_port == 465:
|
with smtplib.SMTP(smtp_host, smtp_port) as server:
|
||||||
server = smtplib.SMTP_SSL(smtp_host, smtp_port)
|
server.starttls()
|
||||||
server.login(smtp_user, smtp_pass)
|
server.login(smtp_user, smtp_pass)
|
||||||
server.send_message(msg)
|
server.send_message(msg)
|
||||||
server.quit()
|
|
||||||
else:
|
|
||||||
server = smtplib.SMTP(smtp_host, smtp_port)
|
|
||||||
server.ehlo()
|
|
||||||
if smtp_port == 587:
|
|
||||||
server.starttls()
|
|
||||||
server.login(smtp_user, smtp_pass)
|
|
||||||
server.send_message(msg)
|
|
||||||
server.quit()
|
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Mail error_send_mail_with_attachment:", e)
|
print("Mail error_send_mail_with_attachment:", e)
|
||||||
@@ -467,13 +441,12 @@ def notify(settings: GlobalSettings, message: str, success: bool):
|
|||||||
settings.smtp_login and settings.smtp_login.strip() and
|
settings.smtp_login and settings.smtp_login.strip() and
|
||||||
settings.smtp_password and settings.smtp_password.strip()):
|
settings.smtp_password and settings.smtp_password.strip()):
|
||||||
try:
|
try:
|
||||||
to_address = settings.recipient_email.strip() if settings.recipient_email and settings.recipient_email.strip() else settings.smtp_login.strip()
|
|
||||||
send_mail_with_attachment(
|
send_mail_with_attachment(
|
||||||
smtp_host=settings.smtp_host.strip(),
|
smtp_host=settings.smtp_host.strip(),
|
||||||
smtp_port=settings.smtp_port,
|
smtp_port=settings.smtp_port,
|
||||||
smtp_user=settings.smtp_login.strip(),
|
smtp_user=settings.smtp_login.strip(),
|
||||||
smtp_pass=settings.smtp_password.strip(),
|
smtp_pass=settings.smtp_password.strip(),
|
||||||
to_address=to_address,
|
to_address=settings.smtp_login.strip(),
|
||||||
subject="RouterOS Backup Notification",
|
subject="RouterOS Backup Notification",
|
||||||
plain_body=message
|
plain_body=message
|
||||||
)
|
)
|
||||||
@@ -518,10 +491,10 @@ def scheduled_auto_backup():
|
|||||||
db.session.add(b)
|
db.session.add(b)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
notify(s, f"Auto-export dla routera {r.name} OK", True)
|
notify(s, f"Auto-export dla routera {r.name} OK", True)
|
||||||
log_operation(f"Automatyczny 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:
|
except Exception as e:
|
||||||
notify(s, f"Auto-export dla routera {r.name} FAILED: {e}", False)
|
notify(s, f"Auto-export dla routera {r.name} FAILED: {e}", False)
|
||||||
log_operation(f"Automatyczny 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():
|
def scheduled_auto_binary_backup():
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
@@ -531,8 +504,7 @@ def scheduled_auto_binary_backup():
|
|||||||
try:
|
try:
|
||||||
backup_name = f"{r.name}_{r.id}_{datetime.now():%Y%m%d_%H%M%S}"
|
backup_name = f"{r.name}_{r.id}_{datetime.now():%Y%m%d_%H%M%S}"
|
||||||
local_path = ssh_backup(r, backup_name)
|
local_path = ssh_backup(r, backup_name)
|
||||||
checksum = compute_checksum(local_path)
|
b = Backup(router_id=r.id, file_path=local_path, backup_type='binary')
|
||||||
b = Backup(router_id=r.id, file_path=local_path, backup_type='binary', checksum=checksum)
|
|
||||||
db.session.add(b)
|
db.session.add(b)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
notify(s, f"Auto-binary backup dla routera {r.name} OK", True)
|
notify(s, f"Auto-binary backup dla routera {r.name} OK", True)
|
||||||
@@ -596,19 +568,6 @@ def schedule_auto_export_job():
|
|||||||
cron_used = s.export_cron if s.export_cron else "0 */12 * * *"
|
cron_used = s.export_cron if s.export_cron else "0 */12 * * *"
|
||||||
print(f"[DEBUG] schedule_auto_export_job -> cron_schedule={cron_used}")
|
print(f"[DEBUG] schedule_auto_export_job -> cron_schedule={cron_used}")
|
||||||
|
|
||||||
def cleanup_old_logs():
|
|
||||||
with app.app_context():
|
|
||||||
s = get_settings()
|
|
||||||
cutoff_date = datetime.utcnow() - timedelta(days=s.log_retention_days)
|
|
||||||
old_logs = OperationLog.query.filter(OperationLog.timestamp < cutoff_date).all()
|
|
||||||
deleted_count = len(old_logs)
|
|
||||||
for log in old_logs:
|
|
||||||
db.session.delete(log)
|
|
||||||
db.session.commit()
|
|
||||||
log_operation(f"Automatyczna retencja logów: usunięto {deleted_count} logów starszych niż {s.log_retention_days} dni.")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Konfiguracja APScheduler - harmonogram zadań
|
# Konfiguracja APScheduler - harmonogram zadań
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@@ -621,8 +580,8 @@ scheduler = BackgroundScheduler()
|
|||||||
# Dodajemy z unikalnymi ID, co ułatwia re-schedulowanie
|
# Dodajemy z unikalnymi ID, co ułatwia re-schedulowanie
|
||||||
scheduler.add_job(func=cleanup_old_backups, trigger='interval', days=1, id="cleanup_job")
|
scheduler.add_job(func=cleanup_old_backups, trigger='interval', days=1, id="cleanup_job")
|
||||||
scheduler.add_job(func=scheduled_auto_backup, trigger='interval', days=1, id="auto_backup_job")
|
scheduler.add_job(func=scheduled_auto_backup, trigger='interval', days=1, id="auto_backup_job")
|
||||||
scheduler.start()
|
|
||||||
|
|
||||||
|
scheduler.start()
|
||||||
# Sprzątanie przy zamykaniu
|
# Sprzątanie przy zamykaniu
|
||||||
atexit.register(lambda: scheduler.shutdown())
|
atexit.register(lambda: scheduler.shutdown())
|
||||||
|
|
||||||
@@ -643,7 +602,6 @@ def reschedule_jobs():
|
|||||||
schedule_auto_export_job()
|
schedule_auto_export_job()
|
||||||
schedule_retention_job()
|
schedule_retention_job()
|
||||||
schedule_auto_binary_backup_job()
|
schedule_auto_binary_backup_job()
|
||||||
scheduler.add_job(func=cleanup_old_logs, trigger='interval', days=1, id="cleanup_logs_job", replace_existing=True)
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# Filtr Jinja2 - basename
|
# Filtr Jinja2 - basename
|
||||||
@@ -691,19 +649,6 @@ def get_data_folder_size():
|
|||||||
pass
|
pass
|
||||||
return total_size
|
return total_size
|
||||||
|
|
||||||
@app.template_global()
|
|
||||||
def bootstrap_alert_category(cat):
|
|
||||||
mapping = {
|
|
||||||
"error": "danger",
|
|
||||||
"fail": "danger",
|
|
||||||
"warn": "warning",
|
|
||||||
"warning": "warning",
|
|
||||||
"ok": "success",
|
|
||||||
"success": "success",
|
|
||||||
"info": "info"
|
|
||||||
}
|
|
||||||
return mapping.get(cat.lower(), "info")
|
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# ROUTES
|
# ROUTES
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@@ -775,20 +720,19 @@ def advanced_schedule():
|
|||||||
s = get_settings()
|
s = get_settings()
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
s.retention_cron = request.form.get('retention_cron', '').strip()
|
s.retention_cron = request.form.get('retention_cron', '').strip()
|
||||||
s.binary_cron = request.form.get('binary_cron', '').strip()
|
s.binary_cron = request.form.get('binary_cron', '').strip() # nowe pole
|
||||||
s.export_cron = request.form.get('export_cron', '').strip()
|
s.export_cron = request.form.get('export_cron', '').strip()
|
||||||
s.backup_retention_days = int(request.form.get('backup_retention_days', s.backup_retention_days))
|
# Checkbox: jeśli nie jest zaznaczony, nie pojawi się w formularzu, więc ustawiamy na False
|
||||||
s.log_retention_days = int(request.form.get('log_retention_days', s.log_retention_days))
|
|
||||||
s.enable_auto_export = True if request.form.get('enable_auto_export') == 'on' else False
|
s.enable_auto_export = True if request.form.get('enable_auto_export') == 'on' else False
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
reschedule_jobs() # Aktualizacja harmonogramu zadań
|
reschedule_jobs() # Aktualizuje harmonogram zadań
|
||||||
flash("Ustawienia harmonogramu zostały zapisane.")
|
flash("Zaawansowane ustawienia harmonogramu zostały zapisane.")
|
||||||
return redirect(url_for('advanced_schedule'))
|
return redirect(url_for('advanced_schedule'))
|
||||||
return render_template('advanced_schedule.html', settings=s)
|
return render_template('advanced_schedule.html', settings=s)
|
||||||
|
|
||||||
@app.route('/toggle_dark_mode')
|
@app.route('/toggle_dark_mode')
|
||||||
def toggle_dark_mode():
|
def toggle_dark_mode():
|
||||||
current_mode = session.get('dark_mode', True)
|
current_mode = session.get('dark_mode', False)
|
||||||
session['dark_mode'] = not current_mode
|
session['dark_mode'] = not current_mode
|
||||||
return redirect(request.referrer or url_for('index'))
|
return redirect(request.referrer or url_for('index'))
|
||||||
|
|
||||||
@@ -835,6 +779,8 @@ def routers_list():
|
|||||||
routers = Router.query.filter_by(owner_id=user.id).order_by(Router.created_at.desc()).all()
|
routers = Router.query.filter_by(owner_id=user.id).order_by(Router.created_at.desc()).all()
|
||||||
return render_template('routers.html', user=user, routers=routers)
|
return render_template('routers.html', user=user, routers=routers)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/routers/add', methods=['GET','POST'])
|
@app.route('/routers/add', methods=['GET','POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def add_router():
|
def add_router():
|
||||||
@@ -899,8 +845,8 @@ def router_export(router_id):
|
|||||||
notify(get_settings(), f"Export {router.name} OK", True)
|
notify(get_settings(), f"Export {router.name} OK", True)
|
||||||
log_operation(f"Export wykonany dla routera {router.name} at {datetime.utcnow()}")
|
log_operation(f"Export wykonany dla routera {router.name} at {datetime.utcnow()}")
|
||||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||||
return {"status": "success", "message": "export zakończony."}
|
return {"status": "success", "message": "Eksport zakończony."}
|
||||||
flash("Export zakończony.")
|
flash("Eksport zakończony.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
notify(get_settings(), f"Export {router.name} FAIL: {e}", False)
|
notify(get_settings(), f"Export {router.name} FAIL: {e}", False)
|
||||||
log_operation(f"Export dla routera {router.name} FAILED: {e}")
|
log_operation(f"Export dla routera {router.name} FAILED: {e}")
|
||||||
@@ -920,8 +866,7 @@ def router_backup(router_id):
|
|||||||
try:
|
try:
|
||||||
backup_name = f"{router.name}_{router.id}_{datetime.now():%Y%m%d_%H%M%S}"
|
backup_name = f"{router.name}_{router.id}_{datetime.now():%Y%m%d_%H%M%S}"
|
||||||
local_path = ssh_backup(router, backup_name)
|
local_path = ssh_backup(router, backup_name)
|
||||||
checksum = compute_checksum(local_path)
|
b = Backup(router_id=router.id, file_path=local_path, backup_type='binary')
|
||||||
b = Backup(router_id=router.id, file_path=local_path, backup_type='binary', checksum=checksum)
|
|
||||||
db.session.add(b)
|
db.session.add(b)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
notify(get_settings(), f"Backup {router.name} OK", True)
|
notify(get_settings(), f"Backup {router.name} OK", True)
|
||||||
@@ -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()
|
b = Backup.query.filter_by(id=backup_id, router_id=router.id, backup_type='binary').first()
|
||||||
if not b:
|
if not b:
|
||||||
flash("Nie znaleziono backupu binarnego.")
|
flash("Nie znaleziono backupu binarnego.")
|
||||||
#return redirect(url_for('router_details', router_id=router.id))
|
return redirect(url_for('router_details', router_id=router.id))
|
||||||
next_url = request.form.get('next') or request.referrer or url_for('dashboard')
|
|
||||||
return redirect(next_url)
|
|
||||||
|
|
||||||
local_checksum = compute_checksum(b.file_path)
|
|
||||||
if b.checksum != local_checksum:
|
|
||||||
flash("Błąd: suma kontrolna backupu nie zgadza się – plik może być uszkodzony.")
|
|
||||||
#return redirect(url_for('router_details', router_id=router.id))
|
|
||||||
next_url = request.form.get('next') or request.referrer or url_for('dashboard')
|
|
||||||
return redirect(next_url)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ssh_upload_backup(router, b.file_path, 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()}")
|
log_operation(f"Backup {os.path.basename(b.file_path)} wgrany do routera {router.name} at {datetime.utcnow()}")
|
||||||
flash("Plik backupu wgrany do routera.")
|
flash("Plik backupu wgrany do routera.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f"Błąd wgrywania: {e}")
|
flash(f"Błąd wgrywania: {e}")
|
||||||
|
#return redirect(url_for('router_details', router_id=router.id))
|
||||||
next_url = request.form.get('next') or request.referrer or url_for('dashboard')
|
next_url = request.form.get('next') or request.referrer or url_for('dashboard')
|
||||||
return redirect(next_url)
|
return redirect(next_url)
|
||||||
|
|
||||||
@@ -1028,6 +964,7 @@ def export_all_routers():
|
|||||||
flash(" | ".join(messages))
|
flash(" | ".join(messages))
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for('dashboard'))
|
||||||
|
|
||||||
|
# Nowa podstrona: diff selector
|
||||||
@app.route('/diff_selector', methods=['GET', 'POST'])
|
@app.route('/diff_selector', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def diff_selector():
|
def diff_selector():
|
||||||
@@ -1047,11 +984,13 @@ def diff_selector():
|
|||||||
def all_files():
|
def all_files():
|
||||||
user = get_current_user()
|
user = get_current_user()
|
||||||
query = Backup.query.join(Router).filter(Router.owner_id == user.id)
|
query = Backup.query.join(Router).filter(Router.owner_id == user.id)
|
||||||
|
|
||||||
# Filtrowanie – wyszukiwanie po nazwie pliku (zastosowanie filtru "basename")
|
# Filtrowanie – wyszukiwanie po nazwie pliku (zastosowanie filtru "basename")
|
||||||
search = request.args.get('search', '')
|
search = request.args.get('search', '')
|
||||||
if search:
|
if search:
|
||||||
query = query.filter(Backup.file_path.ilike(f"%{search}%"))
|
query = query.filter(Backup.file_path.ilike(f"%{search}%"))
|
||||||
|
|
||||||
|
# Sortowanie – sort_by i order
|
||||||
sort_by = request.args.get('sort_by', 'created_at')
|
sort_by = request.args.get('sort_by', 'created_at')
|
||||||
order = request.args.get('order', 'desc')
|
order = request.args.get('order', 'desc')
|
||||||
if sort_by not in ['created_at', 'file_path']:
|
if sort_by not in ['created_at', 'file_path']:
|
||||||
@@ -1081,7 +1020,7 @@ def view_export(backup_id):
|
|||||||
flash("Brak dostępu do backupu.")
|
flash("Brak dostępu do backupu.")
|
||||||
return redirect(url_for('all_files'))
|
return redirect(url_for('all_files'))
|
||||||
if b.backup_type != 'export':
|
if b.backup_type != 'export':
|
||||||
flash("Wybrany backup nie jest plikiem exportu.")
|
flash("Wybrany backup nie jest plikiem eksportu.")
|
||||||
return redirect(url_for('all_files'))
|
return redirect(url_for('all_files'))
|
||||||
try:
|
try:
|
||||||
with open(b.file_path, 'r', encoding='utf-8') as f:
|
with open(b.file_path, 'r', encoding='utf-8') as f:
|
||||||
@@ -1105,10 +1044,10 @@ def send_export_email(backup_id):
|
|||||||
flash("Nie skonfigurowano ustawień SMTP w panelu.")
|
flash("Nie skonfigurowano ustawień SMTP w panelu.")
|
||||||
return redirect(url_for('settings_view'))
|
return redirect(url_for('settings_view'))
|
||||||
subject = f"RouterOS Export: {os.path.basename(b.file_path)}"
|
subject = f"RouterOS Export: {os.path.basename(b.file_path)}"
|
||||||
body = f"Przesyłam 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,
|
if send_mail_with_attachment(s.smtp_host, s.smtp_port, s.smtp_login, s.smtp_password,
|
||||||
s.smtp_login, subject, body, b.file_path):
|
s.smtp_login, subject, body, b.file_path):
|
||||||
flash("Wysłano export mailem.")
|
flash("Wysłano eksport mailem.")
|
||||||
else:
|
else:
|
||||||
flash("Błąd wysyłki mailowej.")
|
flash("Błąd wysyłki mailowej.")
|
||||||
#return redirect(url_for('router_details', router_id=b.router_id))
|
#return redirect(url_for('router_details', router_id=b.router_id))
|
||||||
@@ -1154,9 +1093,8 @@ def settings_view():
|
|||||||
s.smtp_port = int(request.form.get('smtp_port', '587'))
|
s.smtp_port = int(request.form.get('smtp_port', '587'))
|
||||||
s.smtp_login = request.form.get('smtp_login', '')
|
s.smtp_login = request.form.get('smtp_login', '')
|
||||||
s.smtp_password = request.form.get('smtp_password', '')
|
s.smtp_password = request.form.get('smtp_password', '')
|
||||||
s.recipient_email = request.form.get('recipient_email', '')
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
#reschedule_jobs() # Aktualizacja zadań – zadania dotyczące backupu/CRON zostaną teraz sterowane z /advanced_schedule
|
reschedule_jobs() # Aktualizacja zadań – zadania dotyczące backupu/CRON zostaną teraz sterowane z /advanced_schedule
|
||||||
flash("Zapisano ustawienia.")
|
flash("Zapisano ustawienia.")
|
||||||
return redirect(url_for('settings_view'))
|
return redirect(url_for('settings_view'))
|
||||||
return render_template('settings.html', settings=s)
|
return render_template('settings.html', settings=s)
|
||||||
@@ -1270,6 +1208,7 @@ def diff_view(backup_id1, backup_id2):
|
|||||||
lineterm=''
|
lineterm=''
|
||||||
))
|
))
|
||||||
diff_text = "\n".join(diff_lines)
|
diff_text = "\n".join(diff_lines)
|
||||||
|
|
||||||
return render_template('diff.html', diff_text=diff_text, backup1=b1, backup2=b2)
|
return render_template('diff.html', diff_text=diff_text, backup1=b1, backup2=b2)
|
||||||
|
|
||||||
@app.route('/routers/all_backup', methods=['POST'])
|
@app.route('/routers/all_backup', methods=['POST'])
|
||||||
@@ -1362,7 +1301,7 @@ def change_password():
|
|||||||
flash("Nowe hasło i potwierdzenie nie są zgodne.")
|
flash("Nowe hasło i potwierdzenie nie są zgodne.")
|
||||||
return redirect(url_for('change_password'))
|
return redirect(url_for('change_password'))
|
||||||
|
|
||||||
user.password_hash = pwd_context.hash(new_password)
|
user.password_hash = bcrypt.hash(new_password)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Hasło zostało zmienione pomyślnie.")
|
flash("Hasło zostało zmienione pomyślnie.")
|
||||||
return redirect(url_for('dashboard'))
|
return redirect(url_for('dashboard'))
|
||||||
@@ -1381,84 +1320,17 @@ def test_connection(router_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
flash(f"Błąd testu połączenia: {e}")
|
flash(f"Błąd testu połączenia: {e}")
|
||||||
return redirect(url_for('routers_list'))
|
return redirect(url_for('routers_list'))
|
||||||
|
# Jeśli wywołanie zawiera parametr modal=1, zwracamy widok dla modalu
|
||||||
if request.args.get("modal") == "1":
|
if request.args.get("modal") == "1":
|
||||||
return render_template("test_connection_modal.html", router=router, result=result)
|
return render_template("test_connection_modal.html", router=router, result=result)
|
||||||
return render_template("test_connection.html", router=router, result=result)
|
return render_template("test_connection.html", router=router, result=result)
|
||||||
|
|
||||||
@app.route('/logs')
|
|
||||||
@login_required
|
|
||||||
def logs_page():
|
|
||||||
logs = OperationLog.query.order_by(OperationLog.timestamp.desc()).all()
|
|
||||||
return render_template('logs.html', logs=logs)
|
|
||||||
|
|
||||||
@app.route('/logs/delete', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def delete_old_logs():
|
|
||||||
try:
|
|
||||||
delete_days = int(request.form.get('delete_days', 0))
|
|
||||||
except ValueError:
|
|
||||||
flash("Podana wartość jest nieprawidłowa.")
|
|
||||||
return redirect(url_for('logs_page'))
|
|
||||||
|
|
||||||
if delete_days < 1:
|
|
||||||
flash("Podaj wartość większą lub równą 1.")
|
|
||||||
return redirect(url_for('logs_page'))
|
|
||||||
|
|
||||||
cutoff_date = datetime.utcnow() - timedelta(days=delete_days)
|
|
||||||
old_logs = OperationLog.query.filter(OperationLog.timestamp < cutoff_date).all()
|
|
||||||
deleted_count = 0
|
|
||||||
for log in old_logs:
|
|
||||||
db.session.delete(log)
|
|
||||||
deleted_count += 1
|
|
||||||
db.session.commit()
|
|
||||||
flash(f"Usunięto {deleted_count} logów starszych niż {delete_days} dni.")
|
|
||||||
return redirect(url_for('logs_page'))
|
|
||||||
|
|
||||||
@app.route('/test_email', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def test_email():
|
|
||||||
s = get_settings()
|
|
||||||
# Sprawdzamy, czy ustawienia SMTP są skonfigurowane
|
|
||||||
if not (s.smtp_host and s.smtp_login and s.smtp_password):
|
|
||||||
flash("Brak skonfigurowanych ustawień SMTP.")
|
|
||||||
return redirect(url_for('settings_view'))
|
|
||||||
subject = "Testowy e-mail z RouterOS Backup"
|
|
||||||
body = "To jest testowa wiadomość e-mail wysłana z systemu RouterOS Backup."
|
|
||||||
# Używamy recipient_email, jeśli jest ustawiony, w przeciwnym razie smtp_login
|
|
||||||
to_address = s.recipient_email.strip() if s.recipient_email and s.recipient_email.strip() else s.smtp_login.strip()
|
|
||||||
success = send_mail_with_attachment(
|
|
||||||
smtp_host=s.smtp_host,
|
|
||||||
smtp_port=s.smtp_port,
|
|
||||||
smtp_user=s.smtp_login,
|
|
||||||
smtp_pass=s.smtp_password,
|
|
||||||
to_address=to_address,
|
|
||||||
subject=subject,
|
|
||||||
plain_body=body
|
|
||||||
)
|
|
||||||
if success:
|
|
||||||
flash("Testowy e-mail został wysłany.")
|
|
||||||
else:
|
|
||||||
flash("Wysyłka testowego e-maila nie powiodła się.")
|
|
||||||
return redirect(url_for('settings_view'))
|
|
||||||
|
|
||||||
@app.route('/test_pushover', methods=['POST'])
|
|
||||||
@login_required
|
|
||||||
def test_pushover():
|
|
||||||
s = get_settings()
|
|
||||||
# Sprawdzamy, czy ustawienia Pushover są skonfigurowane
|
|
||||||
if not (s.pushover_token and s.pushover_userkey):
|
|
||||||
flash("Brak skonfigurowanych ustawień Pushover.")
|
|
||||||
return redirect(url_for('settings_view'))
|
|
||||||
message = "Testowe powiadomienie Pushover z systemu RouterOS Backup."
|
|
||||||
success = send_pushover(s.pushover_token, s.pushover_userkey, message)
|
|
||||||
if success:
|
|
||||||
flash("Testowe powiadomienie Pushover zostało wysłane.")
|
|
||||||
else:
|
|
||||||
flash("Wysyłka testowego powiadomienia Pushover nie powiodła się.")
|
|
||||||
return redirect(url_for('settings_view'))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
reschedule_jobs()
|
scheduler = BackgroundScheduler()
|
||||||
|
schedule_retention_job()
|
||||||
|
schedule_auto_export_job()
|
||||||
|
schedule_auto_binary_backup_job()
|
||||||
|
scheduler.start()
|
||||||
atexit.register(lambda: scheduler.shutdown())
|
atexit.register(lambda: scheduler.shutdown())
|
||||||
app.run(host='0.0.0.0', port=5581, use_reloader=False, debug=True)
|
app.run(host='0.0.0.0', port=5581, use_reloader=False, debug=True)
|
||||||
|
@@ -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
|
paramiko
|
||||||
APScheduler
|
APScheduler
|
||||||
requests
|
requests
|
||||||
#gunicorn
|
gunicorn
|
||||||
flask_wtf
|
flask_wtf
|
||||||
gevent
|
gevent
|
||||||
#croniter
|
#croniter
|
||||||
waitress
|
|
@@ -1,14 +1,18 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=RouterOS Backup Waitress Service
|
Description=RouterOS Backup Application
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
#User=routeros
|
#User=www-data # Zmień na odpowiedniego użytkownika
|
||||||
#Group=routeros
|
#Group=www-data
|
||||||
WorkingDirectory=/opt/routeros_backup
|
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
|
Restart=always
|
||||||
Environment=PYTHONUNBUFFERED=1
|
RestartSec=5
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
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" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container mt-5">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header">
|
||||||
<h4 class="mb-0">Dodaj nowe urządzenie</h4>
|
<h2 class="mb-0">Dodaj nowe urządzenie</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
@@ -24,16 +24,18 @@
|
|||||||
<input type="text" class="form-control" id="ssh_user" name="ssh_user" required>
|
<input type="text" class="form-control" id="ssh_user" name="ssh_user" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ssh_key" class="form-label"><b>Klucz prywatny</b></label><br>
|
<label for="ssh_key" class="form-label">
|
||||||
<small>Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code>. Jeśli pusty – użyje klucza globalnego.</small>
|
<b>Klucz prywatny</b> | Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code><br>
|
||||||
<textarea class="form-control mt-2" id="ssh_key" name="ssh_key" rows="4"></textarea>
|
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>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
|
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
|
||||||
<small>Jeśli jest klucz SSH lub klucz globalny, hasło może być ignorowane.</small>
|
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 mt-2" id="ssh_password" name="ssh_password">
|
<input type="password" class="form-control" id="ssh_password" name="ssh_password">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">Dodaj urządzenie</button>
|
<button type="submit" class="btn btn-primary">Dodaj</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,61 +1,43 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container mt-5">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header">
|
||||||
<h4 class="mb-0">Zaawansowane ustawienia harmonogramu</h4>
|
<h2 class="mb-0">Zaawansowane ustawienia harmonogramu</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form action="{{ url_for('advanced_schedule') }}" method="POST">
|
<form action="{{ url_for('advanced_schedule') }}" method="POST">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="backup_retention_days" class="form-label">Próg retencji backupów (dni)</label>
|
<label for="retention_cron" class="form-label">Harmonogram retencji (cron)</label>
|
||||||
<small class="text-muted d-block mb-2">Usuwanie danych starszych niż ustawione w progu.</small>
|
|
||||||
<input type="number" class="form-control" id="backup_retention_days" name="backup_retention_days" value="{{ settings.backup_retention_days }}">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="log_retention_days" class="form-label">Próg retencji logów (dni)</label>
|
|
||||||
<input type="number" class="form-control" id="log_retention_days" name="log_retention_days" value="{{ settings.log_retention_days }}">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="retention_cron" class="form-label">Harmonogram retencji <code>cron</code></label>
|
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" id="retention_cron" name="retention_cron" value="{{ settings.retention_cron }}">
|
<input type="text" class="form-control" id="retention_cron" name="retention_cron" value="{{ settings.retention_cron }}">
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('retention_cron')">Generuj cron</button>
|
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('retention_cron')">Generuj cron</button>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<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">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" id="binary_cron" name="binary_cron" value="{{ settings.binary_cron|default('') }}">
|
<input type="text" class="form-control" id="binary_cron" name="binary_cron" value="{{ settings.binary_cron|default('') }}">
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('binary_cron')">Generuj cron</button>
|
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('binary_cron')">Generuj cron</button>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="mb-3">
|
<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">
|
<div class="input-group">
|
||||||
<input type="text" class="form-control" id="export_cron" name="export_cron" value="{{ settings.export_cron }}">
|
<input type="text" class="form-control" id="export_cron" name="export_cron" value="{{ settings.export_cron }}">
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('export_cron')">Generuj cron</button>
|
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('export_cron')">Generuj cron</button>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
|
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3 form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="enable_auto_export" name="enable_auto_export" {% if settings.enable_auto_export %}checked{% endif %}>
|
<input type="checkbox" class="form-check-input" id="enable_auto_export" name="enable_auto_export" {% if settings.enable_auto_export %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="enable_auto_export">Włącz automatyczny export</label>
|
<label class="form-check-label" for="enable_auto_export">Włącz automatyczny eksport</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer text-center">
|
|
||||||
<small class="text-muted">Ustawienia zostaną zapisane i użyte przez cron.</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -98,15 +80,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Zmienna przechowująca ID pola, do którego ma być wpisane wyrażenie cron
|
||||||
var targetCronField = '';
|
var targetCronField = '';
|
||||||
|
|
||||||
function openCronModal(fieldId) {
|
function openCronModal(fieldId) {
|
||||||
targetCronField = fieldId;
|
targetCronField = fieldId;
|
||||||
|
// Wyzeruj wartości w modalu
|
||||||
document.getElementById('cron_minute').value = '*';
|
document.getElementById('cron_minute').value = '*';
|
||||||
document.getElementById('cron_hour').value = '*';
|
document.getElementById('cron_hour').value = '*';
|
||||||
document.getElementById('cron_day').value = '*';
|
document.getElementById('cron_day').value = '*';
|
||||||
document.getElementById('cron_month').value = '*';
|
document.getElementById('cron_month').value = '*';
|
||||||
document.getElementById('cron_dow').value = '*';
|
document.getElementById('cron_dow').value = '*';
|
||||||
|
// Otwórz modal (przy użyciu Bootstrap 5)
|
||||||
var cronModal = new bootstrap.Modal(document.getElementById('cronModal'));
|
var cronModal = new bootstrap.Modal(document.getElementById('cronModal'));
|
||||||
cronModal.show();
|
cronModal.show();
|
||||||
}
|
}
|
||||||
@@ -117,8 +102,10 @@
|
|||||||
var day = document.getElementById('cron_day').value || '*';
|
var day = document.getElementById('cron_day').value || '*';
|
||||||
var month = document.getElementById('cron_month').value || '*';
|
var month = document.getElementById('cron_month').value || '*';
|
||||||
var dow = document.getElementById('cron_dow').value || '*';
|
var dow = document.getElementById('cron_dow').value || '*';
|
||||||
|
|
||||||
var cronExpr = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + dow;
|
var cronExpr = minute + ' ' + hour + ' ' + day + ' ' + month + ' ' + dow;
|
||||||
document.getElementById(targetCronField).value = cronExpr;
|
document.getElementById(targetCronField).value = cronExpr;
|
||||||
|
// Zamknij modal
|
||||||
var modalEl = document.getElementById('cronModal');
|
var modalEl = document.getElementById('cronModal');
|
||||||
var modalInstance = bootstrap.Modal.getInstance(modalEl);
|
var modalInstance = bootstrap.Modal.getInstance(modalEl);
|
||||||
modalInstance.hide();
|
modalInstance.hide();
|
||||||
|
@@ -3,8 +3,8 @@
|
|||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<h2 class="text-center mb-4">Lista wszystkich backupów</h2>
|
<h2 class="text-center mb-4">Lista wszystkich backupów</h2>
|
||||||
|
|
||||||
<!-- Karta filtra -->
|
<!-- Formularz filtrowania -->
|
||||||
<div class="card mb-4 shadow-sm border-0">
|
<div class="card mb-4 shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="GET" action="{{ url_for('all_files') }}" class="row g-2">
|
<form method="GET" action="{{ url_for('all_files') }}" class="row g-2">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Karta tabeli backupów -->
|
<!-- Tabela z backupami -->
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover align-middle">
|
<table class="table table-striped table-hover mb-0">
|
||||||
<thead class="table-dark">
|
<thead class="table-dark">
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 2%;"><input type="checkbox" id="select_all"></th>
|
<th style="width: 2%;"><input type="checkbox" id="select_all"></th>
|
||||||
@@ -63,23 +63,18 @@
|
|||||||
<span class="badge bg-secondary">{{ file.backup_type }}</span>
|
<span class="badge bg-secondary">{{ file.backup_type }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>{{ file.file_path|basename }}</td>
|
||||||
{% if file.backup_type == 'binary' %}
|
|
||||||
<span data-bs-toggle="tooltip" title="Checksum: {{ file.checksum }}">{{ file.file_path|basename }}</span>
|
|
||||||
{% else %}
|
|
||||||
{{ file.file_path|basename }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ file.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
<td>{{ file.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
||||||
<td>{{ file.file_path|filesize }}</td>
|
<td>{{ file.file_path|filesize }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('download_file', filename=file.file_path|basename) }}" class="btn btn-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>
|
<i class="bi bi-download"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form action="{{ url_for('send_by_email', backup_id=file.id) }}" method="POST" class="d-inline">
|
<form action="{{ url_for('send_by_email', backup_id=file.id) }}" method="POST" class="d-inline">
|
||||||
<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>
|
<i class="bi bi-envelope"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -87,7 +82,8 @@
|
|||||||
<td>
|
<td>
|
||||||
{% if file.backup_type == 'binary' %}
|
{% if file.backup_type == 'binary' %}
|
||||||
<form action="{{ url_for('upload_backup', router_id=file.router.id, backup_id=file.id) }}" method="POST" class="d-inline">
|
<form action="{{ url_for('upload_backup', router_id=file.router.id, backup_id=file.id) }}" method="POST" class="d-inline">
|
||||||
<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>
|
<i class="bi bi-upload"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -97,7 +93,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if file.backup_type == 'export' %}
|
{% if file.backup_type == 'export' %}
|
||||||
<a href="{{ url_for('view_export', backup_id=file.id) }}" class="btn btn-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>
|
<i class="bi bi-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -106,7 +102,8 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<form action="{{ url_for('delete_backup', backup_id=file.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
|
<form action="{{ url_for('delete_backup', backup_id=file.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
|
||||||
<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>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -120,21 +117,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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">
|
<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
|
<i class="bi bi-file-earmark-zip"></i> Pobierz zip zaznaczonych
|
||||||
</button>
|
</button>
|
||||||
<button type="submit" name="action" value="delete" class="btn btn-danger" onclick="return confirm('Na pewno usunąć zaznaczone pliki?');">
|
<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
|
<i class="bi bi-trash"></i> Usuń zaznaczone backupy
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('select_all').addEventListener('change', function(e) {
|
document.getElementById('select_all').addEventListener('change', function(e) {
|
||||||
var checkboxes = document.querySelectorAll('input[name="backup_id"]');
|
var checkboxes = document.querySelectorAll('input[name="backup_id"]');
|
||||||
checkboxes.forEach(cb => cb.checked = e.target.checked);
|
for (var i = 0; i < checkboxes.length; i++) {
|
||||||
|
checkboxes[i].checked = e.target.checked;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,392 +1,116 @@
|
|||||||
<!DOCTYPE html>
|
<!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>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<title>Backup RouterOS App</title>
|
||||||
<title>Backup RouterOS</title>
|
|
||||||
<!-- Bootstrap CSS -->
|
<!-- Bootstrap CSS -->
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 1) Poprawa kontrastu dla form-text w trybie ciemnym */
|
|
||||||
.dark-mode .form-text {
|
|
||||||
color: #ccc !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 2) Ogólne style trybu ciemnego */
|
<style>
|
||||||
.dark-mode body {
|
.dark-mode body {
|
||||||
background-color: #121212;
|
background-color: #222;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
.dark-mode a,
|
.dark-mode a, .dark-mode a:hover {
|
||||||
.dark-mode a:hover {
|
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
.dark-mode .navbar, .dark-mode .table {
|
||||||
/* 3) Nawigacja i menu w trybie ciemnym */
|
|
||||||
.dark-mode .navbar,
|
|
||||||
.dark-mode .navbar-nav,
|
|
||||||
.dark-mode .dropdown-menu {
|
|
||||||
background-color: #333 !important;
|
background-color: #333 !important;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
.dark-mode .navbar-nav .nav-link {
|
|
||||||
color: #ddd !important;
|
|
||||||
}
|
|
||||||
.dark-mode .navbar-nav .nav-link:hover {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
/* Dropdown :hover, :focus, active */
|
|
||||||
.dark-mode .dropdown-item:hover,
|
|
||||||
.dark-mode .dropdown-item:focus,
|
|
||||||
.dark-mode .dropdown-item.active {
|
|
||||||
background-color: #444 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 4) Tabele w trybie ciemnym */
|
|
||||||
.dark-mode .table {
|
|
||||||
background-color: #333 !important;
|
|
||||||
border-color: #444;
|
|
||||||
}
|
|
||||||
.dark-mode .table thead th {
|
|
||||||
background-color: #444;
|
|
||||||
color: #fff;
|
|
||||||
border: 1px solid #555;
|
|
||||||
}
|
|
||||||
.dark-mode .table tbody td {
|
|
||||||
color: #ddd;
|
|
||||||
border: 1px solid #555;
|
|
||||||
}
|
|
||||||
html.dark-mode table.table thead th {
|
|
||||||
background-color: #444 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
border: 1px solid #555 !important;
|
|
||||||
}
|
|
||||||
html.dark-mode table.table tbody td {
|
|
||||||
background-color: #333 !important;
|
|
||||||
color: #ddd !important;
|
|
||||||
border: 1px solid #555 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 5) Pola formularzy (input, select, textarea) w trybie ciemnym */
|
|
||||||
.dark-mode input,
|
|
||||||
.dark-mode textarea,
|
|
||||||
.dark-mode select {
|
|
||||||
background-color: #333;
|
|
||||||
color: #fff;
|
|
||||||
border: 1px solid #555;
|
|
||||||
}
|
|
||||||
.dark-mode ::placeholder {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 6) Przyciski w trybie ciemnym: .btn-warning, .btn-secondary, .btn-outline-dark */
|
|
||||||
.dark-mode .btn-warning {
|
|
||||||
background-color: #d39e00;
|
|
||||||
border-color: #b38600;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.dark-mode .btn-warning:hover {
|
|
||||||
background-color: #e6aa00 !important;
|
|
||||||
border-color: #c98f00 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.dark-mode .btn-secondary {
|
|
||||||
background-color: #444;
|
|
||||||
border-color: #555;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.dark-mode .btn-secondary:hover {
|
|
||||||
background-color: #555 !important;
|
|
||||||
border-color: #888888 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.dark-mode .btn-outline-dark:hover {
|
|
||||||
background-color: #444 !important;
|
|
||||||
border-color: #888888 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 7) Karty i bloki w trybie ciemnym */
|
|
||||||
.dark-mode .block,
|
|
||||||
.dark-mode .card {
|
|
||||||
background-color: #171717;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 8) Stopka */
|
|
||||||
.dark-mode footer {
|
|
||||||
background-color: #1e1e1e !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
color: #212529;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 9) Nadpisanie .card-header.bg-light w trybie ciemnym */
|
|
||||||
.dark-mode .card-header.bg-light {
|
|
||||||
background-color: #333 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 10) Style diff2html w trybie ciemnym */
|
|
||||||
.dark-mode .d2h-wrapper,
|
|
||||||
.dark-mode .d2h-file-header,
|
|
||||||
.dark-mode .d2h-file-info,
|
|
||||||
.dark-mode .d2h-file-diff,
|
|
||||||
.dark-mode .d2h-diff-table,
|
|
||||||
.dark-mode .d2h-code-line,
|
|
||||||
.dark-mode .d2h-code-line-ctn,
|
|
||||||
.dark-mode .d2h-code-side-linenumber {
|
|
||||||
background-color: #333 !important;
|
|
||||||
color: #ddd !important;
|
|
||||||
border-color: #444 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 11) Modal w trybie ciemnym */
|
|
||||||
.dark-mode .modal-content {
|
|
||||||
background-color: #333;
|
|
||||||
color: #ddd;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.dark-mode .modal-header,
|
|
||||||
.dark-mode .modal-footer {
|
|
||||||
border-color: #444;
|
|
||||||
}
|
|
||||||
.dark-mode .modal-title {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.dark-mode .btn-close {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 12) Niestandardowy styl trybu jasnego – navbar */
|
|
||||||
.navbar-light.bg-custom-light {
|
|
||||||
background-color: #dcdcdc !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 13) DataTables w trybie ciemnym */
|
|
||||||
.dark-mode .dataTables_wrapper .dataTables_paginate .paginate_button {
|
|
||||||
background-color: #333 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
border: 1px solid #555 !important;
|
|
||||||
}
|
|
||||||
.dark-mode .dataTables_wrapper .dataTables_paginate .paginate_button.current,
|
|
||||||
.dark-mode .dataTables_wrapper .dataTables_paginate .paginate_button:hover {
|
|
||||||
background-color: #444 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
html.dark-mode .dataTables_wrapper .pagination .page-link {
|
|
||||||
background-color: #333 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
border: 1px solid #555 !important;
|
|
||||||
}
|
|
||||||
html.dark-mode .dataTables_wrapper .pagination .page-item.active .page-link,
|
|
||||||
html.dark-mode .dataTables_wrapper .pagination .page-link:hover {
|
|
||||||
background-color: #444 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
html.dark-mode .dataTables_wrapper .dataTables_info,
|
|
||||||
html.dark-mode .dataTables_wrapper .dataTables_length,
|
|
||||||
html.dark-mode .dataTables_wrapper .dataTables_filter {
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
.dark-mode input:focus,
|
|
||||||
.dark-mode textarea:focus,
|
|
||||||
.dark-mode select:focus {
|
|
||||||
background-color: #333 !important;
|
|
||||||
color: #fff !important;
|
|
||||||
border-color: #555 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 14) Drobne poprawki przycisków wylogowania */
|
|
||||||
.btn-logout {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 15) Główne ustawienia, flex layout */
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
main.container {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 16) Alerty (diff-add, diff-rem) – pozostawione bez zmian */
|
|
||||||
.diff-add { color: green; }
|
.diff-add { color: green; }
|
||||||
.diff-rem { color: red; }
|
.diff-rem { color: red; }
|
||||||
|
|
||||||
.dark-mode .text-muted {
|
|
||||||
color: #aaa !important; /* zamiast #aaa możesz wybrać #bbb, #ccc itp. */
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
|
||||||
</head>
|
</head>
|
||||||
<body class="d-flex flex-column">
|
<body>
|
||||||
<!-- Navbar -->
|
<nav class="navbar navbar-expand navbar-dark bg-dark mb-4">
|
||||||
<nav class="navbar navbar-expand-lg
|
|
||||||
{% if session.get('dark_mode', True) %}navbar-dark bg-dark{% else %}navbar-light bg-custom-light{% endif %}
|
|
||||||
mb-4">
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a href="/" class="navbar-brand">Backup RouterOS</a>
|
<a href="{{ url_for('index') }}" class="navbar-brand">Backup RouterOS</a>
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown"
|
<div>
|
||||||
aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
|
{% if session.user_id %}
|
||||||
<span class="navbar-toggler-icon"></span>
|
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary me-2">Dashboard</a>
|
||||||
</button>
|
<a href="{{ url_for('routers_list') }}" class="btn btn-secondary me-2">Urządzenia</a>
|
||||||
<div class="collapse navbar-collapse" id="navbarNavDropdown">
|
<a href="{{ url_for('diff_selector') }}" class="btn btn-secondary me-2">Diff selector</a>
|
||||||
{% if session.get('user_id') %}
|
<a href="{{ url_for('all_files') }}" class="btn btn-secondary me-2">Wszystkie pliki</a>
|
||||||
<ul class="navbar-nav me-auto">
|
<a href="{{ url_for('settings_view') }}" class="btn btn-secondary me-2">Ustawienia</a>
|
||||||
<!-- Dashboard -->
|
<a href="{{ url_for('advanced_schedule') }}" class="btn btn-secondary me-2">Harmonogram</a>
|
||||||
<li class="nav-item">
|
<a href="{{ url_for('change_password') }}" class="btn btn-secondary me-2">Zmiana hasła</a>
|
||||||
<a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
|
<a href="{{ url_for('logout') }}" class="btn btn-secondary me-2">Wyloguj</a>
|
||||||
</li>
|
{% else %}
|
||||||
<!-- Urządzenia dropdown -->
|
<a href="{{ url_for('login') }}" class="btn btn-secondary me-2">Zaloguj</a>
|
||||||
<li class="nav-item dropdown">
|
<a href="{{ url_for('register') }}" class="btn btn-secondary me-2">Utwórz konto</a>
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="devicesDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
Urządzenia
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="devicesDropdown">
|
|
||||||
<li><a class="dropdown-item" href="/routers">Lista</a></li>
|
|
||||||
<li><a class="dropdown-item" href="/routers/add">Dodaj nowe</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<!-- Diff -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/diff_selector">Diff</a>
|
|
||||||
</li>
|
|
||||||
<!-- Wszystkie pliki -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/all_files">Wszystkie pliki</a>
|
|
||||||
</li>
|
|
||||||
<!-- Logi -->
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="/logs">Logi</a>
|
|
||||||
</li>
|
|
||||||
<!-- Ustawienia dropdown -->
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a class="nav-link dropdown-toggle" href="#" id="settingsDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
|
||||||
Ustawienia
|
|
||||||
</a>
|
|
||||||
<ul class="dropdown-menu" aria-labelledby="settingsDropdown">
|
|
||||||
<li><a class="dropdown-item" href="/settings">Główne</a></li>
|
|
||||||
<li><a class="dropdown-item" href="/advanced_schedule">Harmonogram</a></li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<!--<a href="{{ url_for('toggle_dark_mode') }}" class="btn btn-warning">Toggle Dark Mode</a>-->
|
||||||
<!-- Prawa strona navbaru -->
|
|
||||||
<ul class="navbar-nav ms-auto align-items-center">
|
|
||||||
<!-- Przełącznik trybu ciemnego -->
|
|
||||||
<li class="nav-item me-2">
|
|
||||||
<form action="{{ url_for('toggle_dark_mode') }}" method="GET" class="d-flex align-items-center">
|
|
||||||
<div class="form-check form-switch mb-0">
|
|
||||||
<input class="form-check-input" type="checkbox" id="darkModeSwitch"
|
|
||||||
onchange="this.form.submit()"
|
|
||||||
{% if session.get('dark_mode', True) %}checked{% endif %}>
|
|
||||||
<label class="form-check-label ms-1" for="darkModeSwitch" style="user-select:none;">Ciemny</label>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% if session.get('user_id') %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link btn btn-alert ms-2 btn-logout" href="{{ url_for('change_password') }}">Zmień hasło</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link btn btn-danger ms-2 btn-logout" href="{{ url_for('logout') }}">Wyloguj</a>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link btn btn-success ms-2" href="{{ url_for('login') }}">Zaloguj się</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link btn btn-primary ms-2" href="{{ url_for('register') }}">Zarejestruj się</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div class="container">
|
||||||
|
{% with messages = get_flashed_messages() %}
|
||||||
|
|
||||||
|
|
||||||
<!-- Główna zawartość -->
|
|
||||||
<main class="container mb-5">
|
|
||||||
|
|
||||||
|
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, msg in messages %}
|
<div class="alert alert-info">
|
||||||
{% set bs_cat = bootstrap_alert_category(category) %}
|
{% for msg in messages %}
|
||||||
<div class="alert alert-{{ bs_cat }} alert-dismissible fade show" role="alert">
|
<div>{{ msg }}</div>
|
||||||
{{ msg }}
|
{% endfor %}
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
</div>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal Test Połączenia -->
|
<!-- Modal Test Połączenia -->
|
||||||
<div class="modal fade" id="testConnectionModal" tabindex="-1" aria-labelledby="testConnectionModalLabel" aria-hidden="true">
|
<div class="modal fade" id="testConnectionModal" tabindex="-1" aria-labelledby="testConnectionModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-lg">
|
<div class="modal-dialog modal-lg">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="testConnectionModalLabel">Test Połączenia</h5>
|
<h5 class="modal-title" id="testConnectionModalLabel">Test Połączenia</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="testConnectionModalBody">
|
<div class="modal-body" id="testConnectionModalBody">
|
||||||
<!-- Zawartość ładowana przez AJAX -->
|
<!-- Zawartość zostanie załadowana przez AJAX -->
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Stopka -->
|
|
||||||
<footer class="footer py-3 mt-auto">
|
|
||||||
<div class="container text-center">
|
|
||||||
<span>© 2025 Mateusz Gruszczyński, linuxiarz.pl</span>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<!-- Bootstrap Bundle JS -->
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Dodatkowe skrypty -->
|
|
||||||
<script>
|
<script>
|
||||||
// Funkcja do wczytywania modalu testu połączenia
|
function ajaxExport(router_id) {
|
||||||
function openTestConnectionModal(routerId) {
|
fetch("/router/" + router_id + "/export", {
|
||||||
fetch('/router/' + routerId + '/test_connection?modal=1')
|
method: "POST",
|
||||||
.then(response => response.text())
|
headers: {"X-Requested-With": "XMLHttpRequest"}
|
||||||
.then(html => {
|
})
|
||||||
document.getElementById('testConnectionModalBody').innerHTML = html;
|
.then(response => response.json())
|
||||||
var myModal = new bootstrap.Modal(document.getElementById('testConnectionModal'));
|
.then(data => {
|
||||||
myModal.show();
|
if(data.status === "success"){
|
||||||
})
|
alert("Eksport wykonany: " + data.message);
|
||||||
.catch(error => {
|
// Możesz też zaktualizować część strony dynamicznie
|
||||||
console.error("Błąd ładowania modalu: ", error);
|
} else {
|
||||||
alert("Wystąpił błąd podczas ładowania danych.");
|
alert("Błąd eksportu: " + data.message);
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Błąd AJAX:", error);
|
||||||
|
alert("Wystąpił błąd.");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
<script>
|
||||||
{% block scripts %}{% endblock %}
|
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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-5">
|
<div class="container">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card shadow">
|
||||||
<div class="card-header bg-light text-center">
|
<div class="card-header text-center">
|
||||||
<h4 class="mb-0">Zmień hasło</h4>
|
<h2>Zmień hasło</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<h2 class="text-center mb-4">Dashboard</h2>
|
<h2 class="text-center mb-4">Panel administracyjny</h2>
|
||||||
|
|
||||||
<!-- Wiersz akcji ogólnych -->
|
<!-- Wiersz akcji ogólnych -->
|
||||||
<div class="row mb-4">
|
<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">
|
<a href="{{ url_for('routers_list') }}" class="btn btn-lg btn-outline-primary">
|
||||||
<i class="bi bi-hdd-network"></i> Zobacz routery
|
<i class="bi bi-hdd-network"></i> Zobacz routery
|
||||||
</a>
|
</a>
|
||||||
@@ -53,14 +53,14 @@
|
|||||||
<div class="col-md-6 d-flex justify-content-center">
|
<div class="col-md-6 d-flex justify-content-center">
|
||||||
<form action="{{ url_for('export_all_routers') }}" method="POST">
|
<form action="{{ url_for('export_all_routers') }}" method="POST">
|
||||||
<button type="submit" class="btn btn-lg btn-outline-success">
|
<button type="submit" class="btn btn-lg btn-outline-success">
|
||||||
<i class="bi bi-arrow-down-circle"></i> Eksport wszystkich routerów
|
<i class="bi bi-arrow-down-circle"></i> Eksport dla wszystkich routerów
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6 d-flex justify-content-center">
|
<div class="col-md-6 d-flex justify-content-center">
|
||||||
<form action="{{ url_for('backup_all_routers') }}" method="POST">
|
<form action="{{ url_for('backup_all_routers') }}" method="POST">
|
||||||
<button type="submit" class="btn btn-lg btn-outline-secondary">
|
<button type="submit" class="btn btn-lg btn-outline-secondary">
|
||||||
<i class="bi bi-cloud-download"></i> Backup binarny wszystkich routerów
|
<i class="bi bi-cloud-download"></i> Backup binarny dla wszystkich routerów
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,11 +73,11 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
{% set success_percent = 0 %}
|
{% set success_percent = 0 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="card mb-4 shadow-sm border-0">
|
<div class="card mb-4 shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Statystyki operacji</h5>
|
<h5 class="card-title">Statystyki operacji</h5>
|
||||||
<p>Udane operacje: {{ success_ops }}, Nieudane operacje: {{ failure_ops }}</p>
|
<p>Udane operacje: {{ success_ops }}, Nieudane operacje: {{ failure_ops }}</p>
|
||||||
<div class="progress 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">
|
<div class="progress-bar bg-success" role="progressbar" style="width: {{ success_percent }}%;" aria-valuenow="{{ success_percent }}" aria-valuemin="0" aria-valuemax="100">
|
||||||
{{ success_percent }}%
|
{{ success_percent }}%
|
||||||
</div>
|
</div>
|
||||||
@@ -89,12 +89,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Log operacji -->
|
<!-- Log operacji -->
|
||||||
<div class="card shadow-sm border-0 mb-4">
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">
|
<h5 class="card-title">Log operacji</h5>
|
||||||
Log operacji
|
|
||||||
<a href="{{ url_for('logs_page') }}" class="btn btn-sm btn-outline-primary ms-2">Więcej logów</a>
|
|
||||||
</h5>
|
|
||||||
<table class="table table-sm table-bordered">
|
<table class="table table-sm table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -114,8 +111,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dodatkowe statystyki -->
|
<!-- Dodatkowe statystyki przeniesione na sam dół, pod logami -->
|
||||||
<div class="card shadow-sm border-0">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Dodatkowe statystyki</h5>
|
<h5 class="card-title">Dodatkowe statystyki</h5>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -131,5 +128,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,47 +1,28 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<div class="card border-0 shadow-sm">
|
<h2>Porównanie: {{ backup1.file_path|basename }} vs {{ backup2.file_path|basename }}</h2>
|
||||||
<div class="card-header bg-light">
|
<hr>
|
||||||
<h4 class="mb-0">
|
<div id="diffContainer"></div>
|
||||||
Porównanie: {{ backup1.file_path|basename }} vs {{ backup2.file_path|basename }}
|
<a href="{{ url_for('router_details', router_id=backup1.router_id) }}" class="btn btn-secondary mt-3">Powrót</a>
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div id="diffContainer"></div>
|
|
||||||
<a href="{{ url_for('router_details', router_id=backup1.router_id) }}" class="btn btn-secondary mt-3">Powrót</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- diff2html resources -->
|
<!-- Dodajemy diff2html -->
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html@3.4.4/bundles/css/diff2html.min.css" />
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/diff2html@3.4.4/bundles/js/diff2html.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
var diffText = `{{ diff_text|e }}`;
|
// Upewnij się, że diff_text jest poprawnie escapowany
|
||||||
var targetElement = document.getElementById("diffContainer");
|
var diffText = `{{ diff_text|e }}`;
|
||||||
var configuration = {
|
var targetElement = document.getElementById("diffContainer");
|
||||||
drawFileList: true,
|
var configuration = {
|
||||||
matching: 'lines',
|
drawFileList: true,
|
||||||
outputFormat: 'line-by-line'
|
matching: 'lines',
|
||||||
};
|
outputFormat: 'line-by-line'
|
||||||
var diffHtml = Diff2Html.html(diffText, configuration);
|
};
|
||||||
targetElement.innerHTML = diffHtml;
|
var diffHtml = Diff2Html.html(diffText, configuration);
|
||||||
|
targetElement.innerHTML = diffHtml;
|
||||||
// Dark mode styl dla diff2html, jeśli potrzebne
|
|
||||||
if(document.body.classList.contains('dark-mode')) {
|
|
||||||
var darkStyle = document.createElement('style');
|
|
||||||
darkStyle.textContent = `
|
|
||||||
.d2h-wrapper { background-color: #1e1e1e; color: #fff; }
|
|
||||||
.d2h-file-header { background-color: #2e2e2e; color: #fff; }
|
|
||||||
.d2h-diff-table { background-color: #1e1e1e; color: #fff; }
|
|
||||||
.d2h-code-line { background-color: #1e1e1e; color: #fff; }
|
|
||||||
.d2h-code-line-ctn { color: #fff; }
|
|
||||||
`;
|
|
||||||
document.head.appendChild(darkStyle);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,10 +1,8 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<div class="card shadow-sm border-0">
|
<h2 class="text-center mb-4">Porównanie backupów (Diff)</h2>
|
||||||
<div class="card-header bg-light d-flex align-items-center">
|
<div class="card shadow-sm">
|
||||||
<h4 class="mb-0">Porównanie backupów (Diff)</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form action="{{ url_for('diff_selector') }}" method="POST" id="diffForm">
|
<form action="{{ url_for('diff_selector') }}" method="POST" id="diffForm">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
@@ -31,10 +29,8 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-4">
|
<div class="text-center">
|
||||||
<button type="submit" class="btn btn-primary btn-lg">
|
<button type="submit" class="btn btn-primary btn-lg">Porównaj backupy</button>
|
||||||
Porównaj backupy
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,12 +39,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById("diffForm").addEventListener("submit", function(event) {
|
document.getElementById("diffForm").addEventListener("submit", function(event) {
|
||||||
var backup1 = document.getElementById("backup1").value;
|
var backup1 = document.getElementById("backup1").value;
|
||||||
var backup2 = document.getElementById("backup2").value;
|
var backup2 = document.getElementById("backup2").value;
|
||||||
if (backup1 === backup2) {
|
if(backup1 === backup2) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
alert("Wybierz dwa różne backupy do porównania.");
|
alert("Wybierz dwa różne backupy do porównania.");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container mt-5">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header">
|
||||||
<h4 class="mb-0">Edycja urządzenia</h4>
|
<h2 class="mb-0">Edycja urządzenia</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
@@ -25,15 +25,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ssh_key" class="form-label">
|
<label for="ssh_key" class="form-label">
|
||||||
<b>Klucz prywatny</b>
|
<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>
|
||||||
</label><br>
|
Pozostaw puste jeśli ten RouterOS będzie używał <a href="{{ url_for('settings_view') }}">klucza globalnego</a>
|
||||||
<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>
|
</label>
|
||||||
<textarea class="form-control mt-2" id="ssh_key" name="ssh_key" rows="4">{{ router.ssh_key }}</textarea>
|
<textarea class="form-control" id="ssh_key" name="ssh_key" rows="4">{{ router.ssh_key }}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
|
<label for="ssh_password" class="form-label"><b>Hasło SSH</b></label><br>
|
||||||
<small>Jeśli jest klucz SSH lub klucz globalny, hasło może być ignorowane.</small>
|
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 mt-2" id="ssh_password" name="ssh_password" value="{{ router.ssh_password }}">
|
<input type="password" class="form-control" id="ssh_password" name="ssh_password" value="{{ router.ssh_password }}">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success">Zapisz zmiany</button>
|
<button type="submit" class="btn btn-success">Zapisz zmiany</button>
|
||||||
</form>
|
</form>
|
||||||
|
@@ -2,11 +2,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 80vh;">
|
<div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 80vh;">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
{% if session.get('dark_mode', True) %}
|
|
||||||
<img src="https://mikrotik.com/logo/assets/logo-colors-white-E8duxH7y.svg" alt="Mikrotik Logo" class="img-fluid" style="max-width: 200px;">
|
|
||||||
{% else %}
|
|
||||||
<img src="https://mikrotik.com/logo/assets/logo-colors-dark-ToiqSI6u.svg" alt="Mikrotik Logo" class="img-fluid" style="max-width: 200px;">
|
<img src="https://mikrotik.com/logo/assets/logo-colors-dark-ToiqSI6u.svg" alt="Mikrotik Logo" class="img-fluid" style="max-width: 200px;">
|
||||||
{% endif %}
|
|
||||||
<h1 class="mt-3">Witamy w aplikacji Backup RouterOS</h1>
|
<h1 class="mt-3">Witamy w aplikacji Backup RouterOS</h1>
|
||||||
<p class="lead">Zarządzaj backupami swoich urządzeń RouterOS w prosty sposób.</p>
|
<p class="lead">Zarządzaj backupami swoich urządzeń RouterOS w prosty sposób.</p>
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
@@ -16,4 +12,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@@ -1,23 +1,21 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block head %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-5">
|
<div class="container">
|
||||||
<div class="row justify-content-center align-items-center">
|
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card shadow border-0">
|
<div class="card shadow">
|
||||||
<div class="card-header bg-light text-center">
|
<div class="card-header text-center">
|
||||||
<h4 class="mb-0">Zaloguj się</h4>
|
<h2>Zaloguj się</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form action="{{ url_for('login') }}" method="POST">
|
<form action="{{ url_for('login') }}" method="POST">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Nazwa użytkownika</label>
|
<label for="username" class="form-label">Nazwa użytkownika</label>
|
||||||
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika" required>
|
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Hasło</label>
|
<label for="password" class="form-label">Hasło</label>
|
||||||
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło" required>
|
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło">
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button type="submit" class="btn btn-primary">Zaloguj się</button>
|
<button type="submit" class="btn btn-primary">Zaloguj się</button>
|
||||||
@@ -25,10 +23,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<small>
|
<a href="{{ url_for('register') }}">Nie masz konta? Zarejestruj się</a>
|
||||||
Nie masz konta?
|
|
||||||
<a href="{{ url_for('register') }}">Zarejestruj się</a>
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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" %}
|
{% extends "base.html" %}
|
||||||
{% block head %}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-5">
|
<div class="container">
|
||||||
<div class="row justify-content-center align-items-center">
|
<div class="row justify-content-center align-items-center" style="min-height: 100vh;">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card shadow border-0">
|
<div class="card shadow">
|
||||||
<div class="card-header bg-light text-center">
|
<div class="card-header text-center">
|
||||||
<h4 class="mb-0">Rejestracja</h4>
|
<h2>Rejestracja</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form action="{{ url_for('register') }}" method="POST">
|
<form action="{{ url_for('register') }}" method="POST">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="username" class="form-label">Nazwa użytkownika</label>
|
<label for="username" class="form-label">Nazwa użytkownika</label>
|
||||||
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika" required>
|
<input type="text" class="form-control" id="username" name="username" placeholder="Wpisz nazwę użytkownika">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="password" class="form-label">Hasło</label>
|
<label for="password" class="form-label">Hasło</label>
|
||||||
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło" required>
|
<input type="password" class="form-control" id="password" name="password" placeholder="Wpisz hasło">
|
||||||
</div>
|
</div>
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button type="submit" class="btn btn-primary">Zarejestruj się</button>
|
<button type="submit" class="btn btn-primary">Zarejestruj się</button>
|
||||||
@@ -25,10 +23,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<small>
|
<a href="{{ url_for('login') }}">Masz już konto? Zaloguj się</a>
|
||||||
Masz już konto?
|
|
||||||
<a href="{{ url_for('login') }}">Zaloguj się</a>
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -28,7 +28,6 @@
|
|||||||
<h3>Pliki z /export</h3>
|
<h3>Pliki z /export</h3>
|
||||||
{% if export_backups %}
|
{% if export_backups %}
|
||||||
<!-- Tabela z indywidualnymi akcjami -->
|
<!-- Tabela z indywidualnymi akcjami -->
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -55,6 +54,7 @@
|
|||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a>
|
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a>
|
||||||
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-sm btn-outline-primary">Podgląd</a>
|
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-sm btn-outline-primary">Podgląd</a>
|
||||||
|
|
||||||
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" style="display: inline;">
|
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" style="display: inline;">
|
||||||
<button type="submit" class="btn btn-sm btn-primary">Wyślij mailem</button>
|
<button type="submit" class="btn btn-sm btn-primary">Wyślij mailem</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -67,11 +67,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formularz do pobierania ZIP zaznaczonych eksportów -->
|
<!-- Formularz do pobierania ZIP zaznaczonych eksportów -->
|
||||||
<h4>Pobierz wybrane pliki z /export jako zip</h4>
|
<h4>Pobierz wybrane pliki z /export jako zip</h4>
|
||||||
<div class="table-responsive">
|
|
||||||
<form action="{{ url_for('download_zip') }}" method="POST">
|
<form action="{{ url_for('download_zip') }}" method="POST">
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -89,13 +86,12 @@
|
|||||||
<td>{{ b.file_path|basename }}</td>
|
<td>{{ b.file_path|basename }}</td>
|
||||||
<td>{{ b.file_path|filesize }}</td>
|
<td>{{ b.file_path|filesize }}</td>
|
||||||
<td>{{ b.created_at }}</td>
|
<td>{{ b.created_at }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button>
|
<button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted">Pusto</p>
|
<p class="text-muted">Pusto</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -105,7 +101,6 @@
|
|||||||
<!-- Sekcja backupów binarnych -->
|
<!-- Sekcja backupów binarnych -->
|
||||||
<h3>Pliki binarne (.backup)</h3>
|
<h3>Pliki binarne (.backup)</h3>
|
||||||
{% if binary_backups %}
|
{% if binary_backups %}
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -123,6 +118,7 @@
|
|||||||
<td>{{ b.created_at }}</td>
|
<td>{{ b.created_at }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a>
|
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-sm btn-info">Pobierz</a>
|
||||||
|
|
||||||
<form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline">
|
<form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline">
|
||||||
<button type="submit" class="btn btn-sm btn-secondary">Wgraj do routera</button>
|
<button type="submit" class="btn btn-sm btn-secondary">Wgraj do routera</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -138,11 +134,9 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Formularz do pobierania ZIP zaznaczonych backupów binarnych -->
|
<!-- Formularz do pobierania ZIP zaznaczonych backupów binarnych -->
|
||||||
<h4>Pobierz wybrane backupy binarne jako zip</h4>
|
<h4>Pobierz wybrane backupy binarne jako zip</h4>
|
||||||
<div class="table-responsive">
|
|
||||||
<form action="{{ url_for('download_zip') }}" method="POST">
|
<form action="{{ url_for('download_zip') }}" method="POST">
|
||||||
<table class="table table-bordered">
|
<table class="table table-bordered">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -166,7 +160,6 @@
|
|||||||
</table>
|
</table>
|
||||||
<button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button>
|
<button type="submit" class="btn btn-success">Pobierz zaznaczone (.zip)</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted">Pusto</p>
|
<p class="text-muted">Pusto</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@@ -1,17 +1,17 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="text-end mb-3">
|
||||||
|
{% if current_view == 'v1' %}
|
||||||
|
<a href="{{ url_for('router_details', router_id=router.id, view='v2') }}" class="btn btn-outline-secondary">Przełącz na widok v2</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('router_details', router_id=router.id, view='v1') }}" class="btn btn-outline-secondary">Przełącz na widok v1</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<div class="mb-3 text-end">
|
<!-- Informacje o routerze -->
|
||||||
{% if current_view == 'v1' %}
|
<div class="card shadow-sm mb-4">
|
||||||
<a href="{{ url_for('router_details', router_id=router.id, view='v2') }}" class="btn btn-outline-secondary">Przełącz na widok v2</a>
|
<div class="card-header">
|
||||||
{% else %}
|
<h2 class="mb-0">Router: {{ router.name }}</h2>
|
||||||
<a href="{{ url_for('router_details', router_id=router.id, view='v1') }}" class="btn btn-outline-secondary">Przełącz na widok v1</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card border-0 shadow-sm mb-4">
|
|
||||||
<div class="card-header bg-light">
|
|
||||||
<h4 class="mb-0">Router: {{ router.name }}</h4>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
@@ -20,13 +20,14 @@
|
|||||||
<strong>SSH User:</strong> {{ router.ssh_user }}
|
<strong>SSH User:</strong> {{ router.ssh_user }}
|
||||||
</p>
|
</p>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<!-- Mniejsze przyciski górne -->
|
||||||
<form action="{{ url_for('router_export', router_id=router.id) }}" method="POST" class="d-inline">
|
<form action="{{ url_for('router_export', router_id=router.id) }}" method="POST" class="d-inline">
|
||||||
<button type="submit" class="btn btn-primary btn-sm">Wykonaj /export</button>
|
<button type="submit" class="btn btn-primary">Wykonaj /export</button>
|
||||||
</form>
|
</form>
|
||||||
<form action="{{ url_for('router_backup', router_id=router.id) }}" method="POST" class="d-inline">
|
<form action="{{ url_for('router_backup', router_id=router.id) }}" method="POST" class="d-inline">
|
||||||
<button type="submit" class="btn btn-secondary btn-sm">Wykonaj backup binarny</button>
|
<button type="submit" class="btn btn-secondary">Wykonaj backup binarny</button>
|
||||||
</form>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,172 +35,160 @@
|
|||||||
<!-- Zakładki z backupami -->
|
<!-- Zakładki z backupami -->
|
||||||
<ul class="nav nav-tabs" id="routerTab" role="tablist">
|
<ul class="nav nav-tabs" id="routerTab" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link active" id="export-tab" data-bs-toggle="tab" data-bs-target="#export" type="button" role="tab" aria-controls="export" aria-selected="true">
|
<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>
|
||||||
Pliki /export
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link" id="binary-tab" data-bs-toggle="tab" data-bs-target="#binary" type="button" role="tab" aria-controls="binary" aria-selected="false">
|
<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>
|
||||||
Pliki binarne
|
|
||||||
</button>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content" id="routerTabContent">
|
<div class="tab-content" id="routerTabContent">
|
||||||
<!-- Zakładka /export -->
|
<!-- Zakładka /export -->
|
||||||
<div class="tab-pane fade show active" id="export" role="tabpanel" aria-labelledby="export-tab">
|
<div class="tab-pane fade show active" id="export" role="tabpanel" aria-labelledby="export-tab">
|
||||||
<div class="card mt-3 shadow-sm border-0">
|
<div class="card mt-3 shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if export_backups %}
|
{% if export_backups %}
|
||||||
<!-- Formularz masowych akcji dla eksportów -->
|
<!-- Formularz masowych akcji dla eksportów -->
|
||||||
<form id="export_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
|
<form id="export_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<button type="submit" name="action" value="download" class="btn btn-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)
|
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<!-- Tabela z eksportami z podzielonymi kolumnami akcji -->
|
||||||
<div class="table-responsive">
|
<table class="table table-bordered table-striped">
|
||||||
<table class="table table-bordered table-striped align-middle">
|
<thead class="table-dark">
|
||||||
<thead class="table-dark">
|
<tr>
|
||||||
<tr>
|
<th style="width: 3%;"><input type="checkbox" id="select_all_export"></th>
|
||||||
<th style="width: 3%;"><input type="checkbox" id="select_all_export"></th>
|
<th>Nazwa pliku</th>
|
||||||
<th>Nazwa pliku</th>
|
<th>Rozmiar</th>
|
||||||
<th>Rozmiar</th>
|
<th>Data</th>
|
||||||
<th>Data</th>
|
<th>Diff</th>
|
||||||
<th>Diff</th>
|
<th>Pobierz</th>
|
||||||
<th>Pobierz</th>
|
<th>Podgląd</th>
|
||||||
<th>Podgląd</th>
|
<th>Wyślij mailem</th>
|
||||||
<th>Wyślij mailem</th>
|
<th>Usuń</th>
|
||||||
<th>Usuń</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
{% for b in export_backups %}
|
||||||
{% for b in export_backups %}
|
<tr>
|
||||||
<tr>
|
<td>
|
||||||
<td>
|
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="export_mass_actions_form">
|
||||||
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="export_mass_actions_form">
|
</td>
|
||||||
</td>
|
<td>{{ b.file_path|basename }}</td>
|
||||||
<td>{{ b.file_path|basename }}</td>
|
<td>{{ b.file_path|filesize }}</td>
|
||||||
<td>{{ b.file_path|filesize }}</td>
|
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
||||||
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
<td>
|
||||||
<td>
|
{% if loop.index0 > 0 %}
|
||||||
{% 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>
|
||||||
<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 %}
|
||||||
{% else %}
|
<small>Brak nowszego</small>
|
||||||
<small>Brak nowszego</small>
|
{% endif %}
|
||||||
{% endif %}
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz">
|
||||||
<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>
|
||||||
<i class="bi bi-download"></i>
|
</a>
|
||||||
</a>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<a href="{{ url_for('view_export', backup_id=b.id) }}" class="btn btn-lg btn-outline-primary" title="Podgląd">
|
||||||
<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>
|
||||||
<i class="bi bi-eye"></i>
|
</a>
|
||||||
</a>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
|
||||||
<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) }}">
|
||||||
<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">
|
||||||
<button type="submit" class="btn btn-primary btn-sm" title="Wyślij mailem">
|
<i class="bi bi-envelope"></i>
|
||||||
<i class="bi bi-envelope"></i>
|
</button>
|
||||||
</button>
|
</form>
|
||||||
</form>
|
</td>
|
||||||
</td>
|
<td>
|
||||||
<td>
|
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
|
||||||
<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) }}">
|
||||||
<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ń">
|
||||||
<button type="submit" class="btn btn-danger btn-sm" title="Usuń">
|
<i class="bi bi-trash"></i>
|
||||||
<i class="bi bi-trash"></i>
|
</button>
|
||||||
</button>
|
</form>
|
||||||
</form>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
{% endfor %}
|
||||||
{% endfor %}
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted">Brak plików /export.</p>
|
<p class="text-muted">Brak plików /export.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Zakładka backupów binarnych -->
|
<!-- Zakładka backupów binarnych -->
|
||||||
<div class="tab-pane fade" id="binary" role="tabpanel" aria-labelledby="binary-tab">
|
<div class="tab-pane fade" id="binary" role="tabpanel" aria-labelledby="binary-tab">
|
||||||
<div class="card mt-3 shadow-sm border-0">
|
<div class="card mt-3 shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if binary_backups %}
|
{% if binary_backups %}
|
||||||
<!-- Formularz masowych akcji dla backupów binarnych -->
|
<!-- Formularz masowych akcji dla backupów binarnych -->
|
||||||
<form id="binary_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
|
<form id="binary_mass_actions_form" action="{{ url_for('download_zip') }}" method="POST" class="mb-3">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<button type="submit" name="action" value="download" class="btn btn-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)
|
<i class="bi bi-file-earmark-zip"></i> Pobierz zaznaczone (.zip)
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<!-- Tabela z backupami binarnymi z podzielonymi kolumnami akcji -->
|
||||||
<div class="table-responsive">
|
<table class="table table-bordered table-striped">
|
||||||
<table class="table table-bordered table-striped align-middle">
|
<thead class="table-dark">
|
||||||
<thead class="table-dark">
|
<tr>
|
||||||
<tr>
|
<th style="width: 3%;"><input type="checkbox" id="select_all_binary"></th>
|
||||||
<th style="width: 3%;"><input type="checkbox" id="select_all_binary"></th>
|
<th>Nazwa pliku</th>
|
||||||
<th>Nazwa pliku</th>
|
<th>Rozmiar</th>
|
||||||
<th>Rozmiar</th>
|
<th>Data</th>
|
||||||
<th>Data</th>
|
<th>Pobierz</th>
|
||||||
<th>Pobierz</th>
|
<th>Wgraj do routera</th>
|
||||||
<th>Wgraj do routera</th>
|
<th>Wyślij mailem</th>
|
||||||
<th>Wyślij mailem</th>
|
<th>Usuń</th>
|
||||||
<th>Usuń</th>
|
</tr>
|
||||||
</tr>
|
</thead>
|
||||||
</thead>
|
<tbody>
|
||||||
<tbody>
|
{% for b in binary_backups %}
|
||||||
{% for b in binary_backups %}
|
<tr>
|
||||||
<tr>
|
<td>
|
||||||
<td>
|
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form">
|
||||||
<input type="checkbox" name="backup_id" value="{{ b.id }}" form="binary_mass_actions_form">
|
</td>
|
||||||
</td>
|
<td>{{ b.file_path|basename }}</td>
|
||||||
<td>
|
<td>{{ b.file_path|filesize }}</td>
|
||||||
<span data-bs-toggle="tooltip" title="Checksum: {{ b.checksum }}">{{ b.file_path|basename }}</span>
|
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
||||||
</td>
|
<td>
|
||||||
<td>{{ b.file_path|filesize }}</td>
|
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-lg btn-info" title="Pobierz">
|
||||||
<td>{{ b.created_at.strftime("%Y-%m-%d %H:%M:%S") }}</td>
|
<i class="bi bi-download"></i>
|
||||||
<td>
|
</a>
|
||||||
<a href="{{ url_for('download_file', filename=b.file_path|basename) }}" class="btn btn-info btn-sm" title="Pobierz">
|
</td>
|
||||||
<i class="bi bi-download"></i>
|
<td>
|
||||||
</a>
|
<form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline">
|
||||||
</td>
|
<button type="submit" class="btn btn-lg btn-secondary" title="Wgraj do routera">
|
||||||
<td>
|
<i class="bi bi-upload"></i>
|
||||||
<form action="{{ url_for('upload_backup', router_id=router.id, backup_id=b.id) }}" method="POST" class="d-inline">
|
</button>
|
||||||
<button type="submit" class="btn btn-secondary btn-sm" title="Wgraj do routera">
|
</form>
|
||||||
<i class="bi bi-upload"></i>
|
</td>
|
||||||
</button>
|
<td>
|
||||||
</form>
|
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
|
||||||
</td>
|
<button type="submit" class="btn btn-lg btn-primary" title="Wyślij mailem">
|
||||||
<td>
|
<i class="bi bi-envelope"></i>
|
||||||
<form action="{{ url_for('send_by_email', backup_id=b.id) }}" method="POST" class="d-inline">
|
</button>
|
||||||
<button type="submit" class="btn btn-primary btn-sm" title="Wyślij mailem">
|
</form>
|
||||||
<i class="bi bi-envelope"></i>
|
</td>
|
||||||
</button>
|
<td>
|
||||||
</form>
|
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
|
||||||
</td>
|
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
|
||||||
<td>
|
<button type="submit" class="btn btn-lg btn-danger" title="Usuń">
|
||||||
<form action="{{ url_for('delete_backup', backup_id=b.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć backup?');">
|
<i class="bi bi-trash"></i>
|
||||||
<input type="hidden" name="next" value="{{ url_for('router_details', router_id=router.id) }}">
|
</button>
|
||||||
<button type="submit" class="btn btn-danger btn-sm" title="Usuń">
|
</form>
|
||||||
<i class="bi bi-trash"></i>
|
</td>
|
||||||
</button>
|
</tr>
|
||||||
</form>
|
{% endfor %}
|
||||||
</td>
|
</tbody>
|
||||||
</tr>
|
</table>
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-muted">Brak plików binarnych.</p>
|
<p class="text-muted">Brak plików binarnych.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -213,14 +202,18 @@
|
|||||||
<script>
|
<script>
|
||||||
document.getElementById('select_all_export').addEventListener('change', function(e) {
|
document.getElementById('select_all_export').addEventListener('change', function(e) {
|
||||||
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="export_mass_actions_form"]');
|
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="export_mass_actions_form"]');
|
||||||
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) {
|
document.getElementById('select_all_binary').addEventListener('change', function(e) {
|
||||||
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="binary_mass_actions_form"]');
|
var checkboxes = document.querySelectorAll('input[name="backup_id"][form="binary_mass_actions_form"]');
|
||||||
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'));
|
var triggerTabList = [].slice.call(document.querySelectorAll('#routerTab button'));
|
||||||
triggerTabList.forEach(function (triggerEl) {
|
triggerTabList.forEach(function (triggerEl) {
|
||||||
var tabTrigger = new bootstrap.Tab(triggerEl);
|
var tabTrigger = new bootstrap.Tab(triggerEl);
|
||||||
@@ -229,11 +222,5 @@ triggerTabList.forEach(function (triggerEl) {
|
|||||||
tabTrigger.show();
|
tabTrigger.show();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Inicjalizacja tooltipów
|
|
||||||
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
||||||
tooltipTriggerList.forEach(function (tooltipTriggerEl) {
|
|
||||||
new bootstrap.Tooltip(tooltipTriggerEl);
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,67 +1,63 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
<h2>Moje Routery</h2>
|
||||||
<h4 class="mb-0">Lista urządzeń</h4>
|
<a href="{{ url_for('add_router') }}" class="btn btn-success">
|
||||||
<a href="{{ url_for('add_router') }}" class="btn btn-success">
|
<i class="bi bi-plus-lg"></i> Dodaj nowe urządzenie
|
||||||
<i class="bi bi-plus-lg"></i> Dodaj nowe urządzenie
|
</a>
|
||||||
</a>
|
</div>
|
||||||
</div>
|
<div class="table-responsive">
|
||||||
<div class="card-body">
|
<table class="table table-striped table-hover">
|
||||||
<div class="table-responsive-sm">
|
<thead class="table-primary">
|
||||||
<table class="table table-striped table-hover align-middle">
|
<tr>
|
||||||
<thead class="table-primary">
|
<th>Nazwa</th>
|
||||||
<tr>
|
<th>Host</th>
|
||||||
<th>Nazwa</th>
|
<th>Port</th>
|
||||||
<th>Host</th>
|
<th>Exporty</th>
|
||||||
<th>Port</th>
|
<th>Backupy binarne</th>
|
||||||
<th>Exporty</th>
|
<th>Test Połączenia</th>
|
||||||
<th>Backupy binarne</th>
|
<th>Akcje</th>
|
||||||
<th>Test Połączenia</th>
|
</tr>
|
||||||
<th>Akcje</th>
|
</thead>
|
||||||
</tr>
|
<tbody>
|
||||||
</thead>
|
{% for router in routers %}
|
||||||
<tbody>
|
<tr>
|
||||||
{% for router in routers %}
|
<td>{{ router.name }}</td>
|
||||||
<tr>
|
<td>{{ router.host }}</td>
|
||||||
<td>{{ router.name }}</td>
|
<td>{{ router.port }}</td>
|
||||||
<td>{{ router.host }}</td>
|
<td>
|
||||||
<td>{{ router.port }}</td>
|
<span class="badge bg-success">
|
||||||
<td>
|
{{ router.backups|selectattr("backup_type", "equalto", "export")|list|length }}
|
||||||
<span class="badge bg-success">
|
</span>
|
||||||
{{ router.backups|selectattr("backup_type", "equalto", "export")|list|length }}
|
</td>
|
||||||
</span>
|
<td>
|
||||||
</td>
|
<span class="badge bg-info">
|
||||||
<td>
|
{{ router.backups|selectattr("backup_type", "equalto", "binary")|list|length }}
|
||||||
<span class="badge bg-info">
|
</span>
|
||||||
{{ router.backups|selectattr("backup_type", "equalto", "binary")|list|length }}
|
</td>
|
||||||
</span>
|
<td>
|
||||||
</td>
|
<button type="button" class="btn btn-sm btn-info" onclick="openTestConnectionModal({{ router.id }})">
|
||||||
<td>
|
<i class="bi bi-wifi"></i> Test
|
||||||
<button type="button" class="btn btn-sm btn-info" onclick="openTestConnectionModal({{ router.id }})">
|
</button>
|
||||||
<i class="bi bi-wifi"></i> Test
|
</td>
|
||||||
</button>
|
<td>
|
||||||
</td>
|
<a href="{{ url_for('router_details', router_id=router.id) }}" class="btn btn-sm btn-primary">
|
||||||
<td>
|
<i class="bi bi-eye"></i> Szczegóły
|
||||||
<a href="{{ url_for('router_details', router_id=router.id) }}" class="btn btn-sm btn-primary">
|
</a>
|
||||||
<i class="bi bi-eye"></i> Szczegóły
|
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-sm btn-warning">
|
||||||
</a>
|
<i class="bi bi-pencil"></i> Edytuj
|
||||||
<a href="{{ url_for('edit_router', router_id=router.id) }}" class="btn btn-sm btn-warning">
|
</a>
|
||||||
<i class="bi bi-pencil"></i> Edytuj
|
<form action="{{ url_for('delete_router', router_id=router.id) }}" method="POST" class="d-inline">
|
||||||
</a>
|
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Na pewno usunąć urządzenie?');">
|
||||||
<form action="{{ url_for('delete_router', router_id=router.id) }}" method="POST" class="d-inline" onsubmit="return confirm('Na pewno usunąć urządzenie?');">
|
<i class="bi bi-trash"></i> Usuń
|
||||||
<button type="submit" class="btn btn-sm btn-danger">
|
</button>
|
||||||
<i class="bi bi-trash"></i> Usuń
|
</form>
|
||||||
</button>
|
</td>
|
||||||
</form>
|
</tr>
|
||||||
</td>
|
{% endfor %}
|
||||||
</tr>
|
</tbody>
|
||||||
{% endfor %}
|
</table>
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@@ -1,16 +1,15 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-5">
|
||||||
<div class="card border-0 shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-header bg-light">
|
<div class="card-header">
|
||||||
<h4 class="mb-0">Ustawienia globalne</h4>
|
<h2 class="mb-0">Ustawienia globalne</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="POST">
|
<form method="POST">
|
||||||
|
|
||||||
<!-- Sekcja Pushover -->
|
<!-- Sekcja Pushover -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h5 class="mb-3">Powiadomienia - Pushover</h5>
|
<h4 class="mb-3">Powiadomienia - Pushover</h4>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="pushover_token" class="form-label">Pushover Token</label>
|
<label for="pushover_token" class="form-label">Pushover Token</label>
|
||||||
<input type="text" class="form-control" id="pushover_token" name="pushover_token" value="{{ settings.pushover_token }}">
|
<input type="text" class="form-control" id="pushover_token" name="pushover_token" value="{{ settings.pushover_token }}">
|
||||||
@@ -25,11 +24,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<!-- Sekcja SMTP -->
|
<!-- Sekcja SMTP -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h5 class="mb-3">Powiadomienia - SMTP (e-mail)</h5>
|
<h4 class="mb-3">Powiadomienia - SMTP (e-mail)</h4>
|
||||||
<div class="form-check mb-3">
|
<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 %}>
|
<input type="checkbox" class="form-check-input" id="smtp_notifications_enabled" name="smtp_notifications_enabled" {% if settings.smtp_notifications_enabled %}checked{% endif %}>
|
||||||
<label class="form-check-label" for="smtp_notifications_enabled">Włącz powiadomienia SMTP</label>
|
<label class="form-check-label" for="smtp_notifications_enabled">Włącz powiadomienia SMTP</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -49,43 +47,25 @@
|
|||||||
<label for="smtp_password" class="form-label">SMTP Hasło</label>
|
<label for="smtp_password" class="form-label">SMTP Hasło</label>
|
||||||
<input type="password" class="form-control" id="smtp_password" name="smtp_password" value="{{ settings.smtp_password }}">
|
<input type="password" class="form-control" id="smtp_password" name="smtp_password" value="{{ settings.smtp_password }}">
|
||||||
</div>
|
</div>
|
||||||
<div 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>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<!-- Sekcja globalnego klucza SSH -->
|
<!-- Sekcja globalnego klucza SSH -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h5 class="mb-3">Globalny klucz SSH</h5>
|
<h4 class="mb-3">Globalny klucz SSH</h4>
|
||||||
<label for="global_ssh_key" class="form-label">
|
<div class="mb-3">
|
||||||
Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code>
|
<label for="global_ssh_key" class="form-label">
|
||||||
</label>
|
Wklej wraz z <code>-----BEGIN RSA PRIVATE KEY-----</code> i <code>-----END RSA PRIVATE KEY-----</code>
|
||||||
<textarea class="form-control" id="global_ssh_key" name="global_ssh_key" rows="4">{{ settings.global_ssh_key }}</textarea>
|
</label>
|
||||||
|
<textarea class="form-control" id="global_ssh_key" name="global_ssh_key" rows="4">{{ settings.global_ssh_key }}</textarea>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="d-grid">
|
<div class="d-grid">
|
||||||
<button type="submit" class="btn btn-primary btn-lg">Zapisz ustawienia</button>
|
<button type="submit" class="btn btn-primary btn-lg">Zapisz ustawienia</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Przycisk do testowania powiadomień -->
|
|
||||||
<div class="mt-4 text-center">
|
|
||||||
<form method="POST" action="{{ url_for('test_email') }}" class="d-inline">
|
|
||||||
<button type="submit" class="btn btn-info">Testuj wysyłkę e-mail</button>
|
|
||||||
</form>
|
|
||||||
<form method="POST" action="{{ url_for('test_pushover') }}" class="d-inline ms-2">
|
|
||||||
<button type="submit" class="btn btn-warning">Testuj powiadomienie Pushover</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
<p class="mb-0">
|
<p>Ustawienia dotyczące backupu oraz harmonogramu CRON znajdują się na <a href="{{ url_for('advanced_schedule') }}">zaawansowanych ustawieniach harmonogramu</a>.</p>
|
||||||
Ustawienia harmonogramu i retencji:
|
|
||||||
<a href="{{ url_for('advanced_schedule') }}">Zaawansowane ustawienia</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,42 +1,30 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<div class="card border-0 shadow">
|
<h2>Podgląd eksportu: {{ backup.file_path|basename }}</h2>
|
||||||
<div class="card-header bg-light">
|
<hr>
|
||||||
<h4 class="mb-0">
|
<textarea id="exportEditor" readonly>{{ content|e }}</textarea>
|
||||||
Podgląd eksportu: {{ backup.file_path|basename }}
|
<a href="{{ next_url }}" class="btn btn-secondary">Powrót</a>
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<textarea id="exportEditor" readonly>{{ content|e }}</textarea>
|
|
||||||
<div class="mt-3">
|
|
||||||
<a href="{{ next_url }}" class="btn btn-secondary">Powrót</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CodeMirror CSS -->
|
<!-- CodeMirror CSS -->
|
||||||
{% 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/codemirror.min.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/theme/neo.min.css">
|
||||||
<!-- CodeMirror JS -->
|
<!-- CodeMirror JS -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/codemirror.min.js"></script>
|
||||||
|
<!-- Dodajemy tryb dla plików shell, który dobrze radzi sobie z konfiguracjami RouterOS -->
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/shell/shell.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.5/mode/shell/shell.min.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function() {
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
var editor = CodeMirror.fromTextArea(document.getElementById("exportEditor"), {
|
var editor = CodeMirror.fromTextArea(document.getElementById("exportEditor"), {
|
||||||
mode: "text/x-sh",
|
mode: "text/x-sh",
|
||||||
theme: "{{ 'darcula' if session.get('dark_mode', True) else 'neo' }}",
|
theme: "neo",
|
||||||
lineNumbers: true,
|
lineNumbers: true,
|
||||||
readOnly: true
|
readOnly: true
|
||||||
});
|
});
|
||||||
editor.setSize("100%", "600px");
|
// Dopasowanie rozmiaru edytora do zawartości
|
||||||
|
editor.setSize("100%", "800px");
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Reference in New Issue
Block a user