"use client"; /** * Automations — RUN HISTORY (shared by the list row-expander and the detail * page). Lists the automation's runs (GET /automations/:id/runs?limit=50, * `{ data, nextCursor }`), each row showing status (lowercase text — * "succeeded"/"failed"/"dead-letter"), trigger type, startedAt, duration and * retryCount. Clicking a run expands its logs * (GET /automations/:id/runs/:runId → `{ …, logs: [{level, message, data, * createdAt}] }`) as a monospace console panel. FAILED/DEAD_LETTER runs get a * Retry button (POST …/runs/:runId/retry). */ import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2, RefreshCw } from "lucide-react"; import { apiFetch } from "@/lib/api"; import { toast } from "@/stores/ui-store"; import { formatRelativeTime } from "@/lib/utils"; import { runStatusLabel, runStatusCls, type RunDetail, type RunLog, type RunSummary } from "./lib"; interface RunHistoryProps { automationId: string; /** Compact variant (inside a list row): tighter spacing. */ compact?: boolean; } export function RunHistory({ automationId, compact }: RunHistoryProps) { const [expanded, setExpanded] = useState(null); const runs = useQuery({ queryKey: ["automations", automationId, "runs"], queryFn: async (): Promise => { const res = await apiFetch<{ data: RunSummary[]; nextCursor: string | null }>( `/automations/${automationId}/runs?limit=50` ); return res.data; }, enabled: !!automationId, // Poll while a run is in flight so "Run now" / retries settle visibly. refetchInterval: (q) => (q.state.data?.some((r) => r.status === "PENDING" || r.status === "RUNNING") ? 4000 : false) }); const page = runs.data ?? []; return (
    {runs.isLoading &&
  • Loading runs…
  • } {!runs.isLoading && page.length === 0 && (
  • No runs yet. Use the Run now button to trigger one.
  • )} {page.map((r) => (
  • {(r.status === "FAILED" || r.status === "DEAD_LETTER") && ( )}
    {expanded === r.id && }
  • ))}
); } function RetryButton({ automationId, runId }: { automationId: string; runId: string }) { const qc = useQueryClient(); const [busy, setBusy] = useState(false); return ( ); } function RunLogs({ automationId, runId }: { automationId: string; runId: string }) { const detail = useQuery({ queryKey: ["automations", automationId, "run", runId], queryFn: async (): Promise => { // The service returns the run object directly (summary fields spread + // input/output/logs); tolerate an envelope `{ run }` shape too. const res = await apiFetch(`/automations/${automationId}/runs/${runId}`); return res.run ? res.run : res; } }); if (detail.isLoading) return

Loading logs…

; if (detail.isError) return

Failed to load logs: {(detail.error as Error).message}

; const logs: RunLog[] = detail.data?.logs ?? []; return (
{detail.data?.error != null &&

error: {jsonText(detail.data.error)}

} {logs.length === 0 &&

no log lines

} {logs.map((l, i) => (

{new Date(l.createdAt).toISOString()} {String(l.level).toUpperCase().padEnd(5)}{" "} {l.message} {l.data != null ? {jsonText(l.data)} : null}

))}
); } function levelColor(level: string): string { switch (level?.toUpperCase()) { case "ERROR": return "text-red-400"; case "WARN": return "text-amber-300"; case "INFO": return "text-sky-300"; default: return "text-zinc-300"; } } function jsonText(v: unknown): string { try { return typeof v === "string" ? v : JSON.stringify(v); } catch { return String(v); } } function durationOf(r: RunSummary): string { if (!r.startedAt || !r.finishedAt) return ""; const ms = new Date(r.finishedAt).getTime() - new Date(r.startedAt).getTime(); if (!Number.isFinite(ms) || ms < 0) return ""; return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`; }