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"));