All files / web/src/app/api/admin/tasks route.ts

0% Statements 0/134
0% Branches 0/1
0% Functions 0/1
0% Lines 0/134

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135                                                                                                                                                                                                                                                                             
/**
 * Admin Tasks API
 *
 * GET /api/admin/tasks - List recent background tasks (without events for fast loading)
 * GET /api/admin/tasks?taskId=xxx - Get a single task with its events
 */

import { NextResponse } from 'next/server'
import { desc, eq } from 'drizzle-orm'
import { db, schema } from '@/db'
import { withAuth } from '@/lib/auth/withAuth'

export const GET = withAuth(
  async (request) => {
    const url = new URL(request.url)
    const taskId = url.searchParams.get('taskId')

    // Single task with events
    if (taskId) {
      const task = await db.query.backgroundTasks.findFirst({
        where: eq(schema.backgroundTasks.id, taskId),
      })

      if (!task) {
        return NextResponse.json({ error: 'Task not found' }, { status: 404 })
      }

      const events = await db
        .select({
          id: schema.backgroundTaskEvents.id,
          taskId: schema.backgroundTaskEvents.taskId,
          eventType: schema.backgroundTaskEvents.eventType,
          payload: schema.backgroundTaskEvents.payload,
          createdAt: schema.backgroundTaskEvents.createdAt,
        })
        .from(schema.backgroundTaskEvents)
        .where(eq(schema.backgroundTaskEvents.taskId, taskId))
        .orderBy(schema.backgroundTaskEvents.id)
        .limit(200)
        .all()

      return NextResponse.json({
        task: {
          id: task.id,
          type: task.type,
          status: task.status,
          progress: task.progress ?? 0,
          progressMessage: task.progressMessage,
          error: task.error,
          createdAt: task.createdAt,
          startedAt: task.startedAt,
          completedAt: task.completedAt,
          events: events.map((e) => ({ ...e, payload: sanitizePayload(e.payload) })),
        },
      })
    }

    // Task list (no events, no input/output blobs — fast)
    const limit = parseInt(url.searchParams.get('limit') ?? '50', 10)

    const taskList = await db
      .select({
        id: schema.backgroundTasks.id,
        type: schema.backgroundTasks.type,
        status: schema.backgroundTasks.status,
        progress: schema.backgroundTasks.progress,
        progressMessage: schema.backgroundTasks.progressMessage,
        error: schema.backgroundTasks.error,
        createdAt: schema.backgroundTasks.createdAt,
        startedAt: schema.backgroundTasks.startedAt,
        completedAt: schema.backgroundTasks.completedAt,
      })
      .from(schema.backgroundTasks)
      .orderBy(desc(schema.backgroundTasks.createdAt))
      .limit(Math.min(limit, 100))
      .all()

    const tasks = taskList.map((t) => ({
      ...t,
      progress: t.progress ?? 0,
      events: [],
    }))

    return NextResponse.json({ tasks })
  },
  { role: 'admin' }
)

/**
 * Remove or truncate large fields from event payloads
 */
function sanitizePayload(payload: unknown): unknown {
  if (payload === null || payload === undefined) return payload

  if (typeof payload === 'string') {
    // Truncate long strings
    return payload.length > 500 ? payload.substring(0, 500) + '...' : payload
  }

  if (typeof payload !== 'object') return payload

  const obj = payload as Record<string, unknown>
  const result: Record<string, unknown> = {}

  for (const [key, value] of Object.entries(obj)) {
    // Skip image data entirely
    if (key === 'imageDataUrl' || key === 'croppedDataUrl') {
      result[key] = '[IMAGE DATA OMITTED]'
      continue
    }

    // Skip accumulated text (can be very long)
    if (key === 'accumulated') {
      result[key] = '[ACCUMULATED TEXT OMITTED]'
      continue
    }

    // Handle nested objects
    if (typeof value === 'object' && value !== null) {
      result[key] = sanitizePayload(value)
      continue
    }

    // Truncate long strings
    if (typeof value === 'string' && value.length > 500) {
      result[key] = value.substring(0, 500) + '...'
      continue
    }

    result[key] = value
  }

  return result
}