prjct-cli 0.12.2 → 0.13.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/CHANGELOG.md +43 -0
- package/CLAUDE.md +18 -6
- package/core/data/index.ts +19 -5
- package/core/data/md-base-manager.ts +203 -0
- package/core/data/md-queue-manager.ts +179 -0
- package/core/data/md-state-manager.ts +133 -0
- package/core/serializers/index.ts +20 -0
- package/core/serializers/queue-serializer.ts +210 -0
- package/core/serializers/state-serializer.ts +136 -0
- package/core/utils/file-helper.ts +12 -0
- package/package.json +1 -1
- package/packages/web/app/api/projects/[id]/stats/route.ts +6 -29
- package/packages/web/app/page.tsx +1 -6
- package/packages/web/app/project/[id]/page.tsx +34 -1
- package/packages/web/app/project/[id]/stats/page.tsx +11 -5
- package/packages/web/app/settings/page.tsx +2 -221
- package/packages/web/components/BlockersCard/BlockersCard.tsx +67 -0
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +11 -0
- package/packages/web/components/BlockersCard/index.ts +2 -0
- package/packages/web/components/CommandButton/CommandButton.tsx +10 -3
- package/packages/web/lib/projects.ts +28 -27
- package/packages/web/lib/services/projects.server.ts +25 -21
- package/packages/web/lib/services/stats.server.ts +355 -57
- package/packages/web/package.json +0 -2
- package/templates/commands/decision.md +226 -0
- package/templates/commands/done.md +100 -68
- package/templates/commands/feature.md +102 -103
- package/templates/commands/idea.md +41 -38
- package/templates/commands/now.md +94 -33
- package/templates/commands/pause.md +90 -30
- package/templates/commands/ship.md +179 -74
- package/templates/commands/sync.md +324 -200
- package/packages/web/app/api/migrate/route.ts +0 -46
- package/packages/web/app/api/settings/route.ts +0 -97
- package/packages/web/app/api/v2/projects/[id]/unified/route.ts +0 -57
- package/packages/web/components/MigrationGate/MigrationGate.tsx +0 -304
- package/packages/web/components/MigrationGate/index.ts +0 -1
- package/packages/web/lib/json-loader.ts +0 -630
- package/packages/web/lib/services/migration.server.ts +0 -580
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Serializer
|
|
3
|
+
*
|
|
4
|
+
* Parses and serializes next.md for task queue.
|
|
5
|
+
*
|
|
6
|
+
* MD Format (next.md):
|
|
7
|
+
* ```
|
|
8
|
+
* # Priority Queue
|
|
9
|
+
*
|
|
10
|
+
* > Tasks ready to start
|
|
11
|
+
*
|
|
12
|
+
* ## Active Tasks
|
|
13
|
+
* 1. [ ] Task description @agent (from: Feature Name)
|
|
14
|
+
* 2. [x] Completed task ✅
|
|
15
|
+
*
|
|
16
|
+
* ## Previously Active
|
|
17
|
+
* - [ ] Paused task
|
|
18
|
+
*
|
|
19
|
+
* ## Backlog
|
|
20
|
+
* - [ ] 🐛 [HIGH] Bug description
|
|
21
|
+
* - [ ] Feature task
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { QueueJson, QueueTask, Priority, TaskType, TaskSection } from '../schemas/state'
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse next.md content to QueueJson
|
|
29
|
+
*/
|
|
30
|
+
export function parseQueue(content: string): QueueJson {
|
|
31
|
+
if (!content || !content.trim()) {
|
|
32
|
+
return { tasks: [], lastUpdated: '' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const lines = content.split('\n')
|
|
36
|
+
const tasks: QueueTask[] = []
|
|
37
|
+
let currentSection: TaskSection = 'active'
|
|
38
|
+
let taskIndex = 0
|
|
39
|
+
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
// Detect section headers
|
|
42
|
+
if (line.match(/^##\s*Active/i)) {
|
|
43
|
+
currentSection = 'active'
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
if (line.match(/^##\s*Previously/i)) {
|
|
47
|
+
currentSection = 'previously_active'
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
if (line.match(/^##\s*Backlog/i)) {
|
|
51
|
+
currentSection = 'backlog'
|
|
52
|
+
continue
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Parse task lines: "1. [ ] Task" or "- [ ] Task" or "- [x] Task"
|
|
56
|
+
const taskMatch = line.match(/^(?:\d+\.|[-*])\s*\[([\sx])\]\s*(.+)$/i)
|
|
57
|
+
if (!taskMatch) continue
|
|
58
|
+
|
|
59
|
+
const isCompleted = taskMatch[1].toLowerCase() === 'x'
|
|
60
|
+
let taskText = taskMatch[2].trim()
|
|
61
|
+
|
|
62
|
+
// Extract agent: @fe, @be, @fe+be
|
|
63
|
+
const agentMatch = taskText.match(/@(\w+(?:\+\w+)?)/)
|
|
64
|
+
const agent = agentMatch ? agentMatch[1] : undefined
|
|
65
|
+
taskText = taskText.replace(/@\w+(?:\+\w+)?/, '').trim()
|
|
66
|
+
|
|
67
|
+
// Extract origin feature: (from: Feature Name)
|
|
68
|
+
const fromMatch = taskText.match(/\(from:\s*([^)]+)\)/)
|
|
69
|
+
const originFeature = fromMatch ? fromMatch[1].trim() : undefined
|
|
70
|
+
taskText = taskText.replace(/\(from:\s*[^)]+\)/, '').trim()
|
|
71
|
+
|
|
72
|
+
// Detect task type from emoji/prefix
|
|
73
|
+
let type: TaskType = 'feature'
|
|
74
|
+
let priority: Priority = 'medium'
|
|
75
|
+
|
|
76
|
+
if (taskText.includes('🐛') || taskText.toLowerCase().includes('bug')) {
|
|
77
|
+
type = 'bug'
|
|
78
|
+
taskText = taskText.replace('🐛', '').trim()
|
|
79
|
+
}
|
|
80
|
+
if (taskText.includes('🔧') || taskText.toLowerCase().includes('fix')) {
|
|
81
|
+
type = 'improvement'
|
|
82
|
+
}
|
|
83
|
+
if (taskText.includes('♻️') || taskText.toLowerCase().includes('refactor')) {
|
|
84
|
+
type = 'chore'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Extract priority: [HIGH], [MEDIUM], [LOW], [CRITICAL]
|
|
88
|
+
const priorityMatch = taskText.match(/\[(HIGH|MEDIUM|LOW|CRITICAL)\]/i)
|
|
89
|
+
if (priorityMatch) {
|
|
90
|
+
priority = priorityMatch[1].toLowerCase() as Priority
|
|
91
|
+
taskText = taskText.replace(/\[(HIGH|MEDIUM|LOW|CRITICAL)\]/i, '').trim()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Remove checkmarks and clean up
|
|
95
|
+
taskText = taskText.replace(/✅/g, '').trim()
|
|
96
|
+
|
|
97
|
+
// Skip empty descriptions
|
|
98
|
+
if (!taskText) continue
|
|
99
|
+
|
|
100
|
+
const task: QueueTask = {
|
|
101
|
+
id: `task_${Date.now()}_${taskIndex++}`,
|
|
102
|
+
description: taskText,
|
|
103
|
+
priority,
|
|
104
|
+
type,
|
|
105
|
+
completed: isCompleted,
|
|
106
|
+
createdAt: new Date().toISOString(),
|
|
107
|
+
section: currentSection
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (agent) task.agent = agent
|
|
111
|
+
if (originFeature) task.originFeature = originFeature
|
|
112
|
+
if (isCompleted) task.completedAt = new Date().toISOString()
|
|
113
|
+
|
|
114
|
+
tasks.push(task)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Extract updated date
|
|
118
|
+
const updatedMatch = content.match(/_Updated:\s*(\d{4}-\d{2}-\d{2})/)
|
|
119
|
+
const lastUpdated = updatedMatch ? updatedMatch[1] : new Date().toISOString().split('T')[0]
|
|
120
|
+
|
|
121
|
+
return { tasks, lastUpdated }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Serialize QueueJson to next.md format
|
|
126
|
+
*/
|
|
127
|
+
export function serializeQueue(data: QueueJson): string {
|
|
128
|
+
const lines: string[] = [
|
|
129
|
+
'# Priority Queue',
|
|
130
|
+
'',
|
|
131
|
+
'> Tasks ready to start (max 100)',
|
|
132
|
+
'> Auto-updated by prjct',
|
|
133
|
+
''
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
// Group tasks by section
|
|
137
|
+
const active = data.tasks.filter(t => t.section === 'active')
|
|
138
|
+
const previouslyActive = data.tasks.filter(t => t.section === 'previously_active')
|
|
139
|
+
const backlog = data.tasks.filter(t => t.section === 'backlog')
|
|
140
|
+
|
|
141
|
+
// Active Tasks
|
|
142
|
+
if (active.length > 0) {
|
|
143
|
+
lines.push('## Active Tasks', '')
|
|
144
|
+
active.forEach((task, i) => {
|
|
145
|
+
lines.push(formatTask(task, i + 1, true))
|
|
146
|
+
})
|
|
147
|
+
lines.push('')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Previously Active
|
|
151
|
+
if (previouslyActive.length > 0) {
|
|
152
|
+
lines.push('## Previously Active', '')
|
|
153
|
+
previouslyActive.forEach(task => {
|
|
154
|
+
lines.push(formatTask(task, 0, false))
|
|
155
|
+
})
|
|
156
|
+
lines.push('')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Backlog
|
|
160
|
+
lines.push('---', '', '## Backlog', '')
|
|
161
|
+
if (backlog.length > 0) {
|
|
162
|
+
backlog.forEach(task => {
|
|
163
|
+
lines.push(formatTask(task, 0, false))
|
|
164
|
+
})
|
|
165
|
+
} else {
|
|
166
|
+
lines.push('_No backlog items_')
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
lines.push('')
|
|
170
|
+
lines.push('---', '')
|
|
171
|
+
lines.push(`_Updated: ${data.lastUpdated || new Date().toISOString().split('T')[0]}_`)
|
|
172
|
+
|
|
173
|
+
return lines.join('\n')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Format a single task as markdown
|
|
178
|
+
*/
|
|
179
|
+
function formatTask(task: QueueTask, num: number, numbered: boolean): string {
|
|
180
|
+
const checkbox = task.completed ? '[x]' : '[ ]'
|
|
181
|
+
const prefix = numbered && num > 0 ? `${num}.` : '-'
|
|
182
|
+
|
|
183
|
+
let text = task.description
|
|
184
|
+
|
|
185
|
+
// Add emoji for type
|
|
186
|
+
if (task.type === 'bug') text = `🐛 ${text}`
|
|
187
|
+
|
|
188
|
+
// Add priority tag for high/critical
|
|
189
|
+
if (task.priority === 'high' || task.priority === 'critical') {
|
|
190
|
+
text = `[${task.priority.toUpperCase()}] ${text}`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Add agent
|
|
194
|
+
if (task.agent) text = `${text} @${task.agent}`
|
|
195
|
+
|
|
196
|
+
// Add origin feature
|
|
197
|
+
if (task.originFeature) text = `${text} (from: ${task.originFeature})`
|
|
198
|
+
|
|
199
|
+
// Add checkmark for completed
|
|
200
|
+
if (task.completed) text = `${text} ✅`
|
|
201
|
+
|
|
202
|
+
return `${prefix} ${checkbox} ${text}`
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Quick helpers
|
|
207
|
+
*/
|
|
208
|
+
export function createEmptyQueueMd(): string {
|
|
209
|
+
return serializeQueue({ tasks: [], lastUpdated: new Date().toISOString().split('T')[0] })
|
|
210
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Serializer
|
|
3
|
+
*
|
|
4
|
+
* Parses and serializes now.md for current task state.
|
|
5
|
+
*
|
|
6
|
+
* MD Format (now.md):
|
|
7
|
+
* ```
|
|
8
|
+
* # NOW
|
|
9
|
+
*
|
|
10
|
+
* **Task description here**
|
|
11
|
+
*
|
|
12
|
+
* Started: 2025-12-10T10:00:00.000Z
|
|
13
|
+
* Session: sess_abc123
|
|
14
|
+
* Feature: feat_xyz789
|
|
15
|
+
* Agent: fe
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { StateJson, CurrentTask, PreviousTask } from '../schemas/state'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse now.md content to StateJson
|
|
23
|
+
*/
|
|
24
|
+
export function parseState(content: string): StateJson {
|
|
25
|
+
if (!content || !content.trim()) {
|
|
26
|
+
return { currentTask: null, lastUpdated: '' }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines = content.split('\n')
|
|
30
|
+
let currentTask: CurrentTask | null = null
|
|
31
|
+
let previousTask: PreviousTask | null = null
|
|
32
|
+
|
|
33
|
+
// Find task description (bold line after # NOW)
|
|
34
|
+
let description = ''
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
const boldMatch = line.match(/^\*\*(.+)\*\*$/)
|
|
37
|
+
if (boldMatch) {
|
|
38
|
+
description = boldMatch[1].trim()
|
|
39
|
+
break
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!description || description.toLowerCase().includes('no active task')) {
|
|
44
|
+
return { currentTask: null, lastUpdated: '' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Extract metadata
|
|
48
|
+
const startedMatch = content.match(/Started:\s*(.+)/i)
|
|
49
|
+
const sessionMatch = content.match(/Session:\s*(.+)/i)
|
|
50
|
+
const featureMatch = content.match(/Feature:\s*(.+)/i)
|
|
51
|
+
const idMatch = content.match(/ID:\s*(.+)/i)
|
|
52
|
+
const agentMatch = content.match(/Agent:\s*(.+)/i)
|
|
53
|
+
const pausedMatch = content.match(/Paused:\s*(.+)/i)
|
|
54
|
+
|
|
55
|
+
const id = idMatch ? idMatch[1].trim() : `task_${Date.now()}`
|
|
56
|
+
const startedAt = startedMatch ? startedMatch[1].trim() : new Date().toISOString()
|
|
57
|
+
const sessionId = sessionMatch ? sessionMatch[1].trim() : `sess_${Date.now()}`
|
|
58
|
+
|
|
59
|
+
if (pausedMatch) {
|
|
60
|
+
// This is a paused task
|
|
61
|
+
previousTask = {
|
|
62
|
+
id,
|
|
63
|
+
description,
|
|
64
|
+
status: 'paused',
|
|
65
|
+
startedAt,
|
|
66
|
+
pausedAt: pausedMatch[1].trim()
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
// Active task
|
|
70
|
+
currentTask = {
|
|
71
|
+
id,
|
|
72
|
+
description,
|
|
73
|
+
startedAt,
|
|
74
|
+
sessionId
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (featureMatch) {
|
|
78
|
+
currentTask.featureId = featureMatch[1].trim()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
currentTask,
|
|
84
|
+
previousTask,
|
|
85
|
+
lastUpdated: startedAt
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Serialize StateJson to now.md format
|
|
91
|
+
*/
|
|
92
|
+
export function serializeState(data: StateJson): string {
|
|
93
|
+
const lines: string[] = ['# NOW', '']
|
|
94
|
+
|
|
95
|
+
if (!data.currentTask && !data.previousTask) {
|
|
96
|
+
lines.push('_No active task_', '')
|
|
97
|
+
lines.push('Use `/p:now` to start working on something.')
|
|
98
|
+
return lines.join('\n')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const task = data.currentTask || data.previousTask
|
|
102
|
+
|
|
103
|
+
if (task) {
|
|
104
|
+
lines.push(`**${task.description}**`, '')
|
|
105
|
+
|
|
106
|
+
if ('pausedAt' in task && task.pausedAt) {
|
|
107
|
+
lines.push(`Started: ${task.startedAt}`)
|
|
108
|
+
lines.push(`Paused: ${task.pausedAt}`)
|
|
109
|
+
} else if (data.currentTask) {
|
|
110
|
+
lines.push(`Started: ${data.currentTask.startedAt}`)
|
|
111
|
+
lines.push(`Session: ${data.currentTask.sessionId}`)
|
|
112
|
+
if (data.currentTask.featureId) {
|
|
113
|
+
lines.push(`Feature: ${data.currentTask.featureId}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return lines.join('\n')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Quick helpers for common operations
|
|
123
|
+
*/
|
|
124
|
+
export function createCurrentTaskMd(task: CurrentTask): string {
|
|
125
|
+
return serializeState({
|
|
126
|
+
currentTask: task,
|
|
127
|
+
lastUpdated: task.startedAt
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createEmptyStateMd(): string {
|
|
132
|
+
return serializeState({
|
|
133
|
+
currentTask: null,
|
|
134
|
+
lastUpdated: ''
|
|
135
|
+
})
|
|
136
|
+
}
|
|
@@ -66,6 +66,17 @@ export async function writeFile(filePath: string, content: string): Promise<void
|
|
|
66
66
|
await fs.writeFile(filePath, content, 'utf-8')
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Atomic write - writes to temp file then renames (prevents partial writes)
|
|
71
|
+
*/
|
|
72
|
+
export async function atomicWrite(filePath: string, content: string): Promise<void> {
|
|
73
|
+
const dir = path.dirname(filePath)
|
|
74
|
+
await fs.mkdir(dir, { recursive: true })
|
|
75
|
+
const tempPath = `${filePath}.${Date.now()}.tmp`
|
|
76
|
+
await fs.writeFile(tempPath, content, 'utf-8')
|
|
77
|
+
await fs.rename(tempPath, filePath)
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
/**
|
|
70
81
|
* Append to text file
|
|
71
82
|
*/
|
|
@@ -245,6 +256,7 @@ export default {
|
|
|
245
256
|
writeJson,
|
|
246
257
|
readFile,
|
|
247
258
|
writeFile,
|
|
259
|
+
atomicWrite,
|
|
248
260
|
fileExists,
|
|
249
261
|
ensureDir,
|
|
250
262
|
deleteFile,
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import { loadUnifiedJsonData, hasJsonState } from '@/lib/json-loader'
|
|
3
2
|
import { getProjectStats, getRawProjectFiles } from '@/lib/parse-prjct-files'
|
|
4
3
|
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/projects/[id]/stats
|
|
6
|
+
*
|
|
7
|
+
* MD-First Architecture: Returns stats parsed from MD files.
|
|
8
|
+
*/
|
|
5
9
|
export async function GET(
|
|
6
10
|
request: NextRequest,
|
|
7
11
|
{ params }: { params: Promise<{ id: string }> }
|
|
@@ -16,33 +20,6 @@ export async function GET(
|
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
try {
|
|
19
|
-
// Check if JSON files exist (new format)
|
|
20
|
-
const hasJson = await hasJsonState(projectId)
|
|
21
|
-
|
|
22
|
-
if (hasJson) {
|
|
23
|
-
// Use new JSON loader (fast path)
|
|
24
|
-
const jsonData = await loadUnifiedJsonData(projectId)
|
|
25
|
-
|
|
26
|
-
// Convert to stats-compatible format using new architecture
|
|
27
|
-
return NextResponse.json({
|
|
28
|
-
success: true,
|
|
29
|
-
version: 'v2',
|
|
30
|
-
data: {
|
|
31
|
-
currentTask: jsonData.state?.currentTask || null,
|
|
32
|
-
queue: jsonData.queue?.tasks?.filter(t => !t.completed) || [],
|
|
33
|
-
metrics: jsonData.metrics || null,
|
|
34
|
-
agents: jsonData.agents,
|
|
35
|
-
ideas: jsonData.ideas?.ideas || [],
|
|
36
|
-
roadmap: jsonData.roadmap?.features || [],
|
|
37
|
-
shipped: jsonData.shipped?.items || [],
|
|
38
|
-
analysis: jsonData.analysis,
|
|
39
|
-
outcomes: jsonData.outcomes,
|
|
40
|
-
insights: jsonData.insights
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Fallback to legacy markdown parsing
|
|
46
23
|
const [stats, raw] = await Promise.all([
|
|
47
24
|
getProjectStats(projectId),
|
|
48
25
|
getRawProjectFiles(projectId)
|
|
@@ -50,7 +27,7 @@ export async function GET(
|
|
|
50
27
|
|
|
51
28
|
return NextResponse.json({
|
|
52
29
|
success: true,
|
|
53
|
-
version: '
|
|
30
|
+
version: 'md-first',
|
|
54
31
|
data: stats,
|
|
55
32
|
raw
|
|
56
33
|
})
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getProjects } from '@/lib/services/projects.server'
|
|
2
2
|
import { getGlobalStats } from '@/lib/services/stats.server'
|
|
3
3
|
import { DashboardContent } from '@/components/DashboardContent'
|
|
4
|
-
import { MigrationGate } from '@/components/MigrationGate'
|
|
5
4
|
|
|
6
5
|
export default async function Dashboard() {
|
|
7
6
|
const [projects, stats] = await Promise.all([
|
|
@@ -9,9 +8,5 @@ export default async function Dashboard() {
|
|
|
9
8
|
getGlobalStats()
|
|
10
9
|
])
|
|
11
10
|
|
|
12
|
-
return
|
|
13
|
-
<MigrationGate>
|
|
14
|
-
<DashboardContent projects={projects} stats={stats} />
|
|
15
|
-
</MigrationGate>
|
|
16
|
-
)
|
|
11
|
+
return <DashboardContent projects={projects} stats={stats} />
|
|
17
12
|
}
|
|
@@ -44,7 +44,8 @@ import {
|
|
|
44
44
|
Undo2,
|
|
45
45
|
Redo2,
|
|
46
46
|
Command,
|
|
47
|
-
X
|
|
47
|
+
X,
|
|
48
|
+
RefreshCw
|
|
48
49
|
} from 'lucide-react'
|
|
49
50
|
import { cn } from '@/lib/utils'
|
|
50
51
|
|
|
@@ -111,6 +112,15 @@ function CommandSidebarContent({
|
|
|
111
112
|
onCommandClick?.()
|
|
112
113
|
}}
|
|
113
114
|
/>
|
|
115
|
+
{/* Sync button - prominent, always visible */}
|
|
116
|
+
<CommandButton
|
|
117
|
+
cmd="p. sync"
|
|
118
|
+
icon={RefreshCw}
|
|
119
|
+
tip="Sync"
|
|
120
|
+
disabled={!isActiveConnected}
|
|
121
|
+
onClick={() => handleCommand('p. sync')}
|
|
122
|
+
variant="primary"
|
|
123
|
+
/>
|
|
114
124
|
<div className="border-b border-border w-8 my-2 mx-auto" />
|
|
115
125
|
|
|
116
126
|
{COMMAND_GROUPS.map((group, groupIndex) => (
|
|
@@ -268,6 +278,29 @@ function ProjectPageContent({ projectId, project }: { projectId: string; project
|
|
|
268
278
|
<span className="text-xs text-muted-foreground">Stats</span>
|
|
269
279
|
</button>
|
|
270
280
|
|
|
281
|
+
{/* Sync button - prominent */}
|
|
282
|
+
<button
|
|
283
|
+
onClick={() => {
|
|
284
|
+
sendCommandToActive('p. sync')
|
|
285
|
+
setCommandSheetOpen(false)
|
|
286
|
+
}}
|
|
287
|
+
disabled={!isActiveConnected}
|
|
288
|
+
className={cn(
|
|
289
|
+
"flex flex-col items-center gap-1.5 p-3 rounded-lg transition-colors",
|
|
290
|
+
isActiveConnected
|
|
291
|
+
? "hover:bg-primary/10"
|
|
292
|
+
: "opacity-50 cursor-not-allowed"
|
|
293
|
+
)}
|
|
294
|
+
>
|
|
295
|
+
<div className={cn(
|
|
296
|
+
"h-10 w-10 rounded-full flex items-center justify-center",
|
|
297
|
+
isActiveConnected ? "bg-primary text-primary-foreground" : "bg-muted"
|
|
298
|
+
)}>
|
|
299
|
+
<RefreshCw className="h-5 w-5" />
|
|
300
|
+
</div>
|
|
301
|
+
<span className="text-xs text-primary font-medium">Sync</span>
|
|
302
|
+
</button>
|
|
303
|
+
|
|
271
304
|
{WORKFLOW_COMMANDS.map(({ cmd, icon: Icon, tip }) => (
|
|
272
305
|
<button
|
|
273
306
|
key={cmd}
|
|
@@ -21,6 +21,7 @@ import { ShipsCard } from '@/components/ShipsCard'
|
|
|
21
21
|
import { IdeasCard } from '@/components/IdeasCard'
|
|
22
22
|
import { AgentsCard } from '@/components/AgentsCard'
|
|
23
23
|
import { RoadmapCard } from '@/components/RoadmapCard'
|
|
24
|
+
import { BlockersCard } from '@/components/BlockersCard'
|
|
24
25
|
import { ActivityTimeline } from '@/components/ActivityTimeline'
|
|
25
26
|
|
|
26
27
|
// Types for normalized component data
|
|
@@ -118,7 +119,7 @@ function normalizeShipped(stats: StatsResult): NormalizedShip[] {
|
|
|
118
119
|
const items = stats.shipped?.items ?? []
|
|
119
120
|
return items.map(s => ({
|
|
120
121
|
name: s.name,
|
|
121
|
-
date: s.shippedAt,
|
|
122
|
+
date: s.shippedAt || s.date || new Date().toISOString(),
|
|
122
123
|
}))
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -128,7 +129,7 @@ function normalizeIdeas(stats: StatsResult): NormalizedIdea[] {
|
|
|
128
129
|
.filter(i => i.status === 'pending')
|
|
129
130
|
.map(i => ({
|
|
130
131
|
title: i.text,
|
|
131
|
-
impact: i.priority
|
|
132
|
+
impact: i.priority?.toUpperCase() || 'MEDIUM'
|
|
132
133
|
}))
|
|
133
134
|
}
|
|
134
135
|
|
|
@@ -144,10 +145,10 @@ function normalizeAgents(stats: StatsResult): NormalizedAgent[] {
|
|
|
144
145
|
|
|
145
146
|
function normalizeTimeline(stats: StatsResult): TimelineEvent[] {
|
|
146
147
|
if (stats.metrics?.recentActivity?.length) {
|
|
147
|
-
return stats.metrics.recentActivity.map(
|
|
148
|
+
return stats.metrics.recentActivity.map(a => ({
|
|
148
149
|
ts: a.timestamp,
|
|
149
|
-
type: a.action || 'task_completed',
|
|
150
|
-
task: a.description,
|
|
150
|
+
type: a.action || a.type || 'task_completed',
|
|
151
|
+
task: a.description || '',
|
|
151
152
|
}))
|
|
152
153
|
}
|
|
153
154
|
return stats.legacyStats?.timeline ?? []
|
|
@@ -204,6 +205,9 @@ export default async function ProjectStatsPage({ params }: PageProps) {
|
|
|
204
205
|
const totalShips = getTotalShips(stats)
|
|
205
206
|
const tasksCompleted = getTasksCompleted(stats)
|
|
206
207
|
|
|
208
|
+
// Extract insights
|
|
209
|
+
const { estimateAccuracy, blockers } = stats.insights
|
|
210
|
+
|
|
207
211
|
return (
|
|
208
212
|
<div className="flex h-full flex-col p-4 md:p-8 overflow-auto">
|
|
209
213
|
{/* Mobile: Add padding for hamburger menu */}
|
|
@@ -227,11 +231,13 @@ export default async function ProjectStatsPage({ params }: PageProps) {
|
|
|
227
231
|
tasksPerDay={velocity}
|
|
228
232
|
weeklyData={weeklyVelocityData}
|
|
229
233
|
change={velocityChange}
|
|
234
|
+
estimateAccuracy={estimateAccuracy}
|
|
230
235
|
/>
|
|
231
236
|
<RoadmapCard roadmap={roadmap} />
|
|
232
237
|
<StreakCard streak={streak} />
|
|
233
238
|
<QueueCard queue={queue} />
|
|
234
239
|
<ShipsCard ships={shipped} totalShips={totalShips} />
|
|
240
|
+
<BlockersCard blockers={blockers} />
|
|
235
241
|
<IdeasCard ideas={ideas} />
|
|
236
242
|
<AgentsCard agents={agents} />
|
|
237
243
|
</BentoGrid>
|