Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions apps/web/app/api/dashboard/activity/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,21 @@ export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const limit = parseInt(searchParams.get('limit') || '20');

// 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 eventService = AgentEventService.getInstance(projectId);
await eventService.initialize();

// Build event filter
const eventFilter: any = { limit };
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable eventFilter uses any type without justification. According to coding guidelines, any type should be avoided unless explicitly justified. Consider defining a proper type for the event filter.

Copilot uses AI. Check for mistakes.
if (projectId !== undefined) {
eventFilter.projectId = projectId;
}

// Get recent events
const events = await eventService.getEvents({
projectId,
limit,
});
const events = await eventService.getEvents(eventFilter);

return NextResponse.json({
success: true,
Expand Down
44 changes: 30 additions & 14 deletions apps/web/app/api/dashboard/stats/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable eventFilter uses any type without justification. According to coding guidelines, any type should be avoided unless explicitly justified. Consider defining a proper type for the event filter.

Copilot uses AI. Check for mistakes.
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
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable recentFilter uses any type without justification. According to coding guidelines, any type should be avoided unless explicitly justified. Consider defining a proper type for the event filter.

Copilot uses AI. Check for mistakes.
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,
Expand Down
72 changes: 72 additions & 0 deletions apps/web/app/api/events/stream/route.ts
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
},
});
}
64 changes: 64 additions & 0 deletions apps/web/app/api/sessions/[id]/events/route.ts
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 }
);
}
}
48 changes: 48 additions & 0 deletions apps/web/app/api/sessions/[id]/route.ts
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 }
);
}
}
11 changes: 7 additions & 4 deletions apps/web/app/api/sessions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable filter uses any type without justification. According to coding guidelines, any type should be avoided unless explicitly justified. Consider defining a proper type for the session filter.

Copilot uses AI. Check for mistakes.
if (projectId !== undefined) {
filter.projectId = projectId;
}
if (agentId) filter.agentId = agentId;
if (outcome) filter.outcome = outcome;
if (startTimeFrom) filter.startTimeFrom = new Date(startTimeFrom);
Expand Down
20 changes: 13 additions & 7 deletions apps/web/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,40 @@

import { Suspense } from 'react';
import { Skeleton } from '@/components/ui/skeleton';
import { DashboardStats, RecentActivity, ActiveSessions } from '@/components/agent-observability/dashboard';
import { DashboardStatsWrapper, RecentActivity, ActiveSessions } from '@/components/agent-observability/dashboard';
import { ProjectSelector } from '@/components/agent-observability/project-selector';

export default function DashboardPage() {
interface DashboardPageProps {
searchParams?: { [key: string]: string | string[] | undefined };
}

export default function DashboardPage({ searchParams }: DashboardPageProps) {
return (
<div className="container mx-auto py-6 space-y-6">
{/* Header */}
{/* Header with Project Selector */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Agent Activity Dashboard</h1>
<p className="text-muted-foreground mt-2">
Monitor AI coding agents in real-time across all your projects
</p>
</div>
<ProjectSelector />
</div>

{/* Overview Stats */}
{/* Overview Stats with Live Updates */}
<Suspense fallback={<Skeleton className="h-32 w-full" />}>
<DashboardStats />
<DashboardStatsWrapper searchParams={searchParams} />
</Suspense>

{/* Recent Activity */}
<Suspense fallback={<Skeleton className="h-64 w-full" />}>
<RecentActivity />
<RecentActivity searchParams={searchParams} />
</Suspense>

{/* Active Sessions */}
<Suspense fallback={<Skeleton className="h-32 w-full" />}>
<ActiveSessions />
<ActiveSessions searchParams={searchParams} />
</Suspense>
</div>
);
Expand Down
Loading