VIBN Frontend for Coolify deployment
This commit is contained in:
505
app/api/projects/[projectId]/audit/generate/route.ts
Normal file
505
app/api/projects/[projectId]/audit/generate/route.ts
Normal file
@@ -0,0 +1,505 @@
|
||||
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"
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user