prjct-cli 0.18.1 → 0.19.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 +53 -0
- package/CLAUDE.md +74 -211
- package/core/agentic/prompt-builder.ts +3 -7
- package/core/command-registry/optional-commands.ts +0 -20
- package/core/infrastructure/command-installer/command-installer.ts +8 -1
- package/core/infrastructure/command-installer/global-config.ts +31 -1
- package/core/infrastructure/command-installer/index.ts +1 -1
- package/core/infrastructure/setup.ts +3 -0
- package/package.json +3 -17
- package/templates/agentic/subagent-generation.md +8 -6
- package/templates/commands/done.md +57 -258
- package/templates/commands/now.md +72 -277
- package/templates/commands/ship.md +55 -261
- package/templates/commands/sync.md +7 -7
- package/templates/commands/test.md +328 -21
- package/templates/global/CLAUDE.md +40 -205
- package/templates/global/docs/agents.md +88 -0
- package/templates/global/docs/architecture.md +103 -0
- package/templates/global/docs/commands.md +98 -0
- package/templates/global/docs/validation.md +95 -0
- package/templates/mcp-config.json +36 -0
- package/bin/dev.js +0 -216
- package/bin/serve.js +0 -361
- package/packages/web/README.md +0 -36
- package/packages/web/app/api/claude/sessions/route.ts +0 -44
- package/packages/web/app/api/claude/status/route.ts +0 -34
- package/packages/web/app/api/projects/[id]/icon/route.ts +0 -33
- package/packages/web/app/api/projects/[id]/momentum/route.ts +0 -257
- package/packages/web/app/api/projects/[id]/route.ts +0 -29
- package/packages/web/app/api/projects/[id]/stats/route.ts +0 -41
- package/packages/web/app/api/projects/[id]/status/route.ts +0 -21
- package/packages/web/app/api/projects/route.ts +0 -16
- package/packages/web/app/api/sessions/current/route.ts +0 -132
- package/packages/web/app/api/sessions/history/route.ts +0 -204
- package/packages/web/app/error.tsx +0 -34
- package/packages/web/app/favicon.ico +0 -0
- package/packages/web/app/globals.css +0 -198
- package/packages/web/app/layout.tsx +0 -53
- package/packages/web/app/loading.tsx +0 -7
- package/packages/web/app/not-found.tsx +0 -25
- package/packages/web/app/page.tsx +0 -12
- package/packages/web/app/project/[id]/code/layout.tsx +0 -18
- package/packages/web/app/project/[id]/code/page.tsx +0 -408
- package/packages/web/app/project/[id]/error.tsx +0 -41
- package/packages/web/app/project/[id]/loading.tsx +0 -9
- package/packages/web/app/project/[id]/not-found.tsx +0 -27
- package/packages/web/app/project/[id]/page.tsx +0 -384
- package/packages/web/app/project/[id]/reports/page.tsx +0 -59
- package/packages/web/app/project/[id]/reports/print/page.tsx +0 -58
- package/packages/web/app/sessions/page.tsx +0 -165
- package/packages/web/app/settings/page.tsx +0 -151
- package/packages/web/components/ActivityTimeline/ActivityTimeline.constants.ts +0 -2
- package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -49
- package/packages/web/components/ActivityTimeline/ActivityTimeline.types.ts +0 -8
- package/packages/web/components/ActivityTimeline/hooks/index.ts +0 -2
- package/packages/web/components/ActivityTimeline/hooks/useExpandable.ts +0 -9
- package/packages/web/components/ActivityTimeline/hooks/useGroupedEvents.ts +0 -23
- package/packages/web/components/ActivityTimeline/index.ts +0 -2
- package/packages/web/components/AgentsCard/AgentsCard.tsx +0 -93
- package/packages/web/components/AgentsCard/AgentsCard.types.ts +0 -14
- package/packages/web/components/AgentsCard/index.ts +0 -2
- package/packages/web/components/AppSidebar/AppSidebar.tsx +0 -316
- package/packages/web/components/AppSidebar/index.ts +0 -1
- package/packages/web/components/BackLink/BackLink.tsx +0 -18
- package/packages/web/components/BackLink/BackLink.types.ts +0 -5
- package/packages/web/components/BackLink/index.ts +0 -2
- package/packages/web/components/BentoCard/BentoCard.constants.ts +0 -16
- package/packages/web/components/BentoCard/BentoCard.tsx +0 -48
- package/packages/web/components/BentoCard/BentoCard.types.ts +0 -15
- package/packages/web/components/BentoCard/index.ts +0 -2
- package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.constants.ts +0 -9
- package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.tsx +0 -18
- package/packages/web/components/BentoCardSkeleton/BentoCardSkeleton.types.ts +0 -5
- package/packages/web/components/BentoCardSkeleton/index.ts +0 -2
- package/packages/web/components/BentoGrid/BentoGrid.tsx +0 -18
- package/packages/web/components/BentoGrid/BentoGrid.types.ts +0 -4
- package/packages/web/components/BentoGrid/index.ts +0 -2
- package/packages/web/components/BlockersCard/BlockersCard.tsx +0 -75
- package/packages/web/components/BlockersCard/BlockersCard.types.ts +0 -12
- package/packages/web/components/BlockersCard/index.ts +0 -2
- package/packages/web/components/CommandBar/CommandBar.tsx +0 -67
- package/packages/web/components/CommandBar/index.ts +0 -1
- package/packages/web/components/CommandButton/CommandButton.tsx +0 -46
- package/packages/web/components/CommandButton/index.ts +0 -1
- package/packages/web/components/ConnectionStatus/ConnectionStatus.tsx +0 -29
- package/packages/web/components/ConnectionStatus/index.ts +0 -1
- package/packages/web/components/DashboardContent/DashboardContent.tsx +0 -284
- package/packages/web/components/DashboardContent/index.ts +0 -1
- package/packages/web/components/DateGroup/DateGroup.tsx +0 -18
- package/packages/web/components/DateGroup/DateGroup.types.ts +0 -6
- package/packages/web/components/DateGroup/DateGroup.utils.ts +0 -11
- package/packages/web/components/DateGroup/index.ts +0 -2
- package/packages/web/components/EmptyState/EmptyState.tsx +0 -76
- package/packages/web/components/EmptyState/EmptyState.types.ts +0 -11
- package/packages/web/components/EmptyState/index.ts +0 -2
- package/packages/web/components/EventRow/EventRow.constants.ts +0 -10
- package/packages/web/components/EventRow/EventRow.tsx +0 -49
- package/packages/web/components/EventRow/EventRow.types.ts +0 -7
- package/packages/web/components/EventRow/EventRow.utils.ts +0 -49
- package/packages/web/components/EventRow/index.ts +0 -2
- package/packages/web/components/ExpandButton/ExpandButton.tsx +0 -18
- package/packages/web/components/ExpandButton/ExpandButton.types.ts +0 -6
- package/packages/web/components/ExpandButton/index.ts +0 -2
- package/packages/web/components/HealthGradientBackground/HealthGradientBackground.tsx +0 -14
- package/packages/web/components/HealthGradientBackground/HealthGradientBackground.types.ts +0 -5
- package/packages/web/components/HealthGradientBackground/HealthGradientBackground.utils.ts +0 -13
- package/packages/web/components/HealthGradientBackground/index.ts +0 -2
- package/packages/web/components/HeroSection/HeroSection.tsx +0 -92
- package/packages/web/components/HeroSection/HeroSection.types.ts +0 -14
- package/packages/web/components/HeroSection/HeroSection.utils.ts +0 -11
- package/packages/web/components/HeroSection/hooks/index.ts +0 -2
- package/packages/web/components/HeroSection/hooks/useCountUp.ts +0 -45
- package/packages/web/components/HeroSection/hooks/useWeeklyActivity.ts +0 -18
- package/packages/web/components/HeroSection/index.ts +0 -2
- package/packages/web/components/IdeasCard/IdeasCard.tsx +0 -115
- package/packages/web/components/IdeasCard/IdeasCard.types.ts +0 -10
- package/packages/web/components/IdeasCard/index.ts +0 -2
- package/packages/web/components/InsightMessage/InsightMessage.tsx +0 -9
- package/packages/web/components/InsightMessage/InsightMessage.types.ts +0 -3
- package/packages/web/components/InsightMessage/index.ts +0 -2
- package/packages/web/components/Logo/Logo.tsx +0 -65
- package/packages/web/components/Logo/index.ts +0 -1
- package/packages/web/components/MarkdownContent/MarkdownContent.tsx +0 -123
- package/packages/web/components/MarkdownContent/index.ts +0 -1
- package/packages/web/components/MasonryGrid/MasonryGrid.tsx +0 -18
- package/packages/web/components/MasonryGrid/index.ts +0 -1
- package/packages/web/components/MomentumWidget/MomentumWidget.tsx +0 -119
- package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +0 -16
- package/packages/web/components/MomentumWidget/index.ts +0 -2
- package/packages/web/components/NowCard/NowCard.tsx +0 -118
- package/packages/web/components/NowCard/NowCard.types.ts +0 -16
- package/packages/web/components/NowCard/index.ts +0 -2
- package/packages/web/components/PageHeader/PageHeader.tsx +0 -24
- package/packages/web/components/PageHeader/index.ts +0 -1
- package/packages/web/components/ProgressRing/ProgressRing.constants.ts +0 -20
- package/packages/web/components/ProgressRing/ProgressRing.tsx +0 -51
- package/packages/web/components/ProgressRing/ProgressRing.types.ts +0 -11
- package/packages/web/components/ProgressRing/index.ts +0 -2
- package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +0 -54
- package/packages/web/components/ProjectAvatar/index.ts +0 -1
- package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +0 -37
- package/packages/web/components/ProjectColorDot/index.ts +0 -1
- package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +0 -104
- package/packages/web/components/ProjectSelectorModal/index.ts +0 -1
- package/packages/web/components/Providers/Providers.tsx +0 -48
- package/packages/web/components/Providers/index.ts +0 -1
- package/packages/web/components/QueueCard/QueueCard.tsx +0 -125
- package/packages/web/components/QueueCard/QueueCard.types.ts +0 -12
- package/packages/web/components/QueueCard/QueueCard.utils.ts +0 -12
- package/packages/web/components/QueueCard/index.ts +0 -2
- package/packages/web/components/RecoverCard/RecoverCard.tsx +0 -72
- package/packages/web/components/RecoverCard/RecoverCard.types.ts +0 -16
- package/packages/web/components/RecoverCard/index.ts +0 -2
- package/packages/web/components/RoadmapCard/RoadmapCard.tsx +0 -145
- package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +0 -16
- package/packages/web/components/RoadmapCard/index.ts +0 -2
- package/packages/web/components/ShipsCard/ShipsCard.tsx +0 -95
- package/packages/web/components/ShipsCard/ShipsCard.types.ts +0 -14
- package/packages/web/components/ShipsCard/ShipsCard.utils.ts +0 -4
- package/packages/web/components/ShipsCard/index.ts +0 -2
- package/packages/web/components/SparklineChart/SparklineChart.tsx +0 -40
- package/packages/web/components/SparklineChart/SparklineChart.types.ts +0 -6
- package/packages/web/components/SparklineChart/index.ts +0 -2
- package/packages/web/components/StatsMasonry/StatsMasonry.tsx +0 -95
- package/packages/web/components/StatsMasonry/index.ts +0 -1
- package/packages/web/components/StreakCard/StreakCard.constants.ts +0 -2
- package/packages/web/components/StreakCard/StreakCard.tsx +0 -55
- package/packages/web/components/StreakCard/StreakCard.types.ts +0 -4
- package/packages/web/components/StreakCard/index.ts +0 -2
- package/packages/web/components/TasksCounter/TasksCounter.tsx +0 -14
- package/packages/web/components/TasksCounter/TasksCounter.types.ts +0 -3
- package/packages/web/components/TasksCounter/index.ts +0 -2
- package/packages/web/components/TechStackBadges/TechStackBadges.tsx +0 -28
- package/packages/web/components/TechStackBadges/index.ts +0 -1
- package/packages/web/components/TerminalDock/DockToggleTab.tsx +0 -29
- package/packages/web/components/TerminalDock/TerminalDock.tsx +0 -386
- package/packages/web/components/TerminalDock/TerminalDockTab.tsx +0 -130
- package/packages/web/components/TerminalDock/TerminalTabBar.tsx +0 -142
- package/packages/web/components/TerminalDock/index.ts +0 -2
- package/packages/web/components/TerminalTabs/TerminalTab.tsx +0 -95
- package/packages/web/components/TerminalTabs/TerminalTabs.tsx +0 -211
- package/packages/web/components/TerminalTabs/index.ts +0 -1
- package/packages/web/components/VelocityBadge/VelocityBadge.tsx +0 -32
- package/packages/web/components/VelocityBadge/VelocityBadge.types.ts +0 -3
- package/packages/web/components/VelocityBadge/index.ts +0 -2
- package/packages/web/components/VelocityCard/VelocityCard.tsx +0 -73
- package/packages/web/components/VelocityCard/VelocityCard.types.ts +0 -7
- package/packages/web/components/VelocityCard/index.ts +0 -2
- package/packages/web/components/WeeklyReports/PrintableReport.tsx +0 -259
- package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +0 -187
- package/packages/web/components/WeeklyReports/WeekCalendar.tsx +0 -288
- package/packages/web/components/WeeklyReports/WeeklyReports.tsx +0 -149
- package/packages/web/components/WeeklyReports/index.ts +0 -4
- package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +0 -25
- package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +0 -4
- package/packages/web/components/WeeklySparkline/index.ts +0 -2
- package/packages/web/components/charts/SessionsChart.tsx +0 -175
- package/packages/web/components/ui/alert-dialog.tsx +0 -157
- package/packages/web/components/ui/badge.tsx +0 -46
- package/packages/web/components/ui/button.tsx +0 -60
- package/packages/web/components/ui/card.tsx +0 -92
- package/packages/web/components/ui/chart.tsx +0 -385
- package/packages/web/components/ui/dialog.tsx +0 -143
- package/packages/web/components/ui/drawer.tsx +0 -135
- package/packages/web/components/ui/dropdown-menu.tsx +0 -257
- package/packages/web/components/ui/input.tsx +0 -21
- package/packages/web/components/ui/scroll-area.tsx +0 -58
- package/packages/web/components/ui/select.tsx +0 -187
- package/packages/web/components/ui/sheet.tsx +0 -139
- package/packages/web/components/ui/tabs.tsx +0 -66
- package/packages/web/components/ui/tooltip.tsx +0 -61
- package/packages/web/components.json +0 -22
- package/packages/web/context/GlobalTerminalContext.tsx +0 -538
- package/packages/web/context/TerminalContext.tsx +0 -45
- package/packages/web/context/TerminalTabsContext.tsx +0 -181
- package/packages/web/eslint.config.mjs +0 -18
- package/packages/web/hooks/useClaudeTerminal.ts +0 -425
- package/packages/web/hooks/useProjectStats.ts +0 -93
- package/packages/web/hooks/useProjects.ts +0 -73
- package/packages/web/lib/actions/projects.ts +0 -15
- package/packages/web/lib/commands.ts +0 -81
- package/packages/web/lib/format.ts +0 -23
- package/packages/web/lib/generate-week-report.ts +0 -285
- package/packages/web/lib/parse-prjct-files.ts +0 -1123
- package/packages/web/lib/project-colors.ts +0 -58
- package/packages/web/lib/projects.ts +0 -506
- package/packages/web/lib/pty.ts +0 -101
- package/packages/web/lib/query-config.ts +0 -44
- package/packages/web/lib/services/index.ts +0 -9
- package/packages/web/lib/services/projects.server.ts +0 -66
- package/packages/web/lib/services/stats.server.ts +0 -562
- package/packages/web/lib/unified-loader.ts +0 -396
- package/packages/web/lib/utils.ts +0 -6
- package/packages/web/next-env.d.ts +0 -6
- package/packages/web/next.config.ts +0 -7
- package/packages/web/package.json +0 -57
- package/packages/web/postcss.config.mjs +0 -7
- package/packages/web/public/file.svg +0 -1
- package/packages/web/public/globe.svg +0 -1
- package/packages/web/public/next.svg +0 -1
- package/packages/web/public/vercel.svg +0 -1
- package/packages/web/public/window.svg +0 -1
- package/packages/web/server.ts +0 -312
- package/packages/web/tsconfig.json +0 -34
- package/templates/commands/serve.md +0 -121
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { promises as fs } from 'fs'
|
|
3
|
-
import { join } from 'path'
|
|
4
|
-
import { homedir } from 'os'
|
|
5
|
-
|
|
6
|
-
export const dynamic = 'force-dynamic'
|
|
7
|
-
|
|
8
|
-
interface SessionEvent {
|
|
9
|
-
ts?: string
|
|
10
|
-
timestamp?: string
|
|
11
|
-
type?: string
|
|
12
|
-
action?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
type MomentumStatus = 'hot' | 'active' | 'cooling' | 'cold'
|
|
16
|
-
|
|
17
|
-
interface MomentumData {
|
|
18
|
-
dailyTasks: number[]
|
|
19
|
-
totalTasks: number
|
|
20
|
-
totalShips: number
|
|
21
|
-
lastActivityDate: string | null
|
|
22
|
-
daysSinceActivity: number
|
|
23
|
-
streak: number
|
|
24
|
-
status: MomentumStatus
|
|
25
|
-
message: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function getStatus(
|
|
29
|
-
daysSinceActivity: number,
|
|
30
|
-
totalTasks: number,
|
|
31
|
-
dailyTasks: number[],
|
|
32
|
-
streak: number
|
|
33
|
-
): { status: MomentumStatus; message: string } {
|
|
34
|
-
// Abandoned - 7+ days without activity
|
|
35
|
-
if (daysSinceActivity >= 7) {
|
|
36
|
-
return { status: 'cold', message: 'Miss you!' }
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// No activity ever
|
|
40
|
-
if (totalTasks === 0) {
|
|
41
|
-
return { status: 'active', message: 'Start building!' }
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Check if trending up (recent days > earlier days)
|
|
45
|
-
const recentDays = dailyTasks.slice(-3).reduce((a, b) => a + b, 0)
|
|
46
|
-
const earlierDays = dailyTasks.slice(0, 4).reduce((a, b) => a + b, 0)
|
|
47
|
-
const isTrendingUp = recentDays > earlierDays || streak >= 2
|
|
48
|
-
|
|
49
|
-
// Hot - trending up or on a streak
|
|
50
|
-
if (isTrendingUp && daysSinceActivity <= 1) {
|
|
51
|
-
return { status: 'hot', message: streak >= 2 ? `${streak} day streak!` : 'On fire!' }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Normal activity - neutral
|
|
55
|
-
if (daysSinceActivity <= 3) {
|
|
56
|
-
return { status: 'active', message: `${totalTasks} this week` }
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Cooling down but not abandoned
|
|
60
|
-
return { status: 'cooling', message: `${daysSinceActivity}d ago` }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export async function GET(
|
|
64
|
-
request: Request,
|
|
65
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
66
|
-
) {
|
|
67
|
-
try {
|
|
68
|
-
const { id: projectId } = await params
|
|
69
|
-
const globalStorage = join(homedir(), '.prjct-cli', 'projects')
|
|
70
|
-
const projectPath = join(globalStorage, projectId)
|
|
71
|
-
|
|
72
|
-
// Check if project exists
|
|
73
|
-
try {
|
|
74
|
-
await fs.access(projectPath)
|
|
75
|
-
} catch {
|
|
76
|
-
return NextResponse.json(
|
|
77
|
-
{ success: false, error: 'Project not found' },
|
|
78
|
-
{ status: 404 }
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Calculate date range (last 7 days)
|
|
83
|
-
const endDate = new Date()
|
|
84
|
-
const startDate = new Date()
|
|
85
|
-
startDate.setDate(startDate.getDate() - 6) // 7 days including today
|
|
86
|
-
startDate.setHours(0, 0, 0, 0)
|
|
87
|
-
|
|
88
|
-
const dailyMap = new Map<string, { tasks: number; ships: number }>()
|
|
89
|
-
let lastActivityDate: Date | null = null
|
|
90
|
-
|
|
91
|
-
// Read from memory/context.jsonl (legacy)
|
|
92
|
-
const contextPath = join(projectPath, 'memory', 'context.jsonl')
|
|
93
|
-
try {
|
|
94
|
-
const content = await fs.readFile(contextPath, 'utf-8')
|
|
95
|
-
const lines = content.trim().split('\n').filter(Boolean)
|
|
96
|
-
|
|
97
|
-
for (const line of lines) {
|
|
98
|
-
try {
|
|
99
|
-
const event: SessionEvent = JSON.parse(line)
|
|
100
|
-
const timestamp = event.ts || event.timestamp
|
|
101
|
-
if (!timestamp) continue
|
|
102
|
-
|
|
103
|
-
const eventDate = new Date(timestamp)
|
|
104
|
-
if (isNaN(eventDate.getTime())) continue
|
|
105
|
-
|
|
106
|
-
const eventType = event.type || event.action
|
|
107
|
-
|
|
108
|
-
// Track last activity for any relevant event
|
|
109
|
-
if (eventType === 'task_complete' || eventType === 'task_completed' ||
|
|
110
|
-
eventType === 'feature_ship' || eventType === 'feature_shipped') {
|
|
111
|
-
if (!lastActivityDate || eventDate > lastActivityDate) {
|
|
112
|
-
lastActivityDate = eventDate
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Only count last 7 days for sparkline
|
|
117
|
-
if (eventDate < startDate || eventDate > endDate) continue
|
|
118
|
-
|
|
119
|
-
const dateKey = eventDate.toISOString().split('T')[0]
|
|
120
|
-
const current = dailyMap.get(dateKey) || { tasks: 0, ships: 0 }
|
|
121
|
-
|
|
122
|
-
if (eventType === 'task_complete' || eventType === 'task_completed') {
|
|
123
|
-
current.tasks++
|
|
124
|
-
}
|
|
125
|
-
if (eventType === 'feature_ship' || eventType === 'feature_shipped') {
|
|
126
|
-
current.ships++
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
dailyMap.set(dateKey, current)
|
|
130
|
-
} catch {
|
|
131
|
-
// Skip malformed lines
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
} catch {
|
|
135
|
-
// No context.jsonl
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// Read from progress/sessions/{YYYY-MM}/{date}.jsonl (new format)
|
|
139
|
-
const sessionsDir = join(projectPath, 'progress', 'sessions')
|
|
140
|
-
try {
|
|
141
|
-
const monthDirs = await fs.readdir(sessionsDir)
|
|
142
|
-
for (const monthDir of monthDirs) {
|
|
143
|
-
if (!monthDir.match(/^\d{4}-\d{2}$/)) continue
|
|
144
|
-
|
|
145
|
-
const monthPath = join(sessionsDir, monthDir)
|
|
146
|
-
try {
|
|
147
|
-
const dayFiles = await fs.readdir(monthPath)
|
|
148
|
-
for (const dayFile of dayFiles) {
|
|
149
|
-
if (!dayFile.endsWith('.jsonl')) continue
|
|
150
|
-
|
|
151
|
-
const dayPath = join(monthPath, dayFile)
|
|
152
|
-
try {
|
|
153
|
-
const content = await fs.readFile(dayPath, 'utf-8')
|
|
154
|
-
const lines = content.trim().split('\n').filter(Boolean)
|
|
155
|
-
|
|
156
|
-
for (const line of lines) {
|
|
157
|
-
try {
|
|
158
|
-
const event: SessionEvent = JSON.parse(line)
|
|
159
|
-
const timestamp = event.ts || event.timestamp
|
|
160
|
-
if (!timestamp) continue
|
|
161
|
-
|
|
162
|
-
const eventDate = new Date(timestamp)
|
|
163
|
-
if (isNaN(eventDate.getTime())) continue
|
|
164
|
-
|
|
165
|
-
const eventType = event.type || event.action
|
|
166
|
-
|
|
167
|
-
// Track last activity
|
|
168
|
-
if (eventType === 'task_complete' || eventType === 'task_completed' ||
|
|
169
|
-
eventType === 'feature_ship' || eventType === 'feature_shipped') {
|
|
170
|
-
if (!lastActivityDate || eventDate > lastActivityDate) {
|
|
171
|
-
lastActivityDate = eventDate
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Only count last 7 days for sparkline
|
|
176
|
-
if (eventDate < startDate || eventDate > endDate) continue
|
|
177
|
-
|
|
178
|
-
const dateKey = eventDate.toISOString().split('T')[0]
|
|
179
|
-
const current = dailyMap.get(dateKey) || { tasks: 0, ships: 0 }
|
|
180
|
-
|
|
181
|
-
if (eventType === 'task_complete' || eventType === 'task_completed') {
|
|
182
|
-
current.tasks++
|
|
183
|
-
}
|
|
184
|
-
if (eventType === 'feature_ship' || eventType === 'feature_shipped') {
|
|
185
|
-
current.ships++
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
dailyMap.set(dateKey, current)
|
|
189
|
-
} catch {
|
|
190
|
-
// Skip malformed lines
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} catch {
|
|
194
|
-
// Skip unreadable files
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
} catch {
|
|
198
|
-
// Skip unreadable month directories
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
} catch {
|
|
202
|
-
// No sessions directory
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Generate daily tasks array for sparkline (7 days)
|
|
206
|
-
const dailyTasks: number[] = []
|
|
207
|
-
let totalTasks = 0
|
|
208
|
-
let totalShips = 0
|
|
209
|
-
let streak = 0
|
|
210
|
-
let streakBroken = false
|
|
211
|
-
|
|
212
|
-
const currentDate = new Date(startDate)
|
|
213
|
-
while (currentDate <= endDate) {
|
|
214
|
-
const dateKey = currentDate.toISOString().split('T')[0]
|
|
215
|
-
const stats = dailyMap.get(dateKey) || { tasks: 0, ships: 0 }
|
|
216
|
-
dailyTasks.push(stats.tasks + stats.ships)
|
|
217
|
-
totalTasks += stats.tasks
|
|
218
|
-
totalShips += stats.ships
|
|
219
|
-
currentDate.setDate(currentDate.getDate() + 1)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Calculate streak (consecutive days with activity from today backwards)
|
|
223
|
-
for (let i = dailyTasks.length - 1; i >= 0; i--) {
|
|
224
|
-
if (dailyTasks[i] > 0 && !streakBroken) {
|
|
225
|
-
streak++
|
|
226
|
-
} else if (dailyTasks[i] === 0 && i < dailyTasks.length - 1) {
|
|
227
|
-
streakBroken = true
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Calculate days since last activity
|
|
232
|
-
const daysSinceActivity = lastActivityDate
|
|
233
|
-
? Math.floor((endDate.getTime() - lastActivityDate.getTime()) / (1000 * 60 * 60 * 24))
|
|
234
|
-
: 999
|
|
235
|
-
|
|
236
|
-
const { status, message } = getStatus(daysSinceActivity, totalTasks, dailyTasks, streak)
|
|
237
|
-
|
|
238
|
-
const data: MomentumData = {
|
|
239
|
-
dailyTasks,
|
|
240
|
-
totalTasks,
|
|
241
|
-
totalShips,
|
|
242
|
-
lastActivityDate: lastActivityDate?.toISOString() || null,
|
|
243
|
-
daysSinceActivity,
|
|
244
|
-
streak,
|
|
245
|
-
status,
|
|
246
|
-
message
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return NextResponse.json({ success: true, data })
|
|
250
|
-
} catch (error) {
|
|
251
|
-
console.error('Momentum API error:', error)
|
|
252
|
-
return NextResponse.json(
|
|
253
|
-
{ success: false, error: 'Failed to fetch momentum data' },
|
|
254
|
-
{ status: 500 }
|
|
255
|
-
)
|
|
256
|
-
}
|
|
257
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { getProject } from '@/lib/projects'
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic'
|
|
5
|
-
|
|
6
|
-
export async function GET(
|
|
7
|
-
request: Request,
|
|
8
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
-
) {
|
|
10
|
-
const { id } = await params
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const project = await getProject(id)
|
|
14
|
-
|
|
15
|
-
if (!project) {
|
|
16
|
-
return NextResponse.json(
|
|
17
|
-
{ success: false, error: 'Project not found' },
|
|
18
|
-
{ status: 404 }
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return NextResponse.json({ success: true, data: project })
|
|
23
|
-
} catch {
|
|
24
|
-
return NextResponse.json(
|
|
25
|
-
{ success: false, error: 'Failed to get project' },
|
|
26
|
-
{ status: 500 }
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
2
|
-
import { getProjectStats, getRawProjectFiles } from '@/lib/parse-prjct-files'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* GET /api/projects/[id]/stats
|
|
6
|
-
*
|
|
7
|
-
* MD-First Architecture: Returns stats parsed from MD files.
|
|
8
|
-
*/
|
|
9
|
-
export async function GET(
|
|
10
|
-
request: NextRequest,
|
|
11
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
12
|
-
) {
|
|
13
|
-
const { id: projectId } = await params
|
|
14
|
-
|
|
15
|
-
if (!projectId) {
|
|
16
|
-
return NextResponse.json(
|
|
17
|
-
{ success: false, error: 'Project ID required' },
|
|
18
|
-
{ status: 400 }
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
const [stats, raw] = await Promise.all([
|
|
24
|
-
getProjectStats(projectId),
|
|
25
|
-
getRawProjectFiles(projectId)
|
|
26
|
-
])
|
|
27
|
-
|
|
28
|
-
return NextResponse.json({
|
|
29
|
-
success: true,
|
|
30
|
-
version: 'md-first',
|
|
31
|
-
data: stats,
|
|
32
|
-
raw
|
|
33
|
-
})
|
|
34
|
-
} catch (error) {
|
|
35
|
-
console.error('[API] Error getting project stats:', error)
|
|
36
|
-
return NextResponse.json(
|
|
37
|
-
{ success: false, error: 'Failed to get project stats' },
|
|
38
|
-
{ status: 500 }
|
|
39
|
-
)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { getProjectStatus } from '@/lib/projects'
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic'
|
|
5
|
-
|
|
6
|
-
export async function GET(
|
|
7
|
-
request: Request,
|
|
8
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
-
) {
|
|
10
|
-
const { id } = await params
|
|
11
|
-
|
|
12
|
-
try {
|
|
13
|
-
const status = await getProjectStatus(id)
|
|
14
|
-
return NextResponse.json({ success: true, data: status })
|
|
15
|
-
} catch {
|
|
16
|
-
return NextResponse.json(
|
|
17
|
-
{ success: false, error: 'Failed to get status' },
|
|
18
|
-
{ status: 500 }
|
|
19
|
-
)
|
|
20
|
-
}
|
|
21
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { getProjects } from '@/lib/projects'
|
|
3
|
-
|
|
4
|
-
export const dynamic = 'force-dynamic'
|
|
5
|
-
|
|
6
|
-
export async function GET() {
|
|
7
|
-
try {
|
|
8
|
-
const projects = await getProjects()
|
|
9
|
-
return NextResponse.json({ success: true, data: projects })
|
|
10
|
-
} catch {
|
|
11
|
-
return NextResponse.json(
|
|
12
|
-
{ success: false, error: 'Failed to list projects' },
|
|
13
|
-
{ status: 500 }
|
|
14
|
-
)
|
|
15
|
-
}
|
|
16
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { promises as fs } from 'fs'
|
|
3
|
-
import { join } from 'path'
|
|
4
|
-
import { homedir } from 'os'
|
|
5
|
-
|
|
6
|
-
export const dynamic = 'force-dynamic'
|
|
7
|
-
|
|
8
|
-
interface Session {
|
|
9
|
-
id: string
|
|
10
|
-
projectId?: string
|
|
11
|
-
task: string
|
|
12
|
-
status: string
|
|
13
|
-
startedAt: string
|
|
14
|
-
timeline?: Array<{ type: string; at: string }>
|
|
15
|
-
context?: {
|
|
16
|
-
prompt?: string
|
|
17
|
-
promptLength?: number
|
|
18
|
-
files?: string[]
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface AbandonedSession {
|
|
23
|
-
id: string
|
|
24
|
-
task: string
|
|
25
|
-
projectId: string
|
|
26
|
-
projectName?: string
|
|
27
|
-
startedAt: string
|
|
28
|
-
lastActivity?: string
|
|
29
|
-
hoursAgo: number
|
|
30
|
-
prompt?: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export async function GET(request: Request) {
|
|
34
|
-
try {
|
|
35
|
-
const { searchParams } = new URL(request.url)
|
|
36
|
-
const targetProjectId = searchParams.get('projectId')
|
|
37
|
-
|
|
38
|
-
const globalStorage = join(homedir(), '.prjct-cli', 'projects')
|
|
39
|
-
|
|
40
|
-
let projects: string[]
|
|
41
|
-
try {
|
|
42
|
-
projects = await fs.readdir(globalStorage)
|
|
43
|
-
} catch {
|
|
44
|
-
return NextResponse.json({
|
|
45
|
-
success: true,
|
|
46
|
-
data: {
|
|
47
|
-
currentSession: null,
|
|
48
|
-
abandonedSessions: []
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const abandonedSessions: AbandonedSession[] = []
|
|
54
|
-
let currentSession: Session | null = null
|
|
55
|
-
|
|
56
|
-
const now = Date.now()
|
|
57
|
-
const ABANDON_THRESHOLD_HOURS = 8
|
|
58
|
-
|
|
59
|
-
for (const projectId of projects) {
|
|
60
|
-
// Skip hidden directories
|
|
61
|
-
if (projectId.startsWith('.')) continue
|
|
62
|
-
|
|
63
|
-
const sessionPath = join(globalStorage, projectId, 'sessions', 'current.json')
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const content = await fs.readFile(sessionPath, 'utf-8')
|
|
67
|
-
if (!content.trim()) continue
|
|
68
|
-
|
|
69
|
-
const session: Session = JSON.parse(content)
|
|
70
|
-
|
|
71
|
-
// Skip if not active
|
|
72
|
-
if (session.status !== 'active') continue
|
|
73
|
-
|
|
74
|
-
// Get last activity timestamp
|
|
75
|
-
const lastActivity = session.timeline?.[session.timeline.length - 1]?.at || session.startedAt
|
|
76
|
-
const lastActivityDate = new Date(lastActivity)
|
|
77
|
-
if (isNaN(lastActivityDate.getTime())) continue
|
|
78
|
-
|
|
79
|
-
const hoursAgo = (now - lastActivityDate.getTime()) / (1000 * 60 * 60)
|
|
80
|
-
|
|
81
|
-
// If this is the target project and session is recent, it's the current session
|
|
82
|
-
if (projectId === targetProjectId && hoursAgo < ABANDON_THRESHOLD_HOURS) {
|
|
83
|
-
currentSession = session
|
|
84
|
-
}
|
|
85
|
-
// If session is old (>8 hours), it's considered abandoned
|
|
86
|
-
// Only show abandoned sessions for the target project
|
|
87
|
-
else if (hoursAgo >= ABANDON_THRESHOLD_HOURS && projectId === targetProjectId) {
|
|
88
|
-
// Try to get project name from project.json
|
|
89
|
-
let projectName: string | undefined
|
|
90
|
-
try {
|
|
91
|
-
const projectJsonPath = join(globalStorage, projectId, 'project.json')
|
|
92
|
-
const projectJson = await fs.readFile(projectJsonPath, 'utf-8')
|
|
93
|
-
const projectData = JSON.parse(projectJson)
|
|
94
|
-
projectName = projectData.name
|
|
95
|
-
} catch {
|
|
96
|
-
// Ignore - project name is optional
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
abandonedSessions.push({
|
|
100
|
-
id: session.id,
|
|
101
|
-
task: session.task,
|
|
102
|
-
projectId,
|
|
103
|
-
projectName,
|
|
104
|
-
startedAt: session.startedAt,
|
|
105
|
-
lastActivity,
|
|
106
|
-
hoursAgo: Math.round(hoursAgo),
|
|
107
|
-
prompt: session.context?.prompt
|
|
108
|
-
})
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
// Skip projects without valid session files
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Sort abandoned sessions by most recent first
|
|
116
|
-
abandonedSessions.sort((a, b) => a.hoursAgo - b.hoursAgo)
|
|
117
|
-
|
|
118
|
-
return NextResponse.json({
|
|
119
|
-
success: true,
|
|
120
|
-
data: {
|
|
121
|
-
currentSession,
|
|
122
|
-
abandonedSessions
|
|
123
|
-
}
|
|
124
|
-
})
|
|
125
|
-
} catch (error) {
|
|
126
|
-
console.error('Sessions current error:', error)
|
|
127
|
-
return NextResponse.json(
|
|
128
|
-
{ success: false, error: 'Failed to fetch sessions' },
|
|
129
|
-
{ status: 500 }
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from 'next/server'
|
|
2
|
-
import { promises as fs } from 'fs'
|
|
3
|
-
import { join } from 'path'
|
|
4
|
-
import { homedir } from 'os'
|
|
5
|
-
|
|
6
|
-
export const dynamic = 'force-dynamic'
|
|
7
|
-
|
|
8
|
-
interface SessionEvent {
|
|
9
|
-
ts?: string
|
|
10
|
-
timestamp?: string
|
|
11
|
-
type?: string
|
|
12
|
-
action?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
interface DailyStats {
|
|
16
|
-
date: string
|
|
17
|
-
tasks: number
|
|
18
|
-
ships: number
|
|
19
|
-
partial: number
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function GET() {
|
|
23
|
-
try {
|
|
24
|
-
const globalStorage = join(homedir(), '.prjct-cli', 'projects')
|
|
25
|
-
|
|
26
|
-
let projects: string[]
|
|
27
|
-
try {
|
|
28
|
-
projects = await fs.readdir(globalStorage)
|
|
29
|
-
} catch {
|
|
30
|
-
return NextResponse.json({
|
|
31
|
-
success: true,
|
|
32
|
-
data: {
|
|
33
|
-
chartData: [],
|
|
34
|
-
totals: { tasks: 0, ships: 0 },
|
|
35
|
-
dateRange: { start: '', end: '' }
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Aggregate by date
|
|
41
|
-
const dailyMap = new Map<string, { tasks: number; ships: number; partial: number }>()
|
|
42
|
-
|
|
43
|
-
// Calculate date range (last 30 days)
|
|
44
|
-
const endDate = new Date()
|
|
45
|
-
const startDate = new Date()
|
|
46
|
-
startDate.setDate(startDate.getDate() - 30)
|
|
47
|
-
|
|
48
|
-
for (const projectId of projects) {
|
|
49
|
-
// Skip hidden directories
|
|
50
|
-
if (projectId.startsWith('.')) continue
|
|
51
|
-
|
|
52
|
-
// Read from memory/context.jsonl (legacy)
|
|
53
|
-
const contextPath = join(globalStorage, projectId, 'memory', 'context.jsonl')
|
|
54
|
-
try {
|
|
55
|
-
const content = await fs.readFile(contextPath, 'utf-8')
|
|
56
|
-
const lines = content.trim().split('\n').filter(Boolean)
|
|
57
|
-
|
|
58
|
-
for (const line of lines) {
|
|
59
|
-
try {
|
|
60
|
-
const event: SessionEvent = JSON.parse(line)
|
|
61
|
-
const timestamp = event.ts || event.timestamp
|
|
62
|
-
if (!timestamp) continue
|
|
63
|
-
|
|
64
|
-
const eventDate = new Date(timestamp)
|
|
65
|
-
if (isNaN(eventDate.getTime())) continue
|
|
66
|
-
if (eventDate < startDate || eventDate > endDate) continue
|
|
67
|
-
|
|
68
|
-
const dateKey = eventDate.toISOString().split('T')[0]
|
|
69
|
-
const current = dailyMap.get(dateKey) || { tasks: 0, ships: 0, partial: 0 }
|
|
70
|
-
|
|
71
|
-
const eventType = event.type || event.action
|
|
72
|
-
|
|
73
|
-
// Count tasks completed
|
|
74
|
-
if (eventType === 'task_complete' || eventType === 'task_completed' || eventType === 'session_completed') {
|
|
75
|
-
current.tasks++
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Count features shipped
|
|
79
|
-
if (eventType === 'feature_ship' || eventType === 'feature_shipped') {
|
|
80
|
-
current.ships++
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Count partial/abandoned sessions
|
|
84
|
-
if (eventType === 'session_partial' || eventType === 'session_abandoned') {
|
|
85
|
-
current.partial++
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
dailyMap.set(dateKey, current)
|
|
89
|
-
} catch {
|
|
90
|
-
// Skip malformed lines
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
} catch {
|
|
94
|
-
// Skip projects without context.jsonl
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Read from progress/sessions/{YYYY-MM}/{date}.jsonl (new format)
|
|
98
|
-
const sessionsDir = join(globalStorage, projectId, 'progress', 'sessions')
|
|
99
|
-
try {
|
|
100
|
-
const monthDirs = await fs.readdir(sessionsDir)
|
|
101
|
-
for (const monthDir of monthDirs) {
|
|
102
|
-
// Skip non-directory entries
|
|
103
|
-
if (!monthDir.match(/^\d{4}-\d{2}$/)) continue
|
|
104
|
-
|
|
105
|
-
const monthPath = join(sessionsDir, monthDir)
|
|
106
|
-
try {
|
|
107
|
-
const dayFiles = await fs.readdir(monthPath)
|
|
108
|
-
for (const dayFile of dayFiles) {
|
|
109
|
-
if (!dayFile.endsWith('.jsonl')) continue
|
|
110
|
-
|
|
111
|
-
const dayPath = join(monthPath, dayFile)
|
|
112
|
-
try {
|
|
113
|
-
const content = await fs.readFile(dayPath, 'utf-8')
|
|
114
|
-
const lines = content.trim().split('\n').filter(Boolean)
|
|
115
|
-
|
|
116
|
-
for (const line of lines) {
|
|
117
|
-
try {
|
|
118
|
-
const event: SessionEvent = JSON.parse(line)
|
|
119
|
-
const timestamp = event.ts || event.timestamp
|
|
120
|
-
if (!timestamp) continue
|
|
121
|
-
|
|
122
|
-
const eventDate = new Date(timestamp)
|
|
123
|
-
if (isNaN(eventDate.getTime())) continue
|
|
124
|
-
if (eventDate < startDate || eventDate > endDate) continue
|
|
125
|
-
|
|
126
|
-
const dateKey = eventDate.toISOString().split('T')[0]
|
|
127
|
-
const current = dailyMap.get(dateKey) || { tasks: 0, ships: 0, partial: 0 }
|
|
128
|
-
|
|
129
|
-
const eventType = event.type || event.action
|
|
130
|
-
|
|
131
|
-
// Count tasks completed
|
|
132
|
-
if (eventType === 'task_complete' || eventType === 'task_completed') {
|
|
133
|
-
current.tasks++
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Count features shipped
|
|
137
|
-
if (eventType === 'feature_ship' || eventType === 'feature_shipped') {
|
|
138
|
-
current.ships++
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Count partial/abandoned sessions
|
|
142
|
-
if (eventType === 'session_partial' || eventType === 'session_abandoned') {
|
|
143
|
-
current.partial++
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
dailyMap.set(dateKey, current)
|
|
147
|
-
} catch {
|
|
148
|
-
// Skip malformed lines
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
} catch {
|
|
152
|
-
// Skip unreadable files
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
} catch {
|
|
156
|
-
// Skip unreadable month directories
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
} catch {
|
|
160
|
-
// Skip projects without sessions directory
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Generate all dates in range (90 days), filling gaps with zeros
|
|
165
|
-
const data: DailyStats[] = []
|
|
166
|
-
const currentDate = new Date(startDate)
|
|
167
|
-
while (currentDate <= endDate) {
|
|
168
|
-
const dateKey = currentDate.toISOString().split('T')[0]
|
|
169
|
-
const stats = dailyMap.get(dateKey) || { tasks: 0, ships: 0, partial: 0 }
|
|
170
|
-
data.push({
|
|
171
|
-
date: dateKey,
|
|
172
|
-
tasks: stats.tasks,
|
|
173
|
-
ships: stats.ships,
|
|
174
|
-
partial: stats.partial
|
|
175
|
-
})
|
|
176
|
-
currentDate.setDate(currentDate.getDate() + 1)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Calculate totals for summary
|
|
180
|
-
const totals = {
|
|
181
|
-
tasks: data.reduce((sum, d) => sum + d.tasks, 0),
|
|
182
|
-
ships: data.reduce((sum, d) => sum + d.ships, 0),
|
|
183
|
-
partial: data.reduce((sum, d) => sum + d.partial, 0)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return NextResponse.json({
|
|
187
|
-
success: true,
|
|
188
|
-
data: {
|
|
189
|
-
chartData: data,
|
|
190
|
-
totals,
|
|
191
|
-
dateRange: {
|
|
192
|
-
start: startDate.toISOString().split('T')[0],
|
|
193
|
-
end: endDate.toISOString().split('T')[0]
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
})
|
|
197
|
-
} catch (error) {
|
|
198
|
-
console.error('Sessions history error:', error)
|
|
199
|
-
return NextResponse.json(
|
|
200
|
-
{ success: false, error: 'Failed to fetch session history' },
|
|
201
|
-
{ status: 500 }
|
|
202
|
-
)
|
|
203
|
-
}
|
|
204
|
-
}
|