Compare commits
41 Commits
f58e2c5da0
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
73a4e6149a | ||
![]() |
85a37e4a78 | ||
![]() |
d0f1d25063 | ||
![]() |
4e965195f5 | ||
![]() |
8a7cb0a077 | ||
![]() |
700e35af7b | ||
![]() |
a1338a07eb | ||
![]() |
ab99e12224 | ||
![]() |
7045f16e6f | ||
![]() |
913fb4e2a3 | ||
![]() |
79e9dbd5d2 | ||
![]() |
33b465f3e0 | ||
![]() |
3567a0bac8 | ||
![]() |
781840066b | ||
![]() |
9cb1c4e3a2 | ||
![]() |
9509c6fec1 | ||
![]() |
6bc41acebd | ||
![]() |
d8f66a5a15 | ||
![]() |
254355227b | ||
![]() |
f572b0808c | ||
![]() |
fdb1e7b161 | ||
![]() |
12e6d090bf | ||
![]() |
d4a56235aa | ||
![]() |
be081edde8 | ||
![]() |
a0f21ac8e8 | ||
![]() |
0cc4f51c95 | ||
![]() |
b09ea400b1 | ||
![]() |
a17423aaa1 | ||
![]() |
f9215590ea | ||
![]() |
d5c8aedfd4 | ||
![]() |
02858f7b7a | ||
![]() |
c89c1efd67 | ||
![]() |
5d47549c19 | ||
![]() |
1d25b1f2f6 | ||
![]() |
977e969464 | ||
![]() |
54b7d62e72 | ||
![]() |
fe5e0e6374 | ||
![]() |
1e6ca7eb06 | ||
![]() |
f2d2e56d0d | ||
![]() |
2904d3e9f6 | ||
![]() |
04c1e6d49a |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
__pycache__
|
||||
instance
|
||||
venv
|
11
Dockerfile
Normal file
11
Dockerfile
Normal 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"]
|
119
README.md
119
README.md
@@ -0,0 +1,119 @@
|
||||
# RouterOS Update & Changelog Dashboard
|
||||
|
||||
RouterOS Update & Changelog Dashboard to aplikacja webowa oparta na Flask, która służy do monitorowania aktualizacji systemu RouterOS dla urządzeń MikroTik. Aplikacja pobiera changelogi z [MikroTik Changelogs](https://mikrotik.com/download/changelogs) i prezentuje je w przyjaznym interfejsie z podziałem na wersje (6.x, 7.x) oraz kanały wydania (stable, rc, beta). Dodatkowo dashboard wyświetla statystyki dotyczące urządzeń, logów, historii aktualizacji i ostatnich zdarzeń.
|
||||
|
||||
## Funkcjonalności
|
||||
|
||||
### Pobieranie i prezentacja changelogów
|
||||
- Automatyczne pobieranie changelogów z witryny MikroTik.
|
||||
- Filtrowanie i zapisywanie wpisów tylko dla wersji 6.x i 7.x.
|
||||
- Podział changelogów według kanałów wydania: stable, rc, beta.
|
||||
- Formatowanie wersji (np. "7.182025" jest wyświetlane jako "7.18") wraz z datą wydania.
|
||||
- Prezentacja changelogów z wykorzystaniem Prism.js z obsługą trybu ciemnego/jasnego.
|
||||
|
||||
### Zarządzanie urządzeniami
|
||||
- Dodawanie, edycja i monitorowanie urządzeń MikroTik.
|
||||
- Sprawdzanie aktualizacji oprogramowania na urządzeniach.
|
||||
- Rejestrowanie logów i historii aktualizacji.
|
||||
|
||||
### Dashboard
|
||||
- Wyświetlanie liczby urządzeń, logów, urządzeń wymagających aktualizacji oraz wykonanych aktualizacji.
|
||||
- Prezentacja najnowszych wersji systemu (RouterOS 7.x i 6.x) z opcją szybkiego przejścia do szczegółowego changeloga.
|
||||
- Lista ostatnich zdarzeń z systemu.
|
||||
|
||||
### Powiadomienia
|
||||
- Wysyłanie powiadomień za pomocą Pushover i e-mail, gdy dostępne są nowe aktualizacje.
|
||||
- Wbudowana detekcja anomalii na podstawie logów.
|
||||
|
||||
### Planowanie zadań
|
||||
- Automatyczne sprawdzanie urządzeń, pobieranie changelogów oraz czyszczenie starych logów dzięki APScheduler.
|
||||
|
||||
## Technologie
|
||||
|
||||
- **Flask** – framework webowy.
|
||||
- **SQLAlchemy** – ORM do zarządzania bazą danych.
|
||||
- **Flask-Login** – uwierzytelnianie użytkowników.
|
||||
- **APScheduler** – planowanie zadań w tle.
|
||||
- **BeautifulSoup** – parsowanie HTML do pobierania changelogów.
|
||||
- **Prism.js** – prezentacja i podświetlanie składni (z trybem ciemnym/jasnym).
|
||||
- **Bootstrap 5** – responsywny interfejs użytkownika.
|
||||
|
||||
## Instalacja
|
||||
|
||||
### Klonowanie repozytorium
|
||||
|
||||
```bash
|
||||
git clone https://github.com/TwojeRepozytorium/routeros-update.git
|
||||
cd routeros-update
|
||||
```
|
||||
|
||||
### Utworzenie środowiska wirtualnego i instalacja zależności
|
||||
|
||||
```bash
|
||||
python3 -m venv venv
|
||||
source venv/bin/activate # Na Windows: venv\Scriptsctivate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### Inicjalizacja bazy danych
|
||||
|
||||
> *Nie jest to wymagane, chyba że przeprowadzasz aktualizację.*
|
||||
|
||||
Domyślnie aplikacja używa SQLite. Uruchom aplikację lub użyj Flask shell, aby utworzyć bazę:
|
||||
|
||||
```bash
|
||||
flask shell
|
||||
>>> from app import db
|
||||
>>> db.create_all()
|
||||
```
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
### Plik konfiguracyjny
|
||||
W pliku `app.py` ustaw odpowiednią wartość dla `SECRET_KEY` oraz, jeśli potrzebujesz, zmodyfikuj `SQLALCHEMY_DATABASE_URI`.
|
||||
|
||||
### Powiadomienia
|
||||
Po rejestracji użytkownika skonfiguruj ustawienia powiadomień (Pushover, SMTP) w sekcji ustawień.
|
||||
|
||||
### Tryb ciemny/jasny
|
||||
Globalny przełącznik trybu (w menu) umożliwia zmianę wyglądu całej aplikacji, w tym motywu Prism.js wykorzystywanego do prezentacji changelogów.
|
||||
|
||||
## Uruchomienie aplikacji
|
||||
|
||||
### Lokalnie
|
||||
|
||||
```bash
|
||||
python app.py
|
||||
```
|
||||
|
||||
Aplikacja będzie dostępna pod adresem: `http://0.0.0.0:5582`
|
||||
|
||||
### Uruchomienie w Dockerze
|
||||
|
||||
Aplikację można uruchomić w kontenerze Docker (lub Podman). W repozytorium znajdują się następujące pliki:
|
||||
|
||||
- `Dockerfile` – definicja obrazu aplikacji.
|
||||
- `docker-compose.yml` – konfiguracja kontenera.
|
||||
- `start.sh` – skrypt uruchamiający kontenery (wybiera `podman-compose`, jeśli dostępny, lub `docker-compose`).
|
||||
|
||||
Aby uruchomić aplikację w Dockerze, wykonaj:
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
Skrypt zatrzyma i usunie poprzednie kontenery, odbuduje obraz i uruchomi aplikację w tle.
|
||||
|
||||
## Użytkowanie
|
||||
|
||||
1. **Dashboard**: Na stronie głównej dashboardu możesz zobaczyć statystyki urządzeń, liczbę logów, urządzeń wymagających aktualizacji, historię aktualizacji oraz najnowsze wersje systemu (dla RouterOS 7.x i 6.x).
|
||||
2. **Zarządzanie urządzeniami**: Dodawaj i edytuj urządzenia, a także sprawdzaj ich aktualizacje i logi.
|
||||
3. **Changelog**: Przeglądaj changelogi z podziałem na kanały wydania (stable, rc, beta) oraz serie wersji (7.x i 6.x). Możesz wybrać konkretny changelog z listy lub wyświetlić najnowszy wpis.
|
||||
4. **Aktualizacja changelogów**: Użyj przycisku "Aktualizuj changelogi" na stronie changelogów, aby ręcznie pobrać wszystkie nowe wpisy.
|
||||
|
||||
## Wkład i rozwój
|
||||
|
||||
Zapraszam do zgłaszania poprawek i propozycji rozwoju.
|
||||
|
||||
## Licencja
|
||||
Projekt jest dostępny na licencji MIT.
|
569
app.py
569
app.py
@@ -5,7 +5,6 @@ from flask import Flask, render_template, request, redirect, url_for, flash, ses
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, current_user, login_required
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
from datetime import datetime
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
import librouteros
|
||||
import threading
|
||||
@@ -16,7 +15,20 @@ from email.mime.text import MIMEText
|
||||
from flask import current_app as app
|
||||
from flask import render_template
|
||||
import atexit
|
||||
from datetime import timedelta
|
||||
from datetime import timedelta, datetime
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from flask import render_template, flash
|
||||
import logging
|
||||
import re
|
||||
try:
|
||||
from dateutil import parser as date_parser
|
||||
except ImportError:
|
||||
date_parser = None
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# Konfiguracja aplikacji
|
||||
app = Flask(__name__)
|
||||
@@ -31,7 +43,6 @@ login_manager = LoginManager(app)
|
||||
login_manager.login_view = 'login'
|
||||
|
||||
# MODELE BAZY DANYCH
|
||||
|
||||
class User(UserMixin, db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
@@ -45,7 +56,6 @@ class User(UserMixin, db.Model):
|
||||
|
||||
def check_password(self, password):
|
||||
return check_password_hash(self.password_hash, password)
|
||||
|
||||
class Device(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(120))
|
||||
@@ -59,10 +69,10 @@ class Device(db.Model):
|
||||
last_log = db.Column(db.Text)
|
||||
current_version = db.Column(db.String(50))
|
||||
current_firmware = db.Column(db.String(50))
|
||||
upgrade_firmware = db.Column(db.String(50))
|
||||
use_ssl = db.Column(db.Boolean, default=False) # Czy używać SSL?
|
||||
ssl_insecure = db.Column(db.Boolean, default=False) # Jeśli True – nie weryfikować certyfikatu SSL
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
class Settings(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
pushover_user_key = db.Column(db.String(255))
|
||||
@@ -73,16 +83,38 @@ class Settings(db.Model):
|
||||
smtp_username = db.Column(db.String(255))
|
||||
smtp_password = db.Column(db.String(255))
|
||||
email_notifications_enabled = db.Column(db.Boolean, default=False)
|
||||
check_interval = db.Column(db.Integer, default=60) # interwał sprawdzania w sekundach
|
||||
log_retention_days = db.Column(db.Integer, default=30) # nowe pole – retencja logów w dniach
|
||||
check_interval = db.Column(db.Integer, default=60)
|
||||
log_retention_days = db.Column(db.Integer, default=30)
|
||||
recipient_email = db.Column(db.String(120))
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False, unique=True)
|
||||
|
||||
class Log(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
message = db.Column(db.Text)
|
||||
device_id = db.Column(db.Integer, db.ForeignKey('device.id'))
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
|
||||
device = db.relationship('Device', backref='logs')
|
||||
class UpdateHistory(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
device_id = db.Column(db.Integer, db.ForeignKey('device.id'))
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
update_type = db.Column(db.String(50))
|
||||
details = db.Column(db.Text)
|
||||
device = db.relationship('Device', backref='update_histories')
|
||||
class Anomaly(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
device_id = db.Column(db.Integer, db.ForeignKey('device.id'), nullable=True)
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
description = db.Column(db.Text)
|
||||
resolved = db.Column(db.Boolean, default=False)
|
||||
device = db.relationship('Device', backref='anomalies')
|
||||
class ChangelogEntry(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
version = db.Column(db.String(50), nullable=False)
|
||||
details = db.Column(db.Text, nullable=False)
|
||||
category = db.Column(db.String(10), nullable=False) # "6.x" or "7.x"
|
||||
timestamp = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
release_type = db.Column(db.String(10), nullable=False, default="stable")
|
||||
|
||||
# Inicjalizacja bazy (utworzyć bazę przy pierwszym uruchomieniu)
|
||||
with app.app_context():
|
||||
@@ -93,38 +125,35 @@ def load_user(user_id):
|
||||
return User.query.get(int(user_id))
|
||||
|
||||
# FUNKCJE POWIADOMIEŃ
|
||||
|
||||
def send_pushover_notification(user, message):
|
||||
# Sprawdzamy, czy użytkownik posiada ustawienia oraz wymagane pola
|
||||
if not user.settings or not user.settings.pushover_enabled or not user.settings.pushover_user_key or not user.settings.pushover_token:
|
||||
return
|
||||
data = {
|
||||
"token": user.settings.pushover_token, # Używamy pushover_token z ustawień
|
||||
"token": user.settings.pushover_token,
|
||||
"user": user.settings.pushover_user_key,
|
||||
"message": message
|
||||
}
|
||||
try:
|
||||
r = requests.post("https://api.pushover.net/1/messages.json", data=data)
|
||||
# Możesz dodać logowanie odpowiedzi, jeśli potrzebne
|
||||
except Exception as e:
|
||||
print("Błąd przy wysyłaniu powiadomienia Pushover:", e)
|
||||
|
||||
def send_email_notification(user, subject, message):
|
||||
# Sprawdzamy, czy ustawienia e-mail są poprawnie skonfigurowane
|
||||
if not user.settings or not user.settings.email_notifications_enabled or not user.settings.smtp_server:
|
||||
return
|
||||
try:
|
||||
# Uzyskujemy sformatowaną treść e-maila
|
||||
html_body = get_email_template(subject, message)
|
||||
msg = MIMEText(html_body, 'html')
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = user.settings.smtp_username
|
||||
msg["To"] = user.email
|
||||
# Używamy adresu z ustawień, jeśli został podany, lub domyślnie adresu z profilu użytkownika
|
||||
to_email = user.settings.recipient_email if user.settings.recipient_email else user.email
|
||||
msg["To"] = to_email
|
||||
|
||||
s = smtplib.SMTP(user.settings.smtp_server, user.settings.smtp_port)
|
||||
s.starttls()
|
||||
s.login(user.settings.smtp_username, user.settings.smtp_password)
|
||||
s.sendmail(user.settings.smtp_username, [user.email], msg.as_string())
|
||||
s.sendmail(user.settings.smtp_username, [to_email], msg.as_string())
|
||||
s.quit()
|
||||
app.logger.debug("E-mail wysłany pomyślnie")
|
||||
except Exception as e:
|
||||
@@ -136,6 +165,7 @@ def check_device_update(device):
|
||||
update_available = False
|
||||
current_version = None
|
||||
current_firmware = None
|
||||
upgrade_firmware = None
|
||||
try:
|
||||
app.logger.debug(f"Connecting to device {device.ip}:{device.port} using SSL: {device.use_ssl}, ssl_verify: {not device.ssl_insecure}")
|
||||
api = librouteros.connect(
|
||||
@@ -169,10 +199,15 @@ def check_device_update(device):
|
||||
app.logger.debug(f"Routerboard response: {board_resp}")
|
||||
if board_resp:
|
||||
board_info = board_resp[0]
|
||||
# Próba odczytania firmware z kilku możliwych kluczy
|
||||
firmware = board_info.get('firmware', board_info.get('firmware-version', board_info.get('upgrade-firmware', 'N/A')))
|
||||
current_firmware = firmware
|
||||
log_entries.append(f"Firmware: {firmware}")
|
||||
# Pobieramy oddzielnie obie wersje:
|
||||
current_fw = board_info.get('firmware',
|
||||
board_info.get('firmware-version',
|
||||
board_info.get('current-firmware', 'N/A')))
|
||||
upgrade_fw = board_info.get('upgrade-firmware', 'N/A')
|
||||
current_firmware = current_fw
|
||||
upgrade_firmware = upgrade_fw
|
||||
log_entries.append(f"Firmware: {current_fw}")
|
||||
log_entries.append(f"Upgrade Firmware: {upgrade_fw}")
|
||||
|
||||
# Sprawdzenie dostępnych aktualizacji
|
||||
log_entries.append("Checking for updates...")
|
||||
@@ -198,10 +233,12 @@ def check_device_update(device):
|
||||
update_available = True
|
||||
else:
|
||||
log_entries.append("No updates available.")
|
||||
return "\n".join(log_entries), update_available, current_version, current_firmware
|
||||
# Zwracamy 5-elementową krotkę
|
||||
return "\n".join(log_entries), update_available, current_version, current_firmware, upgrade_firmware
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error connecting to device {device.ip}: {e}", exc_info=True)
|
||||
return f"Error: {str(e)}", False, None, None
|
||||
return f"Error: {str(e)}", False, None, None, None
|
||||
|
||||
|
||||
def get_email_template(subject, message):
|
||||
return f"""
|
||||
@@ -252,36 +289,39 @@ def get_email_template(subject, message):
|
||||
<p>{message}</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>Wiadomość wygenerowana automatycznie przez system RouterOS Backup.</p>
|
||||
<p>Wiadomość wygenerowana automatycznie przez system RouterOS Update</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
def clean_old_changelogs():
|
||||
with app.app_context():
|
||||
cutoff_time = datetime.utcnow() - timedelta(days=7)
|
||||
db.session.query(ChangelogEntry).filter(ChangelogEntry.timestamp < cutoff_time).delete()
|
||||
db.session.commit()
|
||||
|
||||
def check_all_devices():
|
||||
with app.app_context():
|
||||
devices = Device.query.all()
|
||||
for device in devices:
|
||||
result, update_available, current_version, current_firmware = check_device_update(device)
|
||||
result, update_available, current_version, current_firmware, upgrade_firmware = check_device_update(device)
|
||||
device.last_log = result
|
||||
device.last_check = datetime.utcnow()
|
||||
device.update_required = update_available
|
||||
device.current_version = current_version
|
||||
device.current_firmware = current_firmware
|
||||
device.upgrade_firmware = upgrade_firmware
|
||||
db.session.commit()
|
||||
# Zapis do tabeli logów
|
||||
log_entry = Log(message=result, device_id=device.id, user_id=device.user_id)
|
||||
db.session.add(log_entry)
|
||||
db.session.commit()
|
||||
# Powiadomienia, jeśli dostępna aktualizacja
|
||||
if update_available:
|
||||
user = device.owner
|
||||
message = f"Urządzenie {device.name or device.ip} ma dostępną aktualizację."
|
||||
send_pushover_notification(user, message)
|
||||
send_email_notification(user, "Aktualizacja dostępna", message)
|
||||
|
||||
|
||||
def bytes_to_human(n):
|
||||
try:
|
||||
n = int(n)
|
||||
@@ -304,10 +344,246 @@ def clean_old_logs():
|
||||
Log.query.filter(Log.user_id == setting.user_id, Log.timestamp < cutoff).delete()
|
||||
db.session.commit()
|
||||
|
||||
def parse_release_date(text):
|
||||
"""
|
||||
Próbuje wyłuskać datę z tekstu.
|
||||
Najpierw używa regex, a w razie niepowodzenia – dateutil (jeśli dostępny).
|
||||
"""
|
||||
date_match = re.search(r"\((\d{4})-([A-Za-z]{3})-(\d{2})", text)
|
||||
if date_match:
|
||||
try:
|
||||
return datetime.strptime(f"{date_match.group(1)}-{date_match.group(2)}-{date_match.group(3)}", "%Y-%b-%d")
|
||||
except Exception as e:
|
||||
logging.error(f"Błąd parsowania daty przy użyciu strptime: {e}")
|
||||
if date_parser:
|
||||
try:
|
||||
return date_parser.parse(text, fuzzy=True)
|
||||
except Exception as e:
|
||||
logging.error(f"Błąd parsowania daty przy użyciu dateutil: {e}")
|
||||
return None
|
||||
|
||||
def get_release_type(version_text):
|
||||
"""
|
||||
Określa typ wydania na podstawie numeru wersji:
|
||||
- Jeśli w tekście występuje "beta" – zwraca "beta"
|
||||
- Jeśli w tekście występuje "rc" – zwraca "rc"
|
||||
- W przeciwnym wypadku – "stable"
|
||||
"""
|
||||
lower = version_text.lower()
|
||||
if "beta" in lower:
|
||||
return "beta"
|
||||
elif "rc" in lower:
|
||||
return "rc"
|
||||
else:
|
||||
return "stable"
|
||||
|
||||
@app.template_filter('format_version')
|
||||
def format_version(value):
|
||||
# Krok 1: Usuń wszystko w nawiasach (np. „(2025-Feb-12 11:20)”).
|
||||
value = re.sub(r'\(.*?\)', '', value).strip()
|
||||
|
||||
# Krok 2: Spróbuj dopasować strukturę: major.minor(.patch)?(beta|rc)(num)?
|
||||
# 1) major (np. 7)
|
||||
# 2) minor (np. 18)
|
||||
# 3) patch (np. 20)
|
||||
# 4) suffix (beta|rc)
|
||||
# 5) suffixnum (np. 2025)
|
||||
pattern = r'^(\d+)' # grupa 1: major
|
||||
pattern += r'(?:\.(\d+))?' # grupa 2: minor (opcjonalnie)
|
||||
pattern += r'(?:\.(\d+))?' # grupa 3: patch (opcjonalnie)
|
||||
pattern += r'(?:(beta|rc)(\d+))?'# grupa 4 i 5: beta|rc + numer (opcjonalnie)
|
||||
|
||||
match = re.match(pattern, value)
|
||||
if not match:
|
||||
# Jeśli nie uda się dopasować, zwracamy oryginał
|
||||
return value
|
||||
|
||||
major, minor, patch, suffix, suffixnum = match.groups()
|
||||
|
||||
# Funkcja pomocnicza do przycinania łańcucha cyfr do maksymalnie 2 znaków
|
||||
def truncate_2_digits(num_str):
|
||||
return num_str[:2] if len(num_str) > 2 else num_str
|
||||
|
||||
# Składamy główne części wersji
|
||||
result = major
|
||||
if minor:
|
||||
# np. "182025" → "18"
|
||||
result += '.' + truncate_2_digits(minor)
|
||||
if patch:
|
||||
# np. "182025" → "18"
|
||||
result += '.' + truncate_2_digits(patch)
|
||||
|
||||
if suffix:
|
||||
# Dodajemy samo "beta"/"rc"
|
||||
result += suffix
|
||||
if suffixnum:
|
||||
# 1) Najpierw sprawdźmy, czy w numerze sufiksu jest (zlepiony) rok, np. "22025" → "2" i "2025"
|
||||
# Wzorzec: do 2 cyfr + "20xx", np. "14" + "2025", "2" + "2025" itd.
|
||||
m_year = re.match(r'^(\d{1,2})(20\d{2})$', suffixnum)
|
||||
if m_year:
|
||||
# Jeśli tak, zostawiamy tylko pierwszą grupę (np. "14" z "14"+"2025")
|
||||
suffixnum = m_year.group(1)
|
||||
|
||||
# 2) Ostatecznie przycinamy do 2 cyfr (jeśli ktoś wpisał np. "beta123")
|
||||
suffixnum = truncate_2_digits(suffixnum)
|
||||
|
||||
result += suffixnum
|
||||
|
||||
return result
|
||||
|
||||
@app.template_global()
|
||||
def bootstrap_alert_category(cat):
|
||||
mapping = {
|
||||
'error': 'danger',
|
||||
'fail': 'danger',
|
||||
'warning': 'warning',
|
||||
'warn': 'warning',
|
||||
'ok': 'success',
|
||||
'success': 'success',
|
||||
'info': 'info'
|
||||
}
|
||||
return mapping.get(cat.lower(), 'info')
|
||||
|
||||
def fetch_changelogs(force=False):
|
||||
changelog_url = "https://mikrotik.com/download/changelogs"
|
||||
current_date = datetime.utcnow()
|
||||
|
||||
def process_section(section):
|
||||
a_tag = section.find("a")
|
||||
if not a_tag:
|
||||
return None
|
||||
|
||||
raw_text = a_tag.get_text(strip=True)
|
||||
logging.info(f"raw_text: {raw_text}")
|
||||
|
||||
# Najpierw próbujemy znaleźć wersję za pomocą wzorca z "in"
|
||||
match = re.search(r"in\s+(\d+\.\d+(?:\.\d+)?(?:beta|rc)?\d*)", raw_text)
|
||||
if match:
|
||||
version_text = match.group(1)
|
||||
logging.info(f"Parsed version (pattern 1): {version_text}")
|
||||
else:
|
||||
# Jeśli nie znaleziono, próbujemy wychwycić wersję na początku łańcucha z dołączoną datą
|
||||
match = re.match(r"^(\d+\.\d+(?:\.\d+)?(?:beta|rc)?\d*)(\d{4}-\d{2}-\d{2})", raw_text)
|
||||
if match:
|
||||
version_text = match.group(1)
|
||||
logging.info(f"Parsed version (pattern 2): {version_text}")
|
||||
else:
|
||||
version_text = raw_text
|
||||
logging.info("Brak dopasowania regex, używam raw_text jako wersji")
|
||||
|
||||
# Pomijamy wersje, które nie zaczynają się od "6." lub "7."
|
||||
if not (version_text.startswith("6.") or version_text.startswith("7.")):
|
||||
logging.info(f"Pomijam wersję {version_text} – nie jest 6.x ani 7.x")
|
||||
return None
|
||||
|
||||
details_div = section.find("div", class_="content")
|
||||
changelog_file_url = details_div.get("data-url") if details_div else None
|
||||
if not changelog_file_url:
|
||||
logging.warning(f"Brak URL changeloga dla wersji {version_text}")
|
||||
return None
|
||||
|
||||
try:
|
||||
changelog_response = requests.get(changelog_file_url, timeout=10)
|
||||
changelog_response.raise_for_status()
|
||||
changelog_lines = changelog_response.text.splitlines()
|
||||
|
||||
if not changelog_lines:
|
||||
logging.warning(f"Pusty changelog dla wersji {version_text}")
|
||||
return None
|
||||
|
||||
first_line = changelog_lines[0].strip()
|
||||
release_date_dt = parse_release_date(first_line)
|
||||
if release_date_dt is None:
|
||||
logging.warning(f"Nie udało się wyłuskać daty dla wersji {version_text}, pomijam ten changelog")
|
||||
return None
|
||||
|
||||
changelog_text = "\n".join(changelog_lines).strip()
|
||||
except Exception as e:
|
||||
logging.error(f"Błąd pobierania changeloga z {changelog_file_url}: {e}")
|
||||
return None
|
||||
|
||||
# Filtrowanie według daty: dla 7.x pomijamy wersje starsze niż 1 rok, dla 6.x starsze niż 2 lata
|
||||
if version_text.startswith("7.") and release_date_dt < (current_date - timedelta(days=365)):
|
||||
logging.info(f"Pomijam wersję {version_text} - starsza niż 1 rok")
|
||||
return None
|
||||
if version_text.startswith("6.") and release_date_dt < (current_date - timedelta(days=730)):
|
||||
logging.info(f"Pomijam wersję {version_text} - starsza niż 2 lata")
|
||||
return None
|
||||
|
||||
release_type = get_release_type(version_text)
|
||||
return {
|
||||
"version": version_text,
|
||||
"details": changelog_text,
|
||||
"category": "6.x" if version_text.startswith("6.") else "7.x",
|
||||
"timestamp": release_date_dt,
|
||||
"release_type": release_type
|
||||
}
|
||||
|
||||
try:
|
||||
logging.info(f"Pobieranie changelogów z {changelog_url}...")
|
||||
response = requests.get(changelog_url, timeout=30)
|
||||
response.raise_for_status()
|
||||
soup = BeautifulSoup(response.text, "html.parser")
|
||||
changelog_sections = soup.find_all("li", class_="accordion-navigation")
|
||||
logging.info(f"Znaleziono {len(changelog_sections)} sekcji changelogów.")
|
||||
|
||||
new_entries = 0
|
||||
results = []
|
||||
# Używamy równoległego przetwarzania sekcji
|
||||
with ThreadPoolExecutor(max_workers=8) as executor:
|
||||
futures = [executor.submit(process_section, section) for section in changelog_sections]
|
||||
for future in as_completed(futures):
|
||||
result = future.result()
|
||||
if result is not None:
|
||||
results.append(result)
|
||||
|
||||
# Dodajemy wyniki do bazy danych
|
||||
for entry in results:
|
||||
new_entry = ChangelogEntry(
|
||||
version=entry["version"],
|
||||
details=entry["details"],
|
||||
category=entry["category"],
|
||||
timestamp=entry["timestamp"],
|
||||
release_type=entry["release_type"]
|
||||
)
|
||||
db.session.add(new_entry)
|
||||
new_entries += 1
|
||||
|
||||
db.session.commit()
|
||||
logging.info(f"Nowe wpisy dodane: {new_entries}")
|
||||
except Exception as e:
|
||||
logging.error(f"Błąd podczas pobierania changelogów: {e}")
|
||||
|
||||
|
||||
|
||||
def detect_anomalies():
|
||||
with app.app_context():
|
||||
# Ustal okres analizy, np. ostatnie 24 godziny
|
||||
cutoff = datetime.utcnow() - timedelta(hours=24)
|
||||
# Pobierz logi użytkowników (lub logi globalne) z tego okresu
|
||||
logs = Log.query.filter(Log.timestamp >= cutoff).all()
|
||||
|
||||
# Przykładowa analiza: wykryj logi zawierające określone słowa kluczowe
|
||||
error_keywords = ["błąd", "error", "niepowodzenie", "exception"]
|
||||
detected = {}
|
||||
for log in logs:
|
||||
lower_msg = log.message.lower()
|
||||
if any(keyword in lower_msg for keyword in error_keywords):
|
||||
detected.setdefault(log.device_id, []).append(log.message)
|
||||
|
||||
# Dla każdego urządzenia, jeżeli wykryto więcej niż określony próg błędów, zapisz anomalię
|
||||
for device_id, messages in detected.items():
|
||||
if len(messages) >= 3: # przykładowy próg
|
||||
description = f"Wykryto {len(messages)} błędne logi w ciągu ostatnich 24 godzin. Przykłady: " + "; ".join(messages[:3])
|
||||
anomaly = Anomaly(device_id=device_id, description=description)
|
||||
db.session.add(anomaly)
|
||||
# Możesz również wysłać powiadomienie, np. e-mail lub Pushover
|
||||
db.session.commit()
|
||||
|
||||
# Harmonogram sprawdzania aktualizacji – wykorzystujemy APScheduler
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# Inicjalizacja bazy i scheduler’a
|
||||
# Inicjalizacja bazy i schedulera
|
||||
with app.app_context():
|
||||
db.create_all() # lub już wcześniej utworzona baza
|
||||
# Pobranie globalnych ustawień – zakładamy, że Settings.query.first() zwróci ustawienia globalne
|
||||
@@ -324,12 +600,33 @@ with app.app_context():
|
||||
id="check_all_devices",
|
||||
max_instances=1
|
||||
)
|
||||
scheduler.add_job(
|
||||
func=detect_anomalies,
|
||||
trigger="interval",
|
||||
minutes=60,
|
||||
id="detect_anomalies",
|
||||
max_instances=1
|
||||
)
|
||||
scheduler.add_job(
|
||||
func=clean_old_changelogs,
|
||||
trigger="interval",
|
||||
days=1,
|
||||
id="clean_changelogs",
|
||||
max_instances=1
|
||||
)
|
||||
scheduler.add_job(
|
||||
func=lambda: fetch_changelogs(force=False),
|
||||
trigger="interval",
|
||||
days=1,
|
||||
id="daily_changelog_fetch",
|
||||
max_instances=1
|
||||
)
|
||||
|
||||
app.logger.debug(f"Scheduler initialized with interval: {interval} seconds")
|
||||
|
||||
scheduler.start()
|
||||
|
||||
# ROUTY APLIKACJI
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
if current_user.is_authenticated:
|
||||
@@ -343,11 +640,24 @@ def dashboard():
|
||||
pending_updates_count = Device.query.filter_by(update_required=True).count()
|
||||
logs_count = Log.query.count()
|
||||
users_count = User.query.count()
|
||||
anomalies_count = Anomaly.query.filter_by(resolved=False).count()
|
||||
update_history_count = UpdateHistory.query.count()
|
||||
recent_logs = Log.query.order_by(Log.timestamp.desc()).limit(5).all()
|
||||
|
||||
# Pobieramy najnowsze wersje stabilne dla 7.x i 6.x
|
||||
latest_version_7 = ChangelogEntry.query.filter_by(category="7.x", release_type="stable").order_by(ChangelogEntry.timestamp.desc()).first()
|
||||
latest_version_6 = ChangelogEntry.query.filter_by(category="6.x", release_type="stable").order_by(ChangelogEntry.timestamp.desc()).first()
|
||||
|
||||
return render_template('dashboard.html',
|
||||
devices_count=devices_count,
|
||||
pending_updates_count=pending_updates_count,
|
||||
logs_count=logs_count,
|
||||
users_count=users_count)
|
||||
users_count=users_count,
|
||||
anomalies_count=anomalies_count,
|
||||
update_history_count=update_history_count,
|
||||
recent_logs=recent_logs,
|
||||
latest_version_7=latest_version_7,
|
||||
latest_version_6=latest_version_6)
|
||||
|
||||
# Rejestracja
|
||||
@app.route('/register', methods=['GET', 'POST'])
|
||||
@@ -358,7 +668,7 @@ def register():
|
||||
password = request.form['password']
|
||||
# Prosta walidacja – warto rozszerzyć
|
||||
if User.query.filter_by(username=username).first():
|
||||
flash("Użytkownik o tej nazwie już istnieje.")
|
||||
flash("Użytkownik o tej nazwie już istnieje.", "error")
|
||||
return redirect(url_for('register'))
|
||||
new_user = User(username=username, email=email)
|
||||
new_user.set_password(password)
|
||||
@@ -368,7 +678,7 @@ def register():
|
||||
default_settings = Settings(user_id=new_user.id, check_interval=60)
|
||||
db.session.add(default_settings)
|
||||
db.session.commit()
|
||||
flash("Rejestracja zakończona. Możesz się zalogować.")
|
||||
flash("Rejestracja zakończona. Możesz się zalogować.", "success")
|
||||
return redirect(url_for('login'))
|
||||
return render_template('register.html')
|
||||
|
||||
@@ -381,10 +691,10 @@ def login():
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user and user.check_password(password):
|
||||
login_user(user)
|
||||
flash("Zalogowano pomyślnie.")
|
||||
flash("Zalogowano pomyślnie.", "success")
|
||||
return redirect(url_for('dashboard'))
|
||||
else:
|
||||
flash("Nieprawidłowa nazwa użytkownika lub hasło.")
|
||||
flash("Nieprawidłowa nazwa użytkownika lub hasło.", "error")
|
||||
return render_template('login.html')
|
||||
|
||||
# Wylogowanie
|
||||
@@ -392,7 +702,7 @@ def login():
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash("Wylogowano.")
|
||||
flash("Wylogowano.", "success")
|
||||
return redirect(url_for('index'))
|
||||
|
||||
# Lista urządzeń użytkownika
|
||||
@@ -426,18 +736,17 @@ def add_device():
|
||||
)
|
||||
db.session.add(new_device)
|
||||
db.session.commit()
|
||||
flash("Urządzenie dodane.")
|
||||
flash("Urządzenie dodane.", "success")
|
||||
return redirect(url_for('devices'))
|
||||
return render_template('add_device.html')
|
||||
|
||||
|
||||
# Szczegóły urządzenia
|
||||
@app.route('/device/<int:device_id>')
|
||||
@login_required
|
||||
def device_detail(device_id):
|
||||
device = Device.query.get_or_404(device_id)
|
||||
if device.user_id != current_user.id:
|
||||
flash("Brak dostępu.")
|
||||
flash("Brak dostępu.", "error")
|
||||
return redirect(url_for('devices'))
|
||||
resource_data = {}
|
||||
try:
|
||||
@@ -462,7 +771,6 @@ def device_detail(device_id):
|
||||
resource_data = {'error': str(e)}
|
||||
return render_template('device_detail.html', device=device, resource=resource_data)
|
||||
|
||||
|
||||
# Strona z logami
|
||||
@app.route('/logs')
|
||||
@login_required
|
||||
@@ -487,33 +795,31 @@ def settings():
|
||||
user_settings.smtp_username = request.form.get('smtp_username')
|
||||
user_settings.smtp_password = request.form.get('smtp_password')
|
||||
user_settings.email_notifications_enabled = bool(request.form.get('email_notifications_enabled'))
|
||||
# Aktualizacja adresu e-mail odbiorcy (może być inny niż email z profilu)
|
||||
user_settings.recipient_email = request.form.get('recipient_email')
|
||||
# Aktualizacja interwału sprawdzania
|
||||
interval = request.form.get('check_interval')
|
||||
user_settings.check_interval = int(interval) if interval else 60
|
||||
# Aktualizacja retencji logów
|
||||
retention = request.form.get('log_retention_days')
|
||||
user_settings.log_retention_days = int(retention) if retention else 30
|
||||
|
||||
db.session.commit()
|
||||
|
||||
# Aktualizacja interwału zadania scheduler'a, jeśli masz takie zadanie
|
||||
try:
|
||||
scheduler.reschedule_job("check_all_devices", trigger="interval", seconds=user_settings.check_interval)
|
||||
app.logger.debug(f"Scheduler rescheduled with new interval: {user_settings.check_interval} seconds")
|
||||
except Exception as e:
|
||||
app.logger.error(f"Error rescheduling job: {e}")
|
||||
|
||||
flash("Ustawienia zapisane.")
|
||||
flash("Ustawienia zapisane.", "success")
|
||||
return redirect(url_for('settings'))
|
||||
return render_template('settings.html', settings=user_settings)
|
||||
|
||||
|
||||
@app.route('/device/<int:device_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_device(device_id):
|
||||
device = Device.query.get_or_404(device_id)
|
||||
if device.user_id != current_user.id:
|
||||
flash("Brak dostępu.")
|
||||
flash("Brak dostępu.", "error")
|
||||
return redirect(url_for('devices'))
|
||||
if request.method == 'POST':
|
||||
device.name = request.form.get('name', device.name)
|
||||
@@ -525,26 +831,26 @@ def edit_device(device_id):
|
||||
device.use_ssl = bool(request.form.get('use_ssl'))
|
||||
device.ssl_insecure = bool(request.form.get('ssl_insecure'))
|
||||
db.session.commit()
|
||||
flash("Urządzenie zaktualizowane.")
|
||||
flash("Urządzenie zaktualizowane.", "success")
|
||||
return redirect(url_for('devices'))
|
||||
return render_template('edit_device.html', device=device)
|
||||
|
||||
|
||||
@app.route('/device/<int:device_id>/force_check')
|
||||
@login_required
|
||||
def force_check(device_id):
|
||||
device = Device.query.get_or_404(device_id)
|
||||
if device.user_id != current_user.id:
|
||||
flash("Brak dostępu.")
|
||||
flash("Brak dostępu.", "error")
|
||||
return redirect(url_for('devices'))
|
||||
result, update_available, current_version, current_firmware = check_device_update(device)
|
||||
result, update_available, current_version, current_firmware, upgrade_firmware = check_device_update(device)
|
||||
device.last_log = result
|
||||
device.last_check = datetime.utcnow()
|
||||
device.update_required = update_available
|
||||
device.current_version = current_version
|
||||
device.current_firmware = current_firmware
|
||||
device.upgrade_firmware = upgrade_firmware
|
||||
db.session.commit()
|
||||
flash("Sprawdzenie urządzenia zakończone.")
|
||||
flash("Sprawdzenie urządzenia zakończone.", "success")
|
||||
return redirect(url_for('devices'))
|
||||
|
||||
@app.route('/device/<int:device_id>/update', methods=['POST'])
|
||||
@@ -552,7 +858,7 @@ def force_check(device_id):
|
||||
def update_device(device_id):
|
||||
device = Device.query.get_or_404(device_id)
|
||||
if device.user_id != current_user.id:
|
||||
flash("Brak dostępu.")
|
||||
flash("Brak dostępu.", "error")
|
||||
return redirect(url_for('devices'))
|
||||
try:
|
||||
app.logger.debug(f"Initiating system update for device {device.ip}")
|
||||
@@ -574,21 +880,28 @@ def update_device(device_id):
|
||||
list(api('/system/package/update/install', branch='beta'))
|
||||
else:
|
||||
list(api('/system/package/update/install'))
|
||||
flash("Aktualizacja systemu została rozpoczęta.")
|
||||
|
||||
history = UpdateHistory(
|
||||
device_id=device.id,
|
||||
update_type="system",
|
||||
details=f"Aktualizacja systemu rozpoczęta na urządzeniu {device.name or device.ip}."
|
||||
)
|
||||
db.session.add(history)
|
||||
db.session.commit()
|
||||
|
||||
flash("Aktualizacja systemu została rozpoczęta.", "success")
|
||||
app.logger.debug("System update command executed successfully")
|
||||
except Exception as e:
|
||||
app.logger.error(f"Błąd podczas aktualizacji urządzenia {device.ip}: {e}", exc_info=True)
|
||||
flash(f"Błąd podczas aktualizacji: {e}")
|
||||
flash(f"Błąd podczas aktualizacji: {e}", "error")
|
||||
return redirect(url_for('device_detail', device_id=device.id))
|
||||
|
||||
|
||||
|
||||
@app.route('/device/<int:device_id>/update_firmware', methods=['POST'])
|
||||
@login_required
|
||||
def update_firmware(device_id):
|
||||
device = Device.query.get_or_404(device_id)
|
||||
if device.user_id != current_user.id:
|
||||
flash("Brak dostępu.")
|
||||
flash("Brak dostępu.", "error")
|
||||
return redirect(url_for('devices'))
|
||||
try:
|
||||
api = librouteros.connect(
|
||||
@@ -600,9 +913,18 @@ def update_firmware(device_id):
|
||||
)
|
||||
# Przykładowa komenda aktualizacji firmware
|
||||
list(api('/system/routerboard/upgrade'))
|
||||
flash("Aktualizacja firmware została rozpoczęta.")
|
||||
|
||||
history = UpdateHistory(
|
||||
device_id=device.id,
|
||||
update_type="firmware",
|
||||
details=f"Aktualizacja firmware rozpoczęta na urządzeniu {device.name or device.ip}."
|
||||
)
|
||||
db.session.add(history)
|
||||
db.session.commit()
|
||||
|
||||
flash("Aktualizacja firmware została rozpoczęta.", "success")
|
||||
except Exception as e:
|
||||
flash(f"Błąd podczas aktualizacji firmware: {e}")
|
||||
flash(f"Błąd podczas aktualizacji firmware: {e}", "error")
|
||||
return redirect(url_for('device_detail', device_id=device.id))
|
||||
|
||||
@app.route('/test_pushover', methods=['POST'])
|
||||
@@ -610,7 +932,7 @@ def update_firmware(device_id):
|
||||
def test_pushover():
|
||||
message = "To jest testowe powiadomienie Pushover z RouterOS Update."
|
||||
send_pushover_notification(current_user, message)
|
||||
flash("Test powiadomienia Pushover wysłany.")
|
||||
flash("Test powiadomienia Pushover wysłany.", "success")
|
||||
return redirect(url_for('settings'))
|
||||
|
||||
@app.route('/test_email', methods=['POST'])
|
||||
@@ -619,30 +941,143 @@ def test_email():
|
||||
subject = "Testowy E-mail z RouterOS Update"
|
||||
message = "To jest testowa wiadomość e-mail wysłana z RouterOS Update."
|
||||
send_email_notification(current_user, subject, message)
|
||||
flash("Testowy e-mail wysłany.")
|
||||
flash("Testowy e-mail wysłany.", "success")
|
||||
return redirect(url_for('settings'))
|
||||
|
||||
@app.route('/reset_password', methods=['GET', 'POST'])
|
||||
@app.route('/change_password', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def reset_password():
|
||||
def change_password():
|
||||
if request.method == 'POST':
|
||||
old_password = request.form.get('old_password')
|
||||
new_password = request.form.get('new_password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
if not current_user.check_password(old_password):
|
||||
flash("Stare hasło jest nieprawidłowe.")
|
||||
flash("Stare hasło jest nieprawidłowe.", "error")
|
||||
return redirect(url_for('reset_password'))
|
||||
if new_password != confirm_password:
|
||||
flash("Nowe hasło i potwierdzenie nie są zgodne.")
|
||||
flash("Nowe hasło i potwierdzenie nie są zgodne.", "warning")
|
||||
return redirect(url_for('reset_password'))
|
||||
current_user.set_password(new_password)
|
||||
db.session.commit()
|
||||
flash("Hasło zostało zresetowane.")
|
||||
flash("Hasło zostało zresetowane.", "success")
|
||||
return redirect(url_for('reset_password'))
|
||||
return render_template('reset_password.html')
|
||||
return render_template('change_password.html')
|
||||
|
||||
@app.route('/logs/clean', methods=['POST'])
|
||||
@login_required
|
||||
def clean_logs():
|
||||
days = request.form.get('days')
|
||||
if not days:
|
||||
flash("Podaj liczbę dni.", "warning")
|
||||
return redirect(url_for('logs'))
|
||||
try:
|
||||
days = int(days)
|
||||
except ValueError:
|
||||
flash("Niepoprawna wartość dni.", "warning")
|
||||
return redirect(url_for('logs'))
|
||||
cutoff = datetime.utcnow() - timedelta(days=days)
|
||||
num_deleted = Log.query.filter(Log.user_id == current_user.id, Log.timestamp < cutoff).delete()
|
||||
db.session.commit()
|
||||
flash(f"Usunięto {num_deleted} logów starszych niż {days} dni.", "success")
|
||||
return redirect(url_for('logs'))
|
||||
|
||||
@app.route('/update_history')
|
||||
@login_required
|
||||
def update_history():
|
||||
histories = UpdateHistory.query.join(Device).filter(Device.user_id == current_user.id).order_by(UpdateHistory.timestamp.desc()).all()
|
||||
return render_template('update_history.html', histories=histories)
|
||||
|
||||
@app.route('/anomalies')
|
||||
@login_required
|
||||
def anomalies():
|
||||
anomalies = Anomaly.query.join(Device).filter(Device.user_id == current_user.id).order_by(Anomaly.timestamp.desc()).all()
|
||||
return render_template('anomalies.html', anomalies=anomalies)
|
||||
|
||||
@app.route('/devices/update_selected', methods=['POST'])
|
||||
@login_required
|
||||
def update_selected_devices():
|
||||
selected_ids = request.form.getlist('selected_devices')
|
||||
if not selected_ids:
|
||||
flash("Nie wybrano żadnych urządzeń.", "error")
|
||||
return redirect(url_for('devices'))
|
||||
for device_id in selected_ids:
|
||||
device = Device.query.get(device_id)
|
||||
if device and device.user_id == current_user.id:
|
||||
result, update_available, current_version, current_firmware, upgrade_firmware = check_device_update(device)
|
||||
device.last_log = result
|
||||
device.last_check = datetime.utcnow()
|
||||
device.update_required = update_available
|
||||
device.current_version = current_version
|
||||
device.current_firmware = current_firmware
|
||||
device.upgrade_firmware = upgrade_firmware
|
||||
db.session.commit()
|
||||
log_entry = Log(message=result, device_id=device.id, user_id=device.user_id)
|
||||
db.session.add(log_entry)
|
||||
db.session.commit()
|
||||
flash("Wybrane urządzenia zostały zaktualizowane.", "success")
|
||||
return redirect(url_for('devices'))
|
||||
|
||||
@app.route('/routeros_changelog')
|
||||
@login_required
|
||||
def routeros_changelog():
|
||||
channel = request.args.get('channel', 'stable') # "stable", "rc" lub "beta"
|
||||
series = request.args.get('series', '7.x') # "7.x" lub "6.x"
|
||||
selected_version = request.args.get('version')
|
||||
|
||||
# Pobieramy wszystkie wpisy dla danego kanału i serii, posortowane malejąco wg daty
|
||||
entries = ChangelogEntry.query.filter_by(release_type=channel, category=series).order_by(ChangelogEntry.timestamp.desc()).all()
|
||||
|
||||
if selected_version:
|
||||
selected_entry = ChangelogEntry.query.filter_by(version=selected_version, release_type=channel, category=series).first()
|
||||
else:
|
||||
selected_entry = entries[0] if entries else None
|
||||
|
||||
return render_template(
|
||||
'routeros_changelog_tabs.html',
|
||||
channel=channel,
|
||||
series=series,
|
||||
entries=entries,
|
||||
selected_entry=selected_entry
|
||||
)
|
||||
|
||||
@app.route('/force_fetch_changelogs')
|
||||
@login_required
|
||||
def force_fetch_changelogs():
|
||||
with app.app_context():
|
||||
# Usuwamy wszystkie stare wpisy
|
||||
db.session.query(ChangelogEntry).delete()
|
||||
db.session.commit()
|
||||
|
||||
# Pobieramy changelogi od nowa
|
||||
fetch_changelogs(force=True)
|
||||
|
||||
flash("Changelog został całkowicie odświeżony.", "success")
|
||||
return redirect(url_for('routeros_changelog'))
|
||||
|
||||
@app.route('/device/<int:device_id>/restart', methods=['POST'])
|
||||
@login_required
|
||||
def restart_device(device_id):
|
||||
device = Device.query.get_or_404(device_id)
|
||||
if device.user_id != current_user.id:
|
||||
flash("Brak dostępu.", "error")
|
||||
return redirect(url_for('devices'))
|
||||
try:
|
||||
api = librouteros.connect(
|
||||
host=device.ip,
|
||||
username=device.device_username,
|
||||
password=device.device_password,
|
||||
port=device.port,
|
||||
timeout=15
|
||||
)
|
||||
# Wysyłamy komendę reboot
|
||||
list(api('/system/reboot'))
|
||||
|
||||
flash("Komenda reboot została wysłana.", "success")
|
||||
except Exception as e:
|
||||
flash(f"Błąd podczas wysyłania komendy reboot: {e}", "error")
|
||||
return ('', 204) # Zwracamy odpowiedź bez treści dla żądania AJAX
|
||||
|
||||
# Zamknięcie harmonogramu przy zatrzymaniu aplikacji
|
||||
|
||||
atexit.register(lambda: scheduler.shutdown())
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
13
docker-compose.yml
Normal file
13
docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: routeros_update
|
||||
ports:
|
||||
- "5582:5582"
|
||||
environment:
|
||||
- FLASK_ENV=production
|
||||
restart: unless-stopped
|
@@ -4,5 +4,5 @@ Flask-Login
|
||||
APScheduler
|
||||
librouteros
|
||||
requests
|
||||
gunicorn
|
||||
requests
|
||||
waitress
|
||||
bs4
|
14
routeros_update.service
Normal file
14
routeros_update.service
Normal file
@@ -0,0 +1,14 @@
|
||||
[Unit]
|
||||
Description=RouterOS Update Waitress Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
#User=routeros
|
||||
#Group=routeros
|
||||
WorkingDirectory=/opt/routeros_update
|
||||
ExecStart=/opt/routeros_update/venv/bin/python3 /opt/routeros_update/run_waitress.py
|
||||
Restart=always
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
9
run_waitress.py
Normal file
9
run_waitress.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from waitress import serve
|
||||
from app import app, scheduler, clean_old_logs
|
||||
import atexit
|
||||
|
||||
with app.app_context():
|
||||
scheduler.add_job(func=clean_old_logs, trigger="interval", days=1)
|
||||
atexit.register(lambda: scheduler.shutdown())
|
||||
|
||||
serve(app, host='0.0.0.0', port=5582, ident='', threads=4)
|
21
start.sh
Executable file
21
start.sh
Executable 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!"
|
@@ -1,39 +1,90 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Dodaj urządzenie - RouterOS Update{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Styl karty w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border-color: #444;
|
||||
}
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2>Dodaj nowe urządzenie</h2>
|
||||
|
||||
<!-- Karta z nagłówkiem i formularzem -->
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">Dodaj nowe urządzenie</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<!-- Pozostałe pola: name, ip, port, username, password -->
|
||||
<!-- Pole nazwy urządzenia -->
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nazwa urządzenia</label>
|
||||
<input type="text" class="form-control" name="name" id="name" required
|
||||
placeholder="np. Mikrotik w biurze">
|
||||
<small class="text-muted">Wpisz przyjazną nazwę, np. „Mikrotik #1” lub „Router na u Kocura.</small>
|
||||
</div>
|
||||
|
||||
<!-- Adres IP -->
|
||||
<div class="mb-3">
|
||||
<label for="ip" class="form-label">Adres IP</label>
|
||||
<input type="text" class="form-control" name="ip" id="ip" required>
|
||||
<input type="text" class="form-control" name="ip" id="ip" required
|
||||
placeholder="np. 192.168.88.1">
|
||||
<small class="text-muted">Adres IP lub domena urządzenia (np. mikrotik.moja-domena.pl).</small>
|
||||
</div>
|
||||
|
||||
<!-- Port -->
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input type="number" class="form-control" name="port" id="port" value="8728" required>
|
||||
<small class="text-muted">Domyślnie 8728 (lub 8729 w przypadku SSL).</small>
|
||||
</div>
|
||||
|
||||
<!-- Nazwa użytkownika -->
|
||||
<div class="mb-3">
|
||||
<label for="device_username" class="form-label">Nazwa użytkownika urządzenia</label>
|
||||
<input type="text" class="form-control" name="device_username" id="device_username" required>
|
||||
<input type="text" class="form-control" name="device_username" id="device_username" required
|
||||
placeholder="np. admin">
|
||||
</div>
|
||||
|
||||
<!-- Hasło -->
|
||||
<div class="mb-3">
|
||||
<label for="device_password" class="form-label">Hasło urządzenia</label>
|
||||
<input type="password" class="form-control" name="device_password" id="device_password" required>
|
||||
</div>
|
||||
|
||||
<!-- Opcja SSL -->
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="use_ssl" id="use_ssl">
|
||||
<label class="form-check-label" for="use_ssl">Używaj SSL</label>
|
||||
</div>
|
||||
<!-- Opcja nie weryfikowania certyfikatu SSL -->
|
||||
|
||||
<!-- Opcja braku weryfikacji certyfikatu SSL -->
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="ssl_insecure" id="ssl_insecure">
|
||||
<label class="form-check-label" for="ssl_insecure">Nie weryfikuj certyfikatu SSL</label>
|
||||
<label class="form-check-label" for="ssl_insecure">
|
||||
Nie weryfikuj certyfikatu SSL
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Przycisk dodania urządzenia -->
|
||||
<button type="submit" class="btn btn-primary">Dodaj urządzenie</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
80
templates/anomalies.html
Normal file
80
templates/anomalies.html
Normal file
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Wykryte anomalie{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Karta w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border-color: #444;
|
||||
}
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
/* Tabela w trybie ciemnym */
|
||||
body.dark-mode .table thead {
|
||||
background-color: #2a2a2a;
|
||||
color: #ccc;
|
||||
}
|
||||
body.dark-mode .table-bordered > :not(caption) > * > * {
|
||||
border-color: #444 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">Wykryte anomalie</h4>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if anomalies and anomalies|length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="white-space: nowrap;">Data</th>
|
||||
<th style="white-space: nowrap;">Urządzenie</th>
|
||||
<th>Opis</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for anomaly in anomalies %}
|
||||
<tr>
|
||||
<td style="white-space: nowrap;">
|
||||
{{ anomaly.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||
</td>
|
||||
<td>
|
||||
{% if anomaly.device %}
|
||||
{{ anomaly.device.name or anomaly.device.ip }}
|
||||
{% else %}
|
||||
<span class="text-muted">Brak</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ anomaly.description }}</td>
|
||||
<td>
|
||||
{% if anomaly.resolved %}
|
||||
<span class="badge bg-success">Rozwiązana</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Otwarta</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-3">
|
||||
<p class="mb-0">Brak aktualnych anomalii.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -6,60 +6,299 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
/* ========== Tryb ciemny (dark-mode) ========== */
|
||||
body.dark-mode {
|
||||
background-color: #121212;
|
||||
color: #cccccc;
|
||||
}
|
||||
/* --- Nawigacja (Navbar) --- */
|
||||
body.dark-mode .navbar {
|
||||
background-color: #333 !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
body.dark-mode .navbar .navbar-brand,
|
||||
body.dark-mode .navbar .nav-link {
|
||||
color: #fff !important;
|
||||
}
|
||||
/* --- Dropdown --- */
|
||||
body.dark-mode .dropdown-menu {
|
||||
background-color: #262626;
|
||||
color: #cccccc;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
body.dark-mode .dropdown-menu a {
|
||||
color: #cccccc !important;
|
||||
}
|
||||
body.dark-mode .dropdown-menu a:hover {
|
||||
background-color: #333333;
|
||||
}
|
||||
/* --- Przyciski --- */
|
||||
body.dark-mode .btn-outline-light {
|
||||
color: #ffffff;
|
||||
border-color: #ffffff;
|
||||
}
|
||||
body.dark-mode .btn-outline-light:hover {
|
||||
color: #ffffff;
|
||||
background-color: #333333;
|
||||
border-color: #333333;
|
||||
}
|
||||
/* Stopka */
|
||||
.dark-mode footer {
|
||||
background-color: #1e1e1e !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
footer {
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
/* Alerty – pozostają bez zmian */
|
||||
.diff-add { color: green; }
|
||||
.diff-rem { color: red; }
|
||||
|
||||
body.dark-mode table {
|
||||
background-color: #1a1a1a;
|
||||
color: #cccccc;
|
||||
}
|
||||
body.dark-mode table thead {
|
||||
background-color: #333333;
|
||||
}
|
||||
body.dark-mode table td {
|
||||
background-color: #1a1a1a !important;
|
||||
color: #cccccc !important;
|
||||
}
|
||||
/* --- Elementy formularzy --- */
|
||||
body.dark-mode input,
|
||||
body.dark-mode textarea,
|
||||
body.dark-mode select,
|
||||
body.dark-mode table input,
|
||||
body.dark-mode table textarea,
|
||||
body.dark-mode table select {
|
||||
background-color: #1a1a1a;
|
||||
color: #cccccc;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
body.dark-mode input:focus,
|
||||
body.dark-mode textarea:focus,
|
||||
body.dark-mode select:focus,
|
||||
body.dark-mode table input:focus,
|
||||
body.dark-mode table textarea:focus,
|
||||
body.dark-mode table select:focus {
|
||||
background-color: #1a1a1a;
|
||||
color: #cccccc;
|
||||
border-color: #777;
|
||||
}
|
||||
/* --- DataTables --- */
|
||||
body.dark-mode .dataTable-wrapper input,
|
||||
body.dark-mode .dataTable-wrapper select,
|
||||
body.dark-mode .dataTable-wrapper .dataTable-info,
|
||||
body.dark-mode .dataTable-wrapper .dataTable-pagination {
|
||||
background-color: #1a1a1a;
|
||||
color: #cccccc;
|
||||
border-color: #555;
|
||||
}
|
||||
body.dark-mode .dataTable-wrapper .dataTable-pagination a {
|
||||
color: #cccccc !important;
|
||||
background-color: #1a1a1a;
|
||||
border-color: #555;
|
||||
}
|
||||
body.dark-mode .dataTable-wrapper .dataTable-pagination a.active {
|
||||
background-color: #333333;
|
||||
border-color: #333333;
|
||||
}
|
||||
/* --- Inne elementy --- */
|
||||
body.dark-mode ::placeholder {
|
||||
color: #cccccc !important;
|
||||
opacity: 1;
|
||||
}
|
||||
body.dark-mode .breadcrumb-item.active,
|
||||
body.dark-mode .text-muted {
|
||||
color: #cccccc !important;
|
||||
}
|
||||
body.dark-mode .card-header.bg-light {
|
||||
background-color: #333333 !important;
|
||||
color: #cccccc !important;
|
||||
}
|
||||
body.dark-mode .list-group.list-group-flush .list-group-item {
|
||||
background-color: #141414 !important;
|
||||
color: #cccccc;
|
||||
border-bottom: 1px solid #333333;
|
||||
}
|
||||
|
||||
/* ========== Tryb jasny (light-mode) ========== */
|
||||
body.light-mode .navbar {
|
||||
background-color: #dcdcdc !important;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
body.light-mode .navbar .btn-outline-primary:hover {
|
||||
background-color: #007bff;
|
||||
color: #ffffff;
|
||||
}
|
||||
footer {
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
body.light-mode .navbar .nav-link {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== Modal w trybie ciemny, ========== */
|
||||
.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);
|
||||
}
|
||||
</style>
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<nav class="navbar navbar-expand-lg">
|
||||
<div class="container-fluid">
|
||||
|
||||
<a href="{{ url_for('index') }}" class="navbar-brand">RouterOS Update</a>
|
||||
<div>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarButtons" aria-controls="navbarButtons" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent"
|
||||
aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarButtons">
|
||||
<div class="ms-auto">
|
||||
<div class="collapse navbar-collapse" id="navbarContent">
|
||||
{% if current_user.is_authenticated %}
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('dashboard') }}'">Dashboard</button>
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('devices') }}'">Urządzenia</button>
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('logs') }}'">Logi</button>
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('settings') }}'">Ustawienia</button>
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('reset_password') }}'">Reset hasła</button>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('logout') }}'">Wyloguj</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="btn-group me-2" role="group">
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('login') }}'">Logowanie</button>
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('register') }}'">Rejestracja</button>
|
||||
</div>
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('dashboard') }}">Dashboard</a>
|
||||
</li>
|
||||
<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="{{ url_for('devices') }}">Lista</a></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('add_device') }}">Dodaj nowe</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('logs') }}">Logi</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('routeros_changelog') }}">RouterOS Changelog</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('update_history') }}">Historia aktualizacji</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('anomalies') }}">Anomalie</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('settings') }}">Ustawienia</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
<ul class="navbar-nav ms-auto align-items-center">
|
||||
<li class="nav-item me-2">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="darkModeToggle">
|
||||
<label class="form-check-label" for="darkModeToggle">Tryb ciemny</label>
|
||||
</div>
|
||||
</li>
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link btn btn-outline-light ms-2" href="{{ url_for('change_password') }}">Zmień hasło</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link btn btn-danger ms-2" href="{{ url_for('logout') }}">Wyloguj</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link btn btn-outline-primary ms-2" href="{{ url_for('login') }}">Zaloguj</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link btn btn-primary ms-2" href="{{ url_for('register') }}">Zarejestruj</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container my-4 flex-fill">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
<!-- Kontener do wyświetlania komunikatów flash -->
|
||||
<div class="container mt-3">
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
<div class="alert alert-info">
|
||||
{% for message in messages %}
|
||||
<div>{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% for category, message in messages %}
|
||||
<!-- Zmapuj do stylu Bootstrapa -->
|
||||
{% set bs_cat = bootstrap_alert_category(category) %}
|
||||
<div class="alert alert-{{ bs_cat }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<main class="container my-4 flex-fill">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer class="footer bg-light py-3 mt-auto">
|
||||
<footer class="footer py-3 mt-auto">
|
||||
<div class="container text-center">
|
||||
<span class="text-muted">© 2025 RouterOS Update</span>
|
||||
<span class="text-muted">© 2025 Mateusz Gruszczyński, linuxiarz.pl</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<!-- Bootstrap JS (bundle z Popper) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||
|
||||
function updatePrismTheme() {
|
||||
const prismLink = document.getElementById('prism-style');
|
||||
if (prismLink) {
|
||||
if (localStorage.getItem("darkMode") === "enabled") {
|
||||
prismLink.setAttribute('href', 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css');
|
||||
} else {
|
||||
prismLink.setAttribute('href', 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-coy.min.css');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTheme() {
|
||||
if (localStorage.getItem("darkMode") === "enabled") {
|
||||
document.body.classList.add("dark-mode");
|
||||
document.body.classList.remove("light-mode");
|
||||
darkModeToggle.checked = true;
|
||||
} else {
|
||||
document.body.classList.add("light-mode");
|
||||
document.body.classList.remove("dark-mode");
|
||||
darkModeToggle.checked = false;
|
||||
}
|
||||
updatePrismTheme();
|
||||
}
|
||||
|
||||
darkModeToggle.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
localStorage.setItem("darkMode", "enabled");
|
||||
} else {
|
||||
localStorage.setItem("darkMode", "disabled");
|
||||
}
|
||||
updateTheme();
|
||||
});
|
||||
|
||||
updateTheme();
|
||||
});
|
||||
</script>
|
||||
{% block extra_scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
71
templates/change_password.html
Normal file
71
templates/change_password.html
Normal file
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Zmiana hasła - RouterOS Update{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Karta w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border-color: #444;
|
||||
}
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">Zmiana hasła</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="old_password" class="form-label">Stare hasło</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
name="old_password"
|
||||
id="old_password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label">Nowe hasło</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
name="new_password"
|
||||
id="new_password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label">Potwierdź nowe hasło</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
name="confirm_password"
|
||||
id="confirm_password"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Zresetuj hasło</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@@ -3,7 +3,10 @@
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h2 class="mb-4">Dashboard</h2>
|
||||
|
||||
<!-- Pierwszy wiersz z podstawowymi statystykami -->
|
||||
<div class="row">
|
||||
<!-- Karta z liczbą urządzeń -->
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-primary mb-3">
|
||||
<div class="card-body">
|
||||
@@ -12,6 +15,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Karta z urządzeniami wymagającymi aktualizacji -->
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-danger mb-3">
|
||||
<div class="card-body">
|
||||
@@ -20,6 +24,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Karta z liczbą logów -->
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-success mb-3">
|
||||
<div class="card-body">
|
||||
@@ -28,15 +33,76 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Karta z wykonanymi aktualizacjami -->
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-info mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Wykonane aktualizacje</h5>
|
||||
<p class="card-text display-6">{{ update_history_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nowy wiersz z najnowszymi wersjami systemu -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card text-white bg-secondary mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Użytkownicy</h5>
|
||||
<p class="card-text display-6">{{ users_count }}</p>
|
||||
<h5 class="card-title">Najnowsza wersja RouterOS 7.x (Stable)</h5>
|
||||
{% if latest_version_7 %}
|
||||
<p class="card-text display-6">{{ latest_version_7.version | format_version }}</p>
|
||||
<p><small>{{ latest_version_7.timestamp.strftime('%Y-%b-%d') }}</small></p>
|
||||
<a href="{{ url_for('routeros_changelog', channel=latest_version_7.release_type, series='7.x', version=latest_version_7.version) }}" class="btn btn-light btn-sm">Czytaj changelog</a>
|
||||
{% else %}
|
||||
<p>Brak danych</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card text-white bg-secondary mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Najnowsza wersja RouterOS 6.x (Stable)</h5>
|
||||
{% if latest_version_6 %}
|
||||
<p class="card-text display-6">{{ latest_version_6.version | format_version }}</p>
|
||||
<p><small>{{ latest_version_6.timestamp.strftime('%Y-%b-%d') }}</small></p>
|
||||
<a href="{{ url_for('routeros_changelog', channel=latest_version_6.release_type, series='6.x', version=latest_version_6.version) }}" class="btn btn-light btn-sm">Czytaj changelog</a>
|
||||
{% else %}
|
||||
<p>Brak danych</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Możesz dodać dodatkowe statystyki lub wykresy poniżej -->
|
||||
|
||||
<!-- Sekcja Ostatnie zdarzenia -->
|
||||
<div class="card mb-3 border-0">
|
||||
<div class="card-header bg-light py-2">
|
||||
<h6 class="mb-0">Ostatnie zdarzenia</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% if recent_logs %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for log in recent_logs %}
|
||||
<li class="list-group-item">
|
||||
<!-- Data w nieco mniejszej czcionce, np. "small text-muted" -->
|
||||
<div class="small text-muted">{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</div>
|
||||
<!-- Treść zdarzenia; "truncate(100)" żeby skrócić tekst -->
|
||||
<div class="mt-1">
|
||||
{{ log.message|truncate(100) }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="p-3">
|
||||
Brak ostatnich zdarzeń.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,8 +1,81 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Szczegóły urządzenia - RouterOS Update{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* ========================
|
||||
Tryb ciemny i styl kart
|
||||
======================== */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
border-color: #444;
|
||||
}
|
||||
body.dark-mode .card .card-header.bg-primary,
|
||||
body.dark-mode .card .card-header.bg-secondary {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
/* Blok logów – przewijalny, zachowanie białych znaków,
|
||||
jasne/dark tło zgodnie z kartą */
|
||||
.log-block {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
padding: 1rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Overlays (system-update / reboot) */
|
||||
#system-update-overlay,
|
||||
#reboot-progress-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 1000;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding-top: 200px;
|
||||
}
|
||||
|
||||
#system-update-overlay h3,
|
||||
#reboot-progress-overlay h3 {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
width: 50%;
|
||||
margin: 20px auto;
|
||||
background: #444;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-bar {
|
||||
width: 0%;
|
||||
height: 30px;
|
||||
background: #4caf50;
|
||||
border-radius: 5px;
|
||||
transition: width 0.5s linear;
|
||||
}
|
||||
|
||||
/* Div do firmware restart (informacja i przycisk) */
|
||||
#firmware-restart-div {
|
||||
display: none;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="my-4">
|
||||
<div class="container my-4">
|
||||
|
||||
<!-- Nagłówek strony z nawigacją breadcrumb -->
|
||||
<h2 class="mb-3">Szczegóły urządzenia</h2>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
@@ -10,38 +83,35 @@
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ device.ip }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Zakładki dla danych urządzenia i informacji o systemie -->
|
||||
<ul class="nav nav-tabs mb-3" id="deviceTab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="device-data-tab" data-bs-toggle="tab" data-bs-target="#device-data" type="button" role="tab" aria-controls="device-data" aria-selected="true">Dane urządzenia</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="system-info-tab" data-bs-toggle="tab" data-bs-target="#system-info" type="button" role="tab" aria-controls="system-info" aria-selected="false">Informacje o systemie</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="deviceTabContent">
|
||||
<!-- Dane urządzenia -->
|
||||
<div class="tab-pane fade show active" id="device-data" role="tabpanel" aria-labelledby="device-data-tab">
|
||||
<div class="card mb-3">
|
||||
<!-- Dwukolumnowy układ: kolumna z danymi urządzenia, kolumna z info systemu -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow mb-3">
|
||||
<div class="card-header bg-primary text-white">
|
||||
Dane urządzenia
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Adres IP:</strong> {{ device.ip }}</p>
|
||||
<p><strong>Port:</strong> {{ device.port }}</p>
|
||||
<p><strong>Ostatnie sprawdzenie:</strong>
|
||||
{% if device.last_check %}{{ device.last_check.strftime('%Y-%m-%d %H:%M:%S') }}{% else %}Brak{% endif %}
|
||||
<p>
|
||||
<strong>Ostatnie sprawdzenie:</strong>
|
||||
{% if device.last_check %}
|
||||
{{ device.last_check.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||
{% else %}
|
||||
Brak
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>System:</strong> {{ device.current_version or 'Brak' }}<br>
|
||||
<strong>Firmware:</strong> {{ device.current_firmware or 'N/A' }}
|
||||
<strong>Firmware:</strong> {{ device.current_firmware or 'N/A' }}<br>
|
||||
<strong>Upgrade Firmware:</strong> {{ device.upgrade_firmware or 'N/A' }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Branch aktualizacji:</strong> {{ device.branch|capitalize }}
|
||||
</p>
|
||||
<!-- Formularz zmiany branch -->
|
||||
|
||||
<!-- Formularz szybkiej zmiany branch -->
|
||||
<form method="POST" action="{{ url_for('edit_device', device_id=device.id) }}" class="mt-3">
|
||||
<div class="input-group">
|
||||
<select class="form-select" name="branch">
|
||||
@@ -49,16 +119,16 @@
|
||||
<option value="dev" {% if device.branch == 'dev' %}selected{% endif %}>Dev</option>
|
||||
<option value="beta" {% if device.branch == 'beta' %}selected{% endif %}>Beta</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary ms-2">Zmień branch</button>
|
||||
<button type="submit" class="btn btn-primary">Zmień branch</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Informacje o systemie -->
|
||||
<div class="tab-pane fade" id="system-info" role="tabpanel" aria-labelledby="system-info-tab">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-info text-white">
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow mb-3">
|
||||
<div class="card-header bg-primary text-white">
|
||||
Informacje o systemie
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -68,8 +138,14 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<p><strong>Wersja systemu:</strong> {{ resource.version or 'Brak danych' }}</p>
|
||||
<p><strong>Czas pracy:</strong> {{ resource.uptime or 'Brak danych' }}</p>
|
||||
<p><strong>Obciążenie CPU:</strong> {{ resource['cpu-load'] or 'Brak' }}%</p>
|
||||
<p><strong>Czas pracy (uptime):</strong> {{ resource.uptime or 'Brak danych' }}</p>
|
||||
<p><strong>Obciążenie CPU:</strong>
|
||||
{% if resource['cpu-load'] is defined %}
|
||||
{{ resource['cpu-load'] }}%
|
||||
{% else %}
|
||||
Brak danych
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
<strong>Pamięć:</strong>
|
||||
{% if resource['free-memory'] and resource['total-memory'] %}
|
||||
@@ -78,43 +154,154 @@
|
||||
Brak danych
|
||||
{% endif %}
|
||||
</p>
|
||||
<p><strong>Wolne miejsce na dysku:</strong> {{ resource['free-hdd-space'] or 'Brak danych' }}</p>
|
||||
<p>
|
||||
<strong>Wolne miejsce na dysku:</strong>
|
||||
{% if resource['free-hdd-space'] %}
|
||||
{{ resource['free-hdd-space'] }}
|
||||
{% else %}
|
||||
Brak danych
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Logi urządzenia w formie accordion -->
|
||||
<div class="accordion mb-3" id="logsAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="headingLogs">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLogs" aria-expanded="false" aria-controls="collapseLogs">
|
||||
Logi urządzenia
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseLogs" class="accordion-collapse collapse" aria-labelledby="headingLogs" data-bs-parent="#logsAccordion">
|
||||
<div class="accordion-body">
|
||||
<pre class="bg-light p-3" style="white-space: pre-wrap;">{{ device.last_log or 'Brak logów' }}</pre>
|
||||
</div>
|
||||
<!-- Logi urządzenia -->
|
||||
<div class="card border-0 shadow mb-3">
|
||||
<div class="card-header bg-secondary text-white">Logi urządzenia</div>
|
||||
<div class="card-body">
|
||||
{% if device.last_log %}
|
||||
<div class="log-block">
|
||||
{{ device.last_log }}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Brak logów</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Akcje urządzenia -->
|
||||
<div class="mb-4">
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<form method="POST" action="{{ url_for('update_device', device_id=device.id) }}">
|
||||
<!-- Akcje urządzenia: aktualizacje, wymuszenie sprawdzenia -->
|
||||
<div class="mb-4 d-flex flex-wrap gap-2">
|
||||
<!-- Aktualizacja systemu -->
|
||||
<form id="system-update-form" method="POST" action="{{ url_for('update_device', device_id=device.id) }}">
|
||||
<button type="submit" class="btn btn-warning">Aktualizuj system</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('update_firmware', device_id=device.id) }}">
|
||||
<!-- Aktualizacja firmware -->
|
||||
<form id="firmware-update-form" method="POST" action="{{ url_for('update_firmware', device_id=device.id) }}">
|
||||
<button type="submit" class="btn btn-danger">Aktualizuj firmware</button>
|
||||
</form>
|
||||
<!-- Wymuszenie sprawdzenia -->
|
||||
<a href="{{ url_for('force_check', device_id=device.id) }}" class="btn btn-secondary">Wymuś sprawdzenie</a>
|
||||
<!-- Link powrotny do listy -->
|
||||
<a href="{{ url_for('devices') }}" class="btn btn-outline-secondary ms-auto">Powrót</a>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<a href="{{ url_for('devices') }}" class="btn btn-outline-secondary">Powrót do listy urządzeń</a>
|
||||
|
||||
<!-- Miejsce na komunikat o konieczności rebootu po firmware update -->
|
||||
<div id="firmware-restart-div">
|
||||
<div class="alert alert-warning mb-3">
|
||||
Urządzenie wymaga restartu po aktualizacji firmware.
|
||||
</div>
|
||||
<button id="restart-device-btn" class="btn btn-danger">Restart urządzenia</button>
|
||||
</div>
|
||||
|
||||
<!-- Overlay: system update (4 min. można dać 5 min) -->
|
||||
<div id="system-update-overlay">
|
||||
<h3>Aktualizacja systemu rozpoczęta...</h3>
|
||||
<div class="progress-container">
|
||||
<div id="system-update-progress" class="progress-bar"></div>
|
||||
</div>
|
||||
<p id="system-update-timer">240 sekund</p>
|
||||
</div>
|
||||
|
||||
<!-- Overlay: reboot po restarcie (2 min) -->
|
||||
<div id="reboot-progress-overlay">
|
||||
<h3>Restart urządzenia...</h3>
|
||||
<div class="progress-container">
|
||||
<div id="reboot-progress-bar" class="progress-bar"></div>
|
||||
</div>
|
||||
<p id="reboot-timer">120 sekund</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ========================
|
||||
// Obsługa aktualizacji systemu
|
||||
// ========================
|
||||
document.getElementById('system-update-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault(); // powstrzymujemy normalne przeładowanie
|
||||
// Pokaż overlay postępu
|
||||
var overlay = document.getElementById('system-update-overlay');
|
||||
overlay.style.display = 'block';
|
||||
|
||||
var timeLeft = 240; // 4 min
|
||||
var progressBar = document.getElementById('system-update-progress');
|
||||
var timerDisplay = document.getElementById('system-update-timer');
|
||||
|
||||
var interval = setInterval(function(){
|
||||
timeLeft--;
|
||||
var percent = ((240 - timeLeft) / 240) * 100;
|
||||
progressBar.style.width = percent + '%';
|
||||
timerDisplay.textContent = timeLeft + ' sekund';
|
||||
if(timeLeft <= 0){
|
||||
clearInterval(interval);
|
||||
// Po zakończeniu: fetch do force_check, potem reload
|
||||
fetch('{{ url_for("force_check", device_id=device.id) }}', { method: 'GET' })
|
||||
.then(()=> location.reload())
|
||||
.catch((err)=> console.error('Błąd force check:', err));
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Wyślij POST do update
|
||||
fetch(this.action, { method: 'POST' })
|
||||
.catch(err => console.error('Błąd aktualizacji systemu:', err));
|
||||
});
|
||||
|
||||
// ========================
|
||||
// Obsługa aktualizacji firmware
|
||||
// ========================
|
||||
document.getElementById('firmware-update-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
fetch(this.action, { method: 'POST' })
|
||||
.catch(err => console.error('Błąd aktualizacji firmware:', err));
|
||||
// Pokazujemy div z przyciskiem do restartu
|
||||
document.getElementById('firmware-restart-div').style.display = 'block';
|
||||
});
|
||||
|
||||
// ========================
|
||||
// Obsługa reboot
|
||||
// ========================
|
||||
document.getElementById('restart-device-btn').addEventListener('click', function(){
|
||||
fetch('{{ url_for("restart_device", device_id=device.id) }}', { method: 'POST' })
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
alert('Komenda reboot wysłana.');
|
||||
// Pokaż overlay z 2 min postępu
|
||||
var overlay = document.getElementById('reboot-progress-overlay');
|
||||
overlay.style.display = 'block';
|
||||
|
||||
var timeLeft = 120; // 2 min
|
||||
var progressBar = document.getElementById('reboot-progress-bar');
|
||||
var timerDisplay = document.getElementById('reboot-timer');
|
||||
var interval = setInterval(function(){
|
||||
timeLeft--;
|
||||
var percent = ((120 - timeLeft) / 120) * 100;
|
||||
progressBar.style.width = percent + '%';
|
||||
timerDisplay.textContent = timeLeft + ' sekund';
|
||||
if(timeLeft <= 0){
|
||||
clearInterval(interval);
|
||||
// Po zakończeniu: fetch do force_check
|
||||
fetch('{{ url_for("force_check", device_id=device.id) }}', { method: 'GET' })
|
||||
.then(()=> location.reload())
|
||||
.catch(err => console.error('Błąd force check:', err));
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
alert('Błąd podczas wysyłania komendy reboot.');
|
||||
}
|
||||
})
|
||||
.catch(err => alert('Błąd: ' + err));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -1,11 +1,103 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Moje urządzenia - RouterOS Update{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Karta w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
/* Nagłówek karty w trybie ciemnym */
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
/* Tabela w trybie ciemnym */
|
||||
body.dark-mode .table thead.table-dark {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #ccc !important;
|
||||
border-color: #444 !important;
|
||||
}
|
||||
body.dark-mode .table-hover tbody tr:hover {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
body.dark-mode .table-bordered > :not(caption) > * > * {
|
||||
border-color: #444 !important;
|
||||
}
|
||||
|
||||
/* Styl paska postępu w overlay */
|
||||
.progress-container {
|
||||
width: 50%;
|
||||
margin: 20px auto;
|
||||
background: #444;
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-bar {
|
||||
width: 0%;
|
||||
height: 30px;
|
||||
background: #4caf50;
|
||||
border-radius: 5px;
|
||||
transition: width 0.5s linear;
|
||||
}
|
||||
#mass-system-update-overlay,
|
||||
#mass-firmware-update-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
z-index: 1000;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding-top: 200px;
|
||||
}
|
||||
|
||||
/* Prompt do firmware w overlay */
|
||||
#mass-firmware-reboot-prompt {
|
||||
display:none;
|
||||
position:fixed;
|
||||
top:0; left:0;
|
||||
width:100%; height:100%;
|
||||
background:rgba(0,0,0,0.7);
|
||||
z-index:1100;
|
||||
color:white;
|
||||
text-align:center;
|
||||
padding-top:200px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Moje urządzenia</h2>
|
||||
<button type="button" class="btn btn-success mb-3" onclick="window.location.href='{{ url_for('add_device') }}'">Dodaj nowe urządzenie</button>
|
||||
<table class="table table-bordered table-hover">
|
||||
<div class="container">
|
||||
<h2 class="mb-4">Moje urządzenia</h2>
|
||||
|
||||
<!-- Karta z tabelą urządzeń i przyciskami masowych akcji -->
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header bg-light d-flex align-items-center justify-content-between">
|
||||
<h5 class="mb-0">Lista urządzeń</h5>
|
||||
<button type="button" class="btn btn-success" onclick="window.location.href='{{ url_for('add_device') }}'">
|
||||
Dodaj nowe urządzenie
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Formularz z przyciskami masowych operacji -->
|
||||
<form id="mass-update-form">
|
||||
<div class="mb-3">
|
||||
<button type="button" id="mass-system-update-btn" class="btn btn-warning me-2">Aktualizuj system (masowo)</button>
|
||||
<button type="button" id="mass-firmware-update-btn" class="btn btn-danger me-2">Aktualizuj firmware (masowo)</button>
|
||||
<button type="submit" class="btn btn-primary">Odśwież wybrane</button>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover table-sm align-middle table-bordered">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th><input type="checkbox" id="select-all"></th>
|
||||
<th>Nazwa / Adres IP</th>
|
||||
<th>Ostatnie sprawdzenie</th>
|
||||
<th>Status</th>
|
||||
@@ -16,39 +108,243 @@
|
||||
<tbody>
|
||||
{% for device in devices %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" name="selected_devices" value="{{ device.id }}">
|
||||
</td>
|
||||
<td>
|
||||
{% if device.name %}
|
||||
{{ device.name }} |
|
||||
<code><small class="text-muted">{{ device.ip }}</small></code>
|
||||
{{ device.name }}
|
||||
<br><code class="text-muted">{{ device.ip }}</code>
|
||||
{% else %}
|
||||
{{ device.ip }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ device.last_check.strftime('%Y-%m-%d %H:%M:%S') if device.last_check else 'Brak' }}</td>
|
||||
<td>
|
||||
{% if device.update_required %}
|
||||
<span class="badge bg-danger">Wymaga aktualizacji</span>
|
||||
{{ device.last_check.strftime('%Y-%m-%d %H:%M:%S') if device.last_check else 'Brak' }}
|
||||
</td>
|
||||
<td>
|
||||
{% if device.update_required or (device.current_firmware and device.upgrade_firmware and device.current_firmware != device.upgrade_firmware) %}
|
||||
{% if device.update_required and (device.current_firmware and device.upgrade_firmware and device.current_firmware != device.upgrade_firmware) %}
|
||||
<span class="badge bg-danger">Wymaga aktualizacji <small>(system i firmware)</small></span>
|
||||
{% elif device.update_required %}
|
||||
<span class="badge bg-danger">Wymaga aktualizacji <small>(system)</small></span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Wymaga aktualizacji <small>(firmware)</small></span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="badge bg-success">Aktualny</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<td style="white-space: nowrap;">
|
||||
<small>
|
||||
<strong>System:</strong> {{ device.current_version or 'Brak' }}<br>
|
||||
<strong>Firmware:</strong> {{ device.current_firmware or 'Brak' }}
|
||||
<strong>Firmware:</strong> {{ device.current_firmware or 'Brak' }}<br>
|
||||
<strong>Upgrade FW:</strong> {{ device.upgrade_firmware or 'N/A' }}
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="btn btn-info btn-sm" onclick="window.location.href='{{ url_for('device_detail', device_id=device.id) }}'">Szczegóły</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm" onclick="window.location.href='{{ url_for('force_check', device_id=device.id) }}'">Wymuś sprawdzenie</button>
|
||||
<button type="button" class="btn btn-warning btn-sm" onclick="window.location.href='{{ url_for('edit_device', device_id=device.id) }}'">Edytuj</button>
|
||||
<button type="button" class="btn btn-info btn-sm"
|
||||
onclick="window.location.href='{{ url_for('device_detail', device_id=device.id) }}'">
|
||||
Szczegóły
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary btn-sm"
|
||||
onclick="window.location.href='{{ url_for('force_check', device_id=device.id) }}'">
|
||||
Sprawdź
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning btn-sm"
|
||||
onclick="window.location.href='{{ url_for('edit_device', device_id=device.id) }}'">
|
||||
Edytuj
|
||||
</button>
|
||||
<!-- Dodane przykładowe przyciski (potrzebna obsługa w backendzie) -->
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
onclick="fetch('/device/{{ device.id }}/restart', {method: 'POST'}) .then(()=>location.reload());">
|
||||
Restart
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5" class="text-center">Brak dodanych urządzeń.</td>
|
||||
<td colspan="6" class="text-center">Brak dodanych urządzeń.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overlay dla masowej aktualizacji systemu -->
|
||||
<div id="mass-system-update-overlay">
|
||||
<h3>Aktualizacja systemu rozpoczęta...</h3>
|
||||
<div class="progress-container">
|
||||
<div id="mass-system-progress" class="progress-bar"></div>
|
||||
</div>
|
||||
<p id="mass-system-timer">300 sekund</p>
|
||||
</div>
|
||||
|
||||
<!-- Overlay dla restartu po aktualizacji firmware -->
|
||||
<div id="mass-firmware-update-overlay">
|
||||
<h3>Restart urządzeń po aktualizacji firmware...</h3>
|
||||
<div class="progress-container">
|
||||
<div id="mass-firmware-progress" class="progress-bar"></div>
|
||||
</div>
|
||||
<p id="mass-firmware-timer">120 sekund</p>
|
||||
</div>
|
||||
|
||||
<!-- Dynamiczny prompt dla masowego restartu firmware -->
|
||||
<div id="mass-firmware-reboot-prompt">
|
||||
<h3>Firmware update zakończony.</h3>
|
||||
<p>Czy chcesz zrestartować wszystkie wybrane urządzenia?</p>
|
||||
<button id="mass-firmware-reboot-btn" class="btn btn-danger">Restart urządzeń</button>
|
||||
<button id="mass-firmware-cancel-btn" class="btn btn-secondary ms-2">Anuluj</button>
|
||||
</div>
|
||||
|
||||
<!-- Overlay informujący o zakończonym odświeżaniu -->
|
||||
<div id="mass-update-overlay" style="display:none; position: fixed; top:0; left:0; width:100%; height:100%; background: rgba(0,0,0,0.7); z-index:1100; color:white; text-align:center; padding-top:200px;">
|
||||
<h3>Polecnie odświeżenia urządzeń wykonane!</h3>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Ten sam skrypt masowych akcji, przeniesiony bez zmian w logice
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('select-all').addEventListener('change', function() {
|
||||
var checkboxes = document.querySelectorAll('input[name="selected_devices"]');
|
||||
for (var checkbox of checkboxes) {
|
||||
checkbox.checked = this.checked;
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('mass-firmware-reboot-btn').addEventListener('click', onMassFirmwareReboot);
|
||||
document.getElementById('mass-firmware-cancel-btn').addEventListener('click', function() {
|
||||
alert("Restart został anulowany. Pamiętaj, że firmware update wymaga rebootu.");
|
||||
document.getElementById('mass-firmware-reboot-prompt').style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
function getSelectedDeviceIds() {
|
||||
var selected = [];
|
||||
var checkboxes = document.querySelectorAll('input[name="selected_devices"]:checked');
|
||||
checkboxes.forEach(function(cb) {
|
||||
selected.push(cb.value);
|
||||
});
|
||||
return selected;
|
||||
}
|
||||
|
||||
// Masowa aktualizacja systemu
|
||||
document.getElementById('mass-system-update-btn').addEventListener('click', function() {
|
||||
var selectedDevices = getSelectedDeviceIds();
|
||||
if(selectedDevices.length === 0) {
|
||||
alert("Wybierz przynajmniej jedno urządzenie.");
|
||||
return;
|
||||
}
|
||||
selectedDevices.forEach(function(id) {
|
||||
fetch(`/device/${id}/update`, { method: 'POST' })
|
||||
.catch(function(error){ console.error('Błąd aktualizacji systemu dla urządzenia ' + id, error); });
|
||||
});
|
||||
// Pokaż overlay z paskiem postępu (5 minut)
|
||||
var overlay = document.getElementById('mass-system-update-overlay');
|
||||
overlay.style.display = 'block';
|
||||
var timeLeft = 200; // 200 sekund
|
||||
var progressBar = document.getElementById('mass-system-progress');
|
||||
var timerDisplay = document.getElementById('mass-system-timer');
|
||||
var interval = setInterval(function(){
|
||||
timeLeft--;
|
||||
var percent = ((200 - timeLeft) / 200) * 100;
|
||||
progressBar.style.width = percent + '%';
|
||||
timerDisplay.textContent = timeLeft + ' sekund';
|
||||
if(timeLeft <= 0){
|
||||
clearInterval(interval);
|
||||
// Po zakończeniu odliczania, force_check
|
||||
selectedDevices.forEach(function(id) {
|
||||
fetch(`/device/${id}/force_check`, { method: 'GET' })
|
||||
.catch(function(error){ console.error('Błąd force check dla urządzenia ' + id, error); });
|
||||
});
|
||||
location.reload();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
// Masowa aktualizacja firmware
|
||||
document.getElementById('mass-firmware-update-btn').addEventListener('click', function() {
|
||||
var selectedDevices = getSelectedDeviceIds();
|
||||
if(selectedDevices.length === 0) {
|
||||
alert("Wybierz przynajmniej jedno urządzenie.");
|
||||
return;
|
||||
}
|
||||
selectedDevices.forEach(function(id) {
|
||||
fetch(`/device/${id}/update_firmware`, { method: 'POST' })
|
||||
.catch(function(error){ console.error('Błąd aktualizacji firmware dla urządzenia ' + id, error); });
|
||||
});
|
||||
// Wyświetlamy dynamiczny prompt restartu
|
||||
document.getElementById('mass-firmware-reboot-prompt').style.display = 'block';
|
||||
});
|
||||
|
||||
// Sekwencyjny reboot
|
||||
function sendRebootSequentially(devices, index = 0) {
|
||||
if (index >= devices.length) return Promise.resolve();
|
||||
return fetch(`/device/${devices[index]}/restart`, { method: 'POST' })
|
||||
.catch(function(error) {
|
||||
console.error('Błąd wysyłania reboot dla urządzenia ' + devices[index], error);
|
||||
})
|
||||
.then(function() {
|
||||
return new Promise(resolve => setTimeout(resolve, 500));
|
||||
})
|
||||
.then(function() {
|
||||
return sendRebootSequentially(devices, index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
function onMassFirmwareReboot() {
|
||||
document.getElementById('mass-firmware-reboot-prompt').style.display = 'none';
|
||||
var selectedDevices = getSelectedDeviceIds();
|
||||
sendRebootSequentially(selectedDevices)
|
||||
.then(function() {
|
||||
var overlay = document.getElementById('mass-firmware-update-overlay');
|
||||
overlay.style.display = 'block';
|
||||
var timeLeft = 90; // 90 sekund
|
||||
var progressBar = document.getElementById('mass-firmware-progress');
|
||||
var timerDisplay = document.getElementById('mass-firmware-timer');
|
||||
var interval = setInterval(function(){
|
||||
timeLeft--;
|
||||
var percent = ((90 - timeLeft) / 90) * 100;
|
||||
progressBar.style.width = percent + '%';
|
||||
timerDisplay.textContent = timeLeft + ' sekund';
|
||||
if(timeLeft <= 0){
|
||||
clearInterval(interval);
|
||||
// Po zakończeniu odliczania, force_check
|
||||
selectedDevices.forEach(function(id) {
|
||||
fetch(`/device/${id}/force_check`, { method: 'GET' })
|
||||
.catch(function(error){ console.error('Błąd force check dla urządzenia ' + id, error); });
|
||||
});
|
||||
location.reload();
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
// Obsługa "Odśwież wybrane"
|
||||
document.getElementById('mass-update-form').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
var selectedDevices = getSelectedDeviceIds();
|
||||
if (selectedDevices.length === 0) {
|
||||
alert("Wybierz przynajmniej jedno urządzenie.");
|
||||
return;
|
||||
}
|
||||
// Dla każdego wybranego urządzenia wykonaj force_check
|
||||
selectedDevices.forEach(function(id) {
|
||||
fetch(`/device/${id}/force_check`, { method: 'GET' })
|
||||
.catch(function(error) {
|
||||
console.error('Błąd force check dla urządzenia ' + id, error);
|
||||
});
|
||||
});
|
||||
// Pokaż overlay informujący o zakończonym odświeżaniu
|
||||
document.getElementById('mass-update-overlay').style.display = 'block';
|
||||
// Po 4 sekundach odśwież stronę
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 4000);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -1,37 +1,86 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Edytuj urządzenie - RouterOS Update{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Karta w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border-color: #444;
|
||||
}
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2>Edytuj urządzenie</h2>
|
||||
|
||||
<!-- Karta: Edycja urządzenia -->
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">Edytuj urządzenie</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<!-- Pola edycji: name, ip, port, username, password, branch -->
|
||||
<!-- Pole nazwy urządzenia -->
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">Nazwa urządzenia</label>
|
||||
<input type="text" class="form-control" name="name" id="name" required
|
||||
value="{{ device.name }}"
|
||||
placeholder="np. Mikrotik w biurze">
|
||||
<small class="text-muted">Przyjazna nazwa, np. „Router w piwnicy” lub „Mikrotik #2”.</small>
|
||||
</div>
|
||||
|
||||
<!-- Adres IP -->
|
||||
<div class="mb-3">
|
||||
<label for="ip" class="form-label">Adres IP</label>
|
||||
<input type="text" class="form-control" name="ip" id="ip" value="{{ device.ip }}" required>
|
||||
<input type="text" class="form-control" name="ip" id="ip" required
|
||||
value="{{ device.ip }}"
|
||||
placeholder="np. 192.168.88.1">
|
||||
</div>
|
||||
|
||||
<!-- Port -->
|
||||
<div class="mb-3">
|
||||
<label for="port" class="form-label">Port</label>
|
||||
<input type="number" class="form-control" name="port" id="port" value="{{ device.port }}" required>
|
||||
<input type="number" class="form-control" name="port" id="port" required
|
||||
value="{{ device.port }}">
|
||||
<small class="text-muted">Domyślnie 8728 (lub 8729 w przypadku SSL).</small>
|
||||
</div>
|
||||
|
||||
<!-- Nazwa użytkownika -->
|
||||
<div class="mb-3">
|
||||
<label for="device_username" class="form-label">Nazwa użytkownika urządzenia</label>
|
||||
<input type="text" class="form-control" name="device_username" id="device_username" value="{{ device.device_username }}" required>
|
||||
<input type="text" class="form-control" name="device_username" id="device_username" required
|
||||
value="{{ device.device_username }}"
|
||||
placeholder="np. admin">
|
||||
</div>
|
||||
|
||||
<!-- Hasło urządzenia -->
|
||||
<div class="mb-3">
|
||||
<label for="device_password" class="form-label">Hasło urządzenia</label>
|
||||
<input type="password" class="form-control" name="device_password" id="device_password" value="{{ device.device_password }}" required>
|
||||
<input type="password" class="form-control" name="device_password" id="device_password" required
|
||||
value="{{ device.device_password }}">
|
||||
</div>
|
||||
<!-- Opcja SSL -->
|
||||
|
||||
<!-- SSL i weryfikacja certyfikatów -->
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="use_ssl" id="use_ssl" {% if device.use_ssl %}checked{% endif %}>
|
||||
<input type="checkbox" class="form-check-input" name="use_ssl" id="use_ssl"
|
||||
{% if device.use_ssl %}checked{% endif %}>
|
||||
<label class="form-check-label" for="use_ssl">Używaj SSL</label>
|
||||
</div>
|
||||
<!-- Opcja nie weryfikowania certyfikatu SSL -->
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" name="ssl_insecure" id="ssl_insecure" {% if device.ssl_insecure %}checked{% endif %}>
|
||||
<input type="checkbox" class="form-check-input" name="ssl_insecure" id="ssl_insecure"
|
||||
{% if device.ssl_insecure %}checked{% endif %}>
|
||||
<label class="form-check-label" for="ssl_insecure">Nie weryfikuj certyfikatu SSL</label>
|
||||
</div>
|
||||
|
||||
<!-- Branch aktualizacji (stable/dev/beta) -->
|
||||
<div class="mb-3">
|
||||
<label for="branch" class="form-label">Wybierz branch aktualizacji</label>
|
||||
<select class="form-select" name="branch" id="branch">
|
||||
@@ -40,8 +89,14 @@
|
||||
<option value="beta" {% if device.branch == 'beta' %}selected{% endif %}>Beta</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Przycisk zapisujący zmiany -->
|
||||
<button type="submit" class="btn btn-primary">Zapisz zmiany</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -3,7 +3,7 @@
|
||||
{% block content %}
|
||||
<div class="d-flex flex-column align-items-center justify-content-center" style="min-height: 80vh;">
|
||||
<div class="text-center">
|
||||
<img src="https://mikrotik.com/logo/assets/logo-colors-dark-ToiqSI6u.svg" alt="Mikrotik Logo" class="img-fluid" style="max-width: 200px;">
|
||||
<img id="logo-img" src="https://mikrotik.com/logo/assets/logo-colors-dark-ToiqSI6u.svg" alt="Mikrotik Logo" class="img-fluid" style="max-width: 200px;">
|
||||
<h1 class="mt-3">Witamy w RouterOS Update</h1>
|
||||
<p class="lead">Zarządzaj aktualizacjami swoich urządzeń RouterOS w prosty sposób.</p>
|
||||
<div class="mt-4">
|
||||
@@ -11,5 +11,58 @@
|
||||
<a href="{{ url_for('register') }}" class="btn btn-success btn-lg">Rejestracja</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||
|
||||
function updatePrismTheme() {
|
||||
const prismLink = document.getElementById('prism-style');
|
||||
if (prismLink) {
|
||||
if (localStorage.getItem("darkMode") === "enabled") {
|
||||
prismLink.setAttribute('href', 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css');
|
||||
} else {
|
||||
prismLink.setAttribute('href', 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-coy.min.css');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateTheme() {
|
||||
if (localStorage.getItem("darkMode") === "enabled") {
|
||||
document.body.classList.add("dark-mode");
|
||||
document.body.classList.remove("light-mode");
|
||||
darkModeToggle.checked = true;
|
||||
} else {
|
||||
document.body.classList.add("light-mode");
|
||||
document.body.classList.remove("dark-mode");
|
||||
darkModeToggle.checked = false;
|
||||
}
|
||||
updatePrismTheme();
|
||||
updateLogoImage();
|
||||
}
|
||||
|
||||
// Nowa funkcja: dynamicznie aktualizuje logo, jeśli element istnieje
|
||||
function updateLogoImage() {
|
||||
const logo = document.getElementById("logo-img");
|
||||
if (logo) {
|
||||
if (localStorage.getItem("darkMode") === "enabled") {
|
||||
logo.src = "https://mikrotik.com/logo/assets/logo-colors-white-E8duxH7y.svg";
|
||||
} else {
|
||||
logo.src = "https://mikrotik.com/logo/assets/logo-colors-dark-ToiqSI6u.svg";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
darkModeToggle.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
localStorage.setItem("darkMode", "enabled");
|
||||
} else {
|
||||
localStorage.setItem("darkMode", "disabled");
|
||||
}
|
||||
updateTheme();
|
||||
});
|
||||
updateTheme();
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -1,20 +1,50 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Logowanie - RouterOS Update{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Karta w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border-color: #444;
|
||||
}
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2>Logowanie</h2>
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">Logowanie</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Nazwa użytkownika</label>
|
||||
<input type="text" class="form-control" name="username" id="username" required>
|
||||
<input type="text" class="form-control" name="username" id="username" required
|
||||
placeholder="np. admin">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Hasło</label>
|
||||
<input type="password" class="form-control" name="password" id="password" required>
|
||||
<input type="password" class="form-control" name="password" id="password" required
|
||||
placeholder="••••••••">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Zaloguj</button>
|
||||
</form>
|
||||
<hr>
|
||||
<p class="mb-0">Nie masz konta?
|
||||
<a href="{{ url_for('register') }}">Zarejestruj się</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,11 +1,90 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Logi - Aplikacja Updatera{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<!-- Styl Vanilla‑DataTables -->
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vanilla-datatables@latest/dist/vanilla-dataTables.min.css">
|
||||
|
||||
<style>
|
||||
/* =======================================
|
||||
Dostosowanie do trybu ciemnego
|
||||
(jeśli w base.html używasz body.dark-mode)
|
||||
======================================= */
|
||||
|
||||
/* Karta w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #cccccc;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
/* Nagłówek karty w trybie ciemnym */
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
/* Tabela w trybie ciemnym
|
||||
- wyjaśnienie: thead.table-dark w trybie jasnym ma swoje domyślne kolory,
|
||||
więc w ciemnym je nadpisujemy, żeby był czytelny. */
|
||||
body.dark-mode .table thead.table-dark {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #ccc !important;
|
||||
border-color: #444 !important;
|
||||
}
|
||||
body.dark-mode .table-hover tbody tr:hover {
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
/* Obramowania w trybie ciemnym */
|
||||
body.dark-mode .table.table-bordered > :not(caption) > * > * {
|
||||
border-color: #444 !important;
|
||||
}
|
||||
|
||||
/* Tabela DataTables – drobne poprawki w trybie ciemnym */
|
||||
body.dark-mode .dataTable-wrapper .dataTable-info,
|
||||
body.dark-mode .dataTable-wrapper .dataTable-pagination,
|
||||
body.dark-mode .dataTable-wrapper .dataTable-dropdown label,
|
||||
body.dark-mode .dataTable-wrapper .dataTable-input {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Logi</h2>
|
||||
<table class="table table-striped">
|
||||
<div class="container">
|
||||
|
||||
<!-- Nagłówek strony -->
|
||||
<h2 class="mb-4">Logi</h2>
|
||||
|
||||
<!-- Karta: Formularz kasowania logów -->
|
||||
<div class="card border-0 shadow mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Usuń logi starsze niż podana liczba dni</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('clean_logs') }}">
|
||||
<div class="mb-3">
|
||||
<label for="days" class="form-label">Liczba dni</label>
|
||||
<input type="number" class="form-control" name="days" id="days" placeholder="Podaj liczbę dni">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger">Usuń logi</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Karta: Tabela logów -->
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Wszystkie logi systemu</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover table-sm align-middle mb-0" id="logsTable">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Data i czas</th>
|
||||
<th style="white-space: nowrap;">Data i czas</th>
|
||||
<th>Urządzenie</th>
|
||||
<th>Wiadomość</th>
|
||||
</tr>
|
||||
@@ -13,21 +92,43 @@
|
||||
<tbody>
|
||||
{% for log in logs %}
|
||||
<tr>
|
||||
<td>{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||
<td style="white-space: nowrap;">{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||
<td>
|
||||
{% if log.device_id %}
|
||||
<a href="{{ url_for('device_detail', device_id=log.device_id) }}">Urządzenie #{{ log.device_id }}</a>
|
||||
<a href="{{ url_for('device_detail', device_id=log.device.id) }}">
|
||||
{{ log.device.name if log.device.name else "Urządzenie #" ~ log.device.id }}
|
||||
</a>
|
||||
{% else %}
|
||||
Ogólne
|
||||
<span class="text-muted">Ogólne</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><pre style="white-space: pre-wrap;">{{ log.message }}</pre></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="text-center">Brak logów.</td>
|
||||
<!-- Treść logu w <pre> z white-space: pre-wrap, żeby łamać długie linie -->
|
||||
<td><pre style="white-space: pre-wrap; margin:0;">{{ log.message }}</pre></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_scripts %}
|
||||
<!-- Vanilla‑DataTables -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/vanilla-datatables@latest/dist/vanilla-dataTables.min.js"></script>
|
||||
<script>
|
||||
// Inicjalizacja Vanilla‑DataTables dla tabeli logów
|
||||
const dataTable = new DataTable("#logsTable", {
|
||||
searchable: true,
|
||||
sortable: true,
|
||||
perPage: 10,
|
||||
labels: {
|
||||
placeholder: "Szukaj...", // placeholder dla pola wyszukiwania
|
||||
perPage: "{select} wpisów na stronę", // tekst przy select
|
||||
noRows: "Brak logów.", // gdy tabela jest pusta
|
||||
info: "Wyświetlono {start} - {end} z {rows} logów" // info paginacja
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
@@ -1,24 +1,55 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Rejestracja - RouterOS Update{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Karta w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border-color: #444;
|
||||
}
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2>Rejestracja</h2>
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">Rejestracja</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Nazwa użytkownika</label>
|
||||
<input type="text" class="form-control" name="username" id="username" required>
|
||||
<input type="text" class="form-control" name="username" id="username" required
|
||||
placeholder="np. admin2">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email</label>
|
||||
<input type="email" class="form-control" name="email" id="email" required>
|
||||
<label for="email" class="form-label">Adres e-mail</label>
|
||||
<input type="email" class="form-control" name="email" id="email" required
|
||||
placeholder="np. user@example.com">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Hasło</label>
|
||||
<input type="password" class="form-control" name="password" id="password" required>
|
||||
<input type="password" class="form-control" name="password" id="password" required
|
||||
placeholder="••••••••">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Zarejestruj się</button>
|
||||
</form>
|
||||
<hr>
|
||||
<p class="mb-0">Masz już konto?
|
||||
<a href="{{ url_for('login') }}">Zaloguj się</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@@ -1,24 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Reset hasła - RouterOS Update{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-4">Reset hasła</h2>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="old_password" class="form-label">Stare hasło</label>
|
||||
<input type="password" class="form-control" name="old_password" id="old_password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="new_password" class="form-label">Nowe hasło</label>
|
||||
<input type="password" class="form-control" name="new_password" id="new_password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="confirm_password" class="form-label">Potwierdź nowe hasło</label>
|
||||
<input type="password" class="form-control" name="confirm_password" id="confirm_password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Zresetuj hasło</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
205
templates/routeros_changelog_tabs.html
Normal file
205
templates/routeros_changelog_tabs.html
Normal file
@@ -0,0 +1,205 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Changelog RouterOS{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<!-- PRISM.JS (temat okaidia lub dowolny inny) -->
|
||||
<link rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-okaidia.min.css"
|
||||
id="prism-style">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
|
||||
|
||||
<style>
|
||||
/*
|
||||
1) Mniejszy rozmiar fontu w bloczku <code>
|
||||
2) Łamanie linii, max-width, itp.
|
||||
3) Tło w jasnym/ciemnym trybie
|
||||
*/
|
||||
|
||||
/* Mniejszy rozmiar i ograniczenie linii */
|
||||
pre code[class*="language-"] {
|
||||
font-size: 0.85rem !important; /* dopasuj do gustu, np. 0.9rem lub 0.8rem */
|
||||
line-height: 1.3 !important;
|
||||
}
|
||||
|
||||
/* Łamanie linii i scroll w razie potrzeby */
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Tło w trybie jasnym */
|
||||
body.light-mode pre {
|
||||
background-color: #ffffff !important;
|
||||
color: #212529 !important;
|
||||
}
|
||||
/* Tło w trybie ciemnym */
|
||||
body.dark-mode pre {
|
||||
background-color: #2b2b2b !important;
|
||||
color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e !important;
|
||||
color: #ccc !important;
|
||||
border-color: #444 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .card-header {
|
||||
background-color: #333 !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
|
||||
<!-- Karta z cieniowaniem, nagłówek i ciało -->
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Changelog RouterOS</h4>
|
||||
<!-- Przycisk do wywołania aktualizacji changelogów -->
|
||||
<a href="#" id="updateChangelog" class="btn btn-danger btn-sm">
|
||||
Aktualizuj changelogi
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
<!-- Nawigacja kanałów (stable / rc / beta) -->
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if channel=='stable' %}active{% endif %}"
|
||||
href="{{ url_for('routeros_changelog', channel='stable', series=series) }}">
|
||||
Stable
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if channel=='rc' %}active{% endif %}"
|
||||
href="{{ url_for('routeros_changelog', channel='rc', series=series) }}">
|
||||
RC
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if channel=='beta' %}active{% endif %}"
|
||||
href="{{ url_for('routeros_changelog', channel='beta', series=series) }}">
|
||||
Beta
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Nawigacja serii (7.x / 6.x) -->
|
||||
<ul class="nav nav-pills mb-3">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if series=='7.x' %}active{% endif %}"
|
||||
href="{{ url_for('routeros_changelog', channel=channel, series='7.x') }}">
|
||||
7.x
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if series=='6.x' %}active{% endif %}"
|
||||
href="{{ url_for('routeros_changelog', channel=channel, series='6.x') }}">
|
||||
6.x
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Prezentacja wybranego changeloga -->
|
||||
{% if selected_entry %}
|
||||
<div class="mb-3">
|
||||
<h5 class="fw-bold">
|
||||
{{ selected_entry.version | format_version }}
|
||||
<small class="text-muted">({{ selected_entry.timestamp.strftime('%Y-%b-%d') }})</small>
|
||||
</h5>
|
||||
<pre><code class="language-plaintext">{{ selected_entry.details }}</code></pre>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning mb-3">
|
||||
Brak wpisów dla wybranych ustawień (kanał: {{ channel }}, seria: {{ series }}).
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Wybór innej wersji, jeśli mamy >1 wpis -->
|
||||
{% if entries|length > 1 %}
|
||||
<form method="GET" action="{{ url_for('routeros_changelog') }}">
|
||||
<input type="hidden" name="channel" value="{{ channel }}">
|
||||
<input type="hidden" name="series" value="{{ series }}">
|
||||
|
||||
<div class="input-group mb-3">
|
||||
<select name="version" class="form-select">
|
||||
{% for entry in entries %}
|
||||
<option value="{{ entry.version }}"
|
||||
{% if selected_entry and entry.version == selected_entry.version %}selected{% endif %}>
|
||||
{{ entry.version | format_version }} ({{ entry.timestamp.strftime('%Y-%b-%d') }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button class="btn btn-primary" type="submit">Pokaż</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Powrót do dashboardu</a>
|
||||
</div>
|
||||
|
||||
</div> <!-- /card-body -->
|
||||
</div> <!-- /card -->
|
||||
</div>
|
||||
|
||||
<!-- Modal z progressem -->
|
||||
<div class="modal fade" id="progressModal" tabindex="-1" role="dialog" aria-labelledby="progressModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="progressModalLabel">Pobieranie changelogów</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Ładowanie...</span>
|
||||
</div>
|
||||
<p>Proszę czekać, trwa pobieranie changelogów...</p>
|
||||
<!-- Możesz dodać miejsce na logi, jeśli chcesz wyświetlać szczegóły postępu -->
|
||||
<div id="progressLog" style="max-height:200px; overflow-y:auto;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Upewnij się, że Bootstrap JS jest załadowany (np. przez CDN)
|
||||
document.getElementById("updateChangelog").addEventListener("click", function(e) {
|
||||
e.preventDefault();
|
||||
// Pokaż modal
|
||||
var progressModalEl = document.getElementById("progressModal");
|
||||
var progressModal = new bootstrap.Modal(progressModalEl);
|
||||
progressModal.show();
|
||||
|
||||
// Opcjonalnie wyczyść poprzednie logi
|
||||
document.getElementById("progressLog").innerHTML = "";
|
||||
|
||||
// Wywołanie endpointu force_fetch_changelogs
|
||||
fetch("{{ url_for('force_fetch_changelogs') }}")
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
// Aktualizacja logu – możesz dodać otrzymane dane, jeśli są potrzebne
|
||||
document.getElementById("progressLog").innerHTML += "<p>Pobieranie zakończone.</p>";
|
||||
// Po krótkiej chwili zamknij modal i odśwież całą stronę
|
||||
setTimeout(() => {
|
||||
progressModal.hide();
|
||||
location.reload();
|
||||
}, 1000);
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById("progressLog").innerHTML += "<p>Błąd: " + error + "</p>";
|
||||
setTimeout(() => {
|
||||
progressModal.hide();
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@@ -1,83 +1,159 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Ustawienia - RouterOS Update{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<h2 class="mb-4">Ustawienia powiadomień i systemu</h2>
|
||||
|
||||
<!-- Główny formularz ustawień -->
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Karta w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
/* Nagłówek karty w trybie ciemnym */
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
|
||||
<!-- Karta z głównym formularzem ustawień -->
|
||||
<div class="card border-0 shadow mb-4">
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">Ustawienia powiadomień i systemu</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST">
|
||||
|
||||
<!-- Sekcja Pushover -->
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend class="w-auto">Pushover</legend>
|
||||
<div class="form-check mb-2">
|
||||
<input type="checkbox" class="form-check-input" name="pushover_enabled" id="pushover_enabled" {% if settings.pushover_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="pushover_enabled">Włącz powiadomienia Pushover</label>
|
||||
<input type="checkbox" class="form-check-input" name="pushover_enabled" id="pushover_enabled"
|
||||
{% if settings.pushover_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="pushover_enabled">
|
||||
Włącz powiadomienia Pushover
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pushover_user_key" class="form-label">Pushover User Key</label>
|
||||
<input type="text" class="form-control" name="pushover_user_key" id="pushover_user_key" value="{{ settings.pushover_user_key or '' }}">
|
||||
<input type="text" class="form-control" name="pushover_user_key" id="pushover_user_key"
|
||||
value="{{ settings.pushover_user_key or '' }}"
|
||||
placeholder="np. ujHGkDYop837...">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="pushover_token" class="form-label">Pushover Token</label>
|
||||
<input type="text" class="form-control" name="pushover_token" id="pushover_token" value="{{ settings.pushover_token or '' }}">
|
||||
<input type="text" class="form-control" name="pushover_token" id="pushover_token"
|
||||
value="{{ settings.pushover_token or '' }}"
|
||||
placeholder="np. a9WsK09mnj3R8aGj...">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Sekcja SMTP (E-mail) -->
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend class="w-auto">SMTP (E-mail)</legend>
|
||||
<div class="form-check mb-2">
|
||||
<input type="checkbox" class="form-check-input" name="email_notifications_enabled" id="email_notifications_enabled" {% if settings.email_notifications_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="email_notifications_enabled">Włącz powiadomienia e-mail</label>
|
||||
<input type="checkbox" class="form-check-input" name="email_notifications_enabled"
|
||||
id="email_notifications_enabled"
|
||||
{% if settings.email_notifications_enabled %}checked{% endif %}>
|
||||
<label class="form-check-label" for="email_notifications_enabled">
|
||||
Włącz powiadomienia e-mail
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smtp_server" class="form-label">SMTP Server</label>
|
||||
<input type="text" class="form-control" name="smtp_server" id="smtp_server" value="{{ settings.smtp_server or '' }}">
|
||||
<input type="text" class="form-control" name="smtp_server" id="smtp_server"
|
||||
value="{{ settings.smtp_server or '' }}"
|
||||
placeholder="np. smtp.gmail.com">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smtp_port" class="form-label">SMTP Port</label>
|
||||
<input type="number" class="form-control" name="smtp_port" id="smtp_port" value="{{ settings.smtp_port or '' }}">
|
||||
<input type="number" class="form-control" name="smtp_port" id="smtp_port"
|
||||
value="{{ settings.smtp_port or '' }}"
|
||||
placeholder="np. 587">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smtp_username" class="form-label">SMTP Username</label>
|
||||
<input type="text" class="form-control" name="smtp_username" id="smtp_username" value="{{ settings.smtp_username or '' }}">
|
||||
<input type="text" class="form-control" name="smtp_username" id="smtp_username"
|
||||
value="{{ settings.smtp_username or '' }}">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="smtp_password" class="form-label">SMTP Password</label>
|
||||
<input type="password" class="form-control" name="smtp_password" id="smtp_password" value="{{ settings.smtp_password or '' }}">
|
||||
<input type="password" class="form-control" name="smtp_password" id="smtp_password"
|
||||
value="{{ settings.smtp_password or '' }}">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Sekcja E-mail odbiorcy -->
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend class="w-auto">Odbiorca powiadomień</legend>
|
||||
<div class="mb-3">
|
||||
<label for="recipient_email" class="form-label">Adres e-mail do otrzymywania powiadomień</label>
|
||||
<input type="email" class="form-control" name="recipient_email" id="recipient_email"
|
||||
value="{{ settings.recipient_email or '' }}"
|
||||
placeholder="np. admin@example.com">
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Sekcja interwału -->
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend class="w-auto">Interwał sprawdzania</legend>
|
||||
<div class="mb-3">
|
||||
<label for="check_interval" class="form-label">Interwał (sekundy)</label>
|
||||
<input type="number" class="form-control" name="check_interval" id="check_interval" value="{{ settings.check_interval or 60 }}">
|
||||
<label for="check_interval" class="form-label">
|
||||
Interwał (sekundy)
|
||||
<code>21600 = 6 godzin</code>
|
||||
</label>
|
||||
<input type="number" class="form-control" name="check_interval" id="check_interval"
|
||||
value="{{ settings.check_interval or 21600 }}">
|
||||
<small class="text-muted">Co ile sekund system będzie sprawdzał aktualizacje.</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="time_input" class="form-label">Czas (HH:MM:SS)</label>
|
||||
<input type="text" class="form-control" id="time_input" placeholder="np. 01:30:00">
|
||||
<button type="button" class="btn btn-secondary mt-2" onclick="convertTime()">Konwertuj na sekundy</button>
|
||||
<button type="button" class="btn btn-secondary mt-2" onclick="convertTime()">
|
||||
Konwertuj na sekundy
|
||||
</button>
|
||||
<small class="text-muted d-block mt-1">
|
||||
Wpisz czas w formacie Godziny:Minuty:Sekundy, a następnie kliknij „Konwertuj”.
|
||||
</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Sekcja retencji logów -->
|
||||
<fieldset class="border p-3 mb-3">
|
||||
<legend class="w-auto">Retencja logów</legend>
|
||||
<div class="mb-3">
|
||||
<label for="log_retention_days" class="form-label">Przechowywać logi przez (dni)</label>
|
||||
<input type="number" class="form-control" name="log_retention_days" id="log_retention_days" value="{{ settings.log_retention_days or 30 }}">
|
||||
<label for="log_retention_days" class="form-label">
|
||||
Przechowywać logi przez (dni)
|
||||
</label>
|
||||
<input type="number" class="form-control" name="log_retention_days" id="log_retention_days"
|
||||
value="{{ settings.log_retention_days or 30 }}">
|
||||
<small class="text-muted">Starsze logi będą automatycznie usuwane po tym czasie.</small>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- Przycisk zapisywania -->
|
||||
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Przyciski testowe w osobnym bloku, ułożone w jednej linii po prawej stronie -->
|
||||
<div class="mt-4">
|
||||
<!-- Karta z przyciskami testowymi -->
|
||||
<div class="card border-0 shadow">
|
||||
<div class="card-header bg-light">
|
||||
<h5 class="mb-0">Test powiadomień</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Skorzystaj z przycisków, aby wysłać testowe powiadomienie Pushover / E-mail.</p>
|
||||
<div class="d-flex justify-content-end">
|
||||
<form method="POST" action="{{ url_for('test_pushover') }}" class="d-inline me-2">
|
||||
<form method="POST" action="{{ url_for('test_pushover') }}" class="me-2">
|
||||
<button type="submit" class="btn btn-secondary">Test Pushover</button>
|
||||
</form>
|
||||
<form method="POST" action="{{ url_for('test_email') }}" class="d-inline">
|
||||
<form method="POST" action="{{ url_for('test_email') }}">
|
||||
<button type="submit" class="btn btn-secondary">Test E-mail</button>
|
||||
</form>
|
||||
</div>
|
||||
|
92
templates/update_history.html
Normal file
92
templates/update_history.html
Normal file
@@ -0,0 +1,92 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Historia aktualizacji{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* === Styl dopasowany do trybu ciemnego i jasnego === */
|
||||
|
||||
/* Tło karty w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* Nagłówek karty w trybie ciemnym */
|
||||
body.dark-mode .card .card-header.bg-light {
|
||||
background-color: #333 !important;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
|
||||
/* Tabela w trybie ciemnym: jaśniejsze wyróżnienie nagłówka */
|
||||
body.dark-mode .card table thead {
|
||||
background-color: #2a2a2a;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* Tło wierszy tabeli w trybie ciemnym */
|
||||
body.dark-mode .card table tbody tr {
|
||||
background-color: #1e1e1e;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
/* Obramowanie w trybie ciemnym */
|
||||
body.dark-mode .table-bordered > :not(caption) > * > * {
|
||||
border-color: #444 !important;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="card border-0 shadow mb-4">
|
||||
<!-- Nagłówek karty -->
|
||||
<div class="card-header bg-light">
|
||||
<h4 class="mb-0">Historia aktualizacji</h4>
|
||||
</div>
|
||||
|
||||
<!-- Zawartość karty -->
|
||||
<div class="card-body p-0">
|
||||
<!-- Jeśli mamy cokolwiek w histories -->
|
||||
{% if histories %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover table-sm align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th class="text-nowrap">Data</th>
|
||||
<th class="text-nowrap">Urządzenie</th>
|
||||
<th class="text-nowrap">Typ aktualizacji</th>
|
||||
<th class="text-nowrap">Szczegóły</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for history in histories %}
|
||||
<tr>
|
||||
<td>{{ history.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
|
||||
<td>
|
||||
{{ history.device.name or history.device.ip }}
|
||||
</td>
|
||||
<td>
|
||||
{# Prosty przykład wyróżnienia typów aktualizacji #}
|
||||
{% if history.update_type == "system" %}
|
||||
<span class="badge bg-primary">System</span>
|
||||
{% elif history.update_type == "firmware" %}
|
||||
<span class="badge bg-success">Firmware</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ history.update_type }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ history.details }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-3">
|
||||
<p class="mb-0">Brak historii aktualizacji.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Reference in New Issue
Block a user