prjct-cli 0.11.3 → 0.11.5
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 +14 -0
- package/bin/prjct +4 -0
- package/bin/serve.js +22 -6
- package/core/__tests__/utils/date-helper.test.js +416 -0
- package/core/agentic/agent-router.js +30 -18
- package/core/agentic/command-executor.js +20 -24
- package/core/agentic/context-builder.js +7 -8
- package/core/agentic/memory-system.js +14 -19
- package/core/agentic/prompt-builder.js +41 -27
- package/core/agentic/template-loader.js +8 -2
- package/core/infrastructure/agent-detector.js +7 -4
- package/core/infrastructure/migrator.js +10 -13
- package/core/infrastructure/session-manager.js +10 -10
- package/package.json +1 -1
- package/packages/web/app/project/[id]/stats/page.tsx +102 -343
- package/packages/web/components/stats/ActivityTimeline.tsx +201 -0
- package/packages/web/components/stats/AgentsCard.tsx +56 -0
- package/packages/web/components/stats/BentoCard.tsx +88 -0
- package/packages/web/components/stats/BentoGrid.tsx +22 -0
- package/packages/web/components/stats/EmptyState.tsx +67 -0
- package/packages/web/components/stats/HeroSection.tsx +172 -0
- package/packages/web/components/stats/IdeasCard.tsx +59 -0
- package/packages/web/components/stats/NowCard.tsx +71 -0
- package/packages/web/components/stats/ProgressRing.tsx +74 -0
- package/packages/web/components/stats/QueueCard.tsx +58 -0
- package/packages/web/components/stats/RoadmapCard.tsx +97 -0
- package/packages/web/components/stats/ShipsCard.tsx +70 -0
- package/packages/web/components/stats/SparklineChart.tsx +44 -0
- package/packages/web/components/stats/StreakCard.tsx +59 -0
- package/packages/web/components/stats/VelocityCard.tsx +60 -0
- package/packages/web/components/stats/index.ts +17 -0
- package/packages/web/components/ui/tooltip.tsx +2 -2
- package/packages/web/next-env.d.ts +1 -1
- package/packages/web/package.json +2 -1
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { use, useMemo
|
|
3
|
+
import { use, useMemo } from 'react'
|
|
4
4
|
import { useRouter } from 'next/navigation'
|
|
5
|
-
import Link from 'next/link'
|
|
6
5
|
import { useProject } from '@/hooks/useProjects'
|
|
7
6
|
import { useProjectStats } from '@/hooks/useProjectStats'
|
|
8
7
|
import { Button } from '@/components/ui/button'
|
|
9
|
-
import {
|
|
10
|
-
ArrowLeft,
|
|
11
|
-
Zap,
|
|
12
|
-
TrendingUp,
|
|
13
|
-
TrendingDown,
|
|
14
|
-
Lightbulb,
|
|
15
|
-
AlertTriangle,
|
|
16
|
-
CheckCircle2,
|
|
17
|
-
Bot,
|
|
18
|
-
Rocket,
|
|
19
|
-
Flame,
|
|
20
|
-
Play,
|
|
21
|
-
Copy,
|
|
22
|
-
FileText,
|
|
23
|
-
Target,
|
|
24
|
-
Clock
|
|
25
|
-
} from 'lucide-react'
|
|
26
|
-
import { cn } from '@/lib/utils'
|
|
8
|
+
import { ArrowLeft } from 'lucide-react'
|
|
27
9
|
import type { TimelineEvent } from '@/lib/parse-prjct-files'
|
|
28
10
|
|
|
29
|
-
|
|
11
|
+
import {
|
|
12
|
+
BentoGrid,
|
|
13
|
+
BentoCardSkeleton,
|
|
14
|
+
HeroSection,
|
|
15
|
+
NowCard,
|
|
16
|
+
VelocityCard,
|
|
17
|
+
StreakCard,
|
|
18
|
+
QueueCard,
|
|
19
|
+
ShipsCard,
|
|
20
|
+
IdeasCard,
|
|
21
|
+
AgentsCard,
|
|
22
|
+
RoadmapCard,
|
|
23
|
+
ActivityTimeline,
|
|
24
|
+
} from '@/components/stats'
|
|
25
|
+
|
|
26
|
+
// Calculate streak from timeline
|
|
30
27
|
function calculateStreak(timeline: TimelineEvent[]): number {
|
|
31
28
|
if (!timeline.length) return 0
|
|
32
29
|
const dates = new Set(timeline.map(e => e.ts?.split('T')[0]).filter(Boolean))
|
|
@@ -51,36 +48,14 @@ function getHealthScore(stats: any): number {
|
|
|
51
48
|
const recentActivity = stats?.timeline?.slice(0, 7).length || 0
|
|
52
49
|
|
|
53
50
|
let score = 0
|
|
54
|
-
score += Math.min(30, velocity * 15)
|
|
55
|
-
score += hasCurrentTask ? 20 : 0
|
|
56
|
-
score += queueSize > 0 && queueSize < 15 ? 20 : queueSize === 0 ? 5 : 10
|
|
57
|
-
score += Math.min(30, recentActivity * 5)
|
|
51
|
+
score += Math.min(30, velocity * 15)
|
|
52
|
+
score += hasCurrentTask ? 20 : 0
|
|
53
|
+
score += queueSize > 0 && queueSize < 15 ? 20 : queueSize === 0 ? 5 : 10
|
|
54
|
+
score += Math.min(30, recentActivity * 5)
|
|
58
55
|
|
|
59
56
|
return Math.min(100, Math.round(score))
|
|
60
57
|
}
|
|
61
58
|
|
|
62
|
-
// Copy to clipboard
|
|
63
|
-
function copyCommand(cmd: string) {
|
|
64
|
-
navigator.clipboard.writeText(cmd)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Format relative time
|
|
68
|
-
function formatRelativeTime(dateString: string): string {
|
|
69
|
-
const date = new Date(dateString)
|
|
70
|
-
const now = new Date()
|
|
71
|
-
const diffMs = now.getTime() - date.getTime()
|
|
72
|
-
const diffMins = Math.floor(diffMs / 60000)
|
|
73
|
-
const diffHours = Math.floor(diffMs / 3600000)
|
|
74
|
-
const diffDays = Math.floor(diffMs / 86400000)
|
|
75
|
-
|
|
76
|
-
if (diffMins < 1) return 'NOW'
|
|
77
|
-
if (diffMins < 60) return `${diffMins}M`
|
|
78
|
-
if (diffHours < 24) return `${diffHours}H`
|
|
79
|
-
if (diffDays === 1) return '1D'
|
|
80
|
-
if (diffDays < 7) return `${diffDays}D`
|
|
81
|
-
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }).toUpperCase()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
59
|
// Contextual insight message
|
|
85
60
|
function getInsightMessage(stats: any, streak: number): string {
|
|
86
61
|
if (!stats) return ''
|
|
@@ -99,72 +74,52 @@ function getInsightMessage(stats: any, streak: number): string {
|
|
|
99
74
|
return 'Steady progress. Pick the next task.'
|
|
100
75
|
}
|
|
101
76
|
|
|
102
|
-
//
|
|
103
|
-
function
|
|
104
|
-
return
|
|
105
|
-
<p className={cn("text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40", className)}>
|
|
106
|
-
{children}
|
|
107
|
-
</p>
|
|
108
|
-
)
|
|
77
|
+
// Calculate velocity change (simulated)
|
|
78
|
+
function getVelocityChange(velocity: number): number {
|
|
79
|
+
return velocity > 2 ? 15 : velocity > 1 ? 5 : velocity > 0 ? 0 : -10
|
|
109
80
|
}
|
|
110
81
|
|
|
111
|
-
//
|
|
112
|
-
function
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
md: { container: 'h-12 w-12', text: 'text-xs' },
|
|
116
|
-
lg: { container: 'h-16 w-16', text: 'text-sm' },
|
|
117
|
-
}
|
|
118
|
-
const { container, text } = sizes[size]
|
|
82
|
+
// Get weekly velocity data from timeline
|
|
83
|
+
function getWeeklyVelocityData(timeline: TimelineEvent[]): number[] {
|
|
84
|
+
const today = new Date()
|
|
85
|
+
const counts: number[] = []
|
|
119
86
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
/>
|
|
130
|
-
</svg>
|
|
131
|
-
<span className={cn('absolute inset-0 flex items-center justify-center font-bold', text)}>
|
|
132
|
-
{score}
|
|
133
|
-
</span>
|
|
134
|
-
</div>
|
|
135
|
-
)
|
|
136
|
-
}
|
|
87
|
+
for (let i = 6; i >= 0; i--) {
|
|
88
|
+
const date = new Date(today)
|
|
89
|
+
date.setDate(date.getDate() - i)
|
|
90
|
+
const dateStr = date.toISOString().split('T')[0]
|
|
91
|
+
|
|
92
|
+
const count = timeline.filter(e => {
|
|
93
|
+
if (!e.ts) return false
|
|
94
|
+
return e.ts.startsWith(dateStr) && e.type === 'task_complete'
|
|
95
|
+
}).length
|
|
137
96
|
|
|
138
|
-
|
|
139
|
-
function StatCard({ value, label, suffix, size = 'md' }: {
|
|
140
|
-
value: string | number
|
|
141
|
-
label: string
|
|
142
|
-
suffix?: string
|
|
143
|
-
size?: 'sm' | 'md' | 'lg'
|
|
144
|
-
}) {
|
|
145
|
-
const sizeClasses = {
|
|
146
|
-
sm: { value: 'text-lg font-bold', label: 'text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40' },
|
|
147
|
-
md: { value: 'text-2xl font-bold', label: 'text-xs text-muted-foreground' },
|
|
148
|
-
lg: { value: 'text-3xl font-black tracking-tight tabular-nums', label: 'text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40 mt-1' },
|
|
97
|
+
counts.push(count)
|
|
149
98
|
}
|
|
150
|
-
const styles = sizeClasses[size]
|
|
151
99
|
|
|
152
|
-
return
|
|
153
|
-
<div>
|
|
154
|
-
<p className={cn(styles.value, 'text-foreground')}>
|
|
155
|
-
{value}
|
|
156
|
-
{suffix && <span className="text-foreground/50 font-normal text-sm ml-1">{suffix}</span>}
|
|
157
|
-
</p>
|
|
158
|
-
<p className={styles.label}>{label}</p>
|
|
159
|
-
</div>
|
|
160
|
-
)
|
|
100
|
+
return counts
|
|
161
101
|
}
|
|
162
102
|
|
|
163
|
-
|
|
164
|
-
function StatsRow({ children, className }: { children: React.ReactNode; className?: string }) {
|
|
103
|
+
function LoadingSkeleton() {
|
|
165
104
|
return (
|
|
166
|
-
<div className=
|
|
167
|
-
|
|
105
|
+
<div className="p-8 space-y-8">
|
|
106
|
+
<div className="flex items-start gap-6">
|
|
107
|
+
<div className="h-20 w-20 rounded-full bg-muted animate-pulse" />
|
|
108
|
+
<div className="space-y-3">
|
|
109
|
+
<div className="h-16 w-32 bg-muted rounded animate-pulse" />
|
|
110
|
+
<div className="h-4 w-48 bg-muted rounded animate-pulse" />
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<BentoGrid>
|
|
114
|
+
<BentoCardSkeleton size="2x2" />
|
|
115
|
+
<BentoCardSkeleton size="1x1" />
|
|
116
|
+
<BentoCardSkeleton size="2x2" />
|
|
117
|
+
<BentoCardSkeleton size="1x1" />
|
|
118
|
+
<BentoCardSkeleton size="1x2" />
|
|
119
|
+
<BentoCardSkeleton size="1x2" />
|
|
120
|
+
<BentoCardSkeleton size="1x1" />
|
|
121
|
+
<BentoCardSkeleton size="1x1" />
|
|
122
|
+
</BentoGrid>
|
|
168
123
|
</div>
|
|
169
124
|
)
|
|
170
125
|
}
|
|
@@ -172,7 +127,6 @@ function StatsRow({ children, className }: { children: React.ReactNode; classNam
|
|
|
172
127
|
export default function ProjectStatsPage({ params }: { params: Promise<{ id: string }> }) {
|
|
173
128
|
const { id: projectId } = use(params)
|
|
174
129
|
const router = useRouter()
|
|
175
|
-
const [expandedSection, setExpandedSection] = useState<string | null>(null)
|
|
176
130
|
|
|
177
131
|
const { data: project, isLoading: projectLoading } = useProject(projectId)
|
|
178
132
|
const { data, isLoading: statsLoading } = useProjectStats(projectId)
|
|
@@ -181,27 +135,12 @@ export default function ProjectStatsPage({ params }: { params: Promise<{ id: str
|
|
|
181
135
|
const streak = useMemo(() => calculateStreak(stats?.timeline || []), [stats?.timeline])
|
|
182
136
|
const healthScore = useMemo(() => getHealthScore(stats), [stats])
|
|
183
137
|
const insightMessage = useMemo(() => getInsightMessage(stats, streak), [stats, streak])
|
|
184
|
-
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
const { tasksStarted, tasksCompleted } = stats.metrics
|
|
188
|
-
return tasksStarted > 0 ? Math.round((tasksCompleted / tasksStarted) * 100) : 0
|
|
189
|
-
}, [stats?.metrics])
|
|
190
|
-
|
|
191
|
-
const velocityChange = useMemo(() => {
|
|
192
|
-
// Simulated - in reality would compare to previous period
|
|
193
|
-
const velocity = stats?.metrics?.velocity?.tasksPerDay || 0
|
|
194
|
-
return velocity > 2 ? 15 : velocity > 1 ? 5 : -10
|
|
195
|
-
}, [stats?.metrics?.velocity?.tasksPerDay])
|
|
196
|
-
|
|
197
|
-
// Recent activity - last 5 for chips
|
|
198
|
-
const recentActivity = useMemo(() => {
|
|
199
|
-
if (!stats?.timeline) return []
|
|
200
|
-
return stats.timeline.slice(0, 5)
|
|
201
|
-
}, [stats?.timeline])
|
|
138
|
+
const velocity = stats?.metrics?.velocity?.tasksPerDay || 0
|
|
139
|
+
const velocityChange = useMemo(() => getVelocityChange(velocity), [velocity])
|
|
140
|
+
const weeklyVelocityData = useMemo(() => getWeeklyVelocityData(stats?.timeline || []), [stats?.timeline])
|
|
202
141
|
|
|
203
142
|
if (projectLoading || statsLoading) {
|
|
204
|
-
return <
|
|
143
|
+
return <LoadingSkeleton />
|
|
205
144
|
}
|
|
206
145
|
|
|
207
146
|
if (!project || !stats) {
|
|
@@ -209,7 +148,10 @@ export default function ProjectStatsPage({ params }: { params: Promise<{ id: str
|
|
|
209
148
|
<div className="flex items-center justify-center h-full">
|
|
210
149
|
<div className="text-center space-y-4">
|
|
211
150
|
<p className="text-4xl text-muted-foreground">404</p>
|
|
212
|
-
<Button variant="ghost" onClick={() => router.back()}
|
|
151
|
+
<Button variant="ghost" onClick={() => router.back()}>
|
|
152
|
+
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
153
|
+
Back
|
|
154
|
+
</Button>
|
|
213
155
|
</div>
|
|
214
156
|
</div>
|
|
215
157
|
)
|
|
@@ -217,228 +159,45 @@ export default function ProjectStatsPage({ params }: { params: Promise<{ id: str
|
|
|
217
159
|
|
|
218
160
|
return (
|
|
219
161
|
<div className="flex h-full flex-col p-8 overflow-auto">
|
|
220
|
-
{/* Hero Section
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
'flex items-center gap-1 text-sm font-medium px-2 py-1 rounded-md',
|
|
241
|
-
velocityChange >= 0 ? 'bg-foreground/5 text-foreground' : 'bg-muted text-muted-foreground'
|
|
242
|
-
)}>
|
|
243
|
-
{velocityChange >= 0 ? <TrendingUp className="h-3.5 w-3.5" /> : <TrendingDown className="h-3.5 w-3.5" />}
|
|
244
|
-
{velocityChange >= 0 ? '+' : ''}{velocityChange}%
|
|
245
|
-
</span>
|
|
246
|
-
<span className="text-sm text-muted-foreground">velocity vs last week</span>
|
|
247
|
-
</div>
|
|
248
|
-
)}
|
|
249
|
-
|
|
250
|
-
{/* Secondary KPIs Row */}
|
|
251
|
-
<StatsRow className="mt-8">
|
|
252
|
-
<div className="flex items-center gap-3">
|
|
253
|
-
<HealthRing score={healthScore} />
|
|
254
|
-
<div>
|
|
255
|
-
<p className="text-sm font-medium">Health</p>
|
|
256
|
-
<p className="text-xs text-muted-foreground">Project score</p>
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
|
-
|
|
260
|
-
<StatCard value={`${completionRate}%`} label="Completion" />
|
|
261
|
-
<StatCard value={stats.summary.totalShipsEver} label="Ships" />
|
|
262
|
-
<StatCard value={stats.metrics.velocity.tasksPerDay} label="Tasks/day" />
|
|
263
|
-
<StatCard
|
|
264
|
-
value={<>{stats.queue.length} / {stats.ideas.pending.length}</>}
|
|
265
|
-
label="Queue / Ideas"
|
|
162
|
+
{/* Hero Section */}
|
|
163
|
+
<HeroSection
|
|
164
|
+
projectId={projectId}
|
|
165
|
+
projectName={project.name || projectId}
|
|
166
|
+
tasksCompleted={stats.metrics.tasksCompleted}
|
|
167
|
+
healthScore={healthScore}
|
|
168
|
+
velocity={velocity}
|
|
169
|
+
velocityChange={velocityChange}
|
|
170
|
+
insightMessage={insightMessage}
|
|
171
|
+
timeline={stats.timeline}
|
|
172
|
+
/>
|
|
173
|
+
|
|
174
|
+
{/* Bento Grid */}
|
|
175
|
+
<BentoGrid className="mt-8">
|
|
176
|
+
{/* Row 1: NOW (2x2), VELOCITY (1x1), ROADMAP (2x2) */}
|
|
177
|
+
<NowCard currentTask={stats.currentTask} />
|
|
178
|
+
<VelocityCard
|
|
179
|
+
tasksPerDay={velocity}
|
|
180
|
+
weeklyData={weeklyVelocityData}
|
|
181
|
+
change={velocityChange}
|
|
266
182
|
/>
|
|
267
|
-
{
|
|
268
|
-
<div className="flex items-center gap-2">
|
|
269
|
-
<Flame className={cn('w-5 h-5', streak > 3 ? 'text-orange-500' : 'text-foreground/40')} />
|
|
270
|
-
<StatCard value={streak} label="Day streak" />
|
|
271
|
-
</div>
|
|
272
|
-
)}
|
|
273
|
-
</StatsRow>
|
|
183
|
+
<RoadmapCard roadmap={stats.roadmap} />
|
|
274
184
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<div className="mt-8">
|
|
278
|
-
<SectionLabel className="mb-4">RECENT</SectionLabel>
|
|
279
|
-
<div className="flex flex-wrap gap-3">
|
|
280
|
-
{recentActivity.map((event: TimelineEvent, i: number) => {
|
|
281
|
-
const e = event as Record<string, unknown>
|
|
282
|
-
const label = event.type === 'feature_ship' ? (e.name as string) :
|
|
283
|
-
event.type === 'task_complete' ? (e.task as string) :
|
|
284
|
-
event.type === 'task_start' ? (e.task as string) :
|
|
285
|
-
event.type === 'sync' ? 'Sync' : event.type
|
|
286
|
-
const status = event.type === 'feature_ship' ? 'SHIP' :
|
|
287
|
-
event.type === 'task_complete' ? 'DONE' :
|
|
288
|
-
event.type === 'task_start' ? 'START' :
|
|
289
|
-
event.type === 'sync' ? 'SYNC' : event.type.toUpperCase()
|
|
290
|
-
return (
|
|
291
|
-
<div
|
|
292
|
-
key={i}
|
|
293
|
-
className="group flex items-center gap-3 rounded-lg border-2 border-foreground/10 px-4 py-2 transition-all hover:border-foreground hover:bg-foreground hover:text-background cursor-default"
|
|
294
|
-
>
|
|
295
|
-
<span className="text-sm font-bold text-foreground group-hover:text-background truncate max-w-[150px]">
|
|
296
|
-
{label}
|
|
297
|
-
</span>
|
|
298
|
-
<span className="text-[9px] font-bold tracking-wider text-foreground/40 group-hover:text-background/60">
|
|
299
|
-
{status}
|
|
300
|
-
</span>
|
|
301
|
-
{event.ts && (
|
|
302
|
-
<span className="text-[9px] font-bold tracking-wider text-foreground/30 group-hover:text-background/40">
|
|
303
|
-
{formatRelativeTime(event.ts)}
|
|
304
|
-
</span>
|
|
305
|
-
)}
|
|
306
|
-
</div>
|
|
307
|
-
)
|
|
308
|
-
})}
|
|
309
|
-
</div>
|
|
310
|
-
</div>
|
|
311
|
-
)}
|
|
185
|
+
{/* STREAK under VELOCITY */}
|
|
186
|
+
<StreakCard streak={streak} />
|
|
312
187
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
<span className="text-[10px] font-medium text-amber-600 uppercase tracking-wider">Working</span>
|
|
323
|
-
</div>
|
|
324
|
-
<p className="text-lg font-semibold leading-tight">{stats.currentTask.task}</p>
|
|
325
|
-
{stats.currentTask.duration && (
|
|
326
|
-
<p className="text-xs text-muted-foreground mt-2">{stats.currentTask.duration}</p>
|
|
327
|
-
)}
|
|
328
|
-
</div>
|
|
329
|
-
) : (
|
|
330
|
-
<p className="text-sm text-muted-foreground">No active task</p>
|
|
331
|
-
)}
|
|
332
|
-
</div>
|
|
333
|
-
|
|
334
|
-
{/* Queue */}
|
|
335
|
-
{stats.queue.length > 0 && (
|
|
336
|
-
<div className="break-inside-avoid mb-8">
|
|
337
|
-
<div className="flex items-center gap-2 mb-3">
|
|
338
|
-
<SectionLabel>QUEUE</SectionLabel>
|
|
339
|
-
<span className="text-xs text-muted-foreground">{stats.queue.length}</span>
|
|
340
|
-
</div>
|
|
341
|
-
<div className="space-y-1.5">
|
|
342
|
-
{stats.queue.slice(0, 6).map((task: any, i: number) => (
|
|
343
|
-
<div key={i} className="flex items-center gap-2 group">
|
|
344
|
-
<span className="text-[10px] text-muted-foreground w-3">{i + 1}</span>
|
|
345
|
-
<p className="flex-1 text-sm truncate">{task.task}</p>
|
|
346
|
-
</div>
|
|
347
|
-
))}
|
|
348
|
-
{stats.queue.length > 6 && (
|
|
349
|
-
<p className="text-[10px] text-muted-foreground">+{stats.queue.length - 6} more</p>
|
|
350
|
-
)}
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
)}
|
|
354
|
-
|
|
355
|
-
{/* Ships */}
|
|
356
|
-
{stats.shipped.length > 0 && (
|
|
357
|
-
<div className="break-inside-avoid mb-8">
|
|
358
|
-
<SectionLabel className="mb-3">SHIPS</SectionLabel>
|
|
359
|
-
<div className="space-y-3">
|
|
360
|
-
{stats.shipped.slice(0, 5).map((ship: any, i: number) => (
|
|
361
|
-
<div key={i}>
|
|
362
|
-
<p className="text-sm font-medium">{ship.name}</p>
|
|
363
|
-
<p className="text-[10px] text-muted-foreground">
|
|
364
|
-
{new Date(ship.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
365
|
-
{ship.version && ` · ${ship.version}`}
|
|
366
|
-
</p>
|
|
367
|
-
</div>
|
|
368
|
-
))}
|
|
369
|
-
</div>
|
|
370
|
-
</div>
|
|
371
|
-
)}
|
|
372
|
-
|
|
373
|
-
{/* Agents */}
|
|
374
|
-
{stats.agents.length > 0 && (
|
|
375
|
-
<div className="break-inside-avoid mb-8">
|
|
376
|
-
<div className="flex items-center gap-2 mb-3">
|
|
377
|
-
<SectionLabel>AGENTS</SectionLabel>
|
|
378
|
-
<span className="text-xs text-muted-foreground">{stats.agents.length}</span>
|
|
379
|
-
</div>
|
|
380
|
-
<div className="flex flex-wrap gap-1.5">
|
|
381
|
-
{stats.agents.slice(0, 10).map((agent: any) => (
|
|
382
|
-
<span
|
|
383
|
-
key={agent.name}
|
|
384
|
-
className="text-xs px-2 py-0.5 bg-foreground/5 rounded"
|
|
385
|
-
>
|
|
386
|
-
{agent.name}
|
|
387
|
-
</span>
|
|
388
|
-
))}
|
|
389
|
-
</div>
|
|
390
|
-
</div>
|
|
391
|
-
)}
|
|
392
|
-
|
|
393
|
-
{/* Ideas */}
|
|
394
|
-
{stats.ideas.pending.length > 0 && (
|
|
395
|
-
<div className="break-inside-avoid mb-8">
|
|
396
|
-
<div className="flex items-center gap-2 mb-3">
|
|
397
|
-
<SectionLabel>IDEAS</SectionLabel>
|
|
398
|
-
<span className="text-xs text-muted-foreground">{stats.ideas.pending.length}</span>
|
|
399
|
-
</div>
|
|
400
|
-
<div className="space-y-2">
|
|
401
|
-
{stats.ideas.pending.slice(0, 5).map((idea: any, i: number) => (
|
|
402
|
-
<div key={i} className="flex items-start gap-2">
|
|
403
|
-
<Lightbulb className={cn(
|
|
404
|
-
'w-3 h-3 mt-0.5 shrink-0',
|
|
405
|
-
idea.impact === 'HIGH' ? 'text-foreground' : 'text-muted-foreground'
|
|
406
|
-
)} />
|
|
407
|
-
<p className="text-sm">{idea.title}</p>
|
|
408
|
-
</div>
|
|
409
|
-
))}
|
|
410
|
-
</div>
|
|
411
|
-
</div>
|
|
412
|
-
)}
|
|
188
|
+
{/* Row 2: QUEUE (1x2), SHIPS (1x2), IDEAS (1x1), AGENTS (1x1) */}
|
|
189
|
+
<QueueCard queue={stats.queue || []} />
|
|
190
|
+
<ShipsCard
|
|
191
|
+
ships={stats.shipped || []}
|
|
192
|
+
totalShips={stats.summary?.totalShipsEver || 0}
|
|
193
|
+
/>
|
|
194
|
+
<IdeasCard ideas={stats.ideas?.pending || []} />
|
|
195
|
+
<AgentsCard agents={stats.agents || []} />
|
|
196
|
+
</BentoGrid>
|
|
413
197
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
<div className="flex items-center justify-between mb-3">
|
|
418
|
-
<SectionLabel>ROADMAP</SectionLabel>
|
|
419
|
-
<span className="text-lg font-bold">{stats.roadmap.progress}%</span>
|
|
420
|
-
</div>
|
|
421
|
-
<div className="space-y-2">
|
|
422
|
-
{stats.roadmap.phases.filter((p: any) => (p.features || []).length > 0).slice(0, 4).map((phase: any) => (
|
|
423
|
-
<div key={phase.name}>
|
|
424
|
-
<div className="flex items-center justify-between text-xs mb-1">
|
|
425
|
-
<span>{phase.name}</span>
|
|
426
|
-
<span className="text-muted-foreground">{phase.progress}%</span>
|
|
427
|
-
</div>
|
|
428
|
-
<div className="h-1 bg-muted rounded-full overflow-hidden">
|
|
429
|
-
<div
|
|
430
|
-
className={cn(
|
|
431
|
-
'h-full rounded-full',
|
|
432
|
-
phase.progress === 100 ? 'bg-emerald-500' : 'bg-foreground'
|
|
433
|
-
)}
|
|
434
|
-
style={{ width: `${phase.progress}%` }}
|
|
435
|
-
/>
|
|
436
|
-
</div>
|
|
437
|
-
</div>
|
|
438
|
-
))}
|
|
439
|
-
</div>
|
|
440
|
-
</div>
|
|
441
|
-
)}
|
|
198
|
+
{/* Activity Timeline */}
|
|
199
|
+
<div className="mt-8">
|
|
200
|
+
<ActivityTimeline timeline={stats.timeline || []} />
|
|
442
201
|
</div>
|
|
443
202
|
|
|
444
203
|
<div className="h-8" />
|