164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { requireRole } from '@/lib/auth'
|
|
import { getDatabase } from '@/lib/db'
|
|
import { logger } from '@/lib/logger'
|
|
|
|
type Outcome = 'success' | 'failed' | 'partial' | 'abandoned'
|
|
|
|
function resolveSince(timeframe: string): number {
|
|
const now = Math.floor(Date.now() / 1000)
|
|
switch (timeframe) {
|
|
case 'day':
|
|
return now - 24 * 60 * 60
|
|
case 'week':
|
|
return now - 7 * 24 * 60 * 60
|
|
case 'month':
|
|
return now - 30 * 24 * 60 * 60
|
|
case 'all':
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
function outcomeBuckets() {
|
|
return {
|
|
success: 0,
|
|
failed: 0,
|
|
partial: 0,
|
|
abandoned: 0,
|
|
unknown: 0,
|
|
}
|
|
}
|
|
|
|
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 workspaceId = auth.user.workspace_id ?? 1
|
|
const { searchParams } = new URL(request.url)
|
|
const timeframe = (searchParams.get('timeframe') || 'all').trim().toLowerCase()
|
|
const since = resolveSince(timeframe)
|
|
|
|
const db = getDatabase()
|
|
const rows = db.prepare(`
|
|
SELECT
|
|
id,
|
|
assigned_to,
|
|
priority,
|
|
outcome,
|
|
error_message,
|
|
retry_count,
|
|
created_at,
|
|
completed_at
|
|
FROM tasks
|
|
WHERE workspace_id = ?
|
|
AND status = 'done'
|
|
AND (? = 0 OR COALESCE(completed_at, updated_at) >= ?)
|
|
`).all(workspaceId, since, since) as Array<{
|
|
id: number
|
|
assigned_to?: string | null
|
|
priority?: string | null
|
|
outcome?: string | null
|
|
error_message?: string | null
|
|
retry_count?: number | null
|
|
created_at?: number | null
|
|
completed_at?: number | null
|
|
}>
|
|
|
|
const summary = {
|
|
total_done: rows.length,
|
|
with_outcome: 0,
|
|
by_outcome: outcomeBuckets(),
|
|
avg_retry_count: 0,
|
|
avg_time_to_resolution_seconds: 0,
|
|
success_rate: 0,
|
|
}
|
|
|
|
const byAgent: Record<string, ReturnType<typeof outcomeBuckets> & { total: number; success_rate: number }> = {}
|
|
const byPriority: Record<string, ReturnType<typeof outcomeBuckets> & { total: number; success_rate: number }> = {}
|
|
const errorMap = new Map<string, number>()
|
|
|
|
let totalRetryCount = 0
|
|
let totalResolutionSeconds = 0
|
|
let resolutionCount = 0
|
|
|
|
for (const row of rows) {
|
|
const outcome = (row.outcome || 'unknown') as Outcome | 'unknown'
|
|
const assignedTo = row.assigned_to || 'unassigned'
|
|
const priority = row.priority || 'unknown'
|
|
const retryCount = Number.isFinite(row.retry_count) ? Number(row.retry_count) : 0
|
|
|
|
if (outcome !== 'unknown') summary.with_outcome += 1
|
|
if (outcome in summary.by_outcome) {
|
|
summary.by_outcome[outcome as keyof typeof summary.by_outcome] += 1
|
|
} else {
|
|
summary.by_outcome.unknown += 1
|
|
}
|
|
|
|
if (!byAgent[assignedTo]) {
|
|
byAgent[assignedTo] = { ...outcomeBuckets(), total: 0, success_rate: 0 }
|
|
}
|
|
byAgent[assignedTo].total += 1
|
|
if (outcome in byAgent[assignedTo]) {
|
|
byAgent[assignedTo][outcome as keyof ReturnType<typeof outcomeBuckets>] += 1
|
|
} else {
|
|
byAgent[assignedTo].unknown += 1
|
|
}
|
|
|
|
if (!byPriority[priority]) {
|
|
byPriority[priority] = { ...outcomeBuckets(), total: 0, success_rate: 0 }
|
|
}
|
|
byPriority[priority].total += 1
|
|
if (outcome in byPriority[priority]) {
|
|
byPriority[priority][outcome as keyof ReturnType<typeof outcomeBuckets>] += 1
|
|
} else {
|
|
byPriority[priority].unknown += 1
|
|
}
|
|
|
|
totalRetryCount += retryCount
|
|
|
|
if (row.completed_at && row.created_at && row.completed_at >= row.created_at) {
|
|
totalResolutionSeconds += (row.completed_at - row.created_at)
|
|
resolutionCount += 1
|
|
}
|
|
|
|
const errorMessage = (row.error_message || '').trim()
|
|
if (errorMessage) {
|
|
errorMap.set(errorMessage, (errorMap.get(errorMessage) || 0) + 1)
|
|
}
|
|
}
|
|
|
|
summary.avg_retry_count = rows.length > 0 ? totalRetryCount / rows.length : 0
|
|
summary.avg_time_to_resolution_seconds = resolutionCount > 0 ? totalResolutionSeconds / resolutionCount : 0
|
|
summary.success_rate = summary.with_outcome > 0 ? summary.by_outcome.success / summary.with_outcome : 0
|
|
|
|
for (const agent of Object.values(byAgent)) {
|
|
const withOutcome = agent.total - agent.unknown
|
|
agent.success_rate = withOutcome > 0 ? agent.success / withOutcome : 0
|
|
}
|
|
|
|
for (const priority of Object.values(byPriority)) {
|
|
const withOutcome = priority.total - priority.unknown
|
|
priority.success_rate = withOutcome > 0 ? priority.success / withOutcome : 0
|
|
}
|
|
|
|
const commonErrors = [...errorMap.entries()]
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 10)
|
|
.map(([error_message, count]) => ({ error_message, count }))
|
|
|
|
return NextResponse.json({
|
|
timeframe,
|
|
summary,
|
|
by_agent: byAgent,
|
|
by_priority: byPriority,
|
|
common_errors: commonErrors,
|
|
record_count: rows.length,
|
|
})
|
|
} catch (error) {
|
|
logger.error({ err: error }, 'GET /api/tasks/outcomes error')
|
|
return NextResponse.json({ error: 'Failed to fetch task outcomes' }, { status: 500 })
|
|
}
|
|
}
|