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,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;
}