'use client' import { Fragment, useCallback, useEffect, useState } from 'react' import type { RunLogSummary, RunStatus } from '@/lib/parse-worker-log' import { cn, formatDuration, relativeTime } from '@/lib/utils' import RunLogDetail from './run-log-detail' const LIMIT_OPTIONS = [10, 25, 50, 100] const COLUMN_COUNT = 7 const STATUS_STYLES: Record = { idle: { badge: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400', dot: 'bg-zinc-400 dark:bg-zinc-500', }, running: { badge: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400', dot: 'bg-amber-500 dark:bg-amber-400', }, success: { badge: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400', dot: 'bg-green-500 dark:bg-green-400', }, error: { badge: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', dot: 'bg-red-500 dark:bg-red-400', }, 'token-expired': { badge: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400', dot: 'bg-red-500 dark:bg-red-400', }, unknown: { badge: 'bg-zinc-100 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400', dot: 'bg-zinc-400 dark:bg-zinc-500', }, } export function StatusBadge({ status }: { status: RunStatus }) { const s = STATUS_STYLES[status] return ( {status} ) } async function fetchLogs(limit: number): Promise { const res = await fetch(`/api/worker-logs?limit=${limit}`, { cache: 'no-store' }) const body = await res.json().catch(() => ({})) if (!res.ok) throw new Error(body?.error ?? `request failed (${res.status})`) return (body.logs ?? []) as RunLogSummary[] } type Props = { initialLogs: RunLogSummary[] initialError: string | null } export default function WorkerLogsView({ initialLogs, initialError }: Props) { const [logs, setLogs] = useState(initialLogs) const [limit, setLimit] = useState(10) const [selected, setSelected] = useState(null) const [error, setError] = useState(initialError) const [refreshing, setRefreshing] = useState(false) const [lastUpdated, setLastUpdated] = useState(new Date()) const refresh = useCallback(async () => { setRefreshing(true) try { const data = await fetchLogs(limit) setLogs(data) setError(null) setLastUpdated(new Date()) } catch (err) { setError(err instanceof Error ? err.message : 'refresh failed') } finally { setRefreshing(false) } }, [limit]) useEffect(() => { refresh() const id = setInterval(refresh, 10000) return () => clearInterval(id) }, [refresh]) return (
toon {LIMIT_OPTIONS.map((opt) => ( ))} {refreshing && ( refreshing… )}
updated {lastUpdated.toLocaleTimeString()} · auto-refreshes every 10s
{error && (
{error}
)}
{logs.length === 0 && !error ? ( ) : ( logs.map((log) => { const isSelected = selected === log.fileName return ( setSelected(isSelected ? null : log.fileName)} title={log.errorSummary ?? undefined} className={cn( 'cursor-pointer border-b border-border transition-colors', isSelected ? 'bg-muted/50' : 'hover:bg-muted/30', )} > {isSelected && ( )} ) }) )}
Started Status Job Model Turns Duration Cost
No worker runs found
{log.startedAt ? ( {relativeTime(new Date(log.startedAt))} ) : ( {log.runId} )} {log.jobId ? `…${log.jobId.slice(-8)}` : '—'} {log.model ?? '—'} {log.numTurns ?? '—'} {log.durationMs != null ? formatDuration(log.durationMs) : '—'} {log.totalCostUsd != null ? `$${log.totalCostUsd.toFixed(2)}` : '—'}
) }