From 12c96af819476fb9dc76514e590573aaa5120fdc Mon Sep 17 00:00:00 2001 From: Chris Coutinho Date: Tue, 11 Nov 2025 21:04:31 +0100 Subject: [PATCH] feat: add dynamic vector sync status updates with htmx polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement real-time vector sync status updates in the /app UI without requiring page refreshes. The status (indexed documents, pending documents, sync state) now updates automatically every 3 seconds. Changes: - Add vector_sync_status_fragment() endpoint that returns HTML fragment with current vector sync status - Modify user_info_html() to use htmx loading for vector sync section with hx-trigger="load" on initial render - Status fragment includes hx-trigger="every 3s" for continuous polling - Add /app/vector-sync/status route to browser_routes The implementation uses htmx (already loaded on page) to poll the status endpoint, providing near real-time updates with minimal overhead. The endpoint queries Qdrant for indexed count and reads from memory streams for pending count, returning only the status HTML fragment. Pattern follows existing webhook management UI which also uses htmx for dynamic loading. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- nextcloud_mcp_server/app.py | 7 ++ nextcloud_mcp_server/auth/userinfo_routes.py | 107 +++++++++++++------ 2 files changed, 79 insertions(+), 35 deletions(-) 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