@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
1,191 lines • 66.4 kB
JavaScript
import { ensureDatabaseReady } from '../../storage/sqlite-manager.js';
import { randomUUID } from 'crypto';
import { setupAgileManagementTools } from '../../modules/agile-management/tools.js';
import { emitSprintCreated, emitStoryCreated, emitStoryUpdated, emitStoryMoved, emitEpicCreated } from '../utils/database-events.js';
// Helper function to safely parse JSON with fallback
function parseJsonSafely(jsonString, fallback = null) {
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() {
const db = await ensureDatabaseReady();
return {
toolName: 'dashboard',
requestId: `req_${Date.now()}_${randomUUID()}`,
timestamp: Date.now(),
projectId: 'default',
db
};
}
export function setupAgileAPI(app, agileManager) {
// Agile management API endpoints setup
// Get all sprints
app.get('/api/agile/sprints', async (req, res) => {
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 = [];
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) => ({
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.message
});
}
});
// Get active sprint
app.get('/api/agile/sprints/active', async (req, res) => {
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.message
});
}
});
// Get specific sprint details
app.get('/api/agile/sprints/:id', async (req, res) => {
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 = {
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) => s.status === 'done');
const totalPoints = stories.reduce((sum, s) => sum + (s.storyPoints || 0), 0);
const completedPoints = completedStories.reduce((sum, s) => 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.message
});
}
});
// Get stories with filtering
app.get('/api/agile/stories', async (req, res) => {
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);
}
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 = [];
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) => ({
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) => story.status === status);
}
if (assignee) {
stories = stories.filter((story) => story.assignee === assignee);
}
if (priority) {
stories = stories.filter((story) => story.priority === priority);
}
if (epicId) {
stories = stories.filter((story) => 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.message
});
}
});
// Move story between columns
app.post('/api/agile/story/move', async (req, res) => {
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.message
});
}
});
// Get individual story details
app.get('/api/agile/stories/:id', async (req, res) => {
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.message
});
}
});
// Update story details
app.put('/api/agile/stories/:id', async (req, res) => {
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.message
});
}
});
// Get velocity data for charts
app.get('/api/agile/velocity', async (req, res) => {
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.message
});
}
});
// Get burndown chart data for a sprint
app.get('/api/agile/burndown/:sprintId', async (req, res) => {
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.message
});
}
});
// Get board configuration (columns, workflow states)
app.get('/api/agile/board/config', async (req, res) => {
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.message
});
}
});
// Health check for agile service
app.get('/api/agile/health', async (req, res) => {
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.message,
timestamp: new Date().toISOString()
});
}
});
// Get all boards
app.get('/api/agile/boards', async (req, res) => {
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.message
});
}
});
// Create a new board
app.post('/api/agile/boards', async (req, res) => {
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.message
});
}
});
// Get backlog items
app.get('/api/agile/backlog', async (req, res) => {
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.message
});
}
});
// Estimate a story
app.post('/api/agile/estimate', async (req, res) => {
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.message
});
}
});
// Get team capacity for sprint
app.get('/api/agile/capacity/:sprintId', async (req, res) => {
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.message
});
}
});
// Generate sprint report
app.get('/api/agile/report/:sprintId', async (req, res) => {
try {
const { sprintId } = req.params;
const options = {
includeMetrics: req.query.includeMetrics === 'true',
includeStories: true,
includeRetrospective: false,
format: req.query.format || '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.message
});
}
});
// Epic endpoints
// Get all epics
app.get('/api/agile/epics', async (req, res) => {
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 = [];
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) => ({
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.message
});
}
});
// Get specific epic details
app.get('/api/agile/epic/:id', async (req, res) => {
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.message
});
}
});
// Get stories for a specific epic
app.get('/api/agile/stories', async (req, res) => {
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) => story.sprintId === sprintId);
}
if (epicId) {
stories = stories.filter((story) => story.epic === epicId);
}
if (status) {
stories = stories.filter((story) => story.status === status);
}
if (assignee) {
stories = stories.filter((story) => story.assignee === assignee);
}
if (priority) {
stories = stories.filter((story) => 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.message
});
}
});
// Add alias endpoint for dashboard compatibility
app.get('/api/agile/sprint/:id', async (req, res) => {
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