Compare commits

...

43 Commits

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

11
Dockerfile Normal file
View File

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

106
README.md
View File

@@ -1,21 +1,101 @@
# RouterOS Backup Manager
# RouterOS backup system
RouterOS Backup Manager to aplikacja Flask umożliwiająca zarządzanie kopiami zapasowymi urządzeń Mikrotik RouterOS. Aplikacja pozwala na eksport konfiguracji, tworzenie backupów binarnych, ich przechowywanie, porównywanie oraz przywracanie.
# Instalation:
- clone (to ex. /opt/routeros_backup)
- create venv
- install requirements via pip
- copy systemd service (routeros_backup.service)
## 🔧 Instalacja
# Start
- systemctl start routeros_backup.service
- go to http://IPADDRESS:5581
### 1. Klonowanie repozytorium
```sh
git clone https://gitea.linuxiarz.pl/gru/routeros_backup.git
cd routeros_backup
```
# Register, Login
### 2. Tworzenie i aktywacja środowiska wirtualnego (opcjonalnie)
```sh
python3 -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
```
# Configure devices, keys, backups, crons
### 3. Instalacja zależności
```sh
pip install -r requirements.txt
```
### 4. Uruchomienie aplikacji lokalnie
```sh
python run_waitress.py
```
Aplikacja będzie dostępna pod adresem: `http://127.0.0.1:5581/`
## Authors
---
- [@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.

147
app.py
View File

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

15
docker-compose.yml Normal file
View File

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

View File

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

View File

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

View File

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

9
run_waitress.py Normal file
View File

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

21
start.sh Executable file
View File

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

View File

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

View File

@@ -1,52 +1,61 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="card shadow-sm">
<div class="card-header">
<h2 class="mb-0">Zaawansowane ustawienia harmonogramu</h2>
<div class="container my-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-light">
<h4 class="mb-0">Zaawansowane ustawienia harmonogramu</h4>
</div>
<div class="card-body">
<form action="{{ url_for('advanced_schedule') }}" method="POST">
<div class="mb-3">
<div class="mb-3">
<label for="backup_retention_days" class="form-label">Próg retencji backupów (dni)</label>
<small>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>
<label for="retention_cron" class="form-label">Harmonogram retencji (cron)</label>
<label for="backup_retention_days" class="form-label">Próg retencji backupów (dni)</label>
<small class="text-muted d-block mb-2">Usuwanie danych starszych niż ustawione w progu.</small>
<input type="number" class="form-control" id="backup_retention_days" name="backup_retention_days" value="{{ settings.backup_retention_days }}">
</div>
<div class="mb-3">
<label for="log_retention_days" class="form-label">Próg retencji logów (dni)</label>
<input type="number" class="form-control" id="log_retention_days" name="log_retention_days" value="{{ settings.log_retention_days }}">
</div>
<div class="mb-3">
<label for="retention_cron" class="form-label">Harmonogram retencji <code>cron</code></label>
<div class="input-group">
<input type="text" class="form-control" id="retention_cron" name="retention_cron" value="{{ settings.retention_cron }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('retention_cron')">Generuj cron</button>
</div>
<div class="form-text">Np. <code>0 */12 * * *</code> co 12 godzin</div>
<small class="text-muted">Np. <code>0 */12 * * *</code> co 12 godzin</small>
</div>
<div class="mb-3">
<label for="binary_cron" class="form-label">Harmonogram kopii zapasowych binarnych (cron)</label>
<label for="binary_cron" class="form-label">Harmonogram kopii zapasowych binarnych <code>cron</code></label>
<div class="input-group">
<input type="text" class="form-control" id="binary_cron" name="binary_cron" value="{{ settings.binary_cron|default('') }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('binary_cron')">Generuj cron</button>
</div>
<div class="form-text">Np. <code>15 2 * * *</code> codziennie o 2:15</div>
<small class="text-muted">Np. <code>15 2 * * *</code> codziennie o 2:15</small>
</div>
<div class="mb-3">
<label for="export_cron" class="form-label">Harmonogram eksportów (cron)</label>
<label for="export_cron" class="form-label">Harmonogram exportów <code>cron</code></label>
<div class="input-group">
<input type="text" class="form-control" id="export_cron" name="export_cron" value="{{ settings.export_cron }}">
<button type="button" class="btn btn-outline-secondary" onclick="openCronModal('export_cron')">Generuj cron</button>
</div>
<div class="form-text">Np. <code>0 */12 * * *</code> co 12 godzin</div>
<small class="text-muted">Np. <code>0 */12 * * *</code> co 12 godzin</small>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enable_auto_export" name="enable_auto_export" {% if settings.enable_auto_export %}checked{% endif %}>
<label class="form-check-label" for="enable_auto_export">Włącz automatyczny eksport</label>
<label class="form-check-label" for="enable_auto_export">Włącz automatyczny export</label>
</div>
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
</form>
</div>
<div class="card-footer text-center">
<small class="text-muted">Ustawienia zostaną zapisane i użyte przez cron.</small>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,12 +10,12 @@
<div class="container my-4">
<h2 class="text-center mb-4">Historia logów operacji</h2>
<!-- Formularz usuwania logów starszych od podanej liczby dni -->
<div class="card mb-4 shadow-sm">
<!-- 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ż:</label>
<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>
@@ -28,7 +28,7 @@
</div>
<!-- Tabela logów -->
<div class="card shadow-sm mb-4">
<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">
@@ -50,14 +50,14 @@
</div>
<div class="text-center mt-3">
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-primary">Powrót do Dashboard</a>
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-primary">Powrót do Dashboardu</a>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<!-- jQuery (jeśli nie jest już dołączone w base.html) -->
<!-- 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>
@@ -66,7 +66,10 @@
$(document).ready(function() {
$('#logsTable').DataTable({
responsive: true,
order: [[0, 'desc']]
order: [[0, 'desc']],
language: {
url: '//cdn.datatables.net/plug-ins/1.13.4/i18n/pl.json'
}
});
});
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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