UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

1,547 lines (1,369 loc) 57.8 kB
import { Application, Request, Response } from 'express'; import { ensureDatabaseReady } from '../../storage/sqlite-manager.js'; import { randomUUID } from 'crypto'; import { setupAgileManagementTools } from '../../modules/agile-management/tools.js'; import { RequestContext } from '../../core/types.js'; import { emitSprintCreated, emitSprintUpdated, emitStoryCreated, emitStoryUpdated, emitStoryMoved, emitEpicCreated, emitEpicUpdated } from '../utils/database-events.js'; // Helper function to safely parse JSON with fallback function parseJsonSafely(jsonString: string | null | undefined, fallback: any = null): any { if (!jsonString) return fallback; try { return JSON.parse(jsonString); } catch (error) { console.warn('Failed to parse JSON:', jsonString, error); return fallback; } } // Helper to create request context for tool execution (used by other endpoints) async function createRequestContext(): Promise<RequestContext> { const db = await ensureDatabaseReady(); return { toolName: 'dashboard', requestId: `req_${Date.now()}_${randomUUID()}`, timestamp: Date.now(), projectId: 'default', db }; } export function setupAgileAPI(app: Application, agileManager: any): void { // Agile management API endpoints setup // Get all sprints app.get('/api/agile/sprints', async (req: Request, res: Response) => { try { const { status, limit = 50 } = req.query; let sprints = []; try { const db = await ensureDatabaseReady(); let sql = 'SELECT * FROM agile_sprints WHERE 1=1'; const params: any[] = []; if (status) { sql += ' AND status = ?'; params.push(status); } sql += ' ORDER BY created_at DESC LIMIT ?'; params.push(Number(limit)); const result = await db.query(sql, params); if (result.success) { sprints = (result.data || []).map((sprint: any) => ({ id: sprint.id, name: sprint.name, status: sprint.status, goal: sprint.goal, startDate: sprint.start_date ? new Date(sprint.start_date).toISOString() : null, endDate: sprint.end_date ? new Date(sprint.end_date).toISOString() : null, duration: sprint.duration, team: sprint.team ? JSON.parse(sprint.team) : [], storyPointsPlanned: sprint.story_points_planned || 0, storyPointsCompleted: sprint.story_points_completed || 0, storiesTotal: sprint.stories_total || 0, storiesCompleted: sprint.stories_completed || 0, velocity: sprint.velocity || 0, createdAt: new Date(sprint.created_at).toISOString(), updatedAt: new Date(sprint.updated_at).toISOString() })); } else { throw new Error(result.error || 'Database query failed'); } } catch (dbError) { console.error('📋 Database error fetching sprints:', dbError); return res.status(503).json({ success: false, error: 'Database unavailable', message: 'Unable to fetch sprints at this time' }); } res.json({ success: true, data: { sprints }, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching sprints:', error); res.status(500).json({ success: false, error: 'Failed to fetch sprints', message: (error as Error).message }); } }); // Get active sprint app.get('/api/agile/sprints/active', async (req: Request, res: Response) => { try { let activeSprint = null; try { const db = await ensureDatabaseReady(); const result = await db.get( 'SELECT * FROM agile_sprints WHERE status = ? ORDER BY created_at DESC LIMIT 1', ['active'] ); if (result.success && result.data) { const sprint = result.data; activeSprint = { id: sprint.id, name: sprint.name, status: sprint.status, goal: sprint.goal, startDate: sprint.start_date ? new Date(sprint.start_date).toISOString() : null, endDate: sprint.end_date ? new Date(sprint.end_date).toISOString() : null, duration: sprint.duration, team: sprint.team ? JSON.parse(sprint.team) : [], storyPointsPlanned: sprint.story_points_planned || 0, storyPointsCompleted: sprint.story_points_completed || 0, storiesTotal: sprint.stories_total || 0, storiesCompleted: sprint.stories_completed || 0, velocity: sprint.velocity || 0, createdAt: new Date(sprint.created_at).toISOString(), updatedAt: new Date(sprint.updated_at).toISOString() }; } } catch (dbError) { console.error('📋 Database error fetching active sprint:', dbError); return res.status(503).json({ success: false, error: 'Database unavailable', message: 'Unable to fetch active sprint at this time' }); } if (!activeSprint) { return res.status(404).json({ success: false, error: 'No active sprint found' }); } res.json({ success: true, data: activeSprint, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching active sprint:', error); res.status(500).json({ success: false, error: 'Failed to fetch active sprint', message: (error as Error).message }); } }); // Get specific sprint details app.get('/api/agile/sprints/:id', async (req: Request, res: Response) => { try { const { id } = req.params; console.error('🔍 Individual sprint request - ID:', id); // Validate that ID is not empty or whitespace if (!id || !id.trim()) { return res.status(404).json({ success: false, error: 'Sprint not found', sprintId: id }); } const db = await ensureDatabaseReady(); console.error('✅ Database ready'); // First, let's check if the sprint exists with a simple query const existsQuery = 'SELECT COUNT(*) as count FROM agile_sprints WHERE id = ?'; const existsResult = await db.get(existsQuery, [id]); console.error('📊 Sprint exists check:', existsResult); // Get sprint from database (using SELECT * like the working list endpoint) const sprintQuery = `SELECT * FROM agile_sprints WHERE id = ?`; console.error('🔎 Executing sprint query with ID:', id); const sprintResult = await db.get(sprintQuery, [id]); console.error('📋 Sprint query result:', sprintResult); if (!sprintResult.success || !sprintResult.data) { return res.status(404).json({ success: false, error: 'Sprint not found', sprintId: id }); } // Convert to mutable object and map field names const rawSprint = sprintResult.data; const sprint: any = { id: rawSprint.id, project_id: rawSprint.project_id, name: rawSprint.name, goal: rawSprint.goal, status: rawSprint.status, startDate: rawSprint.start_date ? new Date(rawSprint.start_date).toISOString() : null, endDate: rawSprint.end_date ? new Date(rawSprint.end_date).toISOString() : null, duration: rawSprint.duration, team: parseJsonSafely(rawSprint.team, []), storyPointsPlanned: rawSprint.story_points_planned || 0, storyPointsCompleted: rawSprint.story_points_completed || 0, storiesTotal: rawSprint.stories_total || 0, storiesCompleted: rawSprint.stories_completed || 0, velocity: rawSprint.velocity || 0, createdAt: new Date(rawSprint.created_at).toISOString(), updatedAt: new Date(rawSprint.updated_at).toISOString() }; // Get stories for this sprint to calculate statistics const storiesQuery = ` SELECT id, title, status, story_points as storyPoints FROM agile_stories WHERE sprint_id = ? `; const storiesResult = await db.all(storiesQuery, [id]); const stories = storiesResult.data || []; // Calculate statistics from stories const completedStories = stories.filter((s: any) => s.status === 'done'); const totalPoints = stories.reduce((sum: number, s: any) => sum + (s.storyPoints || 0), 0); const completedPoints = completedStories.reduce((sum: number, s: any) => sum + (s.storyPoints || 0), 0); // Update sprint with calculated fields sprint.storyPointsPlanned = sprint.storyPointsPlanned || totalPoints; sprint.storyPointsCompleted = completedPoints; sprint.storiesTotal = stories.length; sprint.storiesCompleted = completedStories.length; sprint.velocity = completedPoints; sprint.weeklyRate = sprint.duration ? (completedPoints / (sprint.duration / 7)).toFixed(1) : 0; // Update the sprint in database with calculated values const updateQuery = ` UPDATE agile_sprints SET story_points_planned = ?, story_points_completed = ?, stories_total = ?, stories_completed = ?, velocity = ?, updated_at = ? WHERE id = ? `; await db.run(updateQuery, [ sprint.storyPointsPlanned, sprint.storyPointsCompleted, sprint.storiesTotal, sprint.storiesCompleted, sprint.velocity, Math.floor(Date.now() / 1000), id ]); res.json({ success: true, data: sprint, timestamp: new Date().toISOString() }); } catch (error) { console.error(`📋 Error fetching sprint ${req.params.id}:`, error); res.status(500).json({ success: false, error: 'Failed to fetch sprint details', message: (error as Error).message }); } }); // Get stories with filtering app.get('/api/agile/stories', async (req: Request, res: Response) => { try { const { sprintId, epicId, status, assignee, priority, limit = 50, offset = 0 } = req.query; const filters = { sprintId, status, assignee, priority, limit: Number(limit), offset: Number(offset) }; let stories = []; // Use appropriate method based on filters if (sprintId && agileManager.getStoriesForSprint) { // Get stories for specific sprint stories = await agileManager.getStoriesForSprint(sprintId as string); } else if (agileManager.getAllStories) { // Get all stories stories = await agileManager.getAllStories(); } else if (agileManager.getBacklog) { // Fallback to getBacklog only if no sprintId is specified if (!sprintId) { stories = await agileManager.getBacklog(); } else { // If sprintId is specified but we only have getBacklog, return empty stories = []; } } else { // Try to fetch from database try { const db = await ensureDatabaseReady(); let sql = 'SELECT * FROM agile_stories WHERE 1=1'; const params: any[] = []; if (status) { sql += ' AND status = ?'; params.push(status); } if (assignee) { sql += ' AND assignee LIKE ?'; params.push(`%${assignee}%`); } if (priority) { sql += ' AND priority = ?'; params.push(priority); } if (sprintId) { sql += ' AND sprint_id = ?'; params.push(sprintId); } if (epicId) { sql += ' AND epic_id = ?'; params.push(epicId); } sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; params.push(Number(limit), Number(offset)); const result = await db.query(sql, params); if (result.success) { stories = (result.data || []).map((story: any) => ({ id: story.id, title: story.title, description: story.description, status: story.status, storyPoints: story.story_points, assignee: story.assignee, priority: story.priority, sprintId: story.sprint_id, epicId: story.epic_id, tags: story.tags ? JSON.parse(story.tags) : [], createdAt: new Date(story.created_at).toISOString(), updatedAt: new Date(story.updated_at).toISOString() })); } else { throw new Error(result.error || 'Database query failed'); } } catch (dbError) { console.error('📋 Database error fetching stories:', dbError); return res.status(503).json({ success: false, error: 'Database unavailable', message: 'Unable to fetch stories at this time' }); } } // Apply filters for agileManager-returned data (database queries already filter) if (stories && !sprintId && (status || assignee || priority || epicId)) { if (status) { stories = stories.filter((story: any) => story.status === status); } if (assignee) { stories = stories.filter((story: any) => story.assignee === assignee); } if (priority) { stories = stories.filter((story: any) => story.priority === priority); } if (epicId) { stories = stories.filter((story: any) => story.epic === epicId || story.epicId === epicId); } } // Apply pagination for agileManager-returned data (database queries already paginate) if (stories && agileManager.getAllStories) { const startIndex = Number(offset); const endIndex = startIndex + Number(limit); stories = stories.slice(startIndex, endIndex); } res.json({ success: true, data: { stories }, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching stories:', error); res.status(500).json({ success: false, error: 'Failed to fetch stories', message: (error as Error).message }); } }); // Move story between columns app.post('/api/agile/story/move', async (req: Request, res: Response) => { try { const { storyId, fromStatus, toStatus, index } = req.body; if (!storyId || !toStatus) { return res.status(400).json({ success: false, error: 'Missing required fields: storyId and toStatus are required' }); } let result = null; if (agileManager.moveStory) { result = await agileManager.moveStory(storyId, toStatus, index); } else if (agileManager.updateStory) { result = await agileManager.updateStory(storyId, { status: toStatus }); } else { // Try to update in database try { const db = await ensureDatabaseReady(); const updateResult = await db.run( 'UPDATE agile_stories SET status = ?, updated_at = ? WHERE id = ?', [toStatus, Date.now(), storyId] ); if (updateResult.success && updateResult.data.changes > 0) { result = { storyId, fromStatus, toStatus, index, updatedAt: new Date().toISOString() }; } else { throw new Error('Story not found or update failed'); } } catch (dbError) { console.error('📋 Database error updating story:', dbError); return res.status(503).json({ success: false, error: 'Database unavailable', message: 'Unable to update story at this time' }); } } res.json({ success: true, message: 'Story moved successfully', data: result, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error moving story:', error); res.status(500).json({ success: false, error: 'Failed to move story', message: (error as Error).message }); } }); // Get individual story details app.get('/api/agile/stories/:id', async (req: Request, res: Response) => { try { const { id } = req.params; let story = null; try { const db = await ensureDatabaseReady(); const result = await db.get( 'SELECT * FROM agile_stories WHERE id = ?', [id] ); if (result.success && result.data) { const rawStory = result.data; story = { id: rawStory.id, title: rawStory.title, description: rawStory.description, status: rawStory.status, storyPoints: rawStory.story_points || 0, priority: rawStory.priority, assignee: rawStory.assignee, sprintId: rawStory.sprint_id, epicId: rawStory.epic_id, tags: parseJsonSafely(rawStory.tags, []), acceptanceCriteria: parseJsonSafely(rawStory.acceptance_criteria, []), designDocumentUrl: rawStory.design_document_url, implementationDocumentUrl: rawStory.implementation_document_url, documentationStatus: rawStory.documentation_status, groomedWithUserFeedback: rawStory.groomed_with_user_feedback, createdAt: new Date(rawStory.created_at).toISOString(), updatedAt: new Date(rawStory.updated_at).toISOString() }; } } catch (dbError) { console.error('📋 Database error fetching story details:', dbError); return res.status(503).json({ success: false, error: 'Database unavailable', message: 'Unable to fetch story details at this time' }); } if (!story) { return res.status(404).json({ success: false, error: 'Story not found' }); } res.json({ success: true, data: story, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching story:', error); res.status(500).json({ success: false, error: 'Failed to fetch story', message: (error as Error).message }); } }); // Update story details app.put('/api/agile/stories/:id', async (req: Request, res: Response) => { try { const { id } = req.params; const updates = req.body; let result = null; if (agileManager.updateStory) { result = await agileManager.updateStory(id, updates); } else { // Try to update in database try { const db = await ensureDatabaseReady(); // Build dynamic update query const updateFields = []; const updateValues = []; if (updates.title !== undefined) { updateFields.push('title = ?'); updateValues.push(updates.title); } if (updates.description !== undefined) { updateFields.push('description = ?'); updateValues.push(updates.description); } if (updates.status !== undefined) { updateFields.push('status = ?'); updateValues.push(updates.status); } if (updates.storyPoints !== undefined) { updateFields.push('story_points = ?'); updateValues.push(updates.storyPoints); } if (updates.priority !== undefined) { updateFields.push('priority = ?'); updateValues.push(updates.priority); } if (updates.assignee !== undefined) { updateFields.push('assignee = ?'); updateValues.push(updates.assignee); } if (updates.tags !== undefined) { updateFields.push('tags = ?'); updateValues.push(JSON.stringify(updates.tags)); } if (updates.acceptanceCriteria !== undefined) { updateFields.push('acceptance_criteria = ?'); updateValues.push(JSON.stringify(updates.acceptanceCriteria)); } updateFields.push('updated_at = ?'); updateValues.push(Date.now()); updateValues.push(id); const updateResult = await db.run( `UPDATE agile_stories SET ${updateFields.join(', ')} WHERE id = ?`, updateValues ); if (updateResult.success && updateResult.data.changes > 0) { result = { storyId: id, updates, updatedAt: new Date().toISOString() }; } else { throw new Error('Story not found or update failed'); } } catch (dbError) { console.error('📋 Database error updating story:', dbError); return res.status(503).json({ success: false, error: 'Database unavailable', message: 'Unable to update story at this time' }); } } // Emit story updated event emitStoryUpdated(id, updates); res.json({ success: true, message: 'Story updated successfully', data: result, timestamp: new Date().toISOString() }); } catch (error) { console.error(`📋 Error updating story ${req.params.id}:`, error); res.status(500).json({ success: false, error: 'Failed to update story', message: (error as Error).message }); } }); // Get velocity data for charts app.get('/api/agile/velocity', async (req: Request, res: Response) => { try { const { teamName, sprints = '5' } = req.query; const db = await ensureDatabaseReady(); // Get completed sprints ordered by end date const sprintsResult = await db.all( `SELECT * FROM agile_sprints WHERE status IN ('completed', 'done') ORDER BY end_date DESC LIMIT ?`, [Number(sprints)] ); const completedSprints = (sprintsResult.data || []).reverse(); // Show oldest to newest // If no completed sprints, include active sprint if (completedSprints.length === 0) { const activeResult = await db.all( `SELECT * FROM agile_sprints WHERE status = 'active' ORDER BY start_date DESC LIMIT ?`, [Number(sprints)] ); completedSprints.push(...(activeResult.data || [])); } // Calculate velocity for each sprint const velocityData = { labels: [], plannedPoints: [], completedPoints: [], averageVelocity: 0, currentVelocity: 0, trend: 'stable', sprints: [] }; let totalVelocity = 0; let velocityCount = 0; for (const sprint of completedSprints) { // Get stories for this sprint to calculate actual velocity const storiesResult = await db.all( 'SELECT story_points, status FROM agile_stories WHERE sprint_id = ?', [sprint.id] ); const stories = storiesResult.data || []; const totalPoints = stories.reduce((sum, s) => sum + (s.story_points || 0), 0); const completedPoints = stories .filter(s => s.status === 'done') .reduce((sum, s) => sum + (s.story_points || 0), 0); velocityData.labels.push(sprint.name); velocityData.plannedPoints.push(totalPoints); velocityData.completedPoints.push(completedPoints); if (completedPoints > 0) { totalVelocity += completedPoints; velocityCount++; } velocityData.sprints.push({ id: sprint.id, name: sprint.name, plannedPoints: totalPoints, completedPoints: completedPoints, startDate: sprint.start_date, endDate: sprint.end_date }); } // Calculate average and current velocity velocityData.averageVelocity = velocityCount > 0 ? Math.round(totalVelocity / velocityCount) : 0; velocityData.currentVelocity = velocityData.completedPoints[velocityData.completedPoints.length - 1] || 0; // Determine trend if (velocityData.completedPoints.length >= 2) { const recent = velocityData.completedPoints.slice(-3); const older = velocityData.completedPoints.slice(-6, -3); const recentAvg = recent.reduce((a, b) => a + b, 0) / recent.length; const olderAvg = older.length > 0 ? older.reduce((a, b) => a + b, 0) / older.length : recentAvg; if (recentAvg > olderAvg * 1.1) velocityData.trend = 'improving'; else if (recentAvg < olderAvg * 0.9) velocityData.trend = 'declining'; } res.json({ success: true, data: velocityData, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching velocity data:', error); res.status(500).json({ success: false, error: 'Failed to fetch velocity data', message: (error as Error).message }); } }); // Get burndown chart data for a sprint app.get('/api/agile/burndown/:sprintId', async (req: Request, res: Response) => { try { const { sprintId } = req.params; const db = await ensureDatabaseReady(); // Get sprint details const sprintResult = await db.get( 'SELECT * FROM agile_sprints WHERE id = ?', [sprintId] ); if (!sprintResult.success || !sprintResult.data) { return res.status(404).json({ success: false, error: 'Sprint not found' }); } const sprint = sprintResult.data; // Get all stories for this sprint const storiesResult = await db.all( 'SELECT id, story_points, status, updated_at FROM agile_stories WHERE sprint_id = ?', [sprintId] ); const stories = storiesResult.data || []; const totalPoints = stories.reduce((sum, story) => sum + (story.story_points || 0), 0); // Generate burndown data const startDate = new Date(sprint.start_date * 1000); const endDate = new Date(sprint.end_date * 1000); const duration = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)); // Create ideal burndown line const idealBurndown = []; const actualBurndown = []; const labels = []; for (let day = 0; day <= duration; day++) { const date = new Date(startDate); date.setDate(date.getDate() + day); // Ideal burndown: linear from total to 0 const idealRemaining = totalPoints - (totalPoints * (day / duration)); idealBurndown.push(Math.round(idealRemaining * 10) / 10); // Actual burndown: calculate based on story completions up to this date const dateTimestamp = date.getTime() / 1000; const completedPoints = stories .filter(story => story.status === 'done' && story.updated_at <= dateTimestamp) .reduce((sum, story) => sum + (story.story_points || 0), 0); const actualRemaining = totalPoints - completedPoints; actualBurndown.push(actualRemaining); // Date labels labels.push(date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })); } const burndownData = { sprintId, sprintName: sprint.name, totalStoryPoints: totalPoints, completedPoints: totalPoints - actualBurndown[actualBurndown.length - 1], labels, idealBurndown, actualBurndown, startDate: sprint.start_date, endDate: sprint.end_date }; res.json({ success: true, data: burndownData, timestamp: new Date().toISOString() }); } catch (error) { console.error(`📋 Error fetching burndown data for sprint ${req.params.sprintId}:`, error); res.status(500).json({ success: false, error: 'Failed to fetch burndown data', message: (error as Error).message }); } }); // Get board configuration (columns, workflow states) app.get('/api/agile/board/config', async (req: Request, res: Response) => { try { let boardConfig = null; if (agileManager.getBoardConfig) { boardConfig = await agileManager.getBoardConfig(); } if (!boardConfig) { return res.status(404).json({ success: false, error: 'Board configuration not available' }); } res.json({ success: true, data: boardConfig, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching board configuration:', error); res.status(500).json({ success: false, error: 'Failed to fetch board configuration', message: (error as Error).message }); } }); // Health check for agile service app.get('/api/agile/health', async (req: Request, res: Response) => { try { const features = { sprints: !!agileManager.getSprints || !!agileManager.getCurrentSprints, stories: !!agileManager.getStories || !!agileManager.getActiveStories, velocity: !!agileManager.getVelocityData, burndown: !!agileManager.getBurndownData, storyUpdates: !!agileManager.updateStory, storyMovement: !!agileManager.moveStory }; res.json({ success: true, status: 'healthy', data: { agileManagerAvailable: !!agileManager, features }, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Agile service health check failed:', error); res.status(503).json({ success: false, status: 'unhealthy', error: 'Agile service is not functioning properly', message: (error as Error).message, timestamp: new Date().toISOString() }); } }); // Get all boards app.get('/api/agile/boards', async (req: Request, res: Response) => { try { let boards = []; if (agileManager.getBoards) { boards = await agileManager.getBoards(); } res.json({ success: true, data: boards, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching boards:', error); res.status(500).json({ success: false, error: 'Failed to fetch boards', message: (error as Error).message }); } }); // Create a new board app.post('/api/agile/boards', async (req: Request, res: Response) => { try { const { name, columns } = req.body; if (!name || !columns || !Array.isArray(columns)) { return res.status(400).json({ success: false, error: 'Board name and columns are required' }); } let board = null; if (agileManager.createBoard) { board = await agileManager.createBoard({ name, columns }); } else { return res.status(501).json({ success: false, error: 'Board creation not implemented' }); } res.status(201).json({ success: true, data: board, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error creating board:', error); res.status(500).json({ success: false, error: 'Failed to create board', message: (error as Error).message }); } }); // Get backlog items app.get('/api/agile/backlog', async (req: Request, res: Response) => { try { const options = { includeEstimates: req.query.includeEstimates === 'true', maxItems: Number(req.query.maxItems) || 20 }; let backlog = []; if (agileManager.getBacklogItems) { backlog = await agileManager.getBacklogItems(options); } res.json({ success: true, data: backlog, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching backlog:', error); res.status(500).json({ success: false, error: 'Failed to fetch backlog', message: (error as Error).message }); } }); // Estimate a story app.post('/api/agile/estimate', async (req: Request, res: Response) => { try { const { storyId, estimates, finalEstimate } = req.body; let result = null; if (agileManager.estimateStory) { result = await agileManager.estimateStory(storyId, req.body); } else { return res.status(501).json({ success: false, error: 'Story estimation not implemented' }); } res.json({ success: true, data: result, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error estimating story:', error); res.status(500).json({ success: false, error: 'Failed to estimate story', message: (error as Error).message }); } }); // Get team capacity for sprint app.get('/api/agile/capacity/:sprintId', async (req: Request, res: Response) => { try { const { sprintId } = req.params; let capacity = null; if (agileManager.getTeamCapacity) { capacity = await agileManager.getTeamCapacity(sprintId); } if (!capacity) { return res.status(404).json({ success: false, error: 'Team capacity data not available' }); } res.json({ success: true, data: capacity, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching team capacity:', error); res.status(500).json({ success: false, error: 'Failed to fetch team capacity', message: (error as Error).message }); } }); // Generate sprint report app.get('/api/agile/report/:sprintId', async (req: Request, res: Response) => { try { const { sprintId } = req.params; const options = { includeMetrics: req.query.includeMetrics === 'true', includeStories: true, includeRetrospective: false, format: req.query.format as string || 'detailed' }; let report = null; if (agileManager.generateSprintReport) { report = await agileManager.generateSprintReport(sprintId, options); } if (!report) { return res.status(404).json({ success: false, error: 'Sprint report not available' }); } res.json({ success: true, data: report, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error generating sprint report:', error); res.status(500).json({ success: false, error: 'Failed to generate sprint report', message: (error as Error).message }); } }); // Epic endpoints // Get all epics app.get('/api/agile/epics', async (req: Request, res: Response) => { try { const { status, priority, limit = 100, offset = 0 } = req.query; let epics = []; // Get epics from database instead of agileManager try { const db = await ensureDatabaseReady(); console.error('📋 Fetching epics from database - DB ready'); let sql = 'SELECT * FROM agile_epics WHERE 1=1'; const params: any[] = []; if (status) { sql += ' AND status = ?'; params.push(status); } if (priority) { sql += ' AND priority = ?'; params.push(priority); } sql += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; params.push(Number(limit), Number(offset)); console.error(`📋 Executing epics query: ${sql} with params:`, params); const result = await db.query(sql, params); console.error('📋 Epics query result:', { success: result.success, count: result.data?.length || 0 }); if (result.success) { epics = (result.data || []).map((epic: any) => ({ id: epic.id, title: epic.title, description: epic.description, status: epic.status, storyPoints: epic.story_points || 0, priority: epic.priority, targetQuarter: epic.target_quarter, businessValue: epic.business_value, successCriteria: parseJsonSafely(epic.success_criteria, []), dependencies: parseJsonSafely(epic.dependencies, []), risks: parseJsonSafely(epic.risks, []), createdAt: new Date(epic.created_at * 1000).toISOString(), updatedAt: new Date(epic.updated_at * 1000).toISOString() })); } else { throw new Error(result.error || 'Database query failed'); } } catch (dbError) { console.error('📋 Database error fetching epics:', dbError); return res.status(503).json({ success: false, error: 'Database unavailable', message: 'Unable to fetch epics at this time' }); } res.json({ success: true, data: { epics }, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching epics:', error); res.status(500).json({ success: false, error: 'Failed to fetch epics', message: (error as Error).message }); } }); // Get specific epic details app.get('/api/agile/epic/:id', async (req: Request, res: Response) => { try { const { id } = req.params; let epic = null; // Get epic from database instead of agileManager try { const db = await ensureDatabaseReady(); const result = await db.get('SELECT * FROM agile_epics WHERE id = ?', [id]); if (result.success && result.data) { const rawEpic = result.data; epic = { id: rawEpic.id, title: rawEpic.title, description: rawEpic.description, status: rawEpic.status, storyPoints: rawEpic.story_points || 0, priority: rawEpic.priority, targetQuarter: rawEpic.target_quarter, businessValue: rawEpic.business_value, successCriteria: parseJsonSafely(rawEpic.success_criteria, []), dependencies: parseJsonSafely(rawEpic.dependencies, []), risks: parseJsonSafely(rawEpic.risks, []), createdAt: new Date(rawEpic.created_at * 1000).toISOString(), updatedAt: new Date(rawEpic.updated_at * 1000).toISOString() }; } } catch (dbError) { console.error('📋 Database error fetching epic details:', dbError); return res.status(503).json({ success: false, error: 'Database unavailable', message: 'Unable to fetch epic details at this time' }); } if (!epic) { return res.status(404).json({ success: false, error: 'Epic not found', epicId: id }); } res.json({ success: true, data: epic, timestamp: new Date().toISOString() }); } catch (error) { console.error(`📋 Error fetching epic ${req.params.id}:`, error); res.status(500).json({ success: false, error: 'Failed to fetch epic details', message: (error as Error).message }); } }); // Get stories for a specific epic app.get('/api/agile/stories', async (req: Request, res: Response) => { try { const { sprintId, epicId, status, assignee, priority, limit = 50, offset = 0 } = req.query; const filters = { sprintId, epicId, status, assignee, priority, limit: Number(limit), offset: Number(offset) }; let stories = []; if (agileManager.getBacklog) { // Use the available getBacklog method to get all stories const allStories = await agileManager.getBacklog(); stories = allStories; // Apply filters if (sprintId) { stories = stories.filter((story: any) => story.sprintId === sprintId); } if (epicId) { stories = stories.filter((story: any) => story.epic === epicId); } if (status) { stories = stories.filter((story: any) => story.status === status); } if (assignee) { stories = stories.filter((story: any) => story.assignee === assignee); } if (priority) { stories = stories.filter((story: any) => story.priority === priority); } // Apply pagination const startIndex = Number(offset); const endIndex = startIndex + Number(limit); stories = stories.slice(startIndex, endIndex); } res.json({ success: true, data: { stories }, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching stories:', error); res.status(500).json({ success: false, error: 'Failed to fetch stories', message: (error as Error).message }); } }); // Add alias endpoint for dashboard compatibility app.get('/api/agile/sprint/:id', async (req: Request, res: Response) => { try { const { id } = req.params; const db = await ensureDatabaseReady(); // Get sprint from database const sprintQuery = ` SELECT id, project_id, name, goal, status, start_date as startDate, end_date as endDate, duration, team, story_points_planned as storyPointsPlanned, story_points_completed as storyPointsCompleted, stories_total as storiesTotal, stories_completed as storiesCompleted, velocity, created_at as createdAt, updated_at as updatedAt FROM agile_sprints WHERE id = ? `; const sprintResult = await db.get(sprintQuery, [id]); if (!sprintResult.success || !sprintResult.data) { return res.status(404).json({ success: false, error: 'Sprint not found', sprintId: id }); } // Convert to mutable object const sprint: any = { ...sprintResult.data }; // Parse JSON fields sprint.team = parseJsonSafely(sprint.team, []); // Get stories for this sprint to calculate statistics const storiesQuery = ` SELECT id, title, description, status, story_points as storyPoints, priority, assignee, acceptance_criteria as acceptanceCriteria, created_at as createdAt, updated_at as updatedAt FROM agile_stories WHERE sprint_id = ? `; const storiesResult = await db.all(storiesQuery, [id]); const stories = storiesResult.data || []; // Parse JSON fields in stories stories.forEach((story: any) => { story.acceptanceCriteria = parseJsonSafely(story.acceptanceCriteria, []); }); // Calculate statistics from stories const completedStories = stories.filter((s: any) => s.status === 'done'); const totalPoints = stories.reduce((sum: number, s: any) => sum + (s.storyPoints || 0), 0); const completedPoints = completedStories.reduce((sum: number, s: any) => sum + (s.storyPoints || 0), 0); // Update sprint with calculated fields sprint.storyPointsPlanned = sprint.storyPointsPlanned || totalPoints; sprint.storyPointsCompleted = completedPoints; sprint.storiesTotal = stories.length; sprint.storiesCompleted = completedStories.length; sprint.velocity = completedPoints; sprint.weeklyRate = sprint.duration ? (completedPoints / (sprint.duration / 7)).toFixed(1) : 0; const sprintData = { ...sprint, stories: stories }; res.json({ success: true, data: sprintData, timestamp: new Date().toISOString() }); } catch (error) { console.error('📋 Error fetching sprint:', error); res.status(500).json({ success: false, error: 'Failed to fetch sprint', message: (error as Error).message }); } }); // ========================================== // Data Generation Endpoints (POST) // ========================================== // Create a new sprint app.post('/api/agile/sprints', async (req: Request, res: Response) => { try { const { name, goal, startDate, endDate, duration } = req.body; if (!name || !goal) { return res.status(400).json({ success: false, error: 'Missing required fields: name and goal are required' }); } const db = await ensureDatabaseReady(); const id = randomUUID(); const now = Math.floor(Date.now() / 1000); // Calculate dates const start = startDate ? new Date(startDate) : new Date(); const end = endDate ? new Date(endDate) : new Date(Date.now() + (duration || 14) * 24 * 60 * 60 * 1000); const durationDays = duration || 14; const result = await db.run( `INSERT INTO agile_sprints ( id, project_id, name, goal, status, start_date, end_date, duration, team, story_points_planned, story_points_completed, stories_total, stories_completed, velocity, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ id, 'default', // project_id name, goal, 'planning', // status Math.floor(start.getTime() / 1000), // start_date as unix timestamp Math.floor(end.getTime() / 1000), // end_date as unix timestamp durationDays, '[]', // team as JSON array 0, // story_points_planned 0, // story_points_completed 0, // stories_total 0, // stories_completed 0, // velocity now, // created_at now // updated_at ] ); if (result.success) { const sprint = { id, name, goal, status: 'planning', startDate: start.toISOString(), endDate: end.toISOString(), duration: durationDays, team: [], storyPointsPlanned: 0, storyPointsCompleted: 0, storiesTotal: 0, storiesCompleted: 0, velocity: 0, createdAt: new Date(now * 1000).toISOString(), updatedAt: new Date(now * 1000).toISOString() }; // Emit sprint created event emitSprintCreated(sprint); res.status(201).json({ success: true, data: sprint, timestamp: new Date().toISOString() }); } else { res.status(400).json({ success: false, error: result.error || 'Failed to create sprint' }); } } catch (error) { console.error('📋 Error creating sprint:', error); res.status(500).json({ success: false, error: 'Failed to create sprint', message: (error as Error).message }); } }); // Create a new story app.post('/api/agile/stories', async (req: Request, res: Response) => { try { const { title, description, acceptanceCriteria, storyPoints, priority, tags, sprintId, epicId, assignee } = req.body; if (!title) { return res.status(400).json({ success: false, error: 'Missing required field: title' }); } const db = await ensureDatabaseReady(); const id = randomUUID(); const now = Math.floor(Date.now() / 1000); const result = await db.run( `INSERT INTO agile_stories ( id, project_id, sprint_id, epic_id, title, description, status, story_points, priority, assignee, tags, acceptance_criteria, design_document_url, implementation_document_url, documentation_status, groomed_with_user_feedback, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ id, 'default', // project_id s