Skip to content

Commit 61e7252

Browse files
authored
Merge pull request #55 from codervisor:copilot/continue-implementing-week-4-mvp
Week 4 MVP: Hierarchy navigation UI, cascading filters, and machine activity dashboard
2 parents afe1179 + db6e00e commit 61e7252

File tree

15 files changed

+1776
-0
lines changed

15 files changed

+1776
-0
lines changed
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Machine Activity Stats API
3+
*
4+
* GET /api/stats/machine-activity
5+
* Returns aggregated activity statistics by machine
6+
*/
7+
8+
import { NextRequest, NextResponse } from 'next/server';
9+
import { z } from 'zod';
10+
import { PrismaClient } from '@prisma/client';
11+
12+
const QuerySchema = z.object({
13+
projectId: z.coerce.number().int().positive().optional(),
14+
});
15+
16+
export async function GET(req: NextRequest) {
17+
try {
18+
const searchParams = Object.fromEntries(req.nextUrl.searchParams);
19+
const query = QuerySchema.parse(searchParams);
20+
21+
const prisma = new PrismaClient();
22+
23+
try {
24+
// Aggregate activity by machine
25+
const machines = await prisma.machine.findMany({
26+
where: query.projectId ? {
27+
workspaces: {
28+
some: {
29+
projectId: query.projectId,
30+
},
31+
},
32+
} : undefined,
33+
include: {
34+
workspaces: {
35+
where: query.projectId ? {
36+
projectId: query.projectId,
37+
} : undefined,
38+
include: {
39+
chatSessions: {
40+
select: {
41+
id: true,
42+
},
43+
},
44+
},
45+
},
46+
},
47+
});
48+
49+
// Get event counts for each machine
50+
const machineActivity = await Promise.all(
51+
machines.map(async (machine) => {
52+
const workspaceIds = machine.workspaces.map(w => w.id);
53+
54+
const eventCount = await prisma.agentEvent.count({
55+
where: {
56+
chatSession: {
57+
workspaceId: {
58+
in: workspaceIds,
59+
},
60+
},
61+
},
62+
});
63+
64+
const sessionCount = machine.workspaces.reduce(
65+
(sum, w) => sum + w.chatSessions.length,
66+
0
67+
);
68+
69+
return {
70+
hostname: machine.hostname,
71+
machineType: machine.machineType,
72+
sessionCount,
73+
eventCount,
74+
workspaceCount: machine.workspaces.length,
75+
};
76+
})
77+
);
78+
79+
return NextResponse.json({
80+
success: true,
81+
data: machineActivity,
82+
meta: {
83+
timestamp: new Date().toISOString(),
84+
},
85+
});
86+
} finally {
87+
await prisma.$disconnect();
88+
}
89+
} catch (error) {
90+
console.error('[API] Machine activity error:', error);
91+
92+
if (error instanceof z.ZodError) {
93+
return NextResponse.json(
94+
{
95+
success: false,
96+
error: {
97+
code: 'VALIDATION_FAILED',
98+
message: 'Invalid query parameters',
99+
details: error.errors,
100+
},
101+
meta: {
102+
timestamp: new Date().toISOString(),
103+
},
104+
},
105+
{ status: 422 }
106+
);
107+
}
108+
109+
return NextResponse.json(
110+
{
111+
success: false,
112+
error: {
113+
code: 'INTERNAL_ERROR',
114+
message: error instanceof Error ? error.message : 'Unknown error',
115+
},
116+
meta: {
117+
timestamp: new Date().toISOString(),
118+
},
119+
},
120+
{ status: 500 }
121+
);
122+
}
123+
}

apps/web/app/dashboard/page.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@ import { Suspense } from 'react';
88
import { Skeleton } from '@/components/ui/skeleton';
99
import { DashboardStatsWrapper, RecentActivity, ActiveSessions } from '@/components/agent-observability/dashboard';
1010
import { ProjectSelector } from '@/components/agent-observability/project-selector';
11+
import { HierarchyFilter } from '@/components/agent-observability/hierarchy';
12+
import { MachineActivityWidget } from '@/components/agent-observability/widgets';
1113

1214
interface DashboardPageProps {
1315
searchParams?: { [key: string]: string | string[] | undefined };
1416
}
1517

1618
export default function DashboardPage({ searchParams }: DashboardPageProps) {
19+
const projectId = searchParams?.projectId
20+
? parseInt(Array.isArray(searchParams.projectId) ? searchParams.projectId[0] : searchParams.projectId)
21+
: undefined;
22+
1723
return (
1824
<div className="container mx-auto py-6 space-y-6">
1925
{/* Header with Project Selector */}
@@ -27,11 +33,20 @@ export default function DashboardPage({ searchParams }: DashboardPageProps) {
2733
<ProjectSelector />
2834
</div>
2935

36+
{/* Hierarchy Filter */}
37+
<div className="flex items-center gap-2">
38+
<span className="text-sm text-muted-foreground">Filter by:</span>
39+
<HierarchyFilter />
40+
</div>
41+
3042
{/* Overview Stats with Live Updates */}
3143
<Suspense fallback={<Skeleton className="h-32 w-full" />}>
3244
<DashboardStatsWrapper searchParams={searchParams} />
3345
</Suspense>
3446

47+
{/* Machine Activity Widget */}
48+
<MachineActivityWidget projectId={projectId} />
49+
3550
{/* Recent Activity */}
3651
<Suspense fallback={<Skeleton className="h-64 w-full" />}>
3752
<RecentActivity searchParams={searchParams} />

apps/web/app/projects/[name]/agent-sessions/page.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
*/
66

77
import { Suspense } from 'react';
8+
import Link from 'next/link';
9+
import { Network } from 'lucide-react';
810
import { SessionList } from '@/components/agent-observability/agent-sessions/session-list';
911
import { ActiveSessionsPanel } from '@/components/agent-observability/agent-sessions/active-sessions-panel';
12+
import { Button } from '@/components/ui/button';
1013

1114
export default function AgentSessionsPage({ params }: { params: { name: string } }) {
1215
return (
@@ -18,6 +21,12 @@ export default function AgentSessionsPage({ params }: { params: { name: string }
1821
Monitor and analyze AI coding agent activities for {params.name}
1922
</p>
2023
</div>
24+
<Link href={`/projects/${params.name}/hierarchy`}>
25+
<Button variant="outline" className="gap-2">
26+
<Network className="w-4 h-4" />
27+
View Hierarchy
28+
</Button>
29+
</Link>
2130
</div>
2231

2332
{/* Active Sessions Panel */}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Project Hierarchy Page
3+
*
4+
* Displays the complete project hierarchy with machines, workspaces, and sessions
5+
*/
6+
7+
import { notFound } from 'next/navigation';
8+
import Link from 'next/link';
9+
import { ChevronLeft } from 'lucide-react';
10+
import { HierarchyTree } from '@/components/agent-observability/hierarchy';
11+
import { Card } from '@/components/ui/card';
12+
import { Button } from '@/components/ui/button';
13+
import { ProjectService } from '@codervisor/devlog-core';
14+
import { HierarchyService } from '@codervisor/devlog-core';
15+
16+
interface ProjectHierarchyPageProps {
17+
params: { name: string };
18+
}
19+
20+
export default async function ProjectHierarchyPage({
21+
params,
22+
}: ProjectHierarchyPageProps) {
23+
// Initialize services
24+
const projectService = ProjectService.getInstance();
25+
const hierarchyService = HierarchyService.getInstance();
26+
27+
await projectService.initialize();
28+
await hierarchyService.initialize();
29+
30+
// Fetch project by full name
31+
const project = await projectService.getProjectByFullName(params.name);
32+
33+
if (!project) {
34+
notFound();
35+
}
36+
37+
// Fetch hierarchy data
38+
const hierarchy = await hierarchyService.getProjectHierarchy(project.id);
39+
40+
return (
41+
<div className="container mx-auto py-6 space-y-6">
42+
{/* Navigation */}
43+
<div>
44+
<Link href={`/projects/${params.name}`}>
45+
<Button variant="ghost" className="gap-2">
46+
<ChevronLeft className="w-4 h-4" />
47+
Back to Project
48+
</Button>
49+
</Link>
50+
</div>
51+
52+
{/* Header */}
53+
<div className="mb-6">
54+
<h1 className="text-3xl font-bold">{hierarchy.project.fullName}</h1>
55+
{hierarchy.project.description && (
56+
<p className="text-muted-foreground mt-2">{hierarchy.project.description}</p>
57+
)}
58+
59+
{/* Project metadata */}
60+
<div className="flex gap-4 mt-4 text-sm text-muted-foreground">
61+
{hierarchy.project.repoUrl && (
62+
<a
63+
href={hierarchy.project.repoUrl}
64+
target="_blank"
65+
rel="noopener noreferrer"
66+
className="hover:text-foreground transition-colors"
67+
>
68+
View Repository →
69+
</a>
70+
)}
71+
<span>
72+
{hierarchy.machines.length} {hierarchy.machines.length === 1 ? 'machine' : 'machines'}
73+
</span>
74+
<span>
75+
{hierarchy.machines.reduce((sum, m) => sum + m.workspaces.length, 0)} workspaces
76+
</span>
77+
</div>
78+
</div>
79+
80+
{/* Hierarchy Tree */}
81+
{hierarchy.machines.length === 0 ? (
82+
<Card className="p-8 text-center">
83+
<p className="text-muted-foreground">
84+
No machines or workspaces detected yet.
85+
</p>
86+
<p className="text-sm text-muted-foreground mt-2">
87+
Install the devlog collector to start tracking activity for this project.
88+
</p>
89+
</Card>
90+
) : (
91+
<HierarchyTree hierarchy={hierarchy} />
92+
)}
93+
</div>
94+
);
95+
}

0 commit comments

Comments
 (0)