+
-
-
+
+
-
-
+
+
+
-
-
-
Advanced Options
-
-
+
+
-
-
-
-
-
-
-
- BM25 Hybrid Search: Combines dense semantic vectors with sparse BM25 keyword vectors.
-
-
- RRF: Reciprocal Rank Fusion - Rank-based fusion producing scores in [0.0, 1.0]
-
-
- DBSF: Distribution-Based Score Fusion - Sums normalized scores (can exceed 1.0)
-
-
-
-
-
-
-
-
-
- Executing search and computing PCA projection...
-
-
+
-
-
-
Search Results ()
+
+
+
+
+ Executing search and computing PCA projection...
+
+
+
+
+
+
+
+
Search Results ()
Loading results...
@@ -335,5 +342,6 @@
-
-
+
+
+
diff --git a/nextcloud_mcp_server/auth/userinfo_routes.py b/nextcloud_mcp_server/auth/userinfo_routes.py
index d57806c..c84c039 100644
--- a/nextcloud_mcp_server/auth/userinfo_routes.py
+++ b/nextcloud_mcp_server/auth/userinfo_routes.py
@@ -9,15 +9,21 @@ For OAuth mode: Requires browser-based OAuth login to establish session.
import logging
import os
+from pathlib import Path
from typing import Any
import httpx
+from jinja2 import Environment, FileSystemLoader
from starlette.authentication import requires
from starlette.requests import Request
from starlette.responses import HTMLResponse, JSONResponse
logger = logging.getLogger(__name__)
+# Setup Jinja2 environment for templates
+_template_dir = Path(__file__).parent / "templates"
+_jinja_env = Environment(loader=FileSystemLoader(_template_dir))
+
async def _get_authenticated_client_for_userinfo(request: Request) -> httpx.AsyncClient:
"""Get an authenticated HTTP client for user info page operations.
@@ -431,51 +437,14 @@ async def user_info_html(request: Request) -> HTMLResponse:
oauth_ctx = getattr(request.app.state, "oauth_context", None)
login_url = str(request.url_for("oauth_login")) if oauth_ctx else "/oauth/login"
- error_html = f"""
-
-
-
-
-
-
Error - Nextcloud MCP Server
-
-
-
-
-
Error Retrieving User Info
-
- Error: {user_context["error"]}
-
-
Login again
-
-
-
- """
- return HTMLResponse(content=error_html)
+ template = _jinja_env.get_template("error.html")
+ return HTMLResponse(
+ content=template.render(
+ error_title="Error Retrieving User Info",
+ error_message=user_context["error"],
+ login_url=login_url,
+ )
+ )
# Build HTML response
auth_mode = user_context.get("auth_mode", "unknown")
@@ -654,457 +623,19 @@ async def user_info_html(request: Request) -> HTMLResponse:
"""
- html_content = f"""
-
-
-
-
-
-
Nextcloud MCP Server
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Nextcloud MCP Server
-
-
-
-
- {
- ""
- if not show_vector_sync_tab
- else '''
-
- '''
- }
- {
- ""
- if not show_vector_sync_tab
- else '''
-
- '''
- }
- {
- ""
- if not show_webhooks_tab
- else '''
-
- '''
- }
-
-
-
-
-
-
- {user_info_tab_html}
-
-
- {
- ""
- if not show_vector_sync_tab
- else f'''
-
-
- {vector_sync_tab_html}
-
- '''
- }
-
- {
- ""
- if not show_vector_sync_tab
- else '''
-
-
-
-
Loading vector visualization...
-
-
- '''
- }
-
- {
- ""
- if not show_webhooks_tab
- else f'''
-
-
- {webhooks_tab_html}
-
- '''
- }
-
-
- {
- f'
'
- if auth_mode == "oauth"
- else ""
- }
-
-
-
- """
-
- return HTMLResponse(content=html_content)
+ # Render template
+ template = _jinja_env.get_template("user_info.html")
+ return HTMLResponse(
+ content=template.render(
+ user_info_tab_html=user_info_tab_html,
+ vector_sync_tab_html=vector_sync_tab_html,
+ webhooks_tab_html=webhooks_tab_html,
+ show_vector_sync_tab=show_vector_sync_tab,
+ show_webhooks_tab=show_webhooks_tab,
+ logout_url=logout_url if auth_mode == "oauth" else None,
+ nextcloud_host_for_links=nextcloud_host_for_links,
+ )
+ )
@requires("authenticated", redirect="oauth_login")
@@ -1124,17 +655,12 @@ async def revoke_session(request: Request) -> HTMLResponse:
oauth_ctx = getattr(request.app.state, "oauth_context", None)
if not oauth_ctx:
+ template = _jinja_env.get_template("error.html")
return HTMLResponse(
- """
-
-
-
Error
-
-
Error
-
OAuth mode not enabled
-
-
- """,
+ content=template.render(
+ error_title="Error",
+ error_message="OAuth mode not enabled",
+ ),
status_code=400,
)
@@ -1142,17 +668,12 @@ async def revoke_session(request: Request) -> HTMLResponse:
session_id = request.cookies.get("mcp_session")
if not storage or not session_id:
+ template = _jinja_env.get_template("error.html")
return HTMLResponse(
- """
-
-
-
Error
-
-
Error
-
Session not found
-
-
- """,
+ content=template.render(
+ error_title="Error",
+ error_message="Session not found",
+ ),
status_code=400,
)
@@ -1165,57 +686,26 @@ async def revoke_session(request: Request) -> HTMLResponse:
# Redirect back to user page
user_page_url = str(request.url_for("user_info_html"))
+ template = _jinja_env.get_template("success.html")
return HTMLResponse(
- f"""
-
-
-
-
-
-
Background Access Revoked
-
-
-
-
-
✓ Background Access Revoked
-
Your refresh token has been deleted successfully.
-
Browser session remains active.
-
Redirecting back to user page...
-
-
-
- """
+ content=template.render(
+ success_title="✓ Background Access Revoked",
+ success_messages=[
+ "Your refresh token has been deleted successfully.",
+ "Browser session remains active.",
+ ],
+ redirect_url=user_page_url,
+ redirect_delay=2,
+ )
)
except Exception as e:
logger.error(f"Failed to revoke background access: {e}")
+ template = _jinja_env.get_template("error.html")
return HTMLResponse(
- f"""
-
-
-
Error
-
-
Error
-
Failed to revoke background access: {e}
-
-
- """,
+ content=template.render(
+ error_title="Error",
+ error_message=f"Failed to revoke background access: {e}",
+ ),
status_code=500,
)