From be459a960dde3cd0f222f1095fc1746347b9d134 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Fri, 5 Dec 2025 02:26:51 -0500 Subject: [PATCH 1/3] feat(frontend): Add WebSocket for real-time indexing progress (#17) - Connect to /ws/index/{repo_id} with JWT token - Show real-time files processed, functions indexed, progress % - Graceful fallback to HTTP if WebSocket fails - Add WS_URL helper in config/api.ts Closes #17 --- frontend/package-lock.json | 2 +- frontend/package.json | 2 +- frontend/src/components/RepoOverview.tsx | 111 ++++++++++++++++++----- frontend/src/config/api.ts | 9 +- 4 files changed, 96 insertions(+), 28 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4dbf1ba..5617641 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -37,7 +37,7 @@ "autoprefixer": "^10.4.16", "postcss": "^8.4.32", "tailwindcss": "^3.4.0", - "typescript": "^5.2.2", + "typescript": "^5.9.3", "vite": "^4.5.0" } }, diff --git a/frontend/package.json b/frontend/package.json index 4cac773..40ea0b0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -38,7 +38,7 @@ "autoprefixer": "^10.4.16", "postcss": "^8.4.32", "tailwindcss": "^3.4.0", - "typescript": "^5.2.2", + "typescript": "^5.9.3", "vite": "^4.5.0" } } diff --git a/frontend/src/components/RepoOverview.tsx b/frontend/src/components/RepoOverview.tsx index f4729be..e3a67dc 100644 --- a/frontend/src/components/RepoOverview.tsx +++ b/frontend/src/components/RepoOverview.tsx @@ -1,7 +1,8 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { toast } from 'sonner' import { Progress } from '@/components/ui/progress' import type { Repository } from '../types' +import { WS_URL } from '../config/api' interface RepoOverviewProps { repo: Repository @@ -10,37 +11,104 @@ interface RepoOverviewProps { apiKey: string } +interface IndexProgress { + files_processed: number + functions_indexed: number + total_files: number + progress_pct: number +} + export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewProps) { const [indexing, setIndexing] = useState(false) - const [progress, setProgress] = useState(0) + const [progress, setProgress] = useState(null) + const wsRef = useRef(null) + + // Cleanup WebSocket on unmount + useEffect(() => { + return () => { + if (wsRef.current) { + wsRef.current.close() + } + } + }, []) const handleReindex = async () => { setIndexing(true) - setProgress(10) - toast.loading('Starting re-index...', { id: 'reindex' }) + setProgress({ files_processed: 0, functions_indexed: 0, total_files: 0, progress_pct: 0 }) + + // Connect to WebSocket for real-time progress + const wsUrl = `${WS_URL}/ws/index/${repo.id}?token=${apiKey}` + + try { + const ws = new WebSocket(wsUrl) + wsRef.current = ws + + ws.onopen = () => { + toast.loading('Indexing started...', { id: 'reindex' }) + } + + ws.onmessage = (event) => { + const data = JSON.parse(event.data) + + if (data.type === 'progress') { + setProgress({ + files_processed: data.files_processed, + functions_indexed: data.functions_indexed, + total_files: data.total_files, + progress_pct: data.progress_pct + }) + } else if (data.type === 'complete') { + setProgress(prev => prev ? { ...prev, progress_pct: 100 } : null) + toast.success(`Indexing complete! ${data.total_functions} functions indexed.`, { id: 'reindex' }) + setIndexing(false) + onReindex() // Refresh repo data + } else if (data.type === 'error') { + toast.error(`Indexing failed: ${data.message}`, { id: 'reindex' }) + setIndexing(false) + } + } + + ws.onerror = () => { + // WebSocket error - fall back to HTTP + toast.dismiss('reindex') + fallbackToHttp() + } + + ws.onclose = (event) => { + if (event.code !== 1000 && indexing) { + // Abnormal close while still indexing - fall back to HTTP + fallbackToHttp() + } + } + + } catch { + // WebSocket connection failed - fall back to HTTP + fallbackToHttp() + } + } + + const fallbackToHttp = async () => { + // Fallback: Use HTTP endpoint with simulated progress + toast.loading('Using fallback indexing...', { id: 'reindex' }) try { await onReindex() - toast.success('Re-indexing started!', { - id: 'reindex', - description: 'Using incremental mode - 100x faster!' - }) + toast.success('Re-indexing started!', { id: 'reindex' }) - // Simulate progress + // Simulate progress for HTTP fallback + let pct = 10 const interval = setInterval(() => { - setProgress(prev => { - if (prev >= 90) return prev - return prev + 10 - }) + pct = Math.min(pct + 10, 90) + setProgress(prev => prev ? { ...prev, progress_pct: pct } : null) }, 1000) setTimeout(() => { clearInterval(interval) - setProgress(100) + setProgress(prev => prev ? { ...prev, progress_pct: 100 } : null) setIndexing(false) }, 8000) - } catch (error) { + } catch { setIndexing(false) toast.error('Failed to start re-indexing', { id: 'reindex' }) } @@ -81,16 +149,17 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr {/* Indexing Progress */} - {indexing && ( + {indexing && progress && (

🔄 Indexing in Progress

- {progress}% + {progress.progress_pct}% +
+ +
+ Files: {progress.files_processed}/{progress.total_files || '?'} + Functions: {progress.functions_indexed}
- -

- Incremental mode - only processing changed files for 100x faster updates -

)} diff --git a/frontend/src/config/api.ts b/frontend/src/config/api.ts index 5e91bbc..a9fbb01 100644 --- a/frontend/src/config/api.ts +++ b/frontend/src/config/api.ts @@ -2,12 +2,11 @@ * API Configuration * * Centralizes API URL configuration for all frontend components. - * - * - Production: Set VITE_API_URL in Vercel dashboard to Railway backend URL - * - Development: Defaults to localhost:8000 (Docker Compose) - * - Local dev without Docker: Can override with .env.local */ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000' -export { API_URL } +// WebSocket URL - convert http(s) to ws(s) +const WS_URL = API_URL.replace(/^http/, 'ws') + +export { API_URL, WS_URL } From f1048a4aca4da3c6801eda246adc200bed4af8ef Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Fri, 5 Dec 2025 14:31:12 -0500 Subject: [PATCH 2/3] fix: Use service role key for Supabase + clear progress bar on complete - Backend: Use SUPABASE_SERVICE_ROLE_KEY to bypass RLS - Frontend: Clear progress bar after indexing complete - Frontend: Reset state on error --- backend/services/supabase_service.py | 5 +++-- frontend/src/components/RepoOverview.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/backend/services/supabase_service.py b/backend/services/supabase_service.py index a776812..75ecd67 100644 --- a/backend/services/supabase_service.py +++ b/backend/services/supabase_service.py @@ -17,10 +17,11 @@ class SupabaseService: def __init__(self): supabase_url = os.getenv("SUPABASE_URL") - supabase_key = os.getenv("SUPABASE_KEY") + # Use service role key to bypass RLS for backend operations + supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY") or os.getenv("SUPABASE_KEY") if not supabase_url or not supabase_key: - raise ValueError("SUPABASE_URL and SUPABASE_KEY must be set") + raise ValueError("SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY (or SUPABASE_KEY) must be set") # Create client with options to avoid auth cleanup issues options = ClientOptions( diff --git a/frontend/src/components/RepoOverview.tsx b/frontend/src/components/RepoOverview.tsx index e3a67dc..c7c6edd 100644 --- a/frontend/src/components/RepoOverview.tsx +++ b/frontend/src/components/RepoOverview.tsx @@ -58,13 +58,14 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr progress_pct: data.progress_pct }) } else if (data.type === 'complete') { - setProgress(prev => prev ? { ...prev, progress_pct: 100 } : null) toast.success(`Indexing complete! ${data.total_functions} functions indexed.`, { id: 'reindex' }) setIndexing(false) + setProgress(null) // Clear progress bar onReindex() // Refresh repo data } else if (data.type === 'error') { toast.error(`Indexing failed: ${data.message}`, { id: 'reindex' }) setIndexing(false) + setProgress(null) } } @@ -104,12 +105,13 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr setTimeout(() => { clearInterval(interval) - setProgress(prev => prev ? { ...prev, progress_pct: 100 } : null) + setProgress(null) // Clear progress bar setIndexing(false) }, 8000) } catch { setIndexing(false) + setProgress(null) toast.error('Failed to start re-indexing', { id: 'reindex' }) } } From 623d6705daf2c3f186f1776c7fd260c8fa64c9e5 Mon Sep 17 00:00:00 2001 From: Devanshu Rajesh Chicholikar Date: Fri, 5 Dec 2025 16:30:34 -0500 Subject: [PATCH 3/3] fix: Handle WebSocket disconnection gracefully - Wrap all websocket.send_json in try/except - Prevent crash when client disconnects during indexing - Continue indexing even if client disconnects --- backend/main.py | 38 ++++++++++++++++-------- frontend/src/components/RepoOverview.tsx | 31 +++++++++++-------- 2 files changed, 43 insertions(+), 26 deletions(-) diff --git a/backend/main.py b/backend/main.py index c06590b..2c6c4ca 100644 --- a/backend/main.py +++ b/backend/main.py @@ -413,13 +413,16 @@ async def websocket_index(websocket: WebSocket, repo_id: str): # Index with progress callback async def progress_callback(files_processed: int, functions_indexed: int, total_files: int): - await websocket.send_json({ - "type": "progress", - "files_processed": files_processed, - "functions_indexed": functions_indexed, - "total_files": total_files, - "progress_pct": int((files_processed / total_files) * 100) if total_files > 0 else 0 - }) + try: + await websocket.send_json({ + "type": "progress", + "files_processed": files_processed, + "functions_indexed": functions_indexed, + "total_files": total_files, + "progress_pct": int((files_processed / total_files) * 100) if total_files > 0 else 0 + }) + except Exception: + pass # Client disconnected, continue indexing anyway # Index repository with progress total_functions = await indexer.index_repository_with_progress( @@ -432,18 +435,27 @@ async def progress_callback(files_processed: int, functions_indexed: int, total_ repo_manager.update_file_count(repo_id, total_functions) # Send completion - await websocket.send_json({ - "type": "complete", - "total_functions": total_functions - }) + try: + await websocket.send_json({ + "type": "complete", + "total_functions": total_functions + }) + except Exception: + pass # Client disconnected except WebSocketDisconnect: print(f"WebSocket disconnected for repo {repo_id}") except Exception as e: - await websocket.send_json({"type": "error", "message": str(e)}) + try: + await websocket.send_json({"type": "error", "message": str(e)}) + except Exception: + pass # Connection already closed repo_manager.update_status(repo_id, "error") finally: - await websocket.close() + try: + await websocket.close() + except Exception: + pass # Already closed @app.post("/api/repos/{repo_id}/index") diff --git a/frontend/src/components/RepoOverview.tsx b/frontend/src/components/RepoOverview.tsx index c7c6edd..f33bafa 100644 --- a/frontend/src/components/RepoOverview.tsx +++ b/frontend/src/components/RepoOverview.tsx @@ -22,6 +22,7 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr const [indexing, setIndexing] = useState(false) const [progress, setProgress] = useState(null) const wsRef = useRef(null) + const completedRef = useRef(false) // Track if indexing completed successfully // Cleanup WebSocket on unmount useEffect(() => { @@ -35,6 +36,7 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr const handleReindex = async () => { setIndexing(true) setProgress({ files_processed: 0, functions_indexed: 0, total_files: 0, progress_pct: 0 }) + completedRef.current = false // Connect to WebSocket for real-time progress const wsUrl = `${WS_URL}/ws/index/${repo.id}?token=${apiKey}` @@ -58,11 +60,13 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr progress_pct: data.progress_pct }) } else if (data.type === 'complete') { + completedRef.current = true toast.success(`Indexing complete! ${data.total_functions} functions indexed.`, { id: 'reindex' }) setIndexing(false) - setProgress(null) // Clear progress bar - onReindex() // Refresh repo data + setProgress(null) + onReindex() } else if (data.type === 'error') { + completedRef.current = true toast.error(`Indexing failed: ${data.message}`, { id: 'reindex' }) setIndexing(false) setProgress(null) @@ -70,33 +74,33 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr } ws.onerror = () => { - // WebSocket error - fall back to HTTP - toast.dismiss('reindex') - fallbackToHttp() + if (!completedRef.current) { + toast.dismiss('reindex') + fallbackToHttp() + } } - ws.onclose = (event) => { - if (event.code !== 1000 && indexing) { - // Abnormal close while still indexing - fall back to HTTP + ws.onclose = () => { + // Only fallback if we didn't complete successfully + if (!completedRef.current) { fallbackToHttp() } } } catch { - // WebSocket connection failed - fall back to HTTP fallbackToHttp() } } const fallbackToHttp = async () => { - // Fallback: Use HTTP endpoint with simulated progress + if (completedRef.current) return // Already completed + toast.loading('Using fallback indexing...', { id: 'reindex' }) try { await onReindex() toast.success('Re-indexing started!', { id: 'reindex' }) - // Simulate progress for HTTP fallback let pct = 10 const interval = setInterval(() => { pct = Math.min(pct + 10, 90) @@ -105,8 +109,9 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr setTimeout(() => { clearInterval(interval) - setProgress(null) // Clear progress bar + setProgress(null) setIndexing(false) + completedRef.current = true }, 8000) } catch { @@ -150,7 +155,7 @@ export function RepoOverview({ repo, onReindex, apiUrl, apiKey }: RepoOverviewPr - {/* Indexing Progress */} + {/* Indexing Progress - only show when indexing AND progress exists */} {indexing && progress && (