import { NextRequest, NextResponse } from 'next/server'; import { adminDb } from '@/lib/firebase/admin'; import { getApiUrl } from '@/lib/utils/api-url'; // Types interface WorkSession { sessionId: string; date: string; startTime: Date; endTime: Date; duration: number; // minutes messageCount: number; userMessages: number; aiMessages: number; topics: string[]; filesWorkedOn: string[]; } interface TimelineAnalysis { firstActivity: Date | null; lastActivity: Date | null; totalDays: number; activeDays: number; totalSessions: number; sessions: WorkSession[]; velocity: { messagesPerDay: number; averageSessionLength: number; peakProductivityHours: number[]; }; } interface CostAnalysis { messageStats: { totalMessages: number; userMessages: number; aiMessages: number; avgMessageLength: number; }; estimatedTokens: { input: number; output: number; total: number; }; costs: { inputCost: number; outputCost: number; totalCost: number; currency: string; }; model: string; pricing: { inputPer1M: number; outputPer1M: number; }; } interface Feature { name: string; description: string; pages: string[]; apis: string[]; status: 'complete' | 'in-progress' | 'planned'; } export async function POST( request: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; // 1. Load conversations from Firestore console.log(`🔍 Loading conversations for project ${projectId}...`); const conversationsSnapshot = await adminDb .collection('projects') .doc(projectId) .collection('cursorConversations') .get(); if (conversationsSnapshot.empty) { return NextResponse.json({ error: 'No conversations found for this project', suggestion: 'Import Cursor conversations first' }, { status: 404 }); } const conversations = conversationsSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); console.log(`✅ Found ${conversations.length} conversations`); // 2. Load all messages for each conversation let allMessages: any[] = []; for (const conv of conversations) { const messagesSnapshot = await adminDb .collection('projects') .doc(projectId) .collection('cursorConversations') .doc(conv.id) .collection('messages') .orderBy('createdAt', 'asc') .get(); const messages = messagesSnapshot.docs.map(doc => ({ ...doc.data(), conversationId: conv.id, conversationName: conv.name })); allMessages = allMessages.concat(messages); } console.log(`✅ Loaded ${allMessages.length} total messages`); // 3. Load extension activity data (files edited, sessions) let extensionActivity: any = null; try { const activitySnapshot = await adminDb .collection('sessions') .where('projectId', '==', projectId) .get(); const extensionSessions = activitySnapshot.docs .map(doc => { const data = doc.data(); return { startTime: data.startTime?.toDate?.() || data.startTime, endTime: data.endTime?.toDate?.() || data.endTime, filesModified: data.filesModified || [], conversationSummary: data.conversationSummary || '' }; }) .sort((a, b) => { const aTime = a.startTime ? new Date(a.startTime).getTime() : 0; const bTime = b.startTime ? new Date(b.startTime).getTime() : 0; return aTime - bTime; }); // Analyze file activity const fileActivity: Record = {}; extensionSessions.forEach(session => { session.filesModified.forEach((file: string) => { fileActivity[file] = (fileActivity[file] || 0) + 1; }); }); const topFiles = Object.entries(fileActivity) .map(([file, count]) => ({ file, editCount: count })) .sort((a, b) => b.editCount - a.editCount) .slice(0, 20); extensionActivity = { totalSessions: extensionSessions.length, uniqueFilesEdited: Object.keys(fileActivity).length, topFiles, earliestActivity: extensionSessions[0]?.startTime || null, latestActivity: extensionSessions[extensionSessions.length - 1]?.endTime || null }; console.log(`✅ Loaded ${extensionSessions.length} extension activity sessions`); } catch (error) { console.log(`⚠️ Could not load extension activity: ${error}`); } // 4. Load Git commit history let gitHistory: any = null; try { const gitResponse = await fetch(getApiUrl(`/api/projects/${projectId}/git-history`, request)); if (gitResponse.ok) { gitHistory = await gitResponse.json(); console.log(`✅ Loaded ${gitHistory.totalCommits} Git commits`); } } catch (error) { console.log(`⚠️ Could not load Git history: ${error}`); } // 4b. Load unified timeline (combines all data sources by day) let unifiedTimeline: any = null; try { const timelineResponse = await fetch(getApiUrl(`/api/projects/${projectId}/timeline`, request)); if (timelineResponse.ok) { unifiedTimeline = await timelineResponse.json(); console.log(`✅ Loaded unified timeline with ${unifiedTimeline.days.length} days`); } } catch (error) { console.log(`⚠️ Could not load unified timeline: ${error}`); } // 5. Analyze timeline const timeline = analyzeTimeline(allMessages); // 6. Calculate costs const costs = calculateCosts(allMessages); // 7. Extract features from codebase (static list for now) const features = getFeaturesList(); // 8. Get tech stack const techStack = getTechStack(); // 9. Generate report const report = { projectId, generatedAt: new Date().toISOString(), timeline, costs, features, techStack, extensionActivity, gitHistory, unifiedTimeline, summary: { totalConversations: conversations.length, totalMessages: allMessages.length, developmentPeriod: timeline.totalDays, estimatedCost: costs.costs.totalCost, extensionSessions: extensionActivity?.totalSessions || 0, filesEdited: extensionActivity?.uniqueFilesEdited || 0, gitCommits: gitHistory?.totalCommits || 0, linesAdded: gitHistory?.totalInsertions || 0, linesRemoved: gitHistory?.totalDeletions || 0, timelineDays: unifiedTimeline?.days.length || 0 } }; console.log(`✅ Audit report generated successfully`); return NextResponse.json(report); } catch (error) { console.error('Error generating audit report:', error); return NextResponse.json( { error: 'Failed to generate audit report', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } // Helper: Analyze timeline from messages function analyzeTimeline(messages: any[]): TimelineAnalysis { if (messages.length === 0) { return { firstActivity: null, lastActivity: null, totalDays: 0, activeDays: 0, totalSessions: 0, sessions: [], velocity: { messagesPerDay: 0, averageSessionLength: 0, peakProductivityHours: [] } }; } // Sort messages by time const sorted = [...messages].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() ); const firstActivity = new Date(sorted[0].createdAt); const lastActivity = new Date(sorted[sorted.length - 1].createdAt); const totalDays = Math.ceil((lastActivity.getTime() - firstActivity.getTime()) / (1000 * 60 * 60 * 24)); // Group into sessions (gap > 4 hours = new session) const SESSION_GAP = 4 * 60 * 60 * 1000; // 4 hours const sessions: WorkSession[] = []; let currentSession: any = null; for (const msg of sorted) { const msgTime = new Date(msg.createdAt).getTime(); if (!currentSession || msgTime - currentSession.endTime > SESSION_GAP) { // Start new session if (currentSession) { sessions.push(formatSession(currentSession)); } currentSession = { messages: [msg], startTime: msgTime, endTime: msgTime, date: new Date(msgTime).toISOString().split('T')[0] }; } else { // Add to current session currentSession.messages.push(msg); currentSession.endTime = msgTime; } } // Don't forget the last session if (currentSession) { sessions.push(formatSession(currentSession)); } // Calculate velocity metrics const activeDays = new Set(sorted.map(m => new Date(m.createdAt).toISOString().split('T')[0] )).size; const totalSessionMinutes = sessions.reduce((sum, s) => sum + s.duration, 0); const averageSessionLength = sessions.length > 0 ? totalSessionMinutes / sessions.length : 0; // Find peak productivity hours const hourCounts = new Map(); sorted.forEach(msg => { const hour = new Date(msg.createdAt).getHours(); hourCounts.set(hour, (hourCounts.get(hour) || 0) + 1); }); const peakProductivityHours = Array.from(hourCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([hour]) => hour) .sort((a, b) => a - b); return { firstActivity, lastActivity, totalDays, activeDays, totalSessions: sessions.length, sessions, velocity: { messagesPerDay: messages.length / activeDays, averageSessionLength: Math.round(averageSessionLength), peakProductivityHours } }; } function formatSession(sessionData: any): WorkSession { const duration = Math.ceil((sessionData.endTime - sessionData.startTime) / (1000 * 60)); const userMessages = sessionData.messages.filter((m: any) => m.type === 1).length; const aiMessages = sessionData.messages.filter((m: any) => m.type === 2).length; // Extract topics (first 3 unique conversation names) const topics = [...new Set(sessionData.messages.map((m: any) => m.conversationName))].slice(0, 3); // Extract files const files = [...new Set( sessionData.messages.flatMap((m: any) => m.attachedFiles || []) )]; return { sessionId: `session-${sessionData.date}-${sessionData.startTime}`, date: sessionData.date, startTime: new Date(sessionData.startTime), endTime: new Date(sessionData.endTime), duration, messageCount: sessionData.messages.length, userMessages, aiMessages, topics, filesWorkedOn: files }; } // Helper: Calculate costs function calculateCosts(messages: any[]): CostAnalysis { const userMessages = messages.filter(m => m.type === 1); const aiMessages = messages.filter(m => m.type === 2); // Calculate average message length const totalChars = messages.reduce((sum, m) => sum + (m.text?.length || 0), 0); const avgMessageLength = messages.length > 0 ? Math.round(totalChars / messages.length) : 0; // Estimate tokens (rough: 1 token ≈ 4 characters) const inputChars = userMessages.reduce((sum, m) => sum + (m.text?.length || 0), 0); const outputChars = aiMessages.reduce((sum, m) => sum + (m.text?.length || 0), 0); const inputTokens = Math.ceil(inputChars / 4); const outputTokens = Math.ceil(outputChars / 4); const totalTokens = inputTokens + outputTokens; // Claude Sonnet 3.5 pricing (Nov 2024) const INPUT_COST_PER_1M = 3.0; const OUTPUT_COST_PER_1M = 15.0; const inputCost = (inputTokens / 1_000_000) * INPUT_COST_PER_1M; const outputCost = (outputTokens / 1_000_000) * OUTPUT_COST_PER_1M; const totalAICost = inputCost + outputCost; return { messageStats: { totalMessages: messages.length, userMessages: userMessages.length, aiMessages: aiMessages.length, avgMessageLength }, estimatedTokens: { input: inputTokens, output: outputTokens, total: totalTokens }, costs: { inputCost: Math.round(inputCost * 100) / 100, outputCost: Math.round(outputCost * 100) / 100, totalCost: Math.round(totalAICost * 100) / 100, currency: 'USD' }, model: 'Claude Sonnet 3.5', pricing: { inputPer1M: INPUT_COST_PER_1M, outputPer1M: OUTPUT_COST_PER_1M } }; } // Helper: Get features list function getFeaturesList(): Feature[] { return [ { name: "Project Management", description: "Create, manage, and organize AI-coded projects", pages: ["/projects", "/project/[id]/overview", "/project/[id]/settings"], apis: ["/api/projects/create", "/api/projects/[id]", "/api/projects/delete"], status: "complete" }, { name: "AI Chat Integration", description: "Real-time chat with AI assistants for development", pages: ["/project/[id]/v_ai_chat"], apis: ["/api/ai/chat", "/api/ai/conversation"], status: "complete" }, { name: "Cursor Import", description: "Import historical conversations from Cursor IDE", pages: [], apis: ["/api/cursor/backfill", "/api/cursor/tag-sessions"], status: "complete" }, { name: "GitHub Integration", description: "Connect GitHub repositories and browse code", pages: ["/connections"], apis: ["/api/github/connect", "/api/github/repos", "/api/github/repo-tree"], status: "complete" }, { name: "Session Tracking", description: "Track development sessions and activity", pages: ["/project/[id]/sessions"], apis: ["/api/sessions/track", "/api/sessions/associate-project"], status: "complete" }, { name: "Knowledge Base", description: "Document and organize project knowledge", pages: ["/project/[id]/context"], apis: ["/api/projects/[id]/knowledge/*"], status: "complete" }, { name: "Planning & Automation", description: "Generate development plans and automate workflows", pages: ["/project/[id]/plan", "/project/[id]/automation"], apis: ["/api/projects/[id]/plan/mvp", "/api/projects/[id]/plan/marketing"], status: "in-progress" }, { name: "Analytics & Costs", description: "Track development costs and project analytics", pages: ["/project/[id]/analytics", "/costs"], apis: ["/api/stats", "/api/projects/[id]/aggregate"], status: "in-progress" } ]; } // Helper: Get tech stack function getTechStack() { return { frontend: { framework: "Next.js 16.0.1", react: "19.2.0", typescript: "5.x", styling: "Tailwind CSS 4", uiComponents: "Radix UI + shadcn/ui", icons: "Lucide React", fonts: "Geist Sans, Geist Mono" }, backend: { runtime: "Next.js API Routes", database: "Firebase Firestore", auth: "Firebase Auth", storage: "Firebase Storage" }, integrations: [ "Google Vertex AI", "Google Generative AI", "GitHub OAuth", "v0.dev SDK" ] }; }