VIBN Frontend for Coolify deployment

This commit is contained in:
2026-02-15 19:25:52 -08:00
commit 40bf8428cd
398 changed files with 76513 additions and 0 deletions

View 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"
]
};
}