"use client" import { cn } from "@lib/utils" import { dmSans125ClassName } from "@/lib/fonts" import { useSyncRuns } from "@/hooks/use-sync-runs" import type { SyncRun } from "@/hooks/use-sync-runs" import { formatRelativeTime, TRIGGER_TYPE_LABELS, } from "@/components/settings/sync-utils" const STATUS_COLORS: Record = { completed: { dot: "bg-[#00AC3F]", text: "text-[#00AC3F]" }, failed: { dot: "bg-[#EF4444]", text: "text-[#EF4444]" }, running: { dot: "bg-[#4BA0FA] animate-pulse", text: "text-[#4BA0FA]" }, } function pluralize(count: number, noun: string) { return `${count} ${noun}${count === 1 ? "" : "s"}` } /** Calendar-day bucket label for grouping runs in the timeline. */ function dayLabel(date: string) { const d = new Date(date) if (Number.isNaN(d.getTime())) return "Unknown" const today = new Date() const startOfDay = (x: Date) => new Date(x.getFullYear(), x.getMonth(), x.getDate()).getTime() const diffDays = Math.round((startOfDay(today) - startOfDay(d)) / 86_400_000) if (diffDays <= 0) return "Today" if (diffDays === 1) return "Yesterday" if (diffDays < 7) return `${diffDays} days ago` return d.toLocaleDateString(undefined, { month: "short", day: "numeric" }) } function StatTile({ value, label }: { value: string; label: string }) { return (
{value} {label}
) } function SummaryStats({ runs }: { runs: SyncRun[] }) { const totalItems = runs.reduce((sum, r) => sum + r.itemsProcessed, 0) const finished = runs.filter((r) => r.status !== "running") const succeeded = finished.filter((r) => r.status === "completed").length const successRate = finished.length > 0 ? Math.round((succeeded / finished.length) * 100) : null return (
) } function TimelineRow({ run, isLast }: { run: SyncRun; isLast: boolean }) { const colors = STATUS_COLORS[run.status] ?? { dot: "bg-[#4BA0FA] animate-pulse", text: "text-[#4BA0FA]", } const triggerLabel = TRIGGER_TYPE_LABELS[run.triggerType] ?? run.triggerType const statusLabel = run.status.charAt(0).toUpperCase() + run.status.slice(1) return (
{/* Timeline rail: dot + connecting line */}
{!isLast &&
}
{/* Content */}
{statusLabel} · {triggerLabel}
{formatRelativeTime(run.startedAt)}
{(run.itemsProcessed > 0 || run.itemsFailed > 0) && (
{pluralize(run.itemsProcessed, "item")} processed {run.itemsFailed > 0 && ( {" "} · {run.itemsFailed} failed )}
)} {run.error && (

{run.error}

)}
) } function Timeline({ runs }: { runs: SyncRun[] }) { // Group consecutive runs by calendar-day label, preserving server order. const groups: { label: string; runs: SyncRun[] }[] = [] for (const run of runs) { const label = dayLabel(run.startedAt) const last = groups.at(-1) if (last && last.label === label) { last.runs.push(run) } else { groups.push({ label, runs: [run] }) } } return (
{groups.map((group, gi) => (
{group.label}
{group.runs.map((run, i) => ( ))}
))}
) } interface SyncHistoryPanelProps { connectionId: string /** Only fetch / render when expanded. */ isOpen: boolean } /** Inline sync-history view (stats strip + timeline) rendered inside an expanded connection row. */ export function SyncHistoryPanel({ connectionId, isOpen, }: SyncHistoryPanelProps) { const { data: syncRuns, isLoading, error, refetch, } = useSyncRuns(isOpen ? connectionId : "") if (!isOpen) return null const hasRuns = !isLoading && !error && syncRuns && syncRuns.length > 0 return (
{isLoading && (
)} {error && !isLoading && (
Failed to load sync history
)} {!isLoading && !error && syncRuns && syncRuns.length === 0 && (
No syncs yet — runs will appear here.
)} {hasRuns && ( <>
)}
) }