@prmichaelsen/acp-visualizer 0.8.1 → 0.8.2
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 +1 -1
- package/src/components/DocumentDetail.tsx +100 -0
- package/src/components/DocumentList.tsx +90 -0
- package/src/components/MilestoneKanban.tsx +2 -0
- package/src/components/MilestoneTable.tsx +6 -0
- package/src/components/MilestoneTree.tsx +2 -0
- package/src/components/PriorityBadge.tsx +20 -0
- package/src/components/Sidebar.tsx +4 -1
- package/src/components/TaskList.tsx +2 -0
- package/src/lib/types.ts +8 -0
- package/src/lib/yaml-loader.ts +49 -13
- package/src/routeTree.gen.ts +211 -0
- package/src/routes/designs.$slug.tsx +18 -0
- package/src/routes/designs.index.tsx +10 -0
- package/src/routes/designs.tsx +9 -0
- package/src/routes/milestones.$milestoneId.tsx +6 -0
- package/src/routes/patterns.$slug.tsx +18 -0
- package/src/routes/patterns.index.tsx +10 -0
- package/src/routes/patterns.tsx +9 -0
- package/src/routes/reports.$slug.tsx +18 -0
- package/src/routes/reports.index.tsx +10 -0
- package/src/routes/reports.tsx +9 -0
- package/src/routes/tasks.$taskId.tsx +11 -1
- package/src/services/markdown.service.ts +102 -0
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
2
|
+
import { Breadcrumb } from './Breadcrumb'
|
|
3
|
+
import { MarkdownContent, buildLinkMap } from './MarkdownContent'
|
|
4
|
+
import { getMarkdownContent, listAgentDirectory } from '../services/markdown.service'
|
|
5
|
+
import { useProgressData } from '../contexts/ProgressContext'
|
|
6
|
+
import type { MarkdownResult, AgentFile } from '../services/markdown.service'
|
|
7
|
+
|
|
8
|
+
interface DocumentDetailProps {
|
|
9
|
+
slug: string
|
|
10
|
+
dirPath: string
|
|
11
|
+
sectionLabel: string
|
|
12
|
+
sectionHref: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Read ?repo=owner/repo from URL */
|
|
16
|
+
function getGitHubParams(): { owner: string; repo: string } | undefined {
|
|
17
|
+
if (typeof window === 'undefined') return undefined
|
|
18
|
+
const params = new URLSearchParams(window.location.search)
|
|
19
|
+
const repo = params.get('repo')
|
|
20
|
+
if (!repo) return undefined
|
|
21
|
+
const parts = repo.split('/')
|
|
22
|
+
if (parts.length < 2) return undefined
|
|
23
|
+
return { owner: parts[0], repo: parts[1] }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DocumentDetail({ slug, dirPath, sectionLabel, sectionHref }: DocumentDetailProps) {
|
|
27
|
+
const data = useProgressData()
|
|
28
|
+
const [markdown, setMarkdown] = useState<string | null>(null)
|
|
29
|
+
const [error, setError] = useState<string | null>(null)
|
|
30
|
+
const [loading, setLoading] = useState(true)
|
|
31
|
+
const [filePath, setFilePath] = useState<string | null>(null)
|
|
32
|
+
const linkMap = useMemo(() => (data ? buildLinkMap(data) : {}), [data])
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
setLoading(true)
|
|
36
|
+
setMarkdown(null)
|
|
37
|
+
setError(null)
|
|
38
|
+
|
|
39
|
+
const github = getGitHubParams()
|
|
40
|
+
const relativePath = `${dirPath}/${slug}.md`
|
|
41
|
+
|
|
42
|
+
// Try the direct path first, fallback to directory listing if needed
|
|
43
|
+
getMarkdownContent({ data: { filePath: relativePath, github } })
|
|
44
|
+
.then((result: MarkdownResult) => {
|
|
45
|
+
if (result.ok) {
|
|
46
|
+
setMarkdown(result.content)
|
|
47
|
+
setFilePath(result.filePath)
|
|
48
|
+
} else {
|
|
49
|
+
// Fallback: search directory for a file containing the slug
|
|
50
|
+
return listAgentDirectory({ data: { dirPath, github } }).then((listResult) => {
|
|
51
|
+
if (listResult.ok) {
|
|
52
|
+
const match = listResult.files.find((f: AgentFile) => f.name === slug)
|
|
53
|
+
if (match) {
|
|
54
|
+
return getMarkdownContent({ data: { filePath: match.relativePath, github } })
|
|
55
|
+
.then((mdResult: MarkdownResult) => {
|
|
56
|
+
if (mdResult.ok) {
|
|
57
|
+
setMarkdown(mdResult.content)
|
|
58
|
+
setFilePath(mdResult.filePath)
|
|
59
|
+
} else {
|
|
60
|
+
setError(mdResult.error)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
setError(result.error)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.catch((err: Error) => setError(err.message))
|
|
70
|
+
.finally(() => setLoading(false))
|
|
71
|
+
}, [slug, dirPath])
|
|
72
|
+
|
|
73
|
+
const displayName = slug
|
|
74
|
+
.replace(/^[a-z0-9-]+\./, '')
|
|
75
|
+
.replace(/[-_]/g, ' ')
|
|
76
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<div className="p-6 max-w-4xl">
|
|
80
|
+
<Breadcrumb
|
|
81
|
+
items={[
|
|
82
|
+
{ label: sectionLabel, href: sectionHref },
|
|
83
|
+
{ label: displayName },
|
|
84
|
+
]}
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<h1 className="text-xl font-semibold text-gray-100 mb-6">{displayName}</h1>
|
|
88
|
+
|
|
89
|
+
{loading ? (
|
|
90
|
+
<p className="text-sm text-gray-600">Loading document...</p>
|
|
91
|
+
) : markdown ? (
|
|
92
|
+
<MarkdownContent content={markdown} basePath={filePath ?? undefined} linkMap={linkMap} />
|
|
93
|
+
) : error ? (
|
|
94
|
+
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
|
|
95
|
+
No document found — {error}
|
|
96
|
+
</div>
|
|
97
|
+
) : null}
|
|
98
|
+
</div>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Link } from '@tanstack/react-router'
|
|
2
|
+
import { useState, useEffect } from 'react'
|
|
3
|
+
import { listAgentDirectory } from '../services/markdown.service'
|
|
4
|
+
import type { AgentFile } from '../services/markdown.service'
|
|
5
|
+
import { FileText } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
interface DocumentListProps {
|
|
8
|
+
title: string
|
|
9
|
+
dirPath: string
|
|
10
|
+
baseTo: string
|
|
11
|
+
github?: { owner: string; repo: string; branch?: string; token?: string }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DocumentList({ title, dirPath, baseTo, github }: DocumentListProps) {
|
|
15
|
+
const [files, setFiles] = useState<AgentFile[]>([])
|
|
16
|
+
const [error, setError] = useState<string | null>(null)
|
|
17
|
+
const [loading, setLoading] = useState(true)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
setLoading(true)
|
|
21
|
+
listAgentDirectory({ data: { dirPath, github } })
|
|
22
|
+
.then((result) => {
|
|
23
|
+
if (result.ok) {
|
|
24
|
+
setFiles(result.files)
|
|
25
|
+
} else {
|
|
26
|
+
setError(result.error)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
.catch((err: Error) => setError(err.message))
|
|
30
|
+
.finally(() => setLoading(false))
|
|
31
|
+
}, [dirPath, github])
|
|
32
|
+
|
|
33
|
+
if (loading) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="p-6">
|
|
36
|
+
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
37
|
+
<p className="text-sm text-gray-600">Loading...</p>
|
|
38
|
+
</div>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (error) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="p-6">
|
|
45
|
+
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
46
|
+
<div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
|
|
47
|
+
{error}
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (files.length === 0) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="p-6">
|
|
56
|
+
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
57
|
+
<p className="text-sm text-gray-500">No documents found in <code className="text-gray-400">{dirPath}/</code></p>
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="p-6">
|
|
64
|
+
<h2 className="text-lg font-semibold mb-4">{title}</h2>
|
|
65
|
+
<div className="bg-gray-900/50 border border-gray-800 rounded-xl divide-y divide-gray-800">
|
|
66
|
+
{files.map((file) => (
|
|
67
|
+
<Link
|
|
68
|
+
key={file.name}
|
|
69
|
+
to={baseTo + '/$slug'}
|
|
70
|
+
params={{ slug: file.name }}
|
|
71
|
+
className="flex items-center gap-3 px-4 py-3 text-sm hover:bg-gray-800/50 transition-colors first:rounded-t-xl last:rounded-b-xl"
|
|
72
|
+
>
|
|
73
|
+
<FileText className="w-4 h-4 text-gray-500 shrink-0" />
|
|
74
|
+
<span className="text-gray-200">{formatName(file.name)}</span>
|
|
75
|
+
</Link>
|
|
76
|
+
))}
|
|
77
|
+
</div>
|
|
78
|
+
<p className="text-xs text-gray-600 mt-3">{files.length} document{files.length !== 1 ? 's' : ''}</p>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Turn "local.dashboard-layout-routing" into "Dashboard Layout Routing" */
|
|
84
|
+
function formatName(name: string): string {
|
|
85
|
+
// Strip common prefixes like "local." or "core-sdk."
|
|
86
|
+
const stripped = name.replace(/^[a-z0-9-]+\./, '')
|
|
87
|
+
return stripped
|
|
88
|
+
.replace(/[-_]/g, ' ')
|
|
89
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
90
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Link } from '@tanstack/react-router'
|
|
2
2
|
import { StatusBadge } from './StatusBadge'
|
|
3
|
+
import { PriorityBadge } from './PriorityBadge'
|
|
3
4
|
import { ProgressBar } from './ProgressBar'
|
|
4
5
|
import { TaskList } from './TaskList'
|
|
5
6
|
import type { Milestone, Task, Status } from '../lib/types'
|
|
@@ -37,6 +38,7 @@ function KanbanCard({
|
|
|
37
38
|
>
|
|
38
39
|
{milestone.name}
|
|
39
40
|
</Link>
|
|
41
|
+
<PriorityBadge priority={milestone.priority} />
|
|
40
42
|
</div>
|
|
41
43
|
<div className="flex items-center gap-2 mb-2">
|
|
42
44
|
<div className="flex-1">
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from '@tanstack/react-table'
|
|
11
11
|
import { ChevronDown, ChevronRight, ArrowUpDown } from 'lucide-react'
|
|
12
12
|
import { StatusBadge } from './StatusBadge'
|
|
13
|
+
import { PriorityBadge } from './PriorityBadge'
|
|
13
14
|
import { ProgressBar } from './ProgressBar'
|
|
14
15
|
import { TaskList } from './TaskList'
|
|
15
16
|
import type { Milestone, Task } from '../lib/types'
|
|
@@ -68,6 +69,11 @@ export function MilestoneTable({ milestones, tasks }: MilestoneTableProps) {
|
|
|
68
69
|
cell: (info) => <StatusBadge status={info.getValue()} />,
|
|
69
70
|
size: 120,
|
|
70
71
|
}),
|
|
72
|
+
columnHelper.accessor('priority', {
|
|
73
|
+
header: 'Priority',
|
|
74
|
+
cell: (info) => <PriorityBadge priority={info.getValue()} />,
|
|
75
|
+
size: 100,
|
|
76
|
+
}),
|
|
71
77
|
columnHelper.accessor('progress', {
|
|
72
78
|
header: 'Progress',
|
|
73
79
|
cell: (info) => (
|
|
@@ -2,6 +2,7 @@ import { useState } from 'react'
|
|
|
2
2
|
import { Link } from '@tanstack/react-router'
|
|
3
3
|
import { ChevronDown, ChevronRight } from 'lucide-react'
|
|
4
4
|
import { StatusBadge } from './StatusBadge'
|
|
5
|
+
import { PriorityBadge } from './PriorityBadge'
|
|
5
6
|
import { ProgressBar } from './ProgressBar'
|
|
6
7
|
import { TaskList } from './TaskList'
|
|
7
8
|
import { useCollapse } from '../lib/useCollapse'
|
|
@@ -45,6 +46,7 @@ function MilestoneTreeRow({
|
|
|
45
46
|
{milestone.name}
|
|
46
47
|
</Link>
|
|
47
48
|
<StatusBadge status={milestone.status} />
|
|
49
|
+
<PriorityBadge priority={milestone.priority} />
|
|
48
50
|
<div className="w-20">
|
|
49
51
|
<ProgressBar value={milestone.progress} size="sm" />
|
|
50
52
|
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Priority } from '../lib/types'
|
|
2
|
+
|
|
3
|
+
const priorityStyles: Record<Priority, string> = {
|
|
4
|
+
critical: 'bg-rose-500/20 text-rose-400',
|
|
5
|
+
high: 'bg-amber-500/20 text-amber-400',
|
|
6
|
+
medium: 'bg-blue-500/20 text-blue-400',
|
|
7
|
+
low: 'bg-zinc-500/20 text-zinc-400',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function PriorityBadge({ priority }: { priority: Priority | undefined }) {
|
|
11
|
+
if (!priority) return null
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<span
|
|
15
|
+
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium uppercase ${priorityStyles[priority] ?? priorityStyles.medium}`}
|
|
16
|
+
>
|
|
17
|
+
{priority}
|
|
18
|
+
</span>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Link, useRouterState } from '@tanstack/react-router'
|
|
2
|
-
import { LayoutDashboard, Flag, CheckSquare, Clock, Search } from 'lucide-react'
|
|
2
|
+
import { LayoutDashboard, Flag, CheckSquare, Clock, Search, PenTool, Puzzle, FileBarChart } from 'lucide-react'
|
|
3
3
|
import { ProjectSelector } from './ProjectSelector'
|
|
4
4
|
import { GitHubInput } from './GitHubInput'
|
|
5
5
|
import type { AcpProject } from '../services/projects.service'
|
|
@@ -9,6 +9,9 @@ const navItems = [
|
|
|
9
9
|
{ to: '/milestones' as const, icon: Flag, label: 'Milestones' },
|
|
10
10
|
{ to: '/tasks' as const, icon: CheckSquare, label: 'Tasks' },
|
|
11
11
|
{ to: '/activity' as const, icon: Clock, label: 'Activity' },
|
|
12
|
+
{ to: '/designs' as const, icon: PenTool, label: 'Designs' },
|
|
13
|
+
{ to: '/patterns' as const, icon: Puzzle, label: 'Patterns' },
|
|
14
|
+
{ to: '/reports' as const, icon: FileBarChart, label: 'Reports' },
|
|
12
15
|
]
|
|
13
16
|
|
|
14
17
|
interface SidebarProps {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Link } from '@tanstack/react-router'
|
|
2
2
|
import { StatusDot } from './StatusDot'
|
|
3
|
+
import { PriorityBadge } from './PriorityBadge'
|
|
3
4
|
import { ExtraFieldsBadge } from './ExtraFieldsBadge'
|
|
4
5
|
import type { Task } from '../lib/types'
|
|
5
6
|
|
|
@@ -26,6 +27,7 @@ export function TaskList({ tasks }: { tasks: Task[] }) {
|
|
|
26
27
|
>
|
|
27
28
|
{task.name}
|
|
28
29
|
</Link>
|
|
30
|
+
<PriorityBadge priority={task.priority} />
|
|
29
31
|
{task.notes && (
|
|
30
32
|
<span className="text-xs text-gray-600 ml-auto truncate max-w-[200px]">
|
|
31
33
|
{task.notes}
|
package/src/lib/types.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/** Status values for milestones and tasks */
|
|
2
2
|
export type Status = 'completed' | 'in_progress' | 'not_started' | 'wont_do'
|
|
3
3
|
|
|
4
|
+
/** Priority values for milestones and tasks (ACP 6.0.0+) */
|
|
5
|
+
export type Priority = 'critical' | 'high' | 'medium' | 'low'
|
|
6
|
+
|
|
4
7
|
/** Unknown properties from agent-maintained YAML are preserved here */
|
|
5
8
|
export type ExtraFields = Record<string, unknown>
|
|
6
9
|
|
|
@@ -29,6 +32,8 @@ export interface ProjectMetadata {
|
|
|
29
32
|
export interface Milestone {
|
|
30
33
|
id: string
|
|
31
34
|
name: string
|
|
35
|
+
priority: Priority
|
|
36
|
+
file: string
|
|
32
37
|
status: Status
|
|
33
38
|
progress: number // 0-100
|
|
34
39
|
started: string | null
|
|
@@ -43,10 +48,13 @@ export interface Milestone {
|
|
|
43
48
|
export interface Task {
|
|
44
49
|
id: string
|
|
45
50
|
name: string
|
|
51
|
+
priority: Priority
|
|
46
52
|
status: Status
|
|
47
53
|
milestone_id: string
|
|
48
54
|
file: string
|
|
49
55
|
estimated_hours: string
|
|
56
|
+
actual_hours: number | null
|
|
57
|
+
started: string | null
|
|
50
58
|
completed_date: string | null
|
|
51
59
|
notes: string
|
|
52
60
|
extra: ExtraFields
|
package/src/lib/yaml-loader.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
DocumentationStats,
|
|
9
9
|
ProgressSummary,
|
|
10
10
|
Status,
|
|
11
|
+
Priority,
|
|
11
12
|
ExtraFields,
|
|
12
13
|
} from './types'
|
|
13
14
|
|
|
@@ -51,6 +52,14 @@ function normalizeStatus(value: unknown): Status {
|
|
|
51
52
|
return 'not_started'
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function normalizePriority(value: unknown): Priority {
|
|
56
|
+
const s = String(value || 'medium').toLowerCase()
|
|
57
|
+
if (s === 'critical') return 'critical'
|
|
58
|
+
if (s === 'high') return 'high'
|
|
59
|
+
if (s === 'low') return 'low'
|
|
60
|
+
return 'medium'
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
function safeString(value: unknown, fallback = ''): string {
|
|
55
64
|
if (value == null) return fallback
|
|
56
65
|
return String(value)
|
|
@@ -127,7 +136,7 @@ function normalizeProject(raw: unknown): ProjectMetadata {
|
|
|
127
136
|
}
|
|
128
137
|
|
|
129
138
|
const MILESTONE_KEYS = [
|
|
130
|
-
'id', 'name', 'status', 'progress', 'started', 'completed',
|
|
139
|
+
'id', 'name', 'priority', 'file', 'status', 'progress', 'started', 'completed',
|
|
131
140
|
'estimated_weeks', 'tasks_completed', 'tasks_total', 'notes',
|
|
132
141
|
]
|
|
133
142
|
|
|
@@ -137,6 +146,8 @@ function normalizeMilestone(raw: unknown, index: number): Milestone {
|
|
|
137
146
|
return {
|
|
138
147
|
id: safeString(known.id, `milestone_${index + 1}`),
|
|
139
148
|
name: safeString(known.name, `Milestone ${index + 1}`),
|
|
149
|
+
priority: normalizePriority(known.priority),
|
|
150
|
+
file: safeString(known.file),
|
|
140
151
|
status: normalizeStatus(known.status),
|
|
141
152
|
progress: known.progress != null
|
|
142
153
|
? safeNumber(known.progress)
|
|
@@ -154,13 +165,25 @@ function normalizeMilestone(raw: unknown, index: number): Milestone {
|
|
|
154
165
|
}
|
|
155
166
|
|
|
156
167
|
function normalizeMilestones(raw: unknown): Milestone[] {
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
// v6 format: milestones is a map keyed by ID (e.g. { M1: {...}, M2: {...} })
|
|
169
|
+
if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
|
|
170
|
+
const obj = raw as Record<string, unknown>
|
|
171
|
+
return Object.entries(obj).map(([key, value], i) => {
|
|
172
|
+
const m = normalizeMilestone(value, i)
|
|
173
|
+
// The map key IS the milestone ID — override whatever was parsed
|
|
174
|
+
return { ...m, id: key }
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
// Pre-v6 format: milestones is an array
|
|
178
|
+
if (Array.isArray(raw)) {
|
|
179
|
+
return raw.map((item, i) => normalizeMilestone(item, i))
|
|
180
|
+
}
|
|
181
|
+
return []
|
|
159
182
|
}
|
|
160
183
|
|
|
161
184
|
const TASK_KEYS = [
|
|
162
|
-
'id', 'name', 'status', 'milestone_id', 'file',
|
|
163
|
-
'estimated_hours', 'completed_date', 'notes',
|
|
185
|
+
'id', 'name', 'priority', 'status', 'milestone_id', 'file',
|
|
186
|
+
'estimated_hours', 'actual_hours', 'started', 'completed_date', 'notes',
|
|
164
187
|
]
|
|
165
188
|
|
|
166
189
|
function normalizeTask(raw: unknown, milestoneId: string, index: number): Task {
|
|
@@ -169,10 +192,13 @@ function normalizeTask(raw: unknown, milestoneId: string, index: number): Task {
|
|
|
169
192
|
return {
|
|
170
193
|
id: safeString(known.id, `task_${index + 1}`),
|
|
171
194
|
name: safeString(known.name, `Task ${index + 1}`),
|
|
195
|
+
priority: normalizePriority(known.priority),
|
|
172
196
|
status: normalizeStatus(known.status),
|
|
173
197
|
milestone_id: safeString(known.milestone_id, milestoneId),
|
|
174
198
|
file: safeString(known.file),
|
|
175
199
|
estimated_hours: safeString(known.estimated_hours, '0'),
|
|
200
|
+
actual_hours: known.actual_hours != null ? safeNumber(known.actual_hours) : null,
|
|
201
|
+
started: known.started ? safeString(known.started) : null,
|
|
176
202
|
completed_date: known.completed_date ? safeString(known.completed_date) : null,
|
|
177
203
|
notes: safeString(known.notes),
|
|
178
204
|
extra,
|
|
@@ -388,20 +414,30 @@ export function parseProgressYaml(raw: string): ProgressData {
|
|
|
388
414
|
if (looseTasks.length > 0) {
|
|
389
415
|
const existing = allTasks['_unassigned'] || []
|
|
390
416
|
allTasks['_unassigned'] = [...existing, ...looseTasks]
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Create synthetic milestones for any task keys without a matching milestone.
|
|
420
|
+
// This handles `unassigned:` (or any other key) inside the `tasks:` object
|
|
421
|
+
// that doesn't correspond to a declared milestone.
|
|
422
|
+
for (const key of Object.keys(allTasks)) {
|
|
423
|
+
if (!seenIds.has(key)) {
|
|
424
|
+
seenIds.add(key)
|
|
425
|
+
const tasks = allTasks[key]
|
|
426
|
+
const isUnassigned = key === '_unassigned' || key.toLowerCase() === 'unassigned'
|
|
427
|
+
const displayName = isUnassigned ? 'Unassigned Tasks' : key
|
|
394
428
|
allMilestones.push({
|
|
395
|
-
id:
|
|
396
|
-
name:
|
|
429
|
+
id: key,
|
|
430
|
+
name: displayName,
|
|
431
|
+
priority: 'medium',
|
|
432
|
+
file: '',
|
|
397
433
|
status: 'in_progress',
|
|
398
434
|
progress: 0,
|
|
399
435
|
started: null,
|
|
400
436
|
completed: null,
|
|
401
437
|
estimated_weeks: '0',
|
|
402
|
-
tasks_completed:
|
|
403
|
-
tasks_total:
|
|
404
|
-
notes: 'Tasks not assigned to a specific milestone'
|
|
438
|
+
tasks_completed: tasks.filter((t) => t.status === 'completed').length,
|
|
439
|
+
tasks_total: tasks.length,
|
|
440
|
+
notes: isUnassigned ? 'Tasks not assigned to a specific milestone' : `Tasks under "${key}"`,
|
|
405
441
|
extra: { synthetic: true },
|
|
406
442
|
})
|
|
407
443
|
}
|
package/src/routeTree.gen.ts
CHANGED
|
@@ -11,13 +11,22 @@
|
|
|
11
11
|
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
|
+
import { Route as ReportsRouteImport } from './routes/reports'
|
|
15
|
+
import { Route as PatternsRouteImport } from './routes/patterns'
|
|
14
16
|
import { Route as MilestonesRouteImport } from './routes/milestones'
|
|
17
|
+
import { Route as DesignsRouteImport } from './routes/designs'
|
|
15
18
|
import { Route as ActivityRouteImport } from './routes/activity'
|
|
16
19
|
import { Route as IndexRouteImport } from './routes/index'
|
|
17
20
|
import { Route as TasksIndexRouteImport } from './routes/tasks.index'
|
|
21
|
+
import { Route as ReportsIndexRouteImport } from './routes/reports.index'
|
|
22
|
+
import { Route as PatternsIndexRouteImport } from './routes/patterns.index'
|
|
18
23
|
import { Route as MilestonesIndexRouteImport } from './routes/milestones.index'
|
|
24
|
+
import { Route as DesignsIndexRouteImport } from './routes/designs.index'
|
|
19
25
|
import { Route as TasksTaskIdRouteImport } from './routes/tasks.$taskId'
|
|
26
|
+
import { Route as ReportsSlugRouteImport } from './routes/reports.$slug'
|
|
27
|
+
import { Route as PatternsSlugRouteImport } from './routes/patterns.$slug'
|
|
20
28
|
import { Route as MilestonesMilestoneIdRouteImport } from './routes/milestones.$milestoneId'
|
|
29
|
+
import { Route as DesignsSlugRouteImport } from './routes/designs.$slug'
|
|
21
30
|
import { Route as ApiWatchRouteImport } from './routes/api/watch'
|
|
22
31
|
|
|
23
32
|
const TasksRoute = TasksRouteImport.update({
|
|
@@ -30,11 +39,26 @@ const SearchRoute = SearchRouteImport.update({
|
|
|
30
39
|
path: '/search',
|
|
31
40
|
getParentRoute: () => rootRouteImport,
|
|
32
41
|
} as any)
|
|
42
|
+
const ReportsRoute = ReportsRouteImport.update({
|
|
43
|
+
id: '/reports',
|
|
44
|
+
path: '/reports',
|
|
45
|
+
getParentRoute: () => rootRouteImport,
|
|
46
|
+
} as any)
|
|
47
|
+
const PatternsRoute = PatternsRouteImport.update({
|
|
48
|
+
id: '/patterns',
|
|
49
|
+
path: '/patterns',
|
|
50
|
+
getParentRoute: () => rootRouteImport,
|
|
51
|
+
} as any)
|
|
33
52
|
const MilestonesRoute = MilestonesRouteImport.update({
|
|
34
53
|
id: '/milestones',
|
|
35
54
|
path: '/milestones',
|
|
36
55
|
getParentRoute: () => rootRouteImport,
|
|
37
56
|
} as any)
|
|
57
|
+
const DesignsRoute = DesignsRouteImport.update({
|
|
58
|
+
id: '/designs',
|
|
59
|
+
path: '/designs',
|
|
60
|
+
getParentRoute: () => rootRouteImport,
|
|
61
|
+
} as any)
|
|
38
62
|
const ActivityRoute = ActivityRouteImport.update({
|
|
39
63
|
id: '/activity',
|
|
40
64
|
path: '/activity',
|
|
@@ -50,21 +74,51 @@ const TasksIndexRoute = TasksIndexRouteImport.update({
|
|
|
50
74
|
path: '/',
|
|
51
75
|
getParentRoute: () => TasksRoute,
|
|
52
76
|
} as any)
|
|
77
|
+
const ReportsIndexRoute = ReportsIndexRouteImport.update({
|
|
78
|
+
id: '/',
|
|
79
|
+
path: '/',
|
|
80
|
+
getParentRoute: () => ReportsRoute,
|
|
81
|
+
} as any)
|
|
82
|
+
const PatternsIndexRoute = PatternsIndexRouteImport.update({
|
|
83
|
+
id: '/',
|
|
84
|
+
path: '/',
|
|
85
|
+
getParentRoute: () => PatternsRoute,
|
|
86
|
+
} as any)
|
|
53
87
|
const MilestonesIndexRoute = MilestonesIndexRouteImport.update({
|
|
54
88
|
id: '/',
|
|
55
89
|
path: '/',
|
|
56
90
|
getParentRoute: () => MilestonesRoute,
|
|
57
91
|
} as any)
|
|
92
|
+
const DesignsIndexRoute = DesignsIndexRouteImport.update({
|
|
93
|
+
id: '/',
|
|
94
|
+
path: '/',
|
|
95
|
+
getParentRoute: () => DesignsRoute,
|
|
96
|
+
} as any)
|
|
58
97
|
const TasksTaskIdRoute = TasksTaskIdRouteImport.update({
|
|
59
98
|
id: '/$taskId',
|
|
60
99
|
path: '/$taskId',
|
|
61
100
|
getParentRoute: () => TasksRoute,
|
|
62
101
|
} as any)
|
|
102
|
+
const ReportsSlugRoute = ReportsSlugRouteImport.update({
|
|
103
|
+
id: '/$slug',
|
|
104
|
+
path: '/$slug',
|
|
105
|
+
getParentRoute: () => ReportsRoute,
|
|
106
|
+
} as any)
|
|
107
|
+
const PatternsSlugRoute = PatternsSlugRouteImport.update({
|
|
108
|
+
id: '/$slug',
|
|
109
|
+
path: '/$slug',
|
|
110
|
+
getParentRoute: () => PatternsRoute,
|
|
111
|
+
} as any)
|
|
63
112
|
const MilestonesMilestoneIdRoute = MilestonesMilestoneIdRouteImport.update({
|
|
64
113
|
id: '/$milestoneId',
|
|
65
114
|
path: '/$milestoneId',
|
|
66
115
|
getParentRoute: () => MilestonesRoute,
|
|
67
116
|
} as any)
|
|
117
|
+
const DesignsSlugRoute = DesignsSlugRouteImport.update({
|
|
118
|
+
id: '/$slug',
|
|
119
|
+
path: '/$slug',
|
|
120
|
+
getParentRoute: () => DesignsRoute,
|
|
121
|
+
} as any)
|
|
68
122
|
const ApiWatchRoute = ApiWatchRouteImport.update({
|
|
69
123
|
id: '/api/watch',
|
|
70
124
|
path: '/api/watch',
|
|
@@ -74,13 +128,22 @@ const ApiWatchRoute = ApiWatchRouteImport.update({
|
|
|
74
128
|
export interface FileRoutesByFullPath {
|
|
75
129
|
'/': typeof IndexRoute
|
|
76
130
|
'/activity': typeof ActivityRoute
|
|
131
|
+
'/designs': typeof DesignsRouteWithChildren
|
|
77
132
|
'/milestones': typeof MilestonesRouteWithChildren
|
|
133
|
+
'/patterns': typeof PatternsRouteWithChildren
|
|
134
|
+
'/reports': typeof ReportsRouteWithChildren
|
|
78
135
|
'/search': typeof SearchRoute
|
|
79
136
|
'/tasks': typeof TasksRouteWithChildren
|
|
80
137
|
'/api/watch': typeof ApiWatchRoute
|
|
138
|
+
'/designs/$slug': typeof DesignsSlugRoute
|
|
81
139
|
'/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
|
|
140
|
+
'/patterns/$slug': typeof PatternsSlugRoute
|
|
141
|
+
'/reports/$slug': typeof ReportsSlugRoute
|
|
82
142
|
'/tasks/$taskId': typeof TasksTaskIdRoute
|
|
143
|
+
'/designs/': typeof DesignsIndexRoute
|
|
83
144
|
'/milestones/': typeof MilestonesIndexRoute
|
|
145
|
+
'/patterns/': typeof PatternsIndexRoute
|
|
146
|
+
'/reports/': typeof ReportsIndexRoute
|
|
84
147
|
'/tasks/': typeof TasksIndexRoute
|
|
85
148
|
}
|
|
86
149
|
export interface FileRoutesByTo {
|
|
@@ -88,22 +151,37 @@ export interface FileRoutesByTo {
|
|
|
88
151
|
'/activity': typeof ActivityRoute
|
|
89
152
|
'/search': typeof SearchRoute
|
|
90
153
|
'/api/watch': typeof ApiWatchRoute
|
|
154
|
+
'/designs/$slug': typeof DesignsSlugRoute
|
|
91
155
|
'/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
|
|
156
|
+
'/patterns/$slug': typeof PatternsSlugRoute
|
|
157
|
+
'/reports/$slug': typeof ReportsSlugRoute
|
|
92
158
|
'/tasks/$taskId': typeof TasksTaskIdRoute
|
|
159
|
+
'/designs': typeof DesignsIndexRoute
|
|
93
160
|
'/milestones': typeof MilestonesIndexRoute
|
|
161
|
+
'/patterns': typeof PatternsIndexRoute
|
|
162
|
+
'/reports': typeof ReportsIndexRoute
|
|
94
163
|
'/tasks': typeof TasksIndexRoute
|
|
95
164
|
}
|
|
96
165
|
export interface FileRoutesById {
|
|
97
166
|
__root__: typeof rootRouteImport
|
|
98
167
|
'/': typeof IndexRoute
|
|
99
168
|
'/activity': typeof ActivityRoute
|
|
169
|
+
'/designs': typeof DesignsRouteWithChildren
|
|
100
170
|
'/milestones': typeof MilestonesRouteWithChildren
|
|
171
|
+
'/patterns': typeof PatternsRouteWithChildren
|
|
172
|
+
'/reports': typeof ReportsRouteWithChildren
|
|
101
173
|
'/search': typeof SearchRoute
|
|
102
174
|
'/tasks': typeof TasksRouteWithChildren
|
|
103
175
|
'/api/watch': typeof ApiWatchRoute
|
|
176
|
+
'/designs/$slug': typeof DesignsSlugRoute
|
|
104
177
|
'/milestones/$milestoneId': typeof MilestonesMilestoneIdRoute
|
|
178
|
+
'/patterns/$slug': typeof PatternsSlugRoute
|
|
179
|
+
'/reports/$slug': typeof ReportsSlugRoute
|
|
105
180
|
'/tasks/$taskId': typeof TasksTaskIdRoute
|
|
181
|
+
'/designs/': typeof DesignsIndexRoute
|
|
106
182
|
'/milestones/': typeof MilestonesIndexRoute
|
|
183
|
+
'/patterns/': typeof PatternsIndexRoute
|
|
184
|
+
'/reports/': typeof ReportsIndexRoute
|
|
107
185
|
'/tasks/': typeof TasksIndexRoute
|
|
108
186
|
}
|
|
109
187
|
export interface FileRouteTypes {
|
|
@@ -111,13 +189,22 @@ export interface FileRouteTypes {
|
|
|
111
189
|
fullPaths:
|
|
112
190
|
| '/'
|
|
113
191
|
| '/activity'
|
|
192
|
+
| '/designs'
|
|
114
193
|
| '/milestones'
|
|
194
|
+
| '/patterns'
|
|
195
|
+
| '/reports'
|
|
115
196
|
| '/search'
|
|
116
197
|
| '/tasks'
|
|
117
198
|
| '/api/watch'
|
|
199
|
+
| '/designs/$slug'
|
|
118
200
|
| '/milestones/$milestoneId'
|
|
201
|
+
| '/patterns/$slug'
|
|
202
|
+
| '/reports/$slug'
|
|
119
203
|
| '/tasks/$taskId'
|
|
204
|
+
| '/designs/'
|
|
120
205
|
| '/milestones/'
|
|
206
|
+
| '/patterns/'
|
|
207
|
+
| '/reports/'
|
|
121
208
|
| '/tasks/'
|
|
122
209
|
fileRoutesByTo: FileRoutesByTo
|
|
123
210
|
to:
|
|
@@ -125,28 +212,46 @@ export interface FileRouteTypes {
|
|
|
125
212
|
| '/activity'
|
|
126
213
|
| '/search'
|
|
127
214
|
| '/api/watch'
|
|
215
|
+
| '/designs/$slug'
|
|
128
216
|
| '/milestones/$milestoneId'
|
|
217
|
+
| '/patterns/$slug'
|
|
218
|
+
| '/reports/$slug'
|
|
129
219
|
| '/tasks/$taskId'
|
|
220
|
+
| '/designs'
|
|
130
221
|
| '/milestones'
|
|
222
|
+
| '/patterns'
|
|
223
|
+
| '/reports'
|
|
131
224
|
| '/tasks'
|
|
132
225
|
id:
|
|
133
226
|
| '__root__'
|
|
134
227
|
| '/'
|
|
135
228
|
| '/activity'
|
|
229
|
+
| '/designs'
|
|
136
230
|
| '/milestones'
|
|
231
|
+
| '/patterns'
|
|
232
|
+
| '/reports'
|
|
137
233
|
| '/search'
|
|
138
234
|
| '/tasks'
|
|
139
235
|
| '/api/watch'
|
|
236
|
+
| '/designs/$slug'
|
|
140
237
|
| '/milestones/$milestoneId'
|
|
238
|
+
| '/patterns/$slug'
|
|
239
|
+
| '/reports/$slug'
|
|
141
240
|
| '/tasks/$taskId'
|
|
241
|
+
| '/designs/'
|
|
142
242
|
| '/milestones/'
|
|
243
|
+
| '/patterns/'
|
|
244
|
+
| '/reports/'
|
|
143
245
|
| '/tasks/'
|
|
144
246
|
fileRoutesById: FileRoutesById
|
|
145
247
|
}
|
|
146
248
|
export interface RootRouteChildren {
|
|
147
249
|
IndexRoute: typeof IndexRoute
|
|
148
250
|
ActivityRoute: typeof ActivityRoute
|
|
251
|
+
DesignsRoute: typeof DesignsRouteWithChildren
|
|
149
252
|
MilestonesRoute: typeof MilestonesRouteWithChildren
|
|
253
|
+
PatternsRoute: typeof PatternsRouteWithChildren
|
|
254
|
+
ReportsRoute: typeof ReportsRouteWithChildren
|
|
150
255
|
SearchRoute: typeof SearchRoute
|
|
151
256
|
TasksRoute: typeof TasksRouteWithChildren
|
|
152
257
|
ApiWatchRoute: typeof ApiWatchRoute
|
|
@@ -168,6 +273,20 @@ declare module '@tanstack/react-router' {
|
|
|
168
273
|
preLoaderRoute: typeof SearchRouteImport
|
|
169
274
|
parentRoute: typeof rootRouteImport
|
|
170
275
|
}
|
|
276
|
+
'/reports': {
|
|
277
|
+
id: '/reports'
|
|
278
|
+
path: '/reports'
|
|
279
|
+
fullPath: '/reports'
|
|
280
|
+
preLoaderRoute: typeof ReportsRouteImport
|
|
281
|
+
parentRoute: typeof rootRouteImport
|
|
282
|
+
}
|
|
283
|
+
'/patterns': {
|
|
284
|
+
id: '/patterns'
|
|
285
|
+
path: '/patterns'
|
|
286
|
+
fullPath: '/patterns'
|
|
287
|
+
preLoaderRoute: typeof PatternsRouteImport
|
|
288
|
+
parentRoute: typeof rootRouteImport
|
|
289
|
+
}
|
|
171
290
|
'/milestones': {
|
|
172
291
|
id: '/milestones'
|
|
173
292
|
path: '/milestones'
|
|
@@ -175,6 +294,13 @@ declare module '@tanstack/react-router' {
|
|
|
175
294
|
preLoaderRoute: typeof MilestonesRouteImport
|
|
176
295
|
parentRoute: typeof rootRouteImport
|
|
177
296
|
}
|
|
297
|
+
'/designs': {
|
|
298
|
+
id: '/designs'
|
|
299
|
+
path: '/designs'
|
|
300
|
+
fullPath: '/designs'
|
|
301
|
+
preLoaderRoute: typeof DesignsRouteImport
|
|
302
|
+
parentRoute: typeof rootRouteImport
|
|
303
|
+
}
|
|
178
304
|
'/activity': {
|
|
179
305
|
id: '/activity'
|
|
180
306
|
path: '/activity'
|
|
@@ -196,6 +322,20 @@ declare module '@tanstack/react-router' {
|
|
|
196
322
|
preLoaderRoute: typeof TasksIndexRouteImport
|
|
197
323
|
parentRoute: typeof TasksRoute
|
|
198
324
|
}
|
|
325
|
+
'/reports/': {
|
|
326
|
+
id: '/reports/'
|
|
327
|
+
path: '/'
|
|
328
|
+
fullPath: '/reports/'
|
|
329
|
+
preLoaderRoute: typeof ReportsIndexRouteImport
|
|
330
|
+
parentRoute: typeof ReportsRoute
|
|
331
|
+
}
|
|
332
|
+
'/patterns/': {
|
|
333
|
+
id: '/patterns/'
|
|
334
|
+
path: '/'
|
|
335
|
+
fullPath: '/patterns/'
|
|
336
|
+
preLoaderRoute: typeof PatternsIndexRouteImport
|
|
337
|
+
parentRoute: typeof PatternsRoute
|
|
338
|
+
}
|
|
199
339
|
'/milestones/': {
|
|
200
340
|
id: '/milestones/'
|
|
201
341
|
path: '/'
|
|
@@ -203,6 +343,13 @@ declare module '@tanstack/react-router' {
|
|
|
203
343
|
preLoaderRoute: typeof MilestonesIndexRouteImport
|
|
204
344
|
parentRoute: typeof MilestonesRoute
|
|
205
345
|
}
|
|
346
|
+
'/designs/': {
|
|
347
|
+
id: '/designs/'
|
|
348
|
+
path: '/'
|
|
349
|
+
fullPath: '/designs/'
|
|
350
|
+
preLoaderRoute: typeof DesignsIndexRouteImport
|
|
351
|
+
parentRoute: typeof DesignsRoute
|
|
352
|
+
}
|
|
206
353
|
'/tasks/$taskId': {
|
|
207
354
|
id: '/tasks/$taskId'
|
|
208
355
|
path: '/$taskId'
|
|
@@ -210,6 +357,20 @@ declare module '@tanstack/react-router' {
|
|
|
210
357
|
preLoaderRoute: typeof TasksTaskIdRouteImport
|
|
211
358
|
parentRoute: typeof TasksRoute
|
|
212
359
|
}
|
|
360
|
+
'/reports/$slug': {
|
|
361
|
+
id: '/reports/$slug'
|
|
362
|
+
path: '/$slug'
|
|
363
|
+
fullPath: '/reports/$slug'
|
|
364
|
+
preLoaderRoute: typeof ReportsSlugRouteImport
|
|
365
|
+
parentRoute: typeof ReportsRoute
|
|
366
|
+
}
|
|
367
|
+
'/patterns/$slug': {
|
|
368
|
+
id: '/patterns/$slug'
|
|
369
|
+
path: '/$slug'
|
|
370
|
+
fullPath: '/patterns/$slug'
|
|
371
|
+
preLoaderRoute: typeof PatternsSlugRouteImport
|
|
372
|
+
parentRoute: typeof PatternsRoute
|
|
373
|
+
}
|
|
213
374
|
'/milestones/$milestoneId': {
|
|
214
375
|
id: '/milestones/$milestoneId'
|
|
215
376
|
path: '/$milestoneId'
|
|
@@ -217,6 +378,13 @@ declare module '@tanstack/react-router' {
|
|
|
217
378
|
preLoaderRoute: typeof MilestonesMilestoneIdRouteImport
|
|
218
379
|
parentRoute: typeof MilestonesRoute
|
|
219
380
|
}
|
|
381
|
+
'/designs/$slug': {
|
|
382
|
+
id: '/designs/$slug'
|
|
383
|
+
path: '/$slug'
|
|
384
|
+
fullPath: '/designs/$slug'
|
|
385
|
+
preLoaderRoute: typeof DesignsSlugRouteImport
|
|
386
|
+
parentRoute: typeof DesignsRoute
|
|
387
|
+
}
|
|
220
388
|
'/api/watch': {
|
|
221
389
|
id: '/api/watch'
|
|
222
390
|
path: '/api/watch'
|
|
@@ -227,6 +395,19 @@ declare module '@tanstack/react-router' {
|
|
|
227
395
|
}
|
|
228
396
|
}
|
|
229
397
|
|
|
398
|
+
interface DesignsRouteChildren {
|
|
399
|
+
DesignsSlugRoute: typeof DesignsSlugRoute
|
|
400
|
+
DesignsIndexRoute: typeof DesignsIndexRoute
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const DesignsRouteChildren: DesignsRouteChildren = {
|
|
404
|
+
DesignsSlugRoute: DesignsSlugRoute,
|
|
405
|
+
DesignsIndexRoute: DesignsIndexRoute,
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const DesignsRouteWithChildren =
|
|
409
|
+
DesignsRoute._addFileChildren(DesignsRouteChildren)
|
|
410
|
+
|
|
230
411
|
interface MilestonesRouteChildren {
|
|
231
412
|
MilestonesMilestoneIdRoute: typeof MilestonesMilestoneIdRoute
|
|
232
413
|
MilestonesIndexRoute: typeof MilestonesIndexRoute
|
|
@@ -241,6 +422,33 @@ const MilestonesRouteWithChildren = MilestonesRoute._addFileChildren(
|
|
|
241
422
|
MilestonesRouteChildren,
|
|
242
423
|
)
|
|
243
424
|
|
|
425
|
+
interface PatternsRouteChildren {
|
|
426
|
+
PatternsSlugRoute: typeof PatternsSlugRoute
|
|
427
|
+
PatternsIndexRoute: typeof PatternsIndexRoute
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const PatternsRouteChildren: PatternsRouteChildren = {
|
|
431
|
+
PatternsSlugRoute: PatternsSlugRoute,
|
|
432
|
+
PatternsIndexRoute: PatternsIndexRoute,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const PatternsRouteWithChildren = PatternsRoute._addFileChildren(
|
|
436
|
+
PatternsRouteChildren,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
interface ReportsRouteChildren {
|
|
440
|
+
ReportsSlugRoute: typeof ReportsSlugRoute
|
|
441
|
+
ReportsIndexRoute: typeof ReportsIndexRoute
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const ReportsRouteChildren: ReportsRouteChildren = {
|
|
445
|
+
ReportsSlugRoute: ReportsSlugRoute,
|
|
446
|
+
ReportsIndexRoute: ReportsIndexRoute,
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const ReportsRouteWithChildren =
|
|
450
|
+
ReportsRoute._addFileChildren(ReportsRouteChildren)
|
|
451
|
+
|
|
244
452
|
interface TasksRouteChildren {
|
|
245
453
|
TasksTaskIdRoute: typeof TasksTaskIdRoute
|
|
246
454
|
TasksIndexRoute: typeof TasksIndexRoute
|
|
@@ -256,7 +464,10 @@ const TasksRouteWithChildren = TasksRoute._addFileChildren(TasksRouteChildren)
|
|
|
256
464
|
const rootRouteChildren: RootRouteChildren = {
|
|
257
465
|
IndexRoute: IndexRoute,
|
|
258
466
|
ActivityRoute: ActivityRoute,
|
|
467
|
+
DesignsRoute: DesignsRouteWithChildren,
|
|
259
468
|
MilestonesRoute: MilestonesRouteWithChildren,
|
|
469
|
+
PatternsRoute: PatternsRouteWithChildren,
|
|
470
|
+
ReportsRoute: ReportsRouteWithChildren,
|
|
260
471
|
SearchRoute: SearchRoute,
|
|
261
472
|
TasksRoute: TasksRouteWithChildren,
|
|
262
473
|
ApiWatchRoute: ApiWatchRoute,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DocumentDetail } from '../components/DocumentDetail'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/designs/$slug')({
|
|
5
|
+
component: DesignDetailPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function DesignDetailPage() {
|
|
9
|
+
const { slug } = Route.useParams()
|
|
10
|
+
return (
|
|
11
|
+
<DocumentDetail
|
|
12
|
+
slug={slug}
|
|
13
|
+
dirPath="agent/design"
|
|
14
|
+
sectionLabel="Designs"
|
|
15
|
+
sectionHref="/designs"
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DocumentList } from '../components/DocumentList'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/designs/')({
|
|
5
|
+
component: DesignsPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function DesignsPage() {
|
|
9
|
+
return <DocumentList title="Designs" dirPath="agent/design" baseTo="/designs" />
|
|
10
|
+
}
|
|
@@ -5,6 +5,7 @@ import { Breadcrumb } from '../components/Breadcrumb'
|
|
|
5
5
|
import { DetailHeader } from '../components/DetailHeader'
|
|
6
6
|
import { ProgressBar } from '../components/ProgressBar'
|
|
7
7
|
import { StatusDot } from '../components/StatusDot'
|
|
8
|
+
import { PriorityBadge } from '../components/PriorityBadge'
|
|
8
9
|
import { MarkdownContent, buildLinkMap } from '../components/MarkdownContent'
|
|
9
10
|
import { getMarkdownContent, resolveMilestoneFile } from '../services/markdown.service'
|
|
10
11
|
import type { MarkdownResult, ResolveFileResult } from '../services/markdown.service'
|
|
@@ -105,6 +106,10 @@ function MilestoneDetailPage() {
|
|
|
105
106
|
<span className="text-xs text-gray-500">{milestone.progress}%</span>
|
|
106
107
|
</div>
|
|
107
108
|
|
|
109
|
+
<div className="flex items-center gap-2 mb-4">
|
|
110
|
+
<PriorityBadge priority={milestone.priority} />
|
|
111
|
+
</div>
|
|
112
|
+
|
|
108
113
|
<DetailHeader status={milestone.status} fields={fields} />
|
|
109
114
|
|
|
110
115
|
{milestone.notes && (
|
|
@@ -140,6 +145,7 @@ function MilestoneDetailPage() {
|
|
|
140
145
|
<span className={task.status === 'completed' ? 'text-gray-500' : 'text-gray-200'}>
|
|
141
146
|
{task.name}
|
|
142
147
|
</span>
|
|
148
|
+
<PriorityBadge priority={task.priority} />
|
|
143
149
|
{task.estimated_hours && (
|
|
144
150
|
<span className="text-xs text-gray-600 ml-auto">{task.estimated_hours}h</span>
|
|
145
151
|
)}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DocumentDetail } from '../components/DocumentDetail'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/patterns/$slug')({
|
|
5
|
+
component: PatternDetailPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function PatternDetailPage() {
|
|
9
|
+
const { slug } = Route.useParams()
|
|
10
|
+
return (
|
|
11
|
+
<DocumentDetail
|
|
12
|
+
slug={slug}
|
|
13
|
+
dirPath="agent/patterns"
|
|
14
|
+
sectionLabel="Patterns"
|
|
15
|
+
sectionHref="/patterns"
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DocumentList } from '../components/DocumentList'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/patterns/')({
|
|
5
|
+
component: PatternsPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function PatternsPage() {
|
|
9
|
+
return <DocumentList title="Patterns" dirPath="agent/patterns" baseTo="/patterns" />
|
|
10
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DocumentDetail } from '../components/DocumentDetail'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/reports/$slug')({
|
|
5
|
+
component: ReportDetailPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function ReportDetailPage() {
|
|
9
|
+
const { slug } = Route.useParams()
|
|
10
|
+
return (
|
|
11
|
+
<DocumentDetail
|
|
12
|
+
slug={slug}
|
|
13
|
+
dirPath="agent/reports"
|
|
14
|
+
sectionLabel="Reports"
|
|
15
|
+
sectionHref="/reports"
|
|
16
|
+
/>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
2
|
+
import { DocumentList } from '../components/DocumentList'
|
|
3
|
+
|
|
4
|
+
export const Route = createFileRoute('/reports/')({
|
|
5
|
+
component: ReportsPage,
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
function ReportsPage() {
|
|
9
|
+
return <DocumentList title="Reports" dirPath="agent/reports" baseTo="/reports" />
|
|
10
|
+
}
|
|
@@ -3,6 +3,7 @@ import { useState, useEffect, useMemo } from 'react'
|
|
|
3
3
|
import { useProgressData } from '../contexts/ProgressContext'
|
|
4
4
|
import { Breadcrumb } from '../components/Breadcrumb'
|
|
5
5
|
import { DetailHeader } from '../components/DetailHeader'
|
|
6
|
+
import { PriorityBadge } from '../components/PriorityBadge'
|
|
6
7
|
import { MarkdownContent, buildLinkMap } from '../components/MarkdownContent'
|
|
7
8
|
import { getMarkdownContent } from '../services/markdown.service'
|
|
8
9
|
import { resolveTaskFile } from '../services/markdown.service'
|
|
@@ -94,8 +95,13 @@ function TaskDetailPage() {
|
|
|
94
95
|
)
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
const hoursDisplay = task.actual_hours != null
|
|
99
|
+
? `Est: ${task.estimated_hours}h | Actual: ${task.actual_hours}h`
|
|
100
|
+
: `${task.estimated_hours}h`
|
|
101
|
+
|
|
97
102
|
const fields = [
|
|
98
|
-
{ label: 'Est', value:
|
|
103
|
+
{ label: 'Est', value: hoursDisplay },
|
|
104
|
+
...(task.started ? [{ label: 'Started', value: task.started }] : []),
|
|
99
105
|
...(task.completed_date ? [{ label: 'Completed', value: task.completed_date }] : []),
|
|
100
106
|
{
|
|
101
107
|
label: 'Milestone',
|
|
@@ -123,6 +129,10 @@ function TaskDetailPage() {
|
|
|
123
129
|
|
|
124
130
|
<h1 className="text-xl font-semibold text-gray-100 mb-3">{task.name}</h1>
|
|
125
131
|
|
|
132
|
+
<div className="flex items-center gap-2 mb-4">
|
|
133
|
+
<PriorityBadge priority={task.priority} />
|
|
134
|
+
</div>
|
|
135
|
+
|
|
126
136
|
<DetailHeader status={task.status} fields={fields} />
|
|
127
137
|
|
|
128
138
|
{task.notes && (
|
|
@@ -115,6 +115,108 @@ async function fetchMarkdownFromGitHub(
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
// ---------- listAgentDirectory ----------
|
|
119
|
+
|
|
120
|
+
export type AgentFile = {
|
|
121
|
+
name: string
|
|
122
|
+
/** Relative path from project root, e.g. "agent/design/local.foo.md" */
|
|
123
|
+
relativePath: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export type ListDirResult =
|
|
127
|
+
| { ok: true; files: AgentFile[] }
|
|
128
|
+
| { ok: false; files: []; error: string }
|
|
129
|
+
|
|
130
|
+
export const listAgentDirectory = createServerFn({ method: 'GET' })
|
|
131
|
+
.inputValidator((input: { dirPath: string; github?: { owner: string; repo: string; branch?: string; token?: string } }) => input)
|
|
132
|
+
.handler(async ({ data: input }): Promise<ListDirResult> => {
|
|
133
|
+
if (input.github) {
|
|
134
|
+
return listDirFromGitHub(input.dirPath, input.github)
|
|
135
|
+
}
|
|
136
|
+
return listDirFromDisk(input.dirPath)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
async function listDirFromDisk(dirPath: string): Promise<ListDirResult> {
|
|
140
|
+
try {
|
|
141
|
+
const fs = await import('fs')
|
|
142
|
+
const path = await import('path')
|
|
143
|
+
|
|
144
|
+
const basePath = getBasePath()
|
|
145
|
+
const fullDir = path.resolve(basePath, dirPath)
|
|
146
|
+
|
|
147
|
+
if (!fs.existsSync(fullDir)) {
|
|
148
|
+
return { ok: false, files: [], error: `Directory not found: ${dirPath}` }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const entries = fs.readdirSync(fullDir)
|
|
152
|
+
const files: AgentFile[] = entries
|
|
153
|
+
.filter((f: string) => f.endsWith('.md') && !f.includes('template'))
|
|
154
|
+
.sort()
|
|
155
|
+
.map((f: string) => ({
|
|
156
|
+
name: f.replace(/\.md$/, ''),
|
|
157
|
+
relativePath: `${dirPath}/${f}`,
|
|
158
|
+
}))
|
|
159
|
+
|
|
160
|
+
return { ok: true, files }
|
|
161
|
+
} catch (err: any) {
|
|
162
|
+
return { ok: false, files: [], error: `Failed to list directory: ${dirPath}` }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function listDirFromGitHub(
|
|
167
|
+
dirPath: string,
|
|
168
|
+
github: { owner: string; repo: string; branch?: string; token?: string },
|
|
169
|
+
): Promise<ListDirResult> {
|
|
170
|
+
try {
|
|
171
|
+
let branch = github.branch
|
|
172
|
+
if (!branch) {
|
|
173
|
+
try {
|
|
174
|
+
const metaRes = await fetch(`https://api.github.com/repos/${github.owner}/${github.repo}`, {
|
|
175
|
+
headers: github.token ? { Authorization: `token ${github.token}` } : {},
|
|
176
|
+
})
|
|
177
|
+
if (metaRes.ok) {
|
|
178
|
+
const meta = (await metaRes.json()) as { default_branch?: string }
|
|
179
|
+
branch = meta.default_branch || 'main'
|
|
180
|
+
} else {
|
|
181
|
+
branch = 'main'
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
branch = 'main'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const url = `https://api.github.com/repos/${github.owner}/${github.repo}/contents/${dirPath}?ref=${branch}`
|
|
189
|
+
const headers: Record<string, string> = {
|
|
190
|
+
Accept: 'application/vnd.github.v3+json',
|
|
191
|
+
}
|
|
192
|
+
if (github.token) {
|
|
193
|
+
headers['Authorization'] = `token ${github.token}`
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const response = await fetch(url, { headers })
|
|
197
|
+
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
if (response.status === 404) {
|
|
200
|
+
return { ok: true, files: [] }
|
|
201
|
+
}
|
|
202
|
+
return { ok: false, files: [], error: `GitHub returned ${response.status}` }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const entries = (await response.json()) as Array<{ name: string; path: string; type: string }>
|
|
206
|
+
const files: AgentFile[] = entries
|
|
207
|
+
.filter((e) => e.name.endsWith('.md') && !e.name.includes('template'))
|
|
208
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
209
|
+
.map((e) => ({
|
|
210
|
+
name: e.name.replace(/\.md$/, ''),
|
|
211
|
+
relativePath: e.path,
|
|
212
|
+
}))
|
|
213
|
+
|
|
214
|
+
return { ok: true, files }
|
|
215
|
+
} catch (err) {
|
|
216
|
+
return { ok: false, files: [], error: `Failed to list directory from GitHub` }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
118
220
|
// ---------- resolveMilestoneFile ----------
|
|
119
221
|
|
|
120
222
|
export const resolveMilestoneFile = createServerFn({ method: 'GET' })
|