506 lines
15 KiB
TypeScript
506 lines
15 KiB
TypeScript
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<string, number> = {};
|
|
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<number, number>();
|
|
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"
|
|
]
|
|
};
|
|
}
|
|
|