mission-control/src/app/api/standup/route.ts

251 lines
8.9 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { getDatabase, db_helpers } from '@/lib/db';
import { requireRole } from '@/lib/auth';
/**
* POST /api/standup/generate - Generate daily standup report
* Body: { date?: string, agents?: string[] }
*/
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'operator');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
try {
const db = getDatabase();
const body = await request.json();
// Parse parameters
const targetDate = body.date || new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
const specificAgents = body.agents; // Optional filter for specific agents
// Calculate time range for "today" (start and end of the target date)
const startOfDay = Math.floor(new Date(`${targetDate}T00:00:00Z`).getTime() / 1000);
const endOfDay = Math.floor(new Date(`${targetDate}T23:59:59Z`).getTime() / 1000);
// Get all active agents or filter by specific agents
let agentQuery = 'SELECT * FROM agents';
const agentParams: any[] = [];
if (specificAgents && Array.isArray(specificAgents) && specificAgents.length > 0) {
const placeholders = specificAgents.map(() => '?').join(',');
agentQuery += ` WHERE name IN (${placeholders})`;
agentParams.push(...specificAgents);
}
agentQuery += ' ORDER BY name';
const agents = db.prepare(agentQuery).all(...agentParams) as any[];
// Prepare statements once (avoids N+1 per agent)
const completedTasksStmt = db.prepare(`
SELECT id, title, status, updated_at
FROM tasks
WHERE assigned_to = ?
AND status = 'done'
AND updated_at BETWEEN ? AND ?
ORDER BY updated_at DESC
`);
const inProgressTasksStmt = db.prepare(`
SELECT id, title, status, created_at, due_date
FROM tasks
WHERE assigned_to = ?
AND status = 'in_progress'
ORDER BY created_at ASC
`);
const assignedTasksStmt = db.prepare(`
SELECT id, title, status, created_at, due_date, priority
FROM tasks
WHERE assigned_to = ?
AND status = 'assigned'
ORDER BY priority DESC, created_at ASC
`);
const reviewTasksStmt = db.prepare(`
SELECT id, title, status, updated_at
FROM tasks
WHERE assigned_to = ?
AND status IN ('review', 'quality_review')
ORDER BY updated_at ASC
`);
const blockedTasksStmt = db.prepare(`
SELECT id, title, status, priority, created_at, metadata
FROM tasks
WHERE assigned_to = ?
AND (priority = 'urgent' OR metadata LIKE '%blocked%')
AND status NOT IN ('done')
ORDER BY priority DESC, created_at ASC
`);
const activityCountStmt = db.prepare(`
SELECT COUNT(*) as count
FROM activities
WHERE actor = ?
AND created_at BETWEEN ? AND ?
`);
const commentCountStmt = db.prepare(`
SELECT COUNT(*) as count
FROM comments
WHERE author = ?
AND created_at BETWEEN ? AND ?
`);
// Generate standup data for each agent
const standupData = agents.map(agent => {
const completedTasks = completedTasksStmt.all(agent.name, startOfDay, endOfDay);
const inProgressTasks = inProgressTasksStmt.all(agent.name);
const assignedTasks = assignedTasksStmt.all(agent.name);
const reviewTasks = reviewTasksStmt.all(agent.name);
const blockedTasks = blockedTasksStmt.all(agent.name);
const activityCount = activityCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number };
const commentsToday = commentCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number };
return {
agent: {
name: agent.name,
role: agent.role,
status: agent.status,
last_seen: agent.last_seen,
last_activity: agent.last_activity
},
completedToday: completedTasks,
inProgress: inProgressTasks,
assigned: assignedTasks,
review: reviewTasks,
blocked: blockedTasks,
activity: {
actionCount: activityCount.count,
commentsCount: commentsToday.count
}
};
});
// Generate summary statistics
const totalCompleted = standupData.reduce((sum, agent) => sum + agent.completedToday.length, 0);
const totalInProgress = standupData.reduce((sum, agent) => sum + agent.inProgress.length, 0);
const totalAssigned = standupData.reduce((sum, agent) => sum + agent.assigned.length, 0);
const totalReview = standupData.reduce((sum, agent) => sum + agent.review.length, 0);
const totalBlocked = standupData.reduce((sum, agent) => sum + agent.blocked.length, 0);
const totalActivity = standupData.reduce((sum, agent) => sum + agent.activity.actionCount, 0);
// Identify team accomplishments and blockers
const teamAccomplishments = standupData
.flatMap(agent => agent.completedToday.map(task => ({ ...task as any, agent: agent.agent.name })))
.sort((a: any, b: any) => b.updated_at - a.updated_at);
const teamBlockers = standupData
.flatMap(agent => agent.blocked.map(task => ({ ...task as any, agent: agent.agent.name })))
.sort((a: any, b: any) => {
// Sort by priority then by creation date
const priorityOrder: Record<string, number> = { urgent: 4, high: 3, medium: 2, low: 1 };
return (priorityOrder[b.priority] || 0) - (priorityOrder[a.priority] || 0) || a.created_at - b.created_at;
});
// Get overdue tasks across all agents
const now = Math.floor(Date.now() / 1000);
const overdueTasks = db.prepare(`
SELECT t.*, a.name as agent_name
FROM tasks t
LEFT JOIN agents a ON t.assigned_to = a.name
WHERE t.due_date < ?
AND t.status NOT IN ('done')
ORDER BY t.due_date ASC
`).all(now);
const standupReport = {
date: targetDate,
generatedAt: new Date().toISOString(),
summary: {
totalAgents: agents.length,
totalCompleted,
totalInProgress,
totalAssigned,
totalReview,
totalBlocked,
totalActivity,
overdue: overdueTasks.length
},
agentReports: standupData,
teamAccomplishments: teamAccomplishments.slice(0, 10), // Top 10 recent completions
teamBlockers,
overdueTasks
};
// Persist standup report
const createdAt = Math.floor(Date.now() / 1000);
db.prepare(`
INSERT OR REPLACE INTO standup_reports (date, report, created_at)
VALUES (?, ?, ?)
`).run(targetDate, JSON.stringify(standupReport), createdAt);
// Log the standup generation
db_helpers.logActivity(
'standup_generated',
'standup',
0, // No specific entity
'system',
`Generated daily standup for ${targetDate}`,
{
date: targetDate,
agentCount: agents.length,
tasksSummary: {
completed: totalCompleted,
inProgress: totalInProgress,
assigned: totalAssigned,
review: totalReview,
blocked: totalBlocked
}
}
);
return NextResponse.json({ standup: standupReport });
} catch (error) {
console.error('POST /api/standup/generate error:', error);
return NextResponse.json({ error: 'Failed to generate standup' }, { status: 500 });
}
}
/**
* GET /api/standup/history - Get previous standup reports
* Query params: limit, offset
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'viewer');
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status });
try {
const db = getDatabase();
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get('limit') || '10');
const offset = parseInt(searchParams.get('offset') || '0');
const standupRows = db.prepare(`
SELECT date, report, created_at
FROM standup_reports
ORDER BY created_at DESC
LIMIT ? OFFSET ?
`).all(limit, offset) as Array<{ date: string; report: string; created_at: number }>;
const standupHistory = standupRows.map((row, index) => {
const report = row.report ? JSON.parse(row.report) : {};
return {
id: `${row.date}-${index}`,
date: row.date || report.date || 'Unknown',
generatedAt: report.generatedAt || new Date(row.created_at * 1000).toISOString(),
summary: report.summary || {},
agentCount: report.summary?.totalAgents || 0
};
});
const countRow = db.prepare('SELECT COUNT(*) as total FROM standup_reports').get() as { total: number };
return NextResponse.json({
history: standupHistory,
total: countRow.total,
page: Math.floor(offset / limit) + 1,
limit
});
} catch (error) {
console.error('GET /api/standup/history error:', error);
return NextResponse.json({ error: 'Failed to fetch standup history' }, { status: 500 });
}
}