@leanspec/ui 0.2.3 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/standalone/packages/web/.next/BUILD_ID +1 -1
- package/dist/standalone/packages/web/.next/build-manifest.json +2 -2
- package/dist/standalone/packages/web/.next/prerender-manifest.json +3 -3
- package/dist/standalone/packages/web/.next/server/app/_global-error.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_global-error.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.html +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/[id]/specs/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/revalidate/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/dependency-graph/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/specs/[id]/subspecs/[file]/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/api/stats/route.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.html +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_full.segment.rsc +2 -2
- package/dist/standalone/packages/web/.next/server/app/board.segments/_index.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/_tree.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board/__PAGE__.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/board.segments/board.segment.rsc +1 -1
- package/dist/standalone/packages/web/.next/server/app/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/[id]/page_client-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/app/specs/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/app/stats/page.js.nft.json +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__2e0f9179._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__577d6d08._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__e54bc4b8._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/[root-of-the-server]__f8978f3e._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/[root-of-the-server]__be46bb7c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_7dedc302._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_ad71cd8c._.js +1 -1
- package/dist/standalone/packages/web/.next/server/chunks/ssr/_c5a5c652._.js +1 -1
- package/dist/standalone/packages/web/.next/server/pages/404.html +2 -2
- package/dist/standalone/packages/web/.next/server/pages/500.html +2 -2
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.js +1 -1
- package/dist/standalone/packages/web/.next/server/server-reference-manifest.json +1 -1
- package/dist/{static/chunks/0de258404bcae76f.js → standalone/packages/web/.next/static/chunks/8864b47e107cbe63.js} +1 -1
- package/dist/{static/chunks/09ff02250dd56621.js → standalone/packages/web/.next/static/chunks/a2889ecda42c83e7.js} +1 -1
- package/dist/standalone/packages/web/.next/static/chunks/c22619397bb8368e.js +1 -0
- package/dist/standalone/packages/web/components.json +20 -0
- package/dist/standalone/packages/web/drizzle/0000_reflective_thena.sql +59 -0
- package/dist/standalone/packages/web/drizzle/0001_fresh_carmella_unuscione.sql +1 -0
- package/dist/standalone/packages/web/drizzle/meta/0000_snapshot.json +427 -0
- package/dist/standalone/packages/web/drizzle/meta/0001_snapshot.json +436 -0
- package/dist/standalone/packages/web/drizzle/meta/_journal.json +20 -0
- package/dist/standalone/packages/web/drizzle.config.ts +10 -0
- package/dist/standalone/packages/web/eslint.config.mjs +18 -0
- package/dist/standalone/packages/web/next.config.ts +7 -0
- package/dist/standalone/packages/web/package.json +1 -1
- package/dist/standalone/packages/web/postcss.config.mjs +8 -0
- package/dist/standalone/packages/web/src/app/api/projects/[id]/specs/route.ts +23 -0
- package/dist/standalone/packages/web/src/app/api/projects/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/api/revalidate/route.ts +63 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.test.ts +51 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/dependency-graph/route.ts +171 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/route.ts +36 -0
- package/dist/standalone/packages/web/src/app/api/specs/[id]/subspecs/[file]/route.ts +46 -0
- package/dist/standalone/packages/web/src/app/api/stats/route.ts +19 -0
- package/dist/standalone/packages/web/src/app/board/board-client.tsx +162 -0
- package/dist/standalone/packages/web/src/app/board/loading.tsx +43 -0
- package/dist/standalone/packages/web/src/app/board/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/dashboard-client.tsx +364 -0
- package/dist/standalone/packages/web/src/app/error.tsx +43 -0
- package/dist/standalone/packages/web/src/app/globals.css +531 -0
- package/dist/standalone/packages/web/src/app/home-client.tsx +277 -0
- package/dist/standalone/packages/web/src/app/layout.tsx +70 -0
- package/dist/standalone/packages/web/src/app/loading.tsx +87 -0
- package/dist/standalone/packages/web/src/app/not-found.tsx +27 -0
- package/dist/standalone/packages/web/src/app/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/loading.tsx +5 -0
- package/dist/standalone/packages/web/src/app/specs/[id]/page.tsx +43 -0
- package/dist/standalone/packages/web/src/app/specs/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/specs/specs-client.tsx +425 -0
- package/dist/standalone/packages/web/src/app/stats/page.tsx +18 -0
- package/dist/standalone/packages/web/src/app/stats/stats-client.tsx +283 -0
- package/dist/standalone/packages/web/src/components/back-to-top.tsx +46 -0
- package/dist/standalone/packages/web/src/components/empty-state.tsx +52 -0
- package/dist/standalone/packages/web/src/components/main-sidebar.tsx +175 -0
- package/dist/standalone/packages/web/src/components/markdown-link.test.ts +96 -0
- package/dist/standalone/packages/web/src/components/markdown-link.tsx +95 -0
- package/dist/standalone/packages/web/src/components/navigation.tsx +210 -0
- package/dist/standalone/packages/web/src/components/priority-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/quick-search.tsx +180 -0
- package/dist/standalone/packages/web/src/components/skeletons.tsx +119 -0
- package/dist/standalone/packages/web/src/components/spec-dependency-graph.tsx +369 -0
- package/dist/standalone/packages/web/src/components/spec-detail-client.tsx +372 -0
- package/dist/standalone/packages/web/src/components/spec-detail-loading-shell.tsx +42 -0
- package/dist/standalone/packages/web/src/components/spec-detail-wrapper.tsx +70 -0
- package/dist/standalone/packages/web/src/components/spec-metadata.tsx +136 -0
- package/dist/standalone/packages/web/src/components/spec-sidebar.tsx +127 -0
- package/dist/standalone/packages/web/src/components/spec-timeline.tsx +186 -0
- package/dist/standalone/packages/web/src/components/specs-nav-sidebar.tsx +561 -0
- package/dist/standalone/packages/web/src/components/status-badge.tsx +53 -0
- package/dist/standalone/packages/web/src/components/sub-spec-tabs.tsx +143 -0
- package/dist/standalone/packages/web/src/components/table-of-contents.tsx +130 -0
- package/dist/standalone/packages/web/src/components/theme-provider.tsx +11 -0
- package/dist/standalone/packages/web/src/components/theme-toggle.tsx +37 -0
- package/dist/standalone/packages/web/src/components/ui/avatar.tsx +50 -0
- package/dist/standalone/packages/web/src/components/ui/badge.tsx +36 -0
- package/dist/standalone/packages/web/src/components/ui/breadcrumb.tsx +110 -0
- package/dist/standalone/packages/web/src/components/ui/button.tsx +57 -0
- package/dist/standalone/packages/web/src/components/ui/card.tsx +76 -0
- package/dist/standalone/packages/web/src/components/ui/command.tsx +153 -0
- package/dist/standalone/packages/web/src/components/ui/dialog.tsx +122 -0
- package/dist/standalone/packages/web/src/components/ui/input.tsx +24 -0
- package/dist/standalone/packages/web/src/components/ui/select.tsx +159 -0
- package/dist/standalone/packages/web/src/components/ui/separator.tsx +31 -0
- package/dist/standalone/packages/web/src/components/ui/skeleton.tsx +15 -0
- package/dist/standalone/packages/web/src/components/ui/tabs.tsx +55 -0
- package/dist/standalone/packages/web/src/components/ui/toast.tsx +30 -0
- package/dist/standalone/packages/web/src/components/ui/tooltip.tsx +32 -0
- package/dist/standalone/packages/web/src/lib/date-utils.ts +76 -0
- package/dist/standalone/packages/web/src/lib/db/index.ts +42 -0
- package/dist/standalone/packages/web/src/lib/db/migrate.ts +18 -0
- package/dist/standalone/packages/web/src/lib/db/queries.ts +114 -0
- package/dist/standalone/packages/web/src/lib/db/schema.ts +123 -0
- package/dist/standalone/packages/web/src/lib/db/seed.ts +156 -0
- package/dist/standalone/packages/web/src/lib/db/service-queries.ts +216 -0
- package/dist/standalone/packages/web/src/lib/dependency-graph.ts +105 -0
- package/dist/standalone/packages/web/src/lib/specs/service.ts +120 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/database-source.ts +94 -0
- package/dist/standalone/packages/web/src/lib/specs/sources/filesystem-source.ts +249 -0
- package/dist/standalone/packages/web/src/lib/specs/types.ts +55 -0
- package/dist/standalone/packages/web/src/lib/stores/specs-sidebar-store.ts +152 -0
- package/dist/standalone/packages/web/src/lib/sub-specs.ts +171 -0
- package/dist/standalone/packages/web/src/lib/utils.ts +17 -0
- package/dist/standalone/packages/web/src/types/specs.ts +18 -0
- package/dist/standalone/packages/web/tailwind.config.ts +58 -0
- package/dist/standalone/packages/web/tsconfig.json +34 -0
- package/dist/standalone/packages/web/vitest.config.ts +14 -0
- package/dist/standalone/specs/100-release-process-typecheck-failure/README.md +266 -0
- package/dist/standalone/specs/101-sidebar-scroll-position-drift/README.md +100 -0
- package/package.json +5 -3
- package/dist/BUILD_ID +0 -1
- package/dist/static/chunks/a3e649fcddd3d715.js +0 -1
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_buildManifest.js +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_clientMiddlewareManifest.json +0 -0
- /package/dist/{static/Z_BxkB0KJViCNbULN1S5p → standalone/packages/web/.next/static/DDHAmoL_2Poji-whvFjry}/_ssgManifest.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/0c19c69aa7625475.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/116800b03245a1e5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/19e80edf527aef5c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/2ece90370908f56c.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/36fd2dddb486f6bc.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/5c2072ad938de8ed.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6577fe797a336bab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/6a05a93ec8fa7b83.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/7f732ea69e643219.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a02c1f50ff00204f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a45464b9776dd88e.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/a6dad97d9634a72d.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ae04dcd433be6dab.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/b20313408e970968.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c46095e1a421d93f.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c48dd4c72d7c5ef4.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/c557ac675be79771.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dca0c854c59234cd.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/df1731c03abf1aee.css +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/dfd41488ad062cd5.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/ebd89051637b9a47.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/f3ec9fd77a8618b1.js +0 -0
- /package/dist/{static → standalone/packages/web/.next/static}/chunks/turbopack-7450632b40b2e378.js +0 -0
- /package/dist/{public → standalone/packages/web/public}/f864aa7e7061c0600e35cf3d879b27cf.txt +0 -0
- /package/dist/{public → standalone/packages/web/public}/favicon.ico +0 -0
- /package/dist/{public → standalone/packages/web/public}/file.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark-white.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/github-mark.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/globe.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/icon.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-dark-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo-with-bg.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/logo.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/next.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/vercel.svg +0 -0
- /package/dist/{public → standalone/packages/web/public}/window.svg +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/specs/[id]/dependency-graph - Get complete dependency graph for a spec
|
|
3
|
+
*
|
|
4
|
+
* Returns upstream dependencies, downstream dependents, and related specs
|
|
5
|
+
* for the specified spec using the SpecDependencyGraph from @leanspec/core
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { NextResponse } from 'next/server';
|
|
9
|
+
import { getSpecById } from '@/lib/db/service-queries';
|
|
10
|
+
import { SpecDependencyGraph, type SpecInfo } from '@/lib/dependency-graph';
|
|
11
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import matter from 'gray-matter';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load all specs with relationships from filesystem
|
|
17
|
+
*/
|
|
18
|
+
function loadAllSpecsWithRelationships(specsDir: string): SpecInfo[] {
|
|
19
|
+
const specInfos: SpecInfo[] = [];
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const entries = readdirSync(specsDir);
|
|
23
|
+
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
const specPath = join(specsDir, entry);
|
|
26
|
+
const stat = statSync(specPath);
|
|
27
|
+
|
|
28
|
+
if (!stat.isDirectory()) continue;
|
|
29
|
+
if (entry === 'archived') continue; // Skip archived
|
|
30
|
+
if (!/^\d{2,}-/.test(entry)) continue; // Must be spec pattern (e.g., 001-name)
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const readmePath = join(specPath, 'README.md');
|
|
34
|
+
const raw = readFileSync(readmePath, 'utf-8');
|
|
35
|
+
const { data } = matter(raw);
|
|
36
|
+
|
|
37
|
+
if (!data || !data.status) continue;
|
|
38
|
+
|
|
39
|
+
const frontmatter: any = {
|
|
40
|
+
status: data.status || 'planned',
|
|
41
|
+
created: data.created_at || data.created || new Date().toISOString().split('T')[0],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Parse relationships - normalize to arrays
|
|
45
|
+
if (data.depends_on) {
|
|
46
|
+
frontmatter.depends_on = Array.isArray(data.depends_on)
|
|
47
|
+
? data.depends_on
|
|
48
|
+
: [data.depends_on];
|
|
49
|
+
} else {
|
|
50
|
+
frontmatter.depends_on = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (data.related) {
|
|
54
|
+
frontmatter.related = Array.isArray(data.related)
|
|
55
|
+
? data.related
|
|
56
|
+
: [data.related];
|
|
57
|
+
} else {
|
|
58
|
+
frontmatter.related = [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Add optional fields
|
|
62
|
+
if (data.priority) frontmatter.priority = data.priority;
|
|
63
|
+
if (data.tags) frontmatter.tags = data.tags;
|
|
64
|
+
if (data.assignee) frontmatter.assignee = data.assignee;
|
|
65
|
+
|
|
66
|
+
specInfos.push({
|
|
67
|
+
path: entry,
|
|
68
|
+
fullPath: specPath,
|
|
69
|
+
filePath: readmePath,
|
|
70
|
+
name: entry,
|
|
71
|
+
frontmatter,
|
|
72
|
+
});
|
|
73
|
+
} catch (err) {
|
|
74
|
+
console.warn(`Failed to parse spec ${entry}:`, err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
console.error('Failed to load specs:', error);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return specInfos;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function GET(
|
|
85
|
+
request: Request,
|
|
86
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
87
|
+
) {
|
|
88
|
+
try {
|
|
89
|
+
const { id } = await params;
|
|
90
|
+
|
|
91
|
+
// Get the target spec
|
|
92
|
+
const spec = await getSpecById(id);
|
|
93
|
+
if (!spec) {
|
|
94
|
+
return NextResponse.json(
|
|
95
|
+
{ error: 'Spec not found' },
|
|
96
|
+
{ status: 404 }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Load all specs from filesystem with relationships
|
|
101
|
+
const specsDir = join(process.cwd(), '../../specs');
|
|
102
|
+
const specInfos = loadAllSpecsWithRelationships(specsDir);
|
|
103
|
+
|
|
104
|
+
// Build dependency graph
|
|
105
|
+
const graph = new SpecDependencyGraph(specInfos);
|
|
106
|
+
|
|
107
|
+
// Get complete graph for the target spec
|
|
108
|
+
const completeGraph = graph.getCompleteGraph(spec.specName);
|
|
109
|
+
|
|
110
|
+
// Helper to extract spec number from name
|
|
111
|
+
const getSpecNumber = (name: string): number | null => {
|
|
112
|
+
const match = name.match(/^(\d+)/);
|
|
113
|
+
return match ? parseInt(match[1], 10) : null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// Helper to extract title from name
|
|
117
|
+
const getTitle = (name: string): string => {
|
|
118
|
+
const parts = name.split('-');
|
|
119
|
+
if (parts.length > 1) {
|
|
120
|
+
return parts.slice(1).join('-').replace(/-/g, ' ');
|
|
121
|
+
}
|
|
122
|
+
return name;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Format response with simplified spec metadata
|
|
126
|
+
const response = {
|
|
127
|
+
current: {
|
|
128
|
+
id: spec.id,
|
|
129
|
+
specNumber: spec.specNumber,
|
|
130
|
+
specName: spec.specName,
|
|
131
|
+
title: spec.title,
|
|
132
|
+
status: spec.status,
|
|
133
|
+
priority: spec.priority,
|
|
134
|
+
},
|
|
135
|
+
dependsOn: completeGraph.dependsOn.map(s => ({
|
|
136
|
+
specNumber: getSpecNumber(s.name),
|
|
137
|
+
specName: s.name,
|
|
138
|
+
title: getTitle(s.name),
|
|
139
|
+
status: s.frontmatter.status,
|
|
140
|
+
priority: s.frontmatter.priority,
|
|
141
|
+
})),
|
|
142
|
+
requiredBy: completeGraph.requiredBy.map(s => ({
|
|
143
|
+
specNumber: getSpecNumber(s.name),
|
|
144
|
+
specName: s.name,
|
|
145
|
+
title: getTitle(s.name),
|
|
146
|
+
status: s.frontmatter.status,
|
|
147
|
+
priority: s.frontmatter.priority,
|
|
148
|
+
})),
|
|
149
|
+
related: completeGraph.related.map(s => ({
|
|
150
|
+
specNumber: getSpecNumber(s.name),
|
|
151
|
+
specName: s.name,
|
|
152
|
+
title: getTitle(s.name),
|
|
153
|
+
status: s.frontmatter.status,
|
|
154
|
+
priority: s.frontmatter.priority,
|
|
155
|
+
})),
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Return with cache headers
|
|
159
|
+
return NextResponse.json(response, {
|
|
160
|
+
headers: {
|
|
161
|
+
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120'
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error('Error fetching dependency graph:', error);
|
|
166
|
+
return NextResponse.json(
|
|
167
|
+
{ error: 'Failed to fetch dependency graph' },
|
|
168
|
+
{ status: 500 }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/specs/[id] - Get a specific spec
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NextResponse } from 'next/server';
|
|
6
|
+
import { getSpecById } from '@/lib/db/service-queries';
|
|
7
|
+
|
|
8
|
+
export async function GET(
|
|
9
|
+
request: Request,
|
|
10
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
11
|
+
) {
|
|
12
|
+
try {
|
|
13
|
+
const { id } = await params;
|
|
14
|
+
const spec = await getSpecById(id);
|
|
15
|
+
|
|
16
|
+
if (!spec) {
|
|
17
|
+
return NextResponse.json(
|
|
18
|
+
{ error: 'Spec not found' },
|
|
19
|
+
{ status: 404 }
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Tier 2: Add cache headers for client-side caching
|
|
24
|
+
return NextResponse.json({ spec }, {
|
|
25
|
+
headers: {
|
|
26
|
+
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120'
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Error fetching spec:', error);
|
|
31
|
+
return NextResponse.json(
|
|
32
|
+
{ error: 'Failed to fetch spec' },
|
|
33
|
+
{ status: 500 }
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/specs/[id]/subspecs/[file] - Get a specific sub-spec file
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NextResponse } from 'next/server';
|
|
6
|
+
import { getSpecById } from '@/lib/db/service-queries';
|
|
7
|
+
|
|
8
|
+
export async function GET(
|
|
9
|
+
request: Request,
|
|
10
|
+
{ params }: { params: Promise<{ id: string; file: string }> }
|
|
11
|
+
) {
|
|
12
|
+
try {
|
|
13
|
+
const { id, file } = await params;
|
|
14
|
+
const spec = await getSpecById(id);
|
|
15
|
+
|
|
16
|
+
if (!spec) {
|
|
17
|
+
return NextResponse.json(
|
|
18
|
+
{ error: 'Spec not found' },
|
|
19
|
+
{ status: 404 }
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Find the requested sub-spec file
|
|
24
|
+
const subSpec = spec.subSpecs?.find(s => s.file === file);
|
|
25
|
+
|
|
26
|
+
if (!subSpec) {
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ error: 'Sub-spec not found' },
|
|
29
|
+
{ status: 404 }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Tier 2: Add cache headers for client-side caching
|
|
34
|
+
return NextResponse.json({ subSpec }, {
|
|
35
|
+
headers: {
|
|
36
|
+
'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=120'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error fetching sub-spec:', error);
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'Failed to fetch sub-spec' },
|
|
43
|
+
{ status: 500 }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/stats - Get project and spec statistics
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NextResponse } from 'next/server';
|
|
6
|
+
import { getStats } from '@/lib/db/service-queries';
|
|
7
|
+
|
|
8
|
+
export async function GET() {
|
|
9
|
+
try {
|
|
10
|
+
const stats = await getStats();
|
|
11
|
+
return NextResponse.json(stats);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('Error fetching stats:', error);
|
|
14
|
+
return NextResponse.json(
|
|
15
|
+
{ error: 'Failed to fetch stats' },
|
|
16
|
+
{ status: 500 }
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Clock, PlayCircle, CheckCircle2, Archive } from 'lucide-react';
|
|
7
|
+
import { PriorityBadge } from '@/components/priority-badge';
|
|
8
|
+
import { cn } from '@/lib/utils';
|
|
9
|
+
|
|
10
|
+
interface Spec {
|
|
11
|
+
id: string;
|
|
12
|
+
specNumber: number | null;
|
|
13
|
+
specName: string;
|
|
14
|
+
title: string | null;
|
|
15
|
+
status: string | null;
|
|
16
|
+
priority: string | null;
|
|
17
|
+
tags: string[] | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface BoardClientProps {
|
|
21
|
+
initialSpecs: Spec[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function BoardClient({ initialSpecs }: BoardClientProps) {
|
|
25
|
+
const statusConfig = {
|
|
26
|
+
'planned': {
|
|
27
|
+
icon: Clock,
|
|
28
|
+
title: 'Planned',
|
|
29
|
+
colorClass: 'text-blue-600 dark:text-blue-400',
|
|
30
|
+
bgClass: 'bg-blue-50 dark:bg-blue-900/20',
|
|
31
|
+
borderClass: 'border-blue-200 dark:border-blue-800'
|
|
32
|
+
},
|
|
33
|
+
'in-progress': {
|
|
34
|
+
icon: PlayCircle,
|
|
35
|
+
title: 'In Progress',
|
|
36
|
+
colorClass: 'text-orange-600 dark:text-orange-400',
|
|
37
|
+
bgClass: 'bg-orange-50 dark:bg-orange-900/20',
|
|
38
|
+
borderClass: 'border-orange-200 dark:border-orange-800'
|
|
39
|
+
},
|
|
40
|
+
'complete': {
|
|
41
|
+
icon: CheckCircle2,
|
|
42
|
+
title: 'Complete',
|
|
43
|
+
colorClass: 'text-green-600 dark:text-green-400',
|
|
44
|
+
bgClass: 'bg-green-50 dark:bg-green-900/20',
|
|
45
|
+
borderClass: 'border-green-200 dark:border-green-800'
|
|
46
|
+
},
|
|
47
|
+
'archived': {
|
|
48
|
+
icon: Archive,
|
|
49
|
+
title: 'Archived',
|
|
50
|
+
colorClass: 'text-gray-600 dark:text-gray-400',
|
|
51
|
+
bgClass: 'bg-gray-50 dark:bg-gray-900/20',
|
|
52
|
+
borderClass: 'border-gray-200 dark:border-gray-800'
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const statuses = ['planned', 'in-progress', 'complete', 'archived'] as const;
|
|
57
|
+
const columns = statuses.map(status => ({
|
|
58
|
+
status,
|
|
59
|
+
config: statusConfig[status],
|
|
60
|
+
specs: initialSpecs.filter(spec => spec.status === status),
|
|
61
|
+
}));
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="min-h-screen bg-background">
|
|
65
|
+
<section className="container mx-auto px-4 py-8">
|
|
66
|
+
<div className="mb-8">
|
|
67
|
+
<h1 className="text-4xl font-bold tracking-tight">Kanban Board</h1>
|
|
68
|
+
<p className="text-muted-foreground mt-2">Track spec progress across all statuses</p>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
72
|
+
{columns.map(column => {
|
|
73
|
+
const Icon = column.config.icon;
|
|
74
|
+
return (
|
|
75
|
+
<div key={column.status} className="flex flex-col">
|
|
76
|
+
<div className={cn(
|
|
77
|
+
"sticky top-14 z-40 mb-4 p-3 rounded-lg border-2 bg-background",
|
|
78
|
+
column.config.bgClass,
|
|
79
|
+
column.config.borderClass
|
|
80
|
+
)}>
|
|
81
|
+
<h2 className={cn(
|
|
82
|
+
"text-lg font-semibold flex items-center gap-2",
|
|
83
|
+
column.config.colorClass
|
|
84
|
+
)}>
|
|
85
|
+
<Icon className="h-5 w-5" />
|
|
86
|
+
{column.config.title}
|
|
87
|
+
<Badge variant="outline" className="ml-auto">{column.specs.length}</Badge>
|
|
88
|
+
</h2>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div className="space-y-3 flex-1">
|
|
92
|
+
{column.specs.map(spec => {
|
|
93
|
+
// Get priority color for left border
|
|
94
|
+
const priorityColors = {
|
|
95
|
+
'critical': 'border-l-red-500',
|
|
96
|
+
'high': 'border-l-orange-500',
|
|
97
|
+
'medium': 'border-l-blue-500',
|
|
98
|
+
'low': 'border-l-gray-400'
|
|
99
|
+
};
|
|
100
|
+
const borderColor = priorityColors[spec.priority as keyof typeof priorityColors] || priorityColors.medium;
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Card
|
|
104
|
+
key={spec.id}
|
|
105
|
+
className={cn(
|
|
106
|
+
"hover:shadow-lg transition-all duration-150 hover:scale-[1.02] border-l-4 cursor-pointer border-gray-200 dark:border-gray-800",
|
|
107
|
+
borderColor
|
|
108
|
+
)}
|
|
109
|
+
onClick={() => window.location.href = `/specs/${spec.specNumber || spec.id}`}
|
|
110
|
+
>
|
|
111
|
+
<CardHeader className="pb-3">
|
|
112
|
+
<Link href={`/specs/${spec.specNumber || spec.id}`}>
|
|
113
|
+
<CardTitle className="text-sm font-medium hover:text-primary transition-colors">
|
|
114
|
+
{spec.specNumber ? `#${spec.specNumber}` : spec.specName}
|
|
115
|
+
</CardTitle>
|
|
116
|
+
</Link>
|
|
117
|
+
</CardHeader>
|
|
118
|
+
<CardContent className="space-y-2">
|
|
119
|
+
<p className="text-sm text-muted-foreground line-clamp-2">
|
|
120
|
+
{spec.title || spec.specName}
|
|
121
|
+
</p>
|
|
122
|
+
|
|
123
|
+
<div className="flex items-center gap-2 flex-wrap">
|
|
124
|
+
{spec.priority && <PriorityBadge priority={spec.priority} />}
|
|
125
|
+
|
|
126
|
+
{spec.tags && spec.tags.length > 0 && (
|
|
127
|
+
<>
|
|
128
|
+
{spec.tags.slice(0, 2).map(tag => (
|
|
129
|
+
<Badge key={tag} variant="outline" className="text-xs">
|
|
130
|
+
{tag}
|
|
131
|
+
</Badge>
|
|
132
|
+
))}
|
|
133
|
+
{spec.tags.length > 2 && (
|
|
134
|
+
<Badge variant="outline" className="text-xs">
|
|
135
|
+
+{spec.tags.length - 2}
|
|
136
|
+
</Badge>
|
|
137
|
+
)}
|
|
138
|
+
</>
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</CardContent>
|
|
142
|
+
</Card>
|
|
143
|
+
);
|
|
144
|
+
})}
|
|
145
|
+
|
|
146
|
+
{column.specs.length === 0 && (
|
|
147
|
+
<Card className="border-dashed border-gray-300 dark:border-gray-700">
|
|
148
|
+
<CardContent className="py-8 text-center">
|
|
149
|
+
<Icon className={cn("mx-auto h-8 w-8 mb-2", column.config.colorClass, "opacity-50")} />
|
|
150
|
+
<p className="text-sm text-muted-foreground">No {column.config.title.toLowerCase()} specs</p>
|
|
151
|
+
</CardContent>
|
|
152
|
+
</Card>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
</div>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
</section>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
2
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
3
|
+
|
|
4
|
+
export default function BoardLoading() {
|
|
5
|
+
return (
|
|
6
|
+
<div className="min-h-screen bg-background">
|
|
7
|
+
<section className="container mx-auto px-4 py-8">
|
|
8
|
+
<div className="mb-8">
|
|
9
|
+
<Skeleton className="h-10 w-48 mb-2" />
|
|
10
|
+
<Skeleton className="h-5 w-96" />
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
14
|
+
{[...Array(4)].map((_, colIdx) => (
|
|
15
|
+
<div key={colIdx} className="flex flex-col">
|
|
16
|
+
<div className="mb-4">
|
|
17
|
+
<Skeleton className="h-7 w-32" />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div className="space-y-3 flex-1">
|
|
21
|
+
{[...Array(3)].map((_, i) => (
|
|
22
|
+
<Card key={i}>
|
|
23
|
+
<CardHeader className="pb-3">
|
|
24
|
+
<Skeleton className="h-5 w-24" />
|
|
25
|
+
</CardHeader>
|
|
26
|
+
<CardContent className="space-y-2">
|
|
27
|
+
<Skeleton className="h-4 w-full" />
|
|
28
|
+
<Skeleton className="h-4 w-3/4" />
|
|
29
|
+
<div className="flex gap-2 pt-2">
|
|
30
|
+
<Skeleton className="h-5 w-16" />
|
|
31
|
+
<Skeleton className="h-5 w-16" />
|
|
32
|
+
</div>
|
|
33
|
+
</CardContent>
|
|
34
|
+
</Card>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
</section>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Board page - Redirects to unified specs page with board view
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use client';
|
|
6
|
+
|
|
7
|
+
import { useEffect } from 'react';
|
|
8
|
+
import { useRouter } from 'next/navigation';
|
|
9
|
+
|
|
10
|
+
export default function Board() {
|
|
11
|
+
const router = useRouter();
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
router.replace('/specs?view=board');
|
|
15
|
+
}, [router]);
|
|
16
|
+
|
|
17
|
+
return null;
|
|
18
|
+
}
|