diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index b2a4968fa..78baee06c 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -169,6 +169,20 @@ function formatElapsedDuration(elapsedMs: number): string { return `${elapsedDays}d`; } +export function formatElapsedDurationDone(elapsedMs: number): string { + if (!Number.isFinite(elapsedMs) || elapsedMs < 0) return ""; + if (elapsedMs === 0) return ""; + + const elapsedMinutes = Math.ceil(elapsedMs / 60_000); + if (elapsedMinutes < 59) return `${elapsedMinutes}m`; + + const elapsedHours = Math.ceil(elapsedMs / 3_600_000); + if (elapsedHours < 24) return `${elapsedHours}h`; + + const elapsedDays = Math.ceil(elapsedMs / 86_400_000); + return `${elapsedDays}d`; +} + interface TaskCardProps { task: Task; @@ -692,12 +706,12 @@ function TaskCardComponent({ return null; } - const elapsedLabel = formatElapsedDuration(instrumentedMs); - if (!elapsedLabel) { - return null; - } - if (task.column === "in-progress") { + const elapsedLabel = formatElapsedDuration(instrumentedMs); + if (!elapsedLabel) { + return null; + } + return { label: elapsedLabel, title: `Execution time ${elapsedLabel}`, @@ -705,6 +719,11 @@ function TaskCardComponent({ }; } + const elapsedLabel = formatElapsedDurationDone(instrumentedMs); + if (!elapsedLabel) { + return null; + } + const completionMs = getDoneCompletionMs(task); if (completionMs == null) { return { diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index 9c3e85846..6efa7fa8d 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -1,6 +1,6 @@ import { afterEach, describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; -import { TaskCard } from "../TaskCard"; +import { TaskCard, formatElapsedDurationDone } from "../TaskCard"; import type { Task } from "@fusion/core"; // Mock lucide-react to avoid SVG rendering issues in test env @@ -712,6 +712,72 @@ describe("TaskCard", () => { expect(container.querySelector(".card-time-indicator")).toBeNull(); }); + describe("formatElapsedDuration rounding for done tasks", () => { + it.each([ + [59_999, "1m"], + [60_000, "1m"], + [90_000, "2m"], + [3_540_000, "1h"], + [3_600_000, "1h"], + [86_400_000, "1d"], + ])("formats %dms as %s for done tasks", (elapsedMs, expected) => { + expect(formatElapsedDurationDone(elapsedMs)).toBe(expected); + }); + + it("keeps in-progress rounding with floor semantics", () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2026-04-25T12:01:30.000Z")); + + const { container } = render( + , + ); + + expect(container.querySelector(".card-time-indicator")?.textContent).toContain("1m"); + }); + + it("renders done-card timer with ceiling rounding for fractional minutes", () => { + const { container } = render( + , + ); + + expect(container.querySelector(".card-time-indicator")?.textContent).toContain("5m"); + }); + }); + it("live-ticks workflow runtime for in-progress steps", () => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-04-25T12:00:30.000Z"));