diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index c8909f7..ecf6504 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -1410,6 +1410,7 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): from nextcloud_mcp_server.auth.userinfo_routes import ( revoke_session, user_info_html, + vector_sync_status_fragment, ) from nextcloud_mcp_server.auth.webhook_routes import ( disable_webhook_preset, @@ -1424,6 +1425,12 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): Route( "/revoke", revoke_session, methods=["POST"], name="revoke_session_endpoint" ), # /app/revoke → revoke_session + # Vector sync status fragment (htmx polling) + Route( + "/vector-sync/status", + vector_sync_status_fragment, + methods=["GET"], + ), # /app/vector-sync/status # Webhook management routes (admin-only) Route("/webhooks", webhook_management_pane, methods=["GET"]), # /app/webhooks Route( diff --git a/nextcloud_mcp_server/auth/userinfo_routes.py b/nextcloud_mcp_server/auth/userinfo_routes.py index 0aff68e..8d19fa1 100644 --- a/nextcloud_mcp_server/auth/userinfo_routes.py +++ b/nextcloud_mcp_server/auth/userinfo_routes.py @@ -139,6 +139,72 @@ async def _get_processing_status(request: Request) -> dict[str, Any] | None: return None +@requires("authenticated", redirect="oauth_login") +async def vector_sync_status_fragment(request: Request) -> HTMLResponse: + """Vector sync status fragment endpoint - returns HTML fragment with current status. + + This endpoint is polled by htmx to provide real-time updates of vector sync processing + status without requiring a full page refresh. + + Requires authentication via session cookie (redirects to oauth_login route if not authenticated). + + Args: + request: Starlette request object + + Returns: + HTML response with vector sync status table fragment + """ + processing_status = await _get_processing_status(request) + + # If vector sync is disabled or unavailable, return empty fragment + if not processing_status: + return HTMLResponse( + """ +
+

Vector sync not available

+
+ """ + ) + + indexed_count = processing_status["indexed_count"] + pending_count = processing_status["pending_count"] + status = processing_status["status"] + + # Format numbers with commas for readability + indexed_count_str = f"{indexed_count:,}" + pending_count_str = f"{pending_count:,}" + + # Status badge color and text + if status == "syncing": + status_badge = ( + '⟳ Syncing' + ) + else: + status_badge = '✓ Idle' + + html = f""" +
+

Vector Sync Status

+ + + + + + + + + + + + + +
Indexed Documents{indexed_count_str}
Pending Documents{pending_count_str}
Status{status_badge}
+
+ """ + + return HTMLResponse(html) + + async def _get_userinfo_endpoint(oauth_ctx: dict[str, Any]) -> str | None: """Get the correct userinfo endpoint based on OAuth mode. @@ -507,43 +573,14 @@ async def user_info_html(request: Request) -> HTMLResponse: """ - # Build vector sync status HTML + # Build vector sync status HTML (with htmx auto-refresh) vector_status_html = "" if processing_status: - indexed_count = processing_status["indexed_count"] - pending_count = processing_status["pending_count"] - status = processing_status["status"] - - # Format numbers with commas for readability - indexed_count_str = f"{indexed_count:,}" - pending_count_str = f"{pending_count:,}" - - # Status badge color and text - if status == "syncing": - status_badge = ( - '⟳ Syncing' - ) - else: - status_badge = ( - '✓ Idle' - ) - - vector_status_html = f""" -

Vector Sync Status

- - - - - - - - - - - - - -
Indexed Documents{indexed_count_str}
Pending Documents{pending_count_str}
Status{status_badge}
+ # Use htmx to load and auto-refresh the status fragment + vector_status_html = """ +
+

Loading vector sync status...

+
""" # Build IdP profile HTML