From ee183e1c1cf94572d43f65db5cf198f90a7757de Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Sat, 8 Nov 2025 23:59:18 +0100 Subject: [PATCH] feat: add vector sync processing status to /user/page endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add real-time processing status display to the browser UI at /user/page showing indexed document count, pending queue size, and sync status. Implements the status display described in ADR-007 lines 280-298. Changes: - Store document_queue and related state in app.state for route access - Add _get_processing_status() helper to query Qdrant and check queue - Display status section in user_info_html() with indexed/pending counts - Show color-coded status badge (green "Idle" or orange "Syncing") - Only displays when VECTOR_SYNC_ENABLED=true Status appears in both BasicAuth and OAuth modes, positioned after session info but before logout buttons. Numbers are formatted with commas for readability. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- nextcloud_mcp_server/app.py | 16 +++ nextcloud_mcp_server/auth/userinfo_routes.py | 109 +++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index 314bf1a..6cc31af 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -1026,6 +1026,22 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): shutdown_event = anyio_module.Event() scanner_wake_event = anyio_module.Event() + # Store in app state for access from routes (ADR-007) + app.state.document_queue = document_queue + app.state.shutdown_event = shutdown_event + app.state.scanner_wake_event = scanner_wake_event + + # Also share with browser_app for /user/page route + for route in app.routes: + if isinstance(route, Mount) and route.path == "/user": + route.app.state.document_queue = document_queue + route.app.state.shutdown_event = shutdown_event + route.app.state.scanner_wake_event = scanner_wake_event + logger.info( + "Vector sync state shared with browser_app for /user/page" + ) + break + # Start background tasks using anyio TaskGroup async with anyio_module.create_task_group() as tg: # Start scanner task diff --git a/nextcloud_mcp_server/auth/userinfo_routes.py b/nextcloud_mcp_server/auth/userinfo_routes.py index f67c429..5a32b2e 100644 --- a/nextcloud_mcp_server/auth/userinfo_routes.py +++ b/nextcloud_mcp_server/auth/userinfo_routes.py @@ -19,6 +19,72 @@ from starlette.responses import HTMLResponse, JSONResponse logger = logging.getLogger(__name__) +async def _get_processing_status(request: Request) -> dict[str, Any] | None: + """Get vector sync processing status. + + Returns processing status information including indexed count, pending count, + and sync status. Only available when VECTOR_SYNC_ENABLED=true. + + Args: + request: Starlette request object + + Returns: + Dictionary with processing status, or None if vector sync is disabled + or components are unavailable: + { + "indexed_count": int, # Number of documents in Qdrant + "pending_count": int, # Number of documents in queue + "status": str, # "syncing" or "idle" + } + """ + # Check if vector sync is enabled + vector_sync_enabled = os.getenv("VECTOR_SYNC_ENABLED", "false").lower() == "true" + if not vector_sync_enabled: + return None + + try: + # Get document queue from app state + document_queue = getattr(request.app.state, "document_queue", None) + if document_queue is None: + logger.debug("document_queue not available in app state") + return None + + # Get pending count from queue + pending_count = document_queue.qsize() + + # Get Qdrant client and query indexed count + indexed_count = 0 + try: + from nextcloud_mcp_server.config import get_settings + from nextcloud_mcp_server.vector.qdrant_client import get_qdrant_client + + settings = get_settings() + qdrant_client = await get_qdrant_client() + + # Count documents in collection + count_result = await qdrant_client.count( + collection_name=settings.qdrant_collection + ) + indexed_count = count_result.count + + except Exception as e: + logger.warning(f"Failed to query Qdrant for indexed count: {e}") + # Continue with indexed_count = 0 + + # Determine status + status = "syncing" if pending_count > 0 else "idle" + + return { + "indexed_count": indexed_count, + "pending_count": pending_count, + "status": status, + } + + except Exception as e: + logger.error(f"Error getting processing status: {e}") + return None + + async def _get_userinfo_endpoint(oauth_ctx: dict[str, Any]) -> str | None: """Get the correct userinfo endpoint based on OAuth mode. @@ -224,6 +290,9 @@ async def user_info_html(request: Request) -> HTMLResponse: """ user_context = await _get_user_info(request) + # Get vector sync processing status + processing_status = await _get_processing_status(request) + # Check for error if "error" in user_context and user_context["error"] != "": # Get login URL dynamically @@ -371,6 +440,45 @@ async def user_info_html(request: Request) -> HTMLResponse: """ + # Build vector sync status HTML + 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}
+ """ + # Build IdP profile HTML idp_profile_html = "" if "idp_profile" in user_context: @@ -507,6 +615,7 @@ async def user_info_html(request: Request) -> HTMLResponse: {host_info_html} {session_info_html} + {vector_status_html} {idp_profile_html} {f'' if auth_mode == "oauth" else ""}