import { Router, type Router as RouterType } from 'express'; import { z } from 'zod'; import { prisma } from '@agent-fox/shared'; import { requireAuth } from '../middleware/auth.js'; import { requireAdmin } from '../middleware/admin.js'; const router: RouterType = Router(); router.use(requireAuth, requireAdmin); function parsePagination(query: Record, defaultLimit = 20) { return { page: Math.max(1, parseInt(query.page as string) || 1), limit: Math.min(100, Math.max(1, parseInt(query.limit as string) || defaultLimit)), }; } // ─── Dashboard Stats ──────────────────────────────────────────────── router.get('/stats', async (_req, res) => { const now = new Date(); const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const sevenDaysAgo = new Date(todayStart.getTime() - 7 * 24 * 60 * 60 * 1000); const [ totalUsers, todayUsers, totalProjects, todayProjects, totalCalls, todayCalls, callStats, successCount, activeProjectUsers, ] = await Promise.all([ prisma.user.count(), prisma.user.count({ where: { createdAt: { gte: todayStart } } }), prisma.project.count(), prisma.project.count({ where: { createdAt: { gte: todayStart } } }), prisma.mcpCallLog.count(), prisma.mcpCallLog.count({ where: { calledAt: { gte: todayStart } } }), prisma.mcpCallLog.aggregate({ _avg: { durationMs: true }, _count: { id: true }, where: { calledAt: { gte: sevenDaysAgo } }, }), prisma.mcpCallLog.count({ where: { calledAt: { gte: sevenDaysAgo }, success: true }, }), prisma.mcpCallLog.groupBy({ by: ['projectId'], where: { calledAt: { gte: sevenDaysAgo } }, }), ]); // Resolve unique user IDs from active projects const activeProjectIds = activeProjectUsers.map(g => g.projectId); let activeUserCount = 0; if (activeProjectIds.length > 0) { activeUserCount = await prisma.project.groupBy({ by: ['userId'], where: { id: { in: activeProjectIds } }, }).then(r => r.length); } const recentTotal = callStats._count.id; const successRate = recentTotal > 0 ? Math.round((successCount / recentTotal) * 100) : 100; res.json({ success: true, data: { totalUsers, todayUsers, totalProjects, todayProjects, totalCalls, todayCalls, avgResponseTime: Math.round(callStats._avg.durationMs ?? 0), successRate, activeUsers: activeUserCount, }, }); }); // ─── Trends (7d / 30d) ───────────────────────────────────────────── router.get('/stats/trends', async (req, res) => { const days = req.query.days === '30' ? 30 : 7; const since = new Date(); since.setDate(since.getDate() - days); since.setHours(0, 0, 0, 0); const rows = await prisma.$queryRaw< { date: string; total: bigint; success_count: bigint; avg_duration: number }[] >` SELECT TO_CHAR("calledAt", 'YYYY-MM-DD') AS date, COUNT(*)::bigint AS total, SUM(CASE WHEN success THEN 1 ELSE 0 END)::bigint AS success_count, COALESCE(AVG("durationMs"), 0)::int AS avg_duration FROM "McpCallLog" WHERE "calledAt" >= ${since} GROUP BY TO_CHAR("calledAt", 'YYYY-MM-DD') ORDER BY date `; // Build full date range with zeros for missing days const dataMap = new Map(rows.map(r => [r.date, r])); const trends = []; for (let i = 0; i < days; i++) { const d = new Date(since); d.setDate(d.getDate() + i); const key = d.toISOString().slice(0, 10); const row = dataMap.get(key); const total = row ? Number(row.total) : 0; const successCnt = row ? Number(row.success_count) : 0; trends.push({ date: key, calls: total, successRate: total > 0 ? Math.round((successCnt / total) * 100) : 100, avgDuration: row ? row.avg_duration : 0, }); } res.json({ success: true, data: trends }); }); // ─── User Management ──────────────────────────────────────────────── router.get('/users', async (req, res) => { const { page, limit } = parsePagination(req.query); const search = (req.query.search as string) || ''; const sortBy = (req.query.sortBy as string) || 'createdAt'; const order = req.query.order === 'asc' ? 'asc' as const : 'desc' as const; const where = search ? { OR: [{ name: { contains: search, mode: 'insensitive' as const } }, { email: { contains: search, mode: 'insensitive' as const } }] } : {}; const [users, total] = await Promise.all([ prisma.user.findMany({ where, select: { id: true, email: true, name: true, role: true, disabled: true, createdAt: true, avatarUrl: true, _count: { select: { projects: true } }, }, orderBy: { [sortBy]: order }, skip: (page - 1) * limit, take: limit, }), prisma.user.count({ where }), ]); res.json({ success: true, data: { users, total, page, limit } }); }); router.get('/users/:id', async (req, res) => { const user = await prisma.user.findUnique({ where: { id: req.params.id }, select: { id: true, email: true, name: true, role: true, disabled: true, createdAt: true, avatarUrl: true, oauthAccounts: { select: { provider: true, createdAt: true } }, projects: { select: { id: true, name: true, description: true, createdAt: true, _count: { select: { endpoints: true, modules: true } } }, orderBy: { createdAt: 'desc' }, }, }, }); if (!user) { res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'User not found' } }); return; } res.json({ success: true, data: user }); }); const disableSchema = z.object({ disabled: z.boolean() }); router.patch('/users/:id/disable', async (req, res) => { const parsed = disableSchema.safeParse(req.body); if (!parsed.success) { res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Invalid request body' } }); return; } if (req.params.id === req.user!.userId) { res.status(400).json({ success: false, error: { code: 'VALIDATION', message: 'Cannot disable your own account' } }); return; } const user = await prisma.user.update({ where: { id: req.params.id }, data: { disabled: parsed.data.disabled }, select: { id: true, disabled: true }, }); res.json({ success: true, data: user }); }); // ─── Project Management ───────────────────────────────────────────── router.get('/projects', async (req, res) => { const { page, limit } = parsePagination(req.query); const search = (req.query.search as string) || ''; const where = search ? { name: { contains: search, mode: 'insensitive' as const } } : {}; const [projects, total] = await Promise.all([ prisma.project.findMany({ where, select: { id: true, name: true, description: true, openApiVersion: true, createdAt: true, user: { select: { id: true, name: true, email: true } }, _count: { select: { endpoints: true, modules: true } }, }, orderBy: { createdAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), prisma.project.count({ where }), ]); res.json({ success: true, data: { projects, total, page, limit } }); }); router.get('/projects/:id', async (req, res) => { const project = await prisma.project.findUnique({ where: { id: req.params.id }, select: { id: true, name: true, description: true, baseUrl: true, openApiVersion: true, createdAt: true, updatedAt: true, user: { select: { id: true, name: true, email: true } }, modules: { select: { id: true, name: true, description: true, source: true, _count: { select: { endpoints: true } } }, orderBy: { sortOrder: 'asc' } }, _count: { select: { endpoints: true, modules: true, mcpCallLogs: true } }, }, }); if (!project) { res.status(404).json({ success: false, error: { code: 'NOT_FOUND', message: 'Project not found' } }); return; } res.json({ success: true, data: project }); }); router.delete('/projects/:id', async (req, res) => { await prisma.project.delete({ where: { id: req.params.id } }); res.json({ success: true, data: { message: 'Project deleted' } }); }); // ─── Call Logs ────────────────────────────────────────────────────── router.get('/call-logs', async (req, res) => { const { page, limit } = parsePagination(req.query, 30); const projectId = req.query.projectId as string | undefined; const toolName = req.query.toolName as string | undefined; const success = req.query.success as string | undefined; const dateStart = req.query.dateStart as string | undefined; const dateEnd = req.query.dateEnd as string | undefined; const where: any = {}; if (projectId) where.projectId = projectId; if (toolName) where.toolName = toolName; if (success === 'true') where.success = true; if (success === 'false') where.success = false; if (dateStart || dateEnd) { where.calledAt = {}; if (dateStart) where.calledAt.gte = new Date(dateStart); if (dateEnd) where.calledAt.lte = new Date(dateEnd); } const [logs, total] = await Promise.all([ prisma.mcpCallLog.findMany({ where, select: { id: true, toolName: true, calledAt: true, durationMs: true, success: true, responseSize: true, clientIp: true, estimatedTokens: true, project: { select: { id: true, name: true } }, }, orderBy: { calledAt: 'desc' }, skip: (page - 1) * limit, take: limit, }), prisma.mcpCallLog.count({ where }), ]); res.json({ success: true, data: { logs, total, page, limit } }); }); router.get('/call-logs/recent', async (_req, res) => { const logs = await prisma.mcpCallLog.findMany({ select: { id: true, toolName: true, calledAt: true, durationMs: true, success: true, project: { select: { name: true } }, }, orderBy: { calledAt: 'desc' }, take: 10, }); res.json({ success: true, data: logs }); }); export default router;