Ctrl K
ComunidadGuías…

backend/app_routes_core.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""App-level routes registered directly on the Flask instance.

Extracted from app.py. Hosts /api/v1/metrics and the SPA catch-all fallback.
Blueprint-based routes live under backend/routes/ and are registered via
routes.register_blueprints().
"""


def _register_app_routes(app):
    """Register app-level routes: /metrics and SPA fallback."""
    from flask import jsonify, request, send_from_directory
    from werkzeug.security import safe_join

    @app.route("/api/v1/metrics", methods=["GET"])
    def prometheus_metrics():
        """Prometheus metrics endpoint (protected by auth hook)."""
        from flask import Response

        from config import get_settings
        from metrics import generate_metrics

        body, content_type = generate_metrics(get_settings().db_path)
        return Response(body, mimetype=content_type)

    # Pre-compressed encodings the build emits (see frontend/scripts/compress-dist.mjs),
    # in preference order. Serving these with Content-Encoding avoids per-request
    # compression CPU — important on low-power self-hosts (NAS/Pi) where shipping
    # the raw multi-hundred-KB lazy route chunks made first page loads slow.
    _ENCODINGS = (("br", ".br"), ("gzip", ".gz"))
    # Vite writes content-hashed files under /assets/ → immutable, cache hard.
    _IMMUTABLE_PREFIX = "assets/"

    def _send_static(static_dir, rel_path):
        import mimetypes
        import os

        # Prefer a pre-compressed sibling the client accepts; fall back to raw.
        accept = request.headers.get("Accept-Encoding", "")
        chosen_encoding = None
        serve_rel = rel_path
        for enc_name, suffix in _ENCODINGS:
            candidate = safe_join(static_dir, rel_path + suffix)
            if enc_name in accept and candidate and os.path.isfile(candidate):
                chosen_encoding = enc_name
                serve_rel = rel_path + suffix
                break

        # Content-Type comes from the ORIGINAL name (foo.js), never the .br/.gz.
        content_type = mimetypes.guess_type(rel_path)[0] or "application/octet-stream"
        resp = send_from_directory(static_dir, serve_rel, mimetype=content_type)
        if chosen_encoding:
            resp.headers["Content-Encoding"] = chosen_encoding
            resp.headers["Vary"] = "Accept-Encoding"
        if rel_path.startswith(_IMMUTABLE_PREFIX):
            resp.headers["Cache-Control"] = "public, max-age=31536000, immutable"
        return resp

    @app.route("/", defaults={"path": ""})
    @app.route("/<path:path>")
    def serve_spa(path):
        """Serve the React SPA frontend.

        Unknown `/api/...` paths must NOT fall through to index.html — a mistyped
        endpoint should return a JSON 404 so clients fail loudly instead of
        silently parsing an HTML/landing-page response.
        """
        import os

        if path.startswith("api/"):
            return (
                jsonify({"error": "Not found", "path": f"/{path}"}),
                404,
            )

        static_dir = app.static_folder or "static"

        # safe_join, not os.path.join: this route is reachable without login,
        # and `/..%2F..%2Fetc%2Fpasswd` must not turn the 200-vs-404 answer
        # into a probe for files outside the static folder. A path that
        # escapes it gets index.html like any other unknown client route.
        static_file = safe_join(static_dir, path) if path else None
        if static_file and os.path.isfile(static_file):
            return _send_static(static_dir, path)

        index_path = os.path.join(static_dir, "index.html")
        if os.path.exists(index_path):
            from base_path import get_base_path, inject_base_into_index

            with open(index_path, encoding="utf-8") as fh:
                html = inject_base_into_index(fh.read(), get_base_path())
            resp = app.response_class(html, mimetype="text/html")
            # index.html must always revalidate so a new deploy's hashed asset
            # references are picked up instead of a stale cached shell.
            resp.headers["Cache-Control"] = "no-cache"
            return resp

        from version import __version__

        return jsonify(
            {
                "name": "Sublarr",
                "version": __version__,
                "api": "/api/v1/health",
                "message": "Frontend not built. Run 'npm run build' in frontend/ first.",
            }
        )