Skip to content

Commit d6933b2

Browse files
author
Marvin Zhang
committed
feat: Implement dependency graph API for multi-project mode and enhance spec detail page
1 parent b0d6926 commit d6933b2

File tree

6 files changed

+324
-209
lines changed

6 files changed

+324
-209
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* GET /api/projects/[id]/specs/[spec]/dependency-graph - Get complete dependency graph for a spec (multi-project mode)
3+
*
4+
* Returns upstream dependencies, downstream dependents for the specified spec
5+
* in multi-project mode where specs are loaded from project registries.
6+
*/
7+
8+
import { NextResponse } from 'next/server';
9+
import { getSpecById, getSpecsWithMetadata } from '@/lib/db/service-queries';
10+
11+
/**
12+
* Helper to extract title from name
13+
*/
14+
function getTitle(name: string): string {
15+
const parts = name.split('-');
16+
if (parts.length > 1) {
17+
return parts.slice(1).join('-').replace(/-/g, ' ');
18+
}
19+
return name;
20+
}
21+
22+
export async function GET(
23+
request: Request,
24+
{ params }: { params: Promise<{ id: string; spec: string }> }
25+
) {
26+
try {
27+
const { id: projectId, spec: specId } = await params;
28+
29+
// Get the target spec
30+
const spec = await getSpecById(specId, projectId);
31+
if (!spec) {
32+
return NextResponse.json(
33+
{ error: 'Spec not found' },
34+
{ status: 404 }
35+
);
36+
}
37+
38+
// Get all specs with metadata to build the dependency graph
39+
const allSpecs = await getSpecsWithMetadata(projectId);
40+
41+
// Build lookup maps
42+
const specByName = new Map<string, typeof allSpecs[0]>();
43+
const specByNumber = new Map<string, typeof allSpecs[0]>();
44+
45+
for (const s of allSpecs) {
46+
specByName.set(s.specName, s);
47+
if (s.specNumber) {
48+
specByNumber.set(s.specNumber.toString(), s);
49+
specByNumber.set(s.specNumber.toString().padStart(3, '0'), s);
50+
}
51+
}
52+
53+
// Find this spec's relationships
54+
const currentSpec = allSpecs.find(s =>
55+
s.specNumber?.toString() === specId ||
56+
s.specNumber?.toString().padStart(3, '0') === specId ||
57+
s.specName === specId ||
58+
s.id === specId
59+
);
60+
61+
if (!currentSpec) {
62+
return NextResponse.json(
63+
{ error: 'Spec not found in project' },
64+
{ status: 404 }
65+
);
66+
}
67+
68+
// Resolve dependsOn specs
69+
const dependsOnSpecs = currentSpec.relationships.dependsOn
70+
.map(dep => {
71+
const match = dep.match(/^(\d+)/);
72+
if (match) {
73+
return specByNumber.get(match[1]) || specByNumber.get(match[1].padStart(3, '0'));
74+
}
75+
return specByName.get(dep);
76+
})
77+
.filter((s): s is typeof allSpecs[0] => s !== undefined);
78+
79+
// Resolve requiredBy specs
80+
const requiredBySpecs = currentSpec.relationships.requiredBy
81+
.map(dep => {
82+
const match = dep.match(/^(\d+)/);
83+
if (match) {
84+
return specByNumber.get(match[1]) || specByNumber.get(match[1].padStart(3, '0'));
85+
}
86+
return specByName.get(dep);
87+
})
88+
.filter((s): s is typeof allSpecs[0] => s !== undefined);
89+
90+
// Format response with simplified spec metadata
91+
const response = {
92+
current: {
93+
id: spec.id,
94+
specNumber: spec.specNumber,
95+
specName: spec.specName,
96+
title: spec.title,
97+
status: spec.status,
98+
priority: spec.priority,
99+
},
100+
dependsOn: dependsOnSpecs.map(s => ({
101+
specNumber: s.specNumber,
102+
specName: s.specName,
103+
title: s.title || getTitle(s.specName),
104+
status: s.status,
105+
priority: s.priority,
106+
})),
107+
requiredBy: requiredBySpecs.map(s => ({
108+
specNumber: s.specNumber,
109+
specName: s.specName,
110+
title: s.title || getTitle(s.specName),
111+
status: s.status,
112+
priority: s.priority,
113+
})),
114+
};
115+
116+
// Return with cache headers
117+
return NextResponse.json(response, {
118+
headers: {
119+
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120'
120+
}
121+
});
122+
} catch (error) {
123+
console.error('Error fetching dependency graph:', error);
124+
return NextResponse.json(
125+
{ error: 'Failed to fetch dependency graph' },
126+
{ status: 500 }
127+
);
128+
}
129+
}

packages/ui/src/app/dashboard-client.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -201,10 +201,20 @@ export function DashboardClient({ initialSpecs, initialStats, projectId }: Dashb
201201
<div className="max-w-7xl mx-auto space-y-6 sm:space-y-8">
202202
{/* Header */}
203203
<div>
204-
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight">Dashboard</h1>
205-
<p className="text-muted-foreground mt-2">
206-
Project overview and recent activity
207-
</p>
204+
<div className="flex items-center gap-3">
205+
{currentProject?.color && (
206+
<div
207+
className="h-8 w-2 rounded-full shrink-0"
208+
style={{ backgroundColor: currentProject.color }}
209+
/>
210+
)}
211+
<div>
212+
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight">Dashboard</h1>
213+
<p className="text-muted-foreground mt-2">
214+
{currentProject ? `${currentProject.name} — overview and recent activity` : 'Project overview and recent activity'}
215+
</p>
216+
</div>
217+
</div>
208218
</div>
209219

210220
{/* Stats Cards */}

0 commit comments

Comments
 (0)