functions and interface
This commit is contained in:
parent
5d47549c19
commit
c89c1efd67
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"]
|
132
README.md
132
README.md
@ -0,0 +1,132 @@
|
||||
# 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
|
||||
|
||||
1. **Klonowanie repozytorium:**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/TwojeRepozytorium/routeros-update.git
|
||||
cd routeros-update
|
||||
|
||||
2. Utworzenie środowiska wirtualnego i instalacja zależności:
|
||||
|
||||
``` python3 -m venv venv
|
||||
source venv/bin/activate # Na Windows: venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
3. Inicjalizacja bazy danych: (nie jest to potrzebne, tylko przy aktualizacji)
|
||||
|
||||
Domyślnie aplikacja używa SQLite. Uruchom aplikację lub użyj Flask shell, aby utworzyć bazę:
|
||||
```
|
||||
flask shell
|
||||
>>> from app import db
|
||||
>>> db.create_all()
|
||||
|
||||
## Konfiguracja
|
||||
|
||||
1. :**Plik konfiguracyjny::**
|
||||
W pliku app.py ustaw odpowiednią wartość dla SECRET_KEY oraz, jeśli potrzebujesz, zmodyfikuj SQLALCHEMY_DATABASE_URI.
|
||||
|
||||
2. :**Powiadomienia::**
|
||||
Po rejestracji użytkownika skonfiguruj ustawienia powiadomień (Pushover, SMTP) w sekcji ustawień.
|
||||
|
||||
3. :**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.
|
||||
|
||||
## Instalacja
|
||||
|
||||
# Lokalnie
|
||||
|
||||
1. **Klonowanie repozytorium:**
|
||||
```bash
|
||||
git clone https://github.com/TwojeRepozytorium/routeros-update.git
|
||||
cd routeros-update
|
||||
|
||||
**Klonowanie repozytorium:**
|
||||
|
||||
```python3 -m venv venv
|
||||
source venv/bin/activate # Na Windows: venv\Scripts\activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
3. **Uruchomienie aplikacji:**
|
||||
|
||||
```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:
|
||||
|
||||
1. Dockerfile – definicja obrazu aplikacji.
|
||||
2. docker-compose.yml – konfiguracja kontenera.
|
||||
3. start.sh – skrypt uruchamiający kontenery (wybiera podman-compose, jeśli dostępny, lub docker-compose).
|
||||
|
||||
Aby uruchomić aplikację w Dockerze, wykonaj:
|
||||
|
||||
```./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 (operacja usunie stare wpisy z bazy).
|
||||
|
||||
## Wkład i rozwój
|
||||
|
||||
Zapraszam do zgłaszania poprawek i propozycji rozwoju. Aby wnieść swój wkład:
|
||||
|
||||
1. Forkuj repozytorium.
|
||||
2. Wprowadź zmiany.
|
||||
3. Otwórz pull request.
|
||||
|
||||
## Licencja
|
||||
Projekt jest dostępny na licencji MIT.
|
220
app.py
220
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,17 @@ 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
|
||||
|
||||
|
||||
# Konfiguracja aplikacji
|
||||
app = Flask(__name__)
|
||||
@ -98,6 +107,14 @@ class Anomaly(db.Model):
|
||||
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():
|
||||
db.create_all()
|
||||
@ -272,6 +289,11 @@ def get_email_template(subject, message):
|
||||
</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():
|
||||
@ -318,6 +340,119 @@ 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"
|
||||
|
||||
def fetch_changelogs(force=False):
|
||||
changelog_url = "https://mikrotik.com/download/changelogs"
|
||||
current_date = datetime.utcnow()
|
||||
|
||||
try:
|
||||
logging.info(f"Pobieranie changelogów z {changelog_url}...")
|
||||
response = requests.get(changelog_url, timeout=10)
|
||||
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
|
||||
for section in changelog_sections:
|
||||
a_tag = section.find("a")
|
||||
if not a_tag:
|
||||
continue
|
||||
|
||||
raw_text = a_tag.get_text(strip=True)
|
||||
match = re.match(r"([0-9.]+[a-zA-Z0-9]*)", raw_text)
|
||||
version_text = match.group(1) if match else raw_text
|
||||
|
||||
# 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")
|
||||
continue
|
||||
|
||||
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}")
|
||||
continue
|
||||
|
||||
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}")
|
||||
continue
|
||||
|
||||
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")
|
||||
continue # Pomijamy wpis bez daty
|
||||
|
||||
changelog_text = "\n".join(changelog_lines).strip()
|
||||
except Exception as e:
|
||||
logging.error(f"Błąd pobierania changeloga z {changelog_file_url}: {e}")
|
||||
continue
|
||||
|
||||
# Filtrowanie: 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")
|
||||
continue
|
||||
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")
|
||||
continue
|
||||
|
||||
# Określenie typu wydania (stable, rc, beta)
|
||||
release_type = get_release_type(version_text)
|
||||
|
||||
new_entry = ChangelogEntry(
|
||||
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
|
||||
)
|
||||
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
|
||||
@ -363,12 +498,26 @@ with app.app_context():
|
||||
max_instances=1
|
||||
)
|
||||
scheduler.add_job(
|
||||
func=detect_anomalies,
|
||||
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")
|
||||
|
||||
@ -376,6 +525,13 @@ scheduler.start()
|
||||
|
||||
# ROUTY APLIKACJI
|
||||
|
||||
@app.template_filter('format_version')
|
||||
def format_version(value):
|
||||
import re
|
||||
# Zamieniamy ciąg, który składa się z: liczba.część_1 + czterocyfrowy rok, na samą część_1.
|
||||
# Przykładowo: "7.182025" => "7.18"
|
||||
return re.sub(r"(\d+\.\d+)\d{4}", r"\1", value)
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
if current_user.is_authenticated:
|
||||
@ -389,11 +545,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'])
|
||||
@ -679,9 +848,9 @@ def test_email():
|
||||
flash("Testowy e-mail wysłany.")
|
||||
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')
|
||||
@ -696,7 +865,7 @@ def reset_password():
|
||||
db.session.commit()
|
||||
flash("Hasło zostało zresetowane.")
|
||||
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
|
||||
@ -752,6 +921,43 @@ def update_selected_devices():
|
||||
flash("Wybrane urządzenia zostały zaktualizowane.")
|
||||
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'))
|
||||
|
||||
# Zamknięcie harmonogramu przy zatrzymaniu aplikacji
|
||||
atexit.register(lambda: scheduler.shutdown())
|
||||
|
||||
|
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
|
@ -5,3 +5,4 @@ APScheduler
|
||||
librouteros
|
||||
requests
|
||||
waitress
|
||||
bs4
|
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!"
|
@ -6,65 +6,289 @@
|
||||
<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>
|
||||
body.dark-mode {
|
||||
background-color: #2b2b2b;
|
||||
color: #cccccc;
|
||||
}
|
||||
body.dark-mode .navbar {
|
||||
background-color: #201f1f !important;
|
||||
color: #ffffff;
|
||||
}
|
||||
/* Nazwa aplikacji */
|
||||
body.dark-mode .navbar .navbar-brand {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
body.dark-mode .navbar .nav-link {
|
||||
color: #ffffff !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;
|
||||
}
|
||||
/* Przycisk zmiany hasła */
|
||||
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 - poprawa czytelności tekstu */
|
||||
body.dark-mode .footer {
|
||||
background-color: #262626;
|
||||
}
|
||||
body.dark-mode .footer .text-muted {
|
||||
color: #ffffff !important;
|
||||
}
|
||||
body.light-mode .navbar {
|
||||
background-color: #f8f9fa !important;
|
||||
color: #000;
|
||||
}
|
||||
body.light-mode .navbar .nav-link {
|
||||
color: #000 !important;
|
||||
}
|
||||
body.light-mode .footer {
|
||||
background-color: #f8f9fa;
|
||||
color: #000;
|
||||
}
|
||||
/* Tabele w trybie ciemnym */
|
||||
body.dark-mode table {
|
||||
background-color: #1a1a1a;
|
||||
color: #cccccc;
|
||||
}
|
||||
body.dark-mode table thead {
|
||||
background-color: #333333;
|
||||
}
|
||||
/* Pola formularzy (globalnie oraz w tabelach) */
|
||||
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;
|
||||
}
|
||||
/* Vanilla‑DataTables (logi) w trybie ciemnym */
|
||||
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;
|
||||
}
|
||||
/* Stylizacja komórek tabeli */
|
||||
body.dark-mode table td {
|
||||
background-color: #1a1a1a !important;
|
||||
color: #cccccc !important;
|
||||
}
|
||||
/* Pola formularzy – upewniamy się, że tekst wpisywany ma jasny kolor */
|
||||
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 !important;
|
||||
color: #cccccc !important;
|
||||
border: 1px solid #555 !important;
|
||||
}
|
||||
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 !important;
|
||||
color: #cccccc !important;
|
||||
border-color: #777 !important;
|
||||
}
|
||||
/* Placeholder w trybie ciemnym */
|
||||
body.dark-mode ::placeholder {
|
||||
color: #cccccc !important;
|
||||
opacity: 1;
|
||||
}
|
||||
/* Breadcrumb active - elementy aktywne nawigacji */
|
||||
body.dark-mode .breadcrumb-item.active {
|
||||
color: #cccccc !important;
|
||||
}
|
||||
/* Klasa .text-muted w trybie ciemnym */
|
||||
body.dark-mode .text-muted {
|
||||
color: #cccccc !important;
|
||||
}
|
||||
/* Dostosowanie nagłówka kart typu bg-light (np. "Ostatnie zdarzenia") */
|
||||
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;
|
||||
}
|
||||
</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">
|
||||
{% 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('update_history') }}'">Historia aktualizacji</button>
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('anomalies') }}'">Anomalie</button>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-light" onclick="window.location.href='{{ url_for('reset_password') }}'">Reset hasła</button>
|
||||
<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 class="collapse navbar-collapse" id="navbarContent">
|
||||
{% if current_user.is_authenticated %}
|
||||
<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('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>
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
<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>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container my-4 flex-fill">
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div class="alert alert-info">
|
||||
{% for message in messages %}
|
||||
<div>{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% 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 -->
|
||||
<script src="https://code.jquery.com/jquery-3.5.1.slim.min.js"></script>
|
||||
<!-- 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 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;
|
||||
}
|
||||
}
|
||||
darkModeToggle.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
localStorage.setItem("darkMode", "enabled");
|
||||
} else {
|
||||
localStorage.setItem("darkMode", "disabled");
|
||||
}
|
||||
updateTheme();
|
||||
});
|
||||
updateTheme();
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const darkModeToggle = document.getElementById('darkModeToggle');
|
||||
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();
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
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>
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% block content %}
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<h2 class="mb-4">Reset hasła</h2>
|
||||
<h2 class="mb-4">Zmiana hasła</h2>
|
||||
<form method="POST">
|
||||
<div class="mb-3">
|
||||
<label for="old_password" class="form-label">Stare hasło</label>
|
@ -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,68 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Karta z wykonanymi aktualizacjami -->
|
||||
<div class="col-md-3">
|
||||
<div class="card text-white bg-secondary mb-3">
|
||||
<div class="card text-white bg-info 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">Wykonane aktualizacje</h5>
|
||||
<p class="card-text display-6">{{ update_history_count }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Możesz dodać dodatkowe statystyki lub wykresy poniżej -->
|
||||
|
||||
<!-- 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">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>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
{% if recent_logs %}
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for log in recent_logs %}
|
||||
<li class="list-group-item p-1 border-top-0 border-bottom">
|
||||
<small class="text-muted">{{ log.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</small>
|
||||
– {{ log.message|truncate(100) }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p class="mb-0">Brak ostatnich zdarzeń.</p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -1,5 +1,26 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Szczegóły urządzenia - RouterOS Update{% endblock %}
|
||||
{% block extra_head %}
|
||||
<style>
|
||||
/* Stylizacja kart w trybie ciemnym */
|
||||
body.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
border-color: #444;
|
||||
}
|
||||
/* Stylizacja bloku logów – przewijalny, z odpowiednim tłem i kolorem tekstu */
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<div class="my-4">
|
||||
@ -12,18 +33,9 @@
|
||||
</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">
|
||||
<!-- Dwukolumnowy układ: dane urządzenia i informacje o systemie -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-primary text-white">
|
||||
Dane urządzenia
|
||||
@ -55,10 +67,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Informacje o systemie -->
|
||||
<div class="tab-pane fade" id="system-info" role="tabpanel" aria-labelledby="system-info-tab">
|
||||
<div class="col-md-6">
|
||||
<div class="card mb-3">
|
||||
<div class="card-header bg-info text-white">
|
||||
<div class="card-header bg-primary text-white">
|
||||
Informacje o systemie
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@ -85,19 +96,19 @@
|
||||
</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>
|
||||
<!-- Logi urządzenia jako pojedynczy blok tekstu -->
|
||||
<div class="card 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>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Brak logów</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
91
templates/routeros_changelog_tabs.html
Normal file
91
templates/routeros_changelog_tabs.html
Normal file
@ -0,0 +1,91 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Changelog RouterOS{% endblock %}
|
||||
{% block extra_head %}
|
||||
<!-- Dodajemy Prism.js – globalny arkusz stylów będzie zmieniany przez base.html -->
|
||||
<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>
|
||||
|
||||
pre {
|
||||
background-color: #282c34;
|
||||
color: #abb2bf;
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.5;
|
||||
padding: 1rem;
|
||||
border-radius: 0 0 5px 5px;
|
||||
overflow-x: auto;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<!-- Nagłówek z tytułem i przyciskami akcji (przyciski aktualizacji są tutaj, globalny dark mode toggle już w navbarze) -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Changelog RouterOS</h2>
|
||||
<a href="{{ url_for('force_fetch_changelogs') }}"
|
||||
class="btn btn-danger"
|
||||
onclick="return confirm('Czy na pewno chcesz ręcznie pobrać wszystkie changelogi? Operacja usunie wszystkie stare wpisy.');">
|
||||
Aktualizuj changelogi
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Nawigacja po kanałach -->
|
||||
<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 po seriach wersji -->
|
||||
<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 wpisu changeloga -->
|
||||
{% if selected_entry %}
|
||||
<div class="mb-3">
|
||||
<h4 class="changelog-header">
|
||||
{{ selected_entry.version | format_version }}
|
||||
<small>({{ selected_entry.timestamp.strftime('%Y-%b-%d') }})</small>
|
||||
</h4>
|
||||
<pre><code class="language-plaintext">{{ selected_entry.details }}</code></pre>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>Brak wpisów dla wybranych ustawień.</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Formularz wyboru innego wpisu -->
|
||||
{% 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ż changelog</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">Powrót do dashboardu</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
x
Reference in New Issue
Block a user