-
Notifications
You must be signed in to change notification settings - Fork 0
Implement Phase 2 Interactive Features: Session Details Page and Real-Time Updates via SSE #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
43dd742
b06938f
4e04fd7
39d5058
c7792df
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,9 +13,12 @@ import { AgentSessionService, AgentEventService } from '@codervisor/devlog-core/ | |
|
|
||
| export async function GET(request: NextRequest) { | ||
| try { | ||
| // Get all projects (for now, using projectId 1 as default) | ||
| // TODO: Query across all user's projects | ||
| const projectId = 1; | ||
| const searchParams = request.nextUrl.searchParams; | ||
|
|
||
| // Support optional projectId parameter | ||
| // If not provided, query across all projects (pass undefined) | ||
| const projectIdParam = searchParams.get('projectId'); | ||
| const projectId = projectIdParam ? parseInt(projectIdParam) : undefined; | ||
|
|
||
| const sessionService = AgentSessionService.getInstance(projectId); | ||
| const eventService = AgentEventService.getInstance(projectId); | ||
|
|
@@ -34,28 +37,41 @@ export async function GET(request: NextRequest) { | |
| const tomorrow = new Date(today); | ||
| tomorrow.setDate(tomorrow.getDate() + 1); | ||
|
|
||
| // Get events from today | ||
| const todayEvents = await eventService.getEvents({ | ||
| projectId, | ||
| // Build event filter | ||
| const eventFilter: any = { | ||
| startTime: today, | ||
| endTime: tomorrow, | ||
| }); | ||
| }; | ||
|
Comment on lines
+41
to
+44
|
||
| if (projectId !== undefined) { | ||
| eventFilter.projectId = projectId; | ||
| } | ||
|
|
||
| // Get events from today | ||
| const todayEvents = await eventService.getEvents(eventFilter); | ||
|
|
||
| // Calculate events per minute (based on last hour) | ||
| const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000); | ||
| const recentEvents = await eventService.getEvents({ | ||
| projectId, | ||
| const recentFilter: any = { | ||
| startTime: oneHourAgo, | ||
| }); | ||
| }; | ||
|
Comment on lines
+54
to
+56
|
||
| if (projectId !== undefined) { | ||
| recentFilter.projectId = projectId; | ||
| } | ||
| const recentEvents = await eventService.getEvents(recentFilter); | ||
| const eventsPerMinute = recentEvents.length > 0 | ||
| ? (recentEvents.length / 60).toFixed(2) | ||
| : '0'; | ||
|
|
||
| // Get session stats for average duration | ||
| const sessionStats = await sessionService.getSessionStats({ | ||
| projectId, | ||
| // Build session stats filter | ||
| const statsFilter: any = { | ||
| startTimeFrom: today, | ||
| }); | ||
| }; | ||
| if (projectId !== undefined) { | ||
| statsFilter.projectId = projectId; | ||
| } | ||
|
|
||
| // Get session stats for average duration | ||
| const sessionStats = await sessionService.getSessionStats(statsFilter); | ||
|
|
||
| return NextResponse.json({ | ||
| success: true, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| /** | ||
| * Server-Sent Events (SSE) endpoint for real-time updates | ||
| * | ||
| * Provides a persistent connection that streams updates about: | ||
| * - New agent sessions | ||
| * - Session status changes | ||
| * - New agent events | ||
| * - Dashboard metrics updates | ||
| */ | ||
|
|
||
| import { NextRequest } from 'next/server'; | ||
| import { EventBroadcaster } from '@/lib/realtime/event-broadcaster'; | ||
|
|
||
| // Mark this route as dynamic to prevent static generation | ||
| export const dynamic = 'force-dynamic'; | ||
| export const runtime = 'nodejs'; | ||
|
|
||
| // Keep-alive interval in milliseconds | ||
| const KEEP_ALIVE_INTERVAL = 30000; // 30 seconds | ||
|
|
||
| export async function GET(request: NextRequest) { | ||
| const broadcaster = EventBroadcaster.getInstance(); | ||
|
|
||
| // Create a readable stream for SSE | ||
| const stream = new ReadableStream({ | ||
| start(controller) { | ||
| const encoder = new TextEncoder(); | ||
|
|
||
| // Send initial connection message | ||
| const connectionMessage = `event: connected\ndata: ${JSON.stringify({ | ||
| timestamp: new Date().toISOString(), | ||
| clientCount: broadcaster.getClientCount() + 1 | ||
| })}\n\n`; | ||
| controller.enqueue(encoder.encode(connectionMessage)); | ||
|
|
||
| // Add this client to the broadcaster | ||
| broadcaster.addClient(controller); | ||
|
|
||
| // Set up keep-alive heartbeat | ||
| const keepAliveInterval = setInterval(() => { | ||
| try { | ||
| const heartbeat = `: heartbeat ${Date.now()}\n\n`; | ||
| controller.enqueue(encoder.encode(heartbeat)); | ||
| } catch (error) { | ||
| console.error('Error sending heartbeat:', error); | ||
| clearInterval(keepAliveInterval); | ||
| broadcaster.removeClient(controller); | ||
| } | ||
| }, KEEP_ALIVE_INTERVAL); | ||
|
|
||
| // Clean up when client disconnects | ||
| request.signal.addEventListener('abort', () => { | ||
| clearInterval(keepAliveInterval); | ||
| broadcaster.removeClient(controller); | ||
| try { | ||
| controller.close(); | ||
| } catch (error) { | ||
| // Controller already closed | ||
| } | ||
| }); | ||
| }, | ||
| }); | ||
|
|
||
| return new Response(stream, { | ||
| headers: { | ||
| 'Content-Type': 'text/event-stream', | ||
| 'Cache-Control': 'no-cache, no-transform', | ||
| 'Connection': 'keep-alive', | ||
| 'X-Accel-Buffering': 'no', // Disable nginx buffering | ||
| }, | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| /** | ||
| * API endpoint for session events timeline | ||
| * | ||
| * Returns all events associated with a specific session | ||
| */ | ||
|
|
||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { AgentEventService } from '@codervisor/devlog-core/server'; | ||
|
|
||
| export async function GET( | ||
| request: NextRequest, | ||
| { params }: { params: { id: string } } | ||
| ) { | ||
| try { | ||
| const { id: sessionId } = params; | ||
| const searchParams = request.nextUrl.searchParams; | ||
|
|
||
| // Parse query parameters | ||
| const eventType = searchParams.get('eventType') || undefined; | ||
| const severity = searchParams.get('severity') || undefined; | ||
| const limit = parseInt(searchParams.get('limit') || '100'); | ||
| const offset = parseInt(searchParams.get('offset') || '0'); | ||
|
|
||
| // Get event service | ||
| const eventService = AgentEventService.getInstance(); | ||
| await eventService.initialize(); | ||
|
|
||
| // Get events for this session | ||
| const allEvents = await eventService.getEventsBySession(sessionId); | ||
|
|
||
| // Apply additional filters | ||
| let filteredEvents = allEvents; | ||
|
|
||
| if (eventType) { | ||
| filteredEvents = filteredEvents.filter(e => e.type === eventType); | ||
| } | ||
|
|
||
| if (severity) { | ||
| filteredEvents = filteredEvents.filter(e => e.severity === severity); | ||
| } | ||
|
|
||
| // Apply pagination | ||
| const paginatedEvents = filteredEvents.slice(offset, offset + limit); | ||
|
|
||
| return NextResponse.json({ | ||
| success: true, | ||
| data: paginatedEvents, | ||
| pagination: { | ||
| limit, | ||
| offset, | ||
| total: filteredEvents.length, | ||
| }, | ||
| }); | ||
| } catch (error) { | ||
| console.error('Error fetching session events:', error); | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : 'Failed to fetch session events', | ||
| }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| /** | ||
| * API endpoint for individual session details | ||
| * | ||
| * Returns complete session information including context, metrics, and outcome | ||
| */ | ||
|
|
||
| import { NextRequest, NextResponse } from 'next/server'; | ||
| import { AgentSessionService } from '@codervisor/devlog-core/server'; | ||
|
|
||
| export async function GET( | ||
| request: NextRequest, | ||
| { params }: { params: { id: string } } | ||
| ) { | ||
| try { | ||
| const { id } = params; | ||
|
|
||
| // Get session service (projectId not required for getSession) | ||
| const sessionService = AgentSessionService.getInstance(); | ||
| await sessionService.initialize(); | ||
|
|
||
| // Get session by ID | ||
| const session = await sessionService.getSession(id); | ||
|
|
||
| if (!session) { | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: `Session not found: ${id}`, | ||
| }, | ||
| { status: 404 } | ||
| ); | ||
| } | ||
|
|
||
| return NextResponse.json({ | ||
| success: true, | ||
| data: session, | ||
| }); | ||
| } catch (error) { | ||
| console.error('Error fetching session details:', error); | ||
| return NextResponse.json( | ||
| { | ||
| success: false, | ||
| error: error instanceof Error ? error.message : 'Failed to fetch session details', | ||
| }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,15 +20,18 @@ export async function GET(request: NextRequest) { | |
| const limit = parseInt(searchParams.get('limit') || '50'); | ||
| const offset = parseInt(searchParams.get('offset') || '0'); | ||
|
|
||
| // Get all projects (for now, using projectId 1 as default) | ||
| // TODO: Query across all user's projects | ||
| const projectId = 1; | ||
| // Support optional projectId parameter | ||
| const projectIdParam = searchParams.get('projectId'); | ||
| const projectId = projectIdParam ? parseInt(projectIdParam) : undefined; | ||
|
|
||
| const sessionService = AgentSessionService.getInstance(projectId); | ||
| await sessionService.initialize(); | ||
|
|
||
| // Build filter | ||
| const filter: any = { projectId, limit, offset }; | ||
| const filter: any = { limit, offset }; | ||
|
||
| if (projectId !== undefined) { | ||
| filter.projectId = projectId; | ||
| } | ||
| if (agentId) filter.agentId = agentId; | ||
| if (outcome) filter.outcome = outcome; | ||
| if (startTimeFrom) filter.startTimeFrom = new Date(startTimeFrom); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The variable
eventFilterusesanytype without justification. According to coding guidelines,anytype should be avoided unless explicitly justified. Consider defining a proper type for the event filter.