{{ code }}
+{{ title }}
+{{ message }}
+diff --git a/pytorrent/__init__.py b/pytorrent/__init__.py index 7e63408..383076f 100644 --- a/pytorrent/__init__.py +++ b/pytorrent/__init__.py @@ -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) diff --git a/pytorrent/static/styles.css b/pytorrent/static/styles.css index 8c6011d..ac421d4 100644 --- a/pytorrent/static/styles.css +++ b/pytorrent/static/styles.css @@ -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%; + } +} diff --git a/pytorrent/templates/error.html b/pytorrent/templates/error.html new file mode 100644 index 0000000..46451a0 --- /dev/null +++ b/pytorrent/templates/error.html @@ -0,0 +1,25 @@ + + +
+ + +{{ code }}
+{{ message }}
+