@prmichaelsen/acp-visualizer 0.1.8 → 0.5.0
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/package.json +13 -2
- package/src/components/BurndownChart.tsx +148 -0
- package/src/components/DependencyGraph.tsx +196 -0
- package/src/components/FilterBar.tsx +1 -0
- package/src/components/GitHubInput.tsx +65 -0
- package/src/components/MilestoneGantt.tsx +150 -0
- package/src/components/MilestoneKanban.tsx +117 -0
- package/src/components/ProjectSelector.tsx +66 -0
- package/src/components/Sidebar.tsx +26 -3
- package/src/components/StatusBadge.tsx +2 -0
- package/src/components/StatusDot.tsx +1 -0
- package/src/components/ViewToggle.tsx +25 -22
- package/src/contexts/ProgressContext.tsx +22 -0
- package/src/lib/types.ts +1 -1
- package/src/lib/yaml-loader-real.spec.ts +54 -1
- package/src/lib/yaml-loader.ts +134 -5
- package/src/routeTree.gen.ts +34 -3
- package/src/routes/__root.tsx +88 -19
- package/src/routes/activity.tsx +97 -0
- package/src/routes/index.tsx +6 -1
- package/src/routes/milestones.tsx +27 -10
- package/src/routes/search.tsx +2 -1
- package/src/routes/tasks.tsx +2 -1
- package/src/server.ts +13 -0
- package/src/services/github.service.ts +51 -0
- package/src/services/progress-database.service.ts +17 -39
- package/src/services/projects.service.ts +69 -0
- package/vite.config.ts +7 -2
package/src/routeTree.gen.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { Route as rootRouteImport } from './routes/__root'
|
|
|
12
12
|
import { Route as TasksRouteImport } from './routes/tasks'
|
|
13
13
|
import { Route as SearchRouteImport } from './routes/search'
|
|
14
14
|
import { Route as MilestonesRouteImport } from './routes/milestones'
|
|
15
|
+
import { Route as ActivityRouteImport } from './routes/activity'
|
|
15
16
|
import { Route as IndexRouteImport } from './routes/index'
|
|
16
17
|
import { Route as ApiWatchRouteImport } from './routes/api/watch'
|
|
17
18
|
|
|
@@ -30,6 +31,11 @@ const MilestonesRoute = MilestonesRouteImport.update({
|
|
|
30
31
|
path: '/milestones',
|
|
31
32
|
getParentRoute: () => rootRouteImport,
|
|
32
33
|
} as any)
|
|
34
|
+
const ActivityRoute = ActivityRouteImport.update({
|
|
35
|
+
id: '/activity',
|
|
36
|
+
path: '/activity',
|
|
37
|
+
getParentRoute: () => rootRouteImport,
|
|
38
|
+
} as any)
|
|
33
39
|
const IndexRoute = IndexRouteImport.update({
|
|
34
40
|
id: '/',
|
|
35
41
|
path: '/',
|
|
@@ -43,6 +49,7 @@ const ApiWatchRoute = ApiWatchRouteImport.update({
|
|
|
43
49
|
|
|
44
50
|
export interface FileRoutesByFullPath {
|
|
45
51
|
'/': typeof IndexRoute
|
|
52
|
+
'/activity': typeof ActivityRoute
|
|
46
53
|
'/milestones': typeof MilestonesRoute
|
|
47
54
|
'/search': typeof SearchRoute
|
|
48
55
|
'/tasks': typeof TasksRoute
|
|
@@ -50,6 +57,7 @@ export interface FileRoutesByFullPath {
|
|
|
50
57
|
}
|
|
51
58
|
export interface FileRoutesByTo {
|
|
52
59
|
'/': typeof IndexRoute
|
|
60
|
+
'/activity': typeof ActivityRoute
|
|
53
61
|
'/milestones': typeof MilestonesRoute
|
|
54
62
|
'/search': typeof SearchRoute
|
|
55
63
|
'/tasks': typeof TasksRoute
|
|
@@ -58,6 +66,7 @@ export interface FileRoutesByTo {
|
|
|
58
66
|
export interface FileRoutesById {
|
|
59
67
|
__root__: typeof rootRouteImport
|
|
60
68
|
'/': typeof IndexRoute
|
|
69
|
+
'/activity': typeof ActivityRoute
|
|
61
70
|
'/milestones': typeof MilestonesRoute
|
|
62
71
|
'/search': typeof SearchRoute
|
|
63
72
|
'/tasks': typeof TasksRoute
|
|
@@ -65,14 +74,28 @@ export interface FileRoutesById {
|
|
|
65
74
|
}
|
|
66
75
|
export interface FileRouteTypes {
|
|
67
76
|
fileRoutesByFullPath: FileRoutesByFullPath
|
|
68
|
-
fullPaths:
|
|
77
|
+
fullPaths:
|
|
78
|
+
| '/'
|
|
79
|
+
| '/activity'
|
|
80
|
+
| '/milestones'
|
|
81
|
+
| '/search'
|
|
82
|
+
| '/tasks'
|
|
83
|
+
| '/api/watch'
|
|
69
84
|
fileRoutesByTo: FileRoutesByTo
|
|
70
|
-
to: '/' | '/milestones' | '/search' | '/tasks' | '/api/watch'
|
|
71
|
-
id:
|
|
85
|
+
to: '/' | '/activity' | '/milestones' | '/search' | '/tasks' | '/api/watch'
|
|
86
|
+
id:
|
|
87
|
+
| '__root__'
|
|
88
|
+
| '/'
|
|
89
|
+
| '/activity'
|
|
90
|
+
| '/milestones'
|
|
91
|
+
| '/search'
|
|
92
|
+
| '/tasks'
|
|
93
|
+
| '/api/watch'
|
|
72
94
|
fileRoutesById: FileRoutesById
|
|
73
95
|
}
|
|
74
96
|
export interface RootRouteChildren {
|
|
75
97
|
IndexRoute: typeof IndexRoute
|
|
98
|
+
ActivityRoute: typeof ActivityRoute
|
|
76
99
|
MilestonesRoute: typeof MilestonesRoute
|
|
77
100
|
SearchRoute: typeof SearchRoute
|
|
78
101
|
TasksRoute: typeof TasksRoute
|
|
@@ -102,6 +125,13 @@ declare module '@tanstack/react-router' {
|
|
|
102
125
|
preLoaderRoute: typeof MilestonesRouteImport
|
|
103
126
|
parentRoute: typeof rootRouteImport
|
|
104
127
|
}
|
|
128
|
+
'/activity': {
|
|
129
|
+
id: '/activity'
|
|
130
|
+
path: '/activity'
|
|
131
|
+
fullPath: '/activity'
|
|
132
|
+
preLoaderRoute: typeof ActivityRouteImport
|
|
133
|
+
parentRoute: typeof rootRouteImport
|
|
134
|
+
}
|
|
105
135
|
'/': {
|
|
106
136
|
id: '/'
|
|
107
137
|
path: '/'
|
|
@@ -121,6 +151,7 @@ declare module '@tanstack/react-router' {
|
|
|
121
151
|
|
|
122
152
|
const rootRouteChildren: RootRouteChildren = {
|
|
123
153
|
IndexRoute: IndexRoute,
|
|
154
|
+
ActivityRoute: ActivityRoute,
|
|
124
155
|
MilestonesRoute: MilestonesRoute,
|
|
125
156
|
SearchRoute: SearchRoute,
|
|
126
157
|
TasksRoute: TasksRoute,
|
package/src/routes/__root.tsx
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
|
-
import { HeadContent, Scripts, createRootRoute, Outlet } from '@tanstack/react-router'
|
|
1
|
+
import { HeadContent, Scripts, createRootRoute, Outlet, useRouter } from '@tanstack/react-router'
|
|
2
|
+
import { useState, useCallback } from 'react'
|
|
2
3
|
import { useAutoRefresh } from '../lib/useAutoRefresh'
|
|
3
4
|
import { Sidebar } from '../components/Sidebar'
|
|
4
5
|
import { Header } from '../components/Header'
|
|
5
6
|
import { getProgressData } from '../services/progress-database.service'
|
|
7
|
+
import { listProjects, getProjectProgressPath } from '../services/projects.service'
|
|
8
|
+
import { fetchGitHubProgress } from '../services/github.service'
|
|
6
9
|
import type { ProgressData } from '../lib/types'
|
|
10
|
+
import type { AcpProject } from '../services/projects.service'
|
|
11
|
+
import { ProgressProvider } from '../contexts/ProgressContext'
|
|
7
12
|
|
|
8
13
|
import appCss from '../styles.css?url'
|
|
9
14
|
|
|
10
15
|
export const Route = createRootRoute({
|
|
16
|
+
component: RootLayout,
|
|
17
|
+
notFoundComponent: NotFound,
|
|
18
|
+
shellComponent: RootDocument,
|
|
19
|
+
|
|
11
20
|
beforeLoad: async () => {
|
|
12
21
|
let progressData: ProgressData | null = null
|
|
22
|
+
let projects: AcpProject[] = []
|
|
13
23
|
|
|
14
24
|
try {
|
|
15
|
-
const result = await
|
|
25
|
+
const [result, projectList] = await Promise.all([
|
|
26
|
+
getProgressData({ data: {} }),
|
|
27
|
+
listProjects(),
|
|
28
|
+
])
|
|
16
29
|
if (result.ok) {
|
|
17
30
|
progressData = result.data
|
|
18
31
|
}
|
|
19
|
-
|
|
20
|
-
|
|
32
|
+
projects = projectList
|
|
33
|
+
} catch {
|
|
34
|
+
// Cloudflare Workers or other environment without filesystem
|
|
21
35
|
}
|
|
22
36
|
|
|
23
|
-
return { progressData }
|
|
37
|
+
return { progressData, projects }
|
|
24
38
|
},
|
|
25
39
|
|
|
26
40
|
head: () => ({
|
|
@@ -40,34 +54,89 @@ export const Route = createRootRoute({
|
|
|
40
54
|
{ rel: 'stylesheet', href: appCss },
|
|
41
55
|
],
|
|
42
56
|
}),
|
|
43
|
-
|
|
44
|
-
shellComponent: RootDocument,
|
|
45
57
|
})
|
|
46
58
|
|
|
59
|
+
function NotFound() {
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex items-center justify-center h-full">
|
|
62
|
+
<div className="text-center">
|
|
63
|
+
<h2 className="text-xl font-semibold text-gray-200 mb-2">Page Not Found</h2>
|
|
64
|
+
<p className="text-sm text-gray-400">
|
|
65
|
+
The page you're looking for doesn't exist.
|
|
66
|
+
</p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
47
72
|
function AutoRefresh() {
|
|
48
73
|
useAutoRefresh()
|
|
49
74
|
return null
|
|
50
75
|
}
|
|
51
76
|
|
|
52
|
-
function
|
|
53
|
-
const
|
|
77
|
+
function RootLayout() {
|
|
78
|
+
const context = Route.useRouteContext()
|
|
79
|
+
const [progressData, setProgressData] = useState(context.progressData)
|
|
80
|
+
const [currentProject, setCurrentProject] = useState<string | null>(
|
|
81
|
+
context.progressData?.project.name || null,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const handleGitHubLoad = useCallback(async (owner: string, repo: string) => {
|
|
85
|
+
const result = await fetchGitHubProgress({ data: { owner, repo } })
|
|
86
|
+
if (result.ok) {
|
|
87
|
+
setProgressData(result.data)
|
|
88
|
+
setCurrentProject(`${owner}/${repo}`)
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error(result.message)
|
|
91
|
+
}
|
|
92
|
+
}, [])
|
|
93
|
+
|
|
94
|
+
const handleProjectSwitch = useCallback(async (projectId: string) => {
|
|
95
|
+
try {
|
|
96
|
+
const path = await getProjectProgressPath({ data: { projectId } })
|
|
97
|
+
if (path) {
|
|
98
|
+
const result = await getProgressData({ data: { path } })
|
|
99
|
+
if (result.ok) {
|
|
100
|
+
setProgressData(result.data)
|
|
101
|
+
setCurrentProject(projectId)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Project switch failed — likely no filesystem access
|
|
106
|
+
}
|
|
107
|
+
}, [])
|
|
54
108
|
|
|
55
109
|
return (
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
<>
|
|
111
|
+
<AutoRefresh />
|
|
112
|
+
<div className="flex h-screen bg-gray-950 text-gray-100">
|
|
113
|
+
<Sidebar
|
|
114
|
+
projects={context.projects}
|
|
115
|
+
currentProject={currentProject}
|
|
116
|
+
onProjectSelect={handleProjectSwitch}
|
|
117
|
+
onGitHubLoad={handleGitHubLoad}
|
|
118
|
+
/>
|
|
119
|
+
<ProgressProvider data={progressData}>
|
|
64
120
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
65
121
|
<Header data={progressData} />
|
|
66
122
|
<main className="flex-1 overflow-auto">
|
|
67
|
-
|
|
123
|
+
<Outlet />
|
|
68
124
|
</main>
|
|
69
125
|
</div>
|
|
70
|
-
</
|
|
126
|
+
</ProgressProvider>
|
|
127
|
+
</div>
|
|
128
|
+
</>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function RootDocument({ children }: { children: React.ReactNode }) {
|
|
133
|
+
return (
|
|
134
|
+
<html lang="en">
|
|
135
|
+
<head>
|
|
136
|
+
<HeadContent />
|
|
137
|
+
</head>
|
|
138
|
+
<body>
|
|
139
|
+
{children}
|
|
71
140
|
<Scripts />
|
|
72
141
|
</body>
|
|
73
142
|
</html>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { useProgressData } from '../contexts/ProgressContext'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/activity')({
|
|
5
|
+
component: ActivityPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function ActivityPage() {
|
|
9
|
+
const progressData = useProgressData()
|
|
10
|
+
|
|
11
|
+
if (!progressData) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="p-6">
|
|
14
|
+
<p className="text-gray-600 text-sm">No data loaded</p>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const entries = progressData.recent_work
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="p-6">
|
|
23
|
+
<h2 className="text-lg font-semibold mb-4">Recent Activity</h2>
|
|
24
|
+
|
|
25
|
+
{entries.length === 0 ? (
|
|
26
|
+
<p className="text-gray-600 text-sm">No recent work entries</p>
|
|
27
|
+
) : (
|
|
28
|
+
<div className="relative">
|
|
29
|
+
{/* Timeline line */}
|
|
30
|
+
<div className="absolute left-[15px] top-2 bottom-2 w-px bg-gray-800" />
|
|
31
|
+
|
|
32
|
+
<div className="space-y-6">
|
|
33
|
+
{entries.map((entry, i) => (
|
|
34
|
+
<div key={i} className="relative flex gap-4">
|
|
35
|
+
{/* Timeline dot */}
|
|
36
|
+
<div className="relative z-10 mt-1">
|
|
37
|
+
<div className="w-[9px] h-[9px] rounded-full bg-blue-500 ring-4 ring-gray-950" />
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Content */}
|
|
41
|
+
<div className="flex-1 pb-2">
|
|
42
|
+
<div className="flex items-center gap-3 mb-1">
|
|
43
|
+
<span className="text-xs text-gray-500 font-mono">
|
|
44
|
+
{entry.date}
|
|
45
|
+
</span>
|
|
46
|
+
</div>
|
|
47
|
+
<p className="text-sm text-gray-200 mb-2">
|
|
48
|
+
{entry.description}
|
|
49
|
+
</p>
|
|
50
|
+
{entry.items.length > 0 && (
|
|
51
|
+
<ul className="space-y-1">
|
|
52
|
+
{entry.items.map((item, j) => (
|
|
53
|
+
<li
|
|
54
|
+
key={j}
|
|
55
|
+
className="text-xs text-gray-400 flex items-start gap-2"
|
|
56
|
+
>
|
|
57
|
+
<span className="text-gray-600 mt-0.5">•</span>
|
|
58
|
+
{item}
|
|
59
|
+
</li>
|
|
60
|
+
))}
|
|
61
|
+
</ul>
|
|
62
|
+
)}
|
|
63
|
+
{Object.keys(entry.extra).length > 0 && (
|
|
64
|
+
<div className="mt-2 text-xs text-gray-600">
|
|
65
|
+
{Object.entries(entry.extra).map(([k, v]) => (
|
|
66
|
+
<span key={k} className="mr-3">
|
|
67
|
+
{k}: {String(v)}
|
|
68
|
+
</span>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* Notes section */}
|
|
80
|
+
{progressData.notes.length > 0 && (
|
|
81
|
+
<div className="mt-8">
|
|
82
|
+
<h3 className="text-sm font-semibold text-gray-400 uppercase tracking-wider mb-3">
|
|
83
|
+
Notes
|
|
84
|
+
</h3>
|
|
85
|
+
<ul className="space-y-1.5">
|
|
86
|
+
{progressData.notes.map((note, i) => (
|
|
87
|
+
<li key={i} className="text-sm text-gray-400 flex items-start gap-2">
|
|
88
|
+
<span className="text-gray-600 mt-0.5">•</span>
|
|
89
|
+
{note}
|
|
90
|
+
</li>
|
|
91
|
+
))}
|
|
92
|
+
</ul>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
)
|
|
97
|
+
}
|
package/src/routes/index.tsx
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router'
|
|
2
2
|
import { StatusBadge } from '../components/StatusBadge'
|
|
3
3
|
import { ProgressBar } from '../components/ProgressBar'
|
|
4
|
+
import { BurndownChart } from '../components/BurndownChart'
|
|
5
|
+
import { useProgressData } from '../contexts/ProgressContext'
|
|
4
6
|
|
|
5
7
|
export const Route = createFileRoute('/')({
|
|
6
8
|
component: HomePage,
|
|
7
9
|
})
|
|
8
10
|
|
|
9
11
|
function HomePage() {
|
|
10
|
-
const
|
|
12
|
+
const progressData = useProgressData()
|
|
11
13
|
|
|
12
14
|
if (!progressData) {
|
|
13
15
|
return (
|
|
@@ -78,6 +80,9 @@ function HomePage() {
|
|
|
78
80
|
</div>
|
|
79
81
|
</div>
|
|
80
82
|
|
|
83
|
+
{/* Burndown Chart */}
|
|
84
|
+
<BurndownChart data={data} />
|
|
85
|
+
|
|
81
86
|
{/* Next Steps */}
|
|
82
87
|
{data.next_steps.length > 0 && (
|
|
83
88
|
<div>
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
-
import { useState } from 'react'
|
|
2
|
+
import { useState, lazy, Suspense } from 'react'
|
|
3
3
|
import { MilestoneTable } from '../components/MilestoneTable'
|
|
4
4
|
import { MilestoneTree } from '../components/MilestoneTree'
|
|
5
|
-
import {
|
|
5
|
+
import { MilestoneKanban } from '../components/MilestoneKanban'
|
|
6
|
+
import { MilestoneGantt } from '../components/MilestoneGantt'
|
|
7
|
+
import { ViewToggle, type ViewMode } from '../components/ViewToggle'
|
|
6
8
|
import { FilterBar } from '../components/FilterBar'
|
|
7
9
|
import { SearchInput } from '../components/SearchInput'
|
|
8
10
|
import { useFilteredData } from '../lib/useFilteredData'
|
|
11
|
+
import { useProgressData } from '../contexts/ProgressContext'
|
|
9
12
|
import type { Status } from '../lib/types'
|
|
10
13
|
|
|
14
|
+
// Lazy-load DependencyGraph to keep dagre out of the SSR bundle
|
|
15
|
+
// (dagre uses CommonJS require() which fails on Cloudflare Workers)
|
|
16
|
+
const DependencyGraph = lazy(() => import('../components/DependencyGraph').then(m => ({ default: m.DependencyGraph })))
|
|
17
|
+
|
|
11
18
|
export const Route = createFileRoute('/milestones')({
|
|
12
19
|
component: MilestonesPage,
|
|
13
20
|
})
|
|
14
21
|
|
|
15
22
|
function MilestonesPage() {
|
|
16
|
-
const
|
|
17
|
-
const [view, setView] = useState<
|
|
23
|
+
const progressData = useProgressData()
|
|
24
|
+
const [view, setView] = useState<ViewMode>('table')
|
|
18
25
|
const [status, setStatus] = useState<Status | 'all'>('all')
|
|
19
26
|
const [search, setSearch] = useState('')
|
|
20
27
|
|
|
@@ -34,16 +41,26 @@ function MilestonesPage() {
|
|
|
34
41
|
<h2 className="text-lg font-semibold">Milestones</h2>
|
|
35
42
|
<ViewToggle value={view} onChange={setView} />
|
|
36
43
|
</div>
|
|
37
|
-
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
<
|
|
44
|
+
{view !== 'kanban' && (
|
|
45
|
+
<div className="flex items-center gap-3 mb-4">
|
|
46
|
+
<FilterBar status={status} onStatusChange={setStatus} />
|
|
47
|
+
<div className="w-64">
|
|
48
|
+
<SearchInput value={search} onChange={setSearch} placeholder="Filter milestones..." />
|
|
49
|
+
</div>
|
|
41
50
|
</div>
|
|
42
|
-
|
|
51
|
+
)}
|
|
43
52
|
{view === 'table' ? (
|
|
44
53
|
<MilestoneTable milestones={filtered.milestones} tasks={filtered.tasks} />
|
|
45
|
-
) : (
|
|
54
|
+
) : view === 'tree' ? (
|
|
46
55
|
<MilestoneTree milestones={filtered.milestones} tasks={filtered.tasks} />
|
|
56
|
+
) : view === 'kanban' ? (
|
|
57
|
+
<MilestoneKanban milestones={filtered.milestones} tasks={filtered.tasks} />
|
|
58
|
+
) : view === 'gantt' ? (
|
|
59
|
+
<MilestoneGantt milestones={filtered.milestones} tasks={filtered.tasks} />
|
|
60
|
+
) : (
|
|
61
|
+
<Suspense fallback={<p className="text-gray-500 text-sm">Loading graph...</p>}>
|
|
62
|
+
<DependencyGraph data={filtered} />
|
|
63
|
+
</Suspense>
|
|
47
64
|
)}
|
|
48
65
|
</div>
|
|
49
66
|
)
|
package/src/routes/search.tsx
CHANGED
|
@@ -4,13 +4,14 @@ import { SearchInput } from '../components/SearchInput'
|
|
|
4
4
|
import { StatusBadge } from '../components/StatusBadge'
|
|
5
5
|
import { StatusDot } from '../components/StatusDot'
|
|
6
6
|
import { buildSearchIndex } from '../lib/search'
|
|
7
|
+
import { useProgressData } from '../contexts/ProgressContext'
|
|
7
8
|
|
|
8
9
|
export const Route = createFileRoute('/search')({
|
|
9
10
|
component: SearchPage,
|
|
10
11
|
})
|
|
11
12
|
|
|
12
13
|
function SearchPage() {
|
|
13
|
-
const
|
|
14
|
+
const progressData = useProgressData()
|
|
14
15
|
const [query, setQuery] = useState('')
|
|
15
16
|
|
|
16
17
|
const results = useMemo(() => {
|
package/src/routes/tasks.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createFileRoute } from '@tanstack/react-router'
|
|
2
2
|
import { StatusDot } from '../components/StatusDot'
|
|
3
3
|
import { ExtraFieldsBadge } from '../components/ExtraFieldsBadge'
|
|
4
|
+
import { useProgressData } from '../contexts/ProgressContext'
|
|
4
5
|
import type { Task } from '../lib/types'
|
|
5
6
|
|
|
6
7
|
export const Route = createFileRoute('/tasks')({
|
|
@@ -8,7 +9,7 @@ export const Route = createFileRoute('/tasks')({
|
|
|
8
9
|
})
|
|
9
10
|
|
|
10
11
|
function TasksPage() {
|
|
11
|
-
const
|
|
12
|
+
const progressData = useProgressData()
|
|
12
13
|
|
|
13
14
|
if (!progressData) {
|
|
14
15
|
return (
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workers Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Minimal server — passes all requests to TanStack Start.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import startServer from '@tanstack/react-start/server-entry'
|
|
8
|
+
|
|
9
|
+
export default {
|
|
10
|
+
async fetch(request: Request, env: unknown, ctx: unknown) {
|
|
11
|
+
return (startServer as any).fetch(request, env, ctx)
|
|
12
|
+
},
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
2
|
+
import type { ProgressData } from '../lib/types'
|
|
3
|
+
|
|
4
|
+
export type GitHubResult =
|
|
5
|
+
| { ok: true; data: ProgressData }
|
|
6
|
+
| { ok: false; error: string; message: string }
|
|
7
|
+
|
|
8
|
+
export const fetchGitHubProgress = createServerFn({ method: 'GET' })
|
|
9
|
+
.validator((input: { owner: string; repo: string; branch?: string; token?: string }) => input)
|
|
10
|
+
.handler(async ({ data: input }): Promise<GitHubResult> => {
|
|
11
|
+
const { parseProgressYaml } = await import('../lib/yaml-loader')
|
|
12
|
+
|
|
13
|
+
const branch = input.branch || 'main'
|
|
14
|
+
const url = `https://raw.githubusercontent.com/${input.owner}/${input.repo}/${branch}/agent/progress.yaml`
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const headers: Record<string, string> = {
|
|
18
|
+
'Accept': 'text/plain',
|
|
19
|
+
}
|
|
20
|
+
if (input.token) {
|
|
21
|
+
headers['Authorization'] = `token ${input.token}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const response = await fetch(url, { headers })
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
if (response.status === 404) {
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: 'NOT_FOUND',
|
|
31
|
+
message: `No progress.yaml found at ${input.owner}/${input.repo} (branch: ${branch})`,
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
ok: false,
|
|
36
|
+
error: 'FETCH_ERROR',
|
|
37
|
+
message: `GitHub returned ${response.status}: ${response.statusText}`,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const raw = await response.text()
|
|
42
|
+
const data = parseProgressYaml(raw)
|
|
43
|
+
return { ok: true, data }
|
|
44
|
+
} catch (err) {
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: 'NETWORK_ERROR',
|
|
48
|
+
message: err instanceof Error ? err.message : 'Failed to fetch from GitHub',
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
})
|
|
@@ -3,47 +3,25 @@ import type { ProgressData } from '../lib/types'
|
|
|
3
3
|
|
|
4
4
|
export type ProgressResult =
|
|
5
5
|
| { ok: true; data: ProgressData }
|
|
6
|
-
| { ok: false; error: 'FILE_NOT_FOUND' | 'PARSE_ERROR'; message: string; path: string }
|
|
7
|
-
|
|
8
|
-
export const getProgressData = createServerFn({ method: 'GET' }).handler(
|
|
9
|
-
async (): Promise<ProgressResult> => {
|
|
10
|
-
// Dynamic imports keep fs and yaml-loader out of the client bundle
|
|
11
|
-
const { readFileSync } = await import('fs')
|
|
12
|
-
const { parseProgressYaml } = await import('../lib/yaml-loader')
|
|
13
|
-
const { getProgressYamlPath } = await import('../lib/config')
|
|
14
|
-
|
|
15
|
-
const filePath = getProgressYamlPath()
|
|
6
|
+
| { ok: false; error: 'FILE_NOT_FOUND' | 'PARSE_ERROR' | 'NO_FILESYSTEM'; message: string; path: string }
|
|
16
7
|
|
|
8
|
+
export const getProgressData = createServerFn({ method: 'GET' })
|
|
9
|
+
.validator((input: { path?: string }) => input)
|
|
10
|
+
.handler(async ({ data: input }): Promise<ProgressResult> => {
|
|
17
11
|
try {
|
|
18
|
-
const
|
|
12
|
+
const fs = await import('fs')
|
|
13
|
+
const { parseProgressYaml } = await import('../lib/yaml-loader')
|
|
14
|
+
const { getProgressYamlPath } = await import('../lib/config')
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
message: err instanceof Error ? err.message : 'Failed to parse YAML',
|
|
28
|
-
path: filePath,
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
} catch (err) {
|
|
32
|
-
const code = (err as NodeJS.ErrnoException).code
|
|
33
|
-
if (code === 'ENOENT') {
|
|
34
|
-
return {
|
|
35
|
-
ok: false,
|
|
36
|
-
error: 'FILE_NOT_FOUND',
|
|
37
|
-
message: `progress.yaml not found at: ${filePath}`,
|
|
38
|
-
path: filePath,
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
error: 'PARSE_ERROR',
|
|
44
|
-
message: err instanceof Error ? err.message : 'Failed to read file',
|
|
45
|
-
path: filePath,
|
|
16
|
+
const filePath = input.path || getProgressYamlPath()
|
|
17
|
+
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
18
|
+
const data = parseProgressYaml(raw)
|
|
19
|
+
return { ok: true, data }
|
|
20
|
+
} catch (err: any) {
|
|
21
|
+
if (err?.code === 'ENOENT') {
|
|
22
|
+
return { ok: false, error: 'FILE_NOT_FOUND', message: `progress.yaml not found`, path: input.path || '' }
|
|
46
23
|
}
|
|
24
|
+
// Cloudflare Workers: fs module exists but readFileSync throws
|
|
25
|
+
return { ok: false, error: 'NO_FILESYSTEM', message: 'No local filesystem — use GitHub input to load a project', path: '' }
|
|
47
26
|
}
|
|
48
|
-
}
|
|
49
|
-
)
|
|
27
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createServerFn } from '@tanstack/react-start'
|
|
2
|
+
|
|
3
|
+
export interface AcpProject {
|
|
4
|
+
id: string
|
|
5
|
+
path: string
|
|
6
|
+
type: string
|
|
7
|
+
description: string
|
|
8
|
+
status: string
|
|
9
|
+
hasProgress: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const listProjects = createServerFn({ method: 'GET' }).handler(
|
|
13
|
+
async (): Promise<AcpProject[]> => {
|
|
14
|
+
try {
|
|
15
|
+
const { readFileSync, existsSync } = await import('fs')
|
|
16
|
+
const { resolve } = await import('path')
|
|
17
|
+
const { homedir } = await import('os')
|
|
18
|
+
const yaml = await import('js-yaml')
|
|
19
|
+
|
|
20
|
+
const projectsFile = resolve(homedir(), '.acp', 'projects.yaml')
|
|
21
|
+
if (!existsSync(projectsFile)) return []
|
|
22
|
+
|
|
23
|
+
const raw = readFileSync(projectsFile, 'utf-8')
|
|
24
|
+
const doc = yaml.load(raw, { json: true }) as Record<string, unknown>
|
|
25
|
+
const projects = (doc?.projects || {}) as Record<string, Record<string, unknown>>
|
|
26
|
+
|
|
27
|
+
return Object.entries(projects).map(([id, p]) => {
|
|
28
|
+
const projectPath = String(p.path || '')
|
|
29
|
+
const progressPath = resolve(projectPath, 'agent', 'progress.yaml')
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
path: projectPath,
|
|
33
|
+
type: String(p.type || 'unknown'),
|
|
34
|
+
description: String(p.description || ''),
|
|
35
|
+
status: String(p.status || 'unknown'),
|
|
36
|
+
hasProgress: existsSync(progressPath),
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
} catch {
|
|
40
|
+
// Cloudflare Workers: no filesystem
|
|
41
|
+
return []
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
export const getProjectProgressPath = createServerFn({ method: 'GET' })
|
|
47
|
+
.validator((input: { projectId: string }) => input)
|
|
48
|
+
.handler(async ({ data }): Promise<string | null> => {
|
|
49
|
+
try {
|
|
50
|
+
const { readFileSync, existsSync } = await import('fs')
|
|
51
|
+
const { resolve } = await import('path')
|
|
52
|
+
const { homedir } = await import('os')
|
|
53
|
+
const yaml = await import('js-yaml')
|
|
54
|
+
|
|
55
|
+
const projectsFile = resolve(homedir(), '.acp', 'projects.yaml')
|
|
56
|
+
if (!existsSync(projectsFile)) return null
|
|
57
|
+
|
|
58
|
+
const raw = readFileSync(projectsFile, 'utf-8')
|
|
59
|
+
const doc = yaml.load(raw, { json: true }) as Record<string, unknown>
|
|
60
|
+
const projects = (doc?.projects || {}) as Record<string, Record<string, unknown>>
|
|
61
|
+
const project = projects[data.projectId]
|
|
62
|
+
if (!project) return null
|
|
63
|
+
|
|
64
|
+
const progressPath = resolve(String(project.path), 'agent', 'progress.yaml')
|
|
65
|
+
return existsSync(progressPath) ? progressPath : null
|
|
66
|
+
} catch {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
})
|