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