import { NextRequest, NextResponse } from 'next/server'; import admin from '@/lib/firebase/admin'; import { getApiUrl } from '@/lib/utils/api-url'; /** * Timeline View Data * Structures MVP checklist pages with their development sessions on a timeline */ export async function GET( request: NextRequest, { params }: { params: Promise<{ projectId: string }> } ) { try { const { projectId } = await params; // Load project data, MVP checklist, git history, and activity in parallel const db = admin.firestore(); const projectRef = db.collection('projects').doc(projectId); const [projectDoc, checklistResponse, gitResponse, activityResponse] = await Promise.all([ projectRef.get(), fetch(getApiUrl(`/api/projects/${projectId}/mvp-checklist`, request)), fetch(getApiUrl(`/api/projects/${projectId}/git-history`, request)), fetch(getApiUrl(`/api/projects/${projectId}/activity`, request)) ]); const projectData = projectDoc.exists ? projectDoc.data() : null; const checklist = await checklistResponse.json(); const git = await gitResponse.json(); const activity = await activityResponse.json(); // Check if checklist exists and has the expected structure if (!checklist || checklist.error || !checklist.mvpChecklist || !Array.isArray(checklist.mvpChecklist)) { return NextResponse.json({ workItems: [], timeline: { start: new Date().toISOString(), end: new Date().toISOString(), totalDays: 0 }, summary: { totalWorkItems: 0, withActivity: 0, noActivity: 0, built: 0, missing: 0 }, projectCreator: projectData?.createdBy || projectData?.owner || 'You', message: 'No MVP checklist generated yet. Click "Regenerate Plan" to create one.' }); } // Build lightweight history object with just what we need const history = { chronologicalEvents: [ // Add git commits ...(git.commits || []).map((commit: any) => ({ type: 'git_commit', timestamp: new Date(commit.date).toISOString(), data: { hash: commit.hash, message: commit.message, filesChanged: commit.filesChanged, insertions: commit.insertions, deletions: commit.deletions } })), // Add extension sessions ...(activity.sessions || []).map((session: any) => ({ type: 'extension_session', timestamp: session.startTime, data: { duration: session.duration, filesModified: session.filesModified } })) ] }; // Map pages to work items with session data const workItems = []; for (const category of checklist.mvpChecklist) { for (const item of category.pages) { const relatedSessions = findRelatedSessions(item, history); const relatedCommits = findRelatedCommits(item, history); const hasActivity = relatedSessions.length > 0 || relatedCommits.length > 0; const startDate = hasActivity ? getEarliestDate([...relatedSessions, ...relatedCommits]) : null; const endDate = hasActivity ? getLatestDate([...relatedSessions, ...relatedCommits]) : null; workItems.push({ id: `${category.category.toLowerCase().replace(/\s+/g, '-')}-${item.title.toLowerCase().replace(/\s+/g, '-')}`, title: item.title, category: category.category, path: item.path, status: item.status, priority: item.priority, startDate, endDate, duration: calculateDuration(startDate, endDate), sessionsCount: relatedSessions.length, commitsCount: relatedCommits.length, totalActivity: relatedSessions.length + relatedCommits.length, sessions: relatedSessions, commits: relatedCommits, requirements: generateRequirements(item, { name: category.category }), evidence: item.evidence || [], note: item.note }); } } // Sort by category order and status // Priority: Core Features -> Marketing -> Social -> Content -> Settings const categoryOrder = [ 'Core Features', 'Marketing', 'Social', 'Content', 'Settings' ]; workItems.sort((a, b) => { // First by category const catCompare = categoryOrder.indexOf(a.category) - categoryOrder.indexOf(b.category); if (catCompare !== 0) return catCompare; // Then by status (built first, then in_progress, then missing) const statusOrder = { 'built': 0, 'in_progress': 1, 'missing': 2 }; return (statusOrder[a.status as keyof typeof statusOrder] || 3) - (statusOrder[b.status as keyof typeof statusOrder] || 3); }); // Calculate timeline range const allDates = workItems .filter(w => w.startDate) .flatMap(w => [w.startDate, w.endDate].filter(Boolean)) .map(d => new Date(d!)); const timelineStart = allDates.length > 0 ? new Date(Math.min(...allDates.map(d => d.getTime()))) : new Date(); const timelineEnd = new Date(); // Today return NextResponse.json({ workItems, timeline: { start: timelineStart.toISOString(), end: timelineEnd.toISOString(), totalDays: Math.ceil((timelineEnd.getTime() - timelineStart.getTime()) / (1000 * 60 * 60 * 24)) }, summary: { totalWorkItems: workItems.length, withActivity: workItems.filter(w => w.totalActivity > 0).length, noActivity: workItems.filter(w => w.totalActivity === 0).length, built: workItems.filter(w => w.status === 'built').length, missing: workItems.filter(w => w.status === 'missing').length }, projectCreator: projectData?.createdBy || projectData?.owner || 'You' }); } catch (error) { console.error('Error generating timeline view:', error); return NextResponse.json( { error: 'Failed to generate timeline view', details: error instanceof Error ? error.message : String(error) }, { status: 500 } ); } } function findRelatedSessions(page: any, history: any) { const pagePath = page.path.toLowerCase(); const pageTitle = page.title.toLowerCase(); return history.chronologicalEvents .filter((e: any) => e.type === 'extension_session') .filter((e: any) => { const filesModified = e.data.filesModified || []; return filesModified.some((f: string) => { const lowerFile = f.toLowerCase(); return lowerFile.includes(pagePath) || lowerFile.includes(pageTitle.replace(/\s+/g, '-')) || (page.evidence && page.evidence.some((ev: string) => lowerFile.includes(ev.toLowerCase()))); }); }) .map((e: any) => ({ timestamp: e.timestamp, duration: e.data.duration, filesModified: e.data.filesModified })); } function findRelatedCommits(page: any, history: any) { const pagePath = page.path.toLowerCase(); const pageTitle = page.title.toLowerCase(); return history.chronologicalEvents .filter((e: any) => e.type === 'git_commit') .filter((e: any) => { const message = e.data.message.toLowerCase(); return message.includes(pagePath) || message.includes(pageTitle.replace(/\s+/g, ' ')) || (page.evidence && page.evidence.some((ev: string) => message.includes(ev.toLowerCase()))); }) .map((e: any) => ({ timestamp: e.timestamp, hash: e.data.hash, message: e.data.message, insertions: e.data.insertions, deletions: e.data.deletions })); } function getEarliestDate(events: any[]) { if (events.length === 0) return null; const dates = events.map(e => new Date(e.timestamp).getTime()); return new Date(Math.min(...dates)).toISOString(); } function getLatestDate(events: any[]) { if (events.length === 0) return null; const dates = events.map(e => new Date(e.timestamp).getTime()); return new Date(Math.max(...dates)).toISOString(); } function calculateDuration(startDate: string | null, endDate: string | null): number { if (!startDate || !endDate) return 0; const diff = new Date(endDate).getTime() - new Date(startDate).getTime(); return Math.ceil(diff / (1000 * 60 * 60 * 24)); } function generateRequirements(page: any, category: any): any[] { const requirements = []; // Generate specific requirements based on page type if (page.title.includes('Sign In') || page.title.includes('Sign Up')) { requirements.push( { id: 1, text: 'Email/password authentication', status: 'built' }, { id: 2, text: 'GitHub OAuth integration', status: 'built' }, { id: 3, text: 'Password reset flow', status: 'missing' }, { id: 4, text: 'Session management', status: 'built' } ); } else if (page.title.includes('Checklist')) { requirements.push( { id: 1, text: 'Display generated tasks from API', status: 'missing' }, { id: 2, text: 'Mark tasks as complete', status: 'missing' }, { id: 3, text: 'Drag-and-drop reordering', status: 'missing' }, { id: 4, text: 'Save checklist state', status: 'missing' }, { id: 5, text: 'Export to markdown/PDF', status: 'missing' } ); } else if (page.title.includes('Vision') || page.title.includes('Mission')) { requirements.push( { id: 1, text: 'Capture product vision text', status: 'missing' }, { id: 2, text: 'AI-assisted vision refinement', status: 'missing' }, { id: 3, text: 'Upload supporting documents', status: 'missing' }, { id: 4, text: 'Save vision to project metadata', status: 'built' } ); } else if (page.title.includes('Marketing Automation')) { requirements.push( { id: 1, text: 'Connect to /plan/marketing API', status: 'missing' }, { id: 2, text: 'Generate landing page copy', status: 'missing' }, { id: 3, text: 'Generate email sequences', status: 'missing' }, { id: 4, text: 'Export marketing materials', status: 'missing' } ); } else if (page.title.includes('Communication Automation')) { requirements.push( { id: 1, text: 'Email template builder', status: 'missing' }, { id: 2, text: 'Slack integration', status: 'missing' }, { id: 3, text: 'Automated project updates', status: 'missing' }, { id: 4, text: 'Team notifications', status: 'missing' } ); } else if (page.title.includes('Import') && page.title.includes('Modal')) { requirements.push( { id: 1, text: 'Start from scratch option', status: 'built' }, { id: 2, text: 'Import from GitHub', status: 'built' }, { id: 3, text: 'Import from local folder', status: 'missing' }, { id: 4, text: 'Auto-detect project type', status: 'missing' }, { id: 5, text: 'Trigger Cursor import', status: 'built' }, { id: 6, text: 'Create .vibn file', status: 'built' } ); } else if (page.status === 'built') { requirements.push( { id: 1, text: 'Page built and accessible', status: 'built' }, { id: 2, text: 'Connected to backend API', status: 'built' } ); } else { requirements.push( { id: 1, text: 'Design page layout', status: 'missing' }, { id: 2, text: 'Implement core functionality', status: 'missing' }, { id: 3, text: 'Connect to backend API', status: 'missing' }, { id: 4, text: 'Add error handling', status: 'missing' } ); } return requirements; }