about and errorpages

This commit is contained in:
Mateusz Gruszczyński
2026-05-06 11:06:08 +02:00
parent 1baf4a8495
commit 6587e74892
4 changed files with 249 additions and 1 deletions

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from pathlib import Path
from flask import Flask, request, url_for
from flask import Flask, jsonify, render_template, request, url_for
from flask_socketio import SocketIO
from werkzeug.middleware.proxy_fix import ProxyFix
from .config import (
@@ -22,6 +22,39 @@ socketio = SocketIO(cors_allowed_origins=SOCKETIO_CORS_ALLOWED_ORIGINS, ping_tim
_static_md5_cache: dict[tuple, str] = {}
def _wants_json_response() -> bool:
"""Return true for API/error clients that should not receive an HTML page."""
best = request.accept_mimetypes.best_match(["application/json", "text/html"])
return request.path.startswith("/api/") or best == "application/json"
def register_error_pages(app: Flask) -> None:
# Notatka: własne strony błędów zastępują generyczne 404/500 i zachowują JSON dla API.
@app.errorhandler(404)
def not_found(error):
if _wants_json_response():
return jsonify({"ok": False, "error": "Not found"}), 404
return render_template(
"error.html",
code=404,
title="Page not found",
message="The requested pyTorrent view does not exist or is not available.",
icon="fa-compass-drafting",
), 404
@app.errorhandler(500)
def server_error(error):
if _wants_json_response():
return jsonify({"ok": False, "error": "Internal server error"}), 500
return render_template(
"error.html",
code=500,
title="Application error",
message="pyTorrent hit an internal error while handling this request.",
icon="fa-bug",
), 500
def create_app() -> Flask:
app = Flask(__name__)
if PROXY_FIX_ENABLE:
@@ -71,6 +104,7 @@ def create_app() -> Flask:
from .routes.api import bp as api_bp
app.register_blueprint(main_bp)
app.register_blueprint(api_bp)
register_error_pages(app)
init_db()
from .services.auth import install_guards
install_guards(app)

View File

@@ -2001,3 +2001,161 @@ body.mobile-mode #mobileList {
padding-left: 0;
}
}
/* Notatka: style About i stron błędów są zgrupowane, bez duplikowania istniejących klas. */
.about-modal-content {
overflow: hidden;
}
.about-nav-btn {
opacity: 0.82;
}
.about-nav-btn:hover,
.about-nav-btn:focus-visible {
opacity: 1;
}
.about-hero {
display: flex;
align-items: center;
gap: 0.85rem;
margin-bottom: 1rem;
padding: 0.9rem;
border: 1px solid var(--bs-border-color);
border-radius: 0.85rem;
background: rgba(var(--bs-secondary-bg-rgb), 0.38);
}
.about-logo {
display: inline-grid;
width: 2.8rem;
height: 2.8rem;
flex: 0 0 auto;
place-items: center;
border-radius: 0.8rem;
background: var(--bs-primary-bg-subtle);
color: var(--bs-primary-text-emphasis);
font-size: 1.25rem;
}
.about-hero h6,
.about-hero p {
margin: 0;
}
.about-hero h6 {
font-weight: 800;
}
.about-hero p {
color: var(--bs-secondary-color);
}
.about-list {
display: grid;
gap: 0.55rem;
margin: 0;
}
.about-list div {
display: grid;
grid-template-columns: 7rem minmax(0, 1fr);
gap: 0.75rem;
padding: 0.55rem 0;
border-bottom: 1px solid var(--bs-border-color);
}
.about-list div:last-child {
border-bottom: 0;
}
.about-list dt {
color: var(--bs-secondary-color);
font-weight: 700;
}
.about-list dd {
margin: 0;
}
.error-page {
display: grid;
min-height: 100vh;
place-items: center;
padding: 1rem;
background: radial-gradient(
circle at 50% 35%,
rgba(var(--bs-secondary-bg-rgb), 0.98),
var(--bs-body-bg) 68%
);
color: var(--bs-body-color);
}
.error-card {
width: min(92vw, 460px);
padding: 2rem;
border: 1px solid var(--bs-border-color);
border-radius: 18px;
background: rgba(var(--bs-secondary-bg-rgb), 0.9);
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.48);
text-align: center;
}
.error-brand {
font-size: 1.2rem;
font-weight: 800;
}
.error-icon {
display: inline-grid;
width: 4rem;
height: 4rem;
margin: 1.4rem 0 1rem;
place-items: center;
border: 1px solid var(--bs-border-color);
border-radius: 1rem;
background: var(--bs-primary-bg-subtle);
color: var(--bs-primary-text-emphasis);
font-size: 1.55rem;
}
.error-code {
margin: 0;
color: var(--bs-secondary-color);
font-size: 0.78rem;
font-weight: 800;
letter-spacing: 0.18em;
text-transform: uppercase;
}
.error-card h1 {
margin: 0.25rem 0 0.55rem;
font-size: 1.45rem;
font-weight: 800;
}
.error-card p:not(.error-code) {
margin: 0;
color: var(--bs-secondary-color);
}
.error-actions {
display: flex;
justify-content: center;
gap: 0.55rem;
flex-wrap: wrap;
margin-top: 1.35rem;
}
@media (max-width: 576px) {
.about-list div {
grid-template-columns: 1fr;
gap: 0.15rem;
}
.error-actions .btn {
width: 100%;
}
}

View File

@@ -0,0 +1,25 @@
<!doctype html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>pyTorrent {{ code }}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" rel="stylesheet">
<link href="{{ static_url('styles.css') }}" rel="stylesheet">
</head>
<body class="error-page">
<!-- Notatka: dedykowany widok błędu utrzymuje wygląd aplikacji zamiast domyślnej strony Flask/Werkzeug. -->
<main class="error-card" role="alert">
<div class="error-brand"><i class="fa-solid fa-robot"></i> pyTorrent</div>
<div class="error-icon" aria-hidden="true"><i class="fa-solid {{ icon }}"></i></div>
<p class="error-code">{{ code }}</p>
<h1>{{ title }}</h1>
<p>{{ message }}</p>
<div class="error-actions">
<a class="btn btn-primary" href="{{ url_for('main.index') }}"><i class="fa-solid fa-house"></i> Back to dashboard</a>
<a class="btn btn-outline-secondary" href="{{ url_for('main.docs') }}"><i class="fa-solid fa-book"></i> API docs</a>
</div>
</main>
</body>
</html>

View File

@@ -42,6 +42,8 @@
<span id="connBadge" class="badge text-bg-secondary">offline</span>
<button class="btn btn-xs btn-outline-info nav-btn" id="mobileToggle" title="Mobile/simple mode"><i class="fa-solid fa-mobile-screen"></i></button>
<button id="themeToggle" class="btn btn-xs btn-outline-secondary nav-btn" title="Change theme"><i class="fa-solid fa-moon"></i></button>
<!-- Notatka: subtelny przycisk otwiera okno About bez zmiany istniejącej nawigacji. -->
<button class="btn btn-xs btn-outline-secondary nav-btn about-nav-btn" data-bs-toggle="modal" data-bs-target="#aboutModal" title="About pyTorrent"><i class="fa-solid fa-circle-info"></i></button>
{% if auth_enabled %}<a class="btn btn-xs btn-outline-danger nav-btn" href="/logout" title="Log out"><i class="fa-solid fa-right-from-bracket"></i><span> {{ current_user.username if current_user else 'logout' }}</span></a>{% endif %}
</div>
</header>
@@ -151,6 +153,35 @@
<div class="modal fade" id="trafficModal" tabindex="-1"><div class="modal-dialog modal-xl"><div class="modal-content"><div class="modal-header"><h5 class="modal-title"><i class="fa-solid fa-chart-column"></i> Transfer history</h5><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="d-flex gap-2 mb-3 flex-wrap"><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="15m"><i class="fa-solid fa-clock"></i> 15m</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="1h"><i class="fa-solid fa-clock"></i> 1h</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="3h"><i class="fa-solid fa-clock"></i> 3h</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="6h"><i class="fa-solid fa-clock"></i> 6h</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="24h"><i class="fa-solid fa-calendar-day"></i> 24h</button><button class="btn btn-sm btn-primary traffic-range" data-range="7d"><i class="fa-solid fa-calendar-week"></i> 7d</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="30d"><i class="fa-solid fa-calendar"></i> 30d</button><button class="btn btn-sm btn-outline-secondary traffic-range" data-range="90d"><i class="fa-solid fa-calendar"></i> 90d</button></div><div class="history-grid"><div class="history-card"><div class="history-title">Transferred data</div><canvas id="trafficHistoryChart"></canvas></div><div class="history-card"><div class="history-title">Speed trend</div><canvas id="trafficSpeedChart"></canvas></div></div><div id="trafficHistoryInfo" class="small text-muted mt-2"></div></div></div></div></div>
<div class="modal fade" id="aboutModal" tabindex="-1" aria-labelledby="aboutModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content about-modal-content">
<div class="modal-header">
<h5 id="aboutModalLabel" class="modal-title"><i class="fa-solid fa-robot"></i> About pyTorrent</h5>
<button class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Notatka: krótkie informacje o licencji, autorze i stacku bez technicznych szczegółów wdrożenia. -->
<div class="about-hero">
<div class="about-logo"><i class="fa-solid fa-robot"></i></div>
<div>
<h6>pyTorrent</h6>
<p>Lightweight web panel for rTorrent management.</p>
</div>
</div>
<dl class="about-list">
<div><dt>License</dt><dd>Open source</dd></div>
<div><dt>Author</dt><dd>linuxiarz.pl</dd></div>
<div><dt>Backend</dt><dd>Python, Flask, Flask-SocketIO</dd></div>
<div><dt>Frontend</dt><dd>Bootstrap, vanilla JavaScript, Font Awesome</dd></div>
<div><dt>Runtime</dt><dd>Gunicorn compatible, rTorrent over SCGI</dd></div>
</dl>
</div>
</div>
</div>
</div>
<div id="toastHost" class="toast-host"></div>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>