From 3b55e2c96ce5219584321af33f5ee4e5efbd1359 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 00:36:09 -0700 Subject: [PATCH] fix(dashboard): the time indicator was gated on a hardcoded legacy lane set (#3014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## This is a correction to #2996, and that's why it exists #2996 fixed the **subscription**: `wantsLiveTimeIndicator` kept a pre-load answer, so the card never joined the shared ticker. I described it as making renamed-lane cards *"show their live elapsed-time indicator."* It made them **eligible to**. They still rendered nothing, because a second gate rejects them first — and I didn't look past the seam I'd just fixed. ```ts const TIME_INDICATOR_COLUMNS = new Set(["in-progress", "in-review", "done"]); ``` Both the `timeIndicator` memo and the `chipFarRight` layout test `task.column` against that set directly, so a card in a renamed WIP, review or completion lane returns `null` whatever its resolved traits say. ## Why no check saw it The census counts **comparisons** against legacy ids. This is a `Set` literal — a **definition**. Nothing in the backlog ever pointed here, which is the same blind spot that hid `BLOCKER_ESCALATION_COLUMNS` until someone read the code rather than the report. ## The fix The gate becomes a role question, with the legacy set kept as the **no-flags fallback** and marked `DELIBERATE-LITERAL`. A card whose traits haven't resolved — first paint, or a lane its workflow no longer declares — behaves exactly as before. ## Measured | check | result | |---|---| | test written first | red for the right reason — control and negative passed, only the renamed case failed (`expected false to be true`) | | after the fix | 3 passed | | reverting the memo gate to the raw set | that case fails again | | five `TaskCard` suites | **418 tests green** | | gates | all five green; lint and `tsc` clean | ## The negative case Resolving traits must not put a live timer on every lane. A card in the renamed **intake** lane hasn't started, so it stays out — otherwise the fix trades a missing indicator for a running clock on work that hasn't begun. ## Worth noting for the pattern Two of my last four findings came from re-examining my own merged work rather than from new code: this one, and the `bounded` heuristic correction in #3012. Fixing one seam and declaring the symptom gone is its own failure mode — the user-visible behaviour needed *both* halves, and I only checked the half I'd touched. Co-authored-by: Claude Opus 5 (1M context) --- .../taskcard-time-indicator-renamed-lanes.md | 7 ++ .../dashboard/app/components/TaskCard.tsx | 24 ++++- ...Card.time-indicator-renamed-lanes.test.tsx | 90 +++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 .changeset/taskcard-time-indicator-renamed-lanes.md create mode 100644 packages/dashboard/app/components/__tests__/TaskCard.time-indicator-renamed-lanes.test.tsx diff --git a/.changeset/taskcard-time-indicator-renamed-lanes.md b/.changeset/taskcard-time-indicator-renamed-lanes.md new file mode 100644 index 0000000000..858d8a728c --- /dev/null +++ b/.changeset/taskcard-time-indicator-renamed-lanes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Cards in renamed in-progress, review or completion lanes now show their elapsed-time indicator. +category: fix +dev: The `TIME_INDICATOR_COLUMNS` legacy id set gated both the indicator memo and the chip layout; it is now a role question with that set kept as the no-flags fallback. diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index e96da7faf8..9e1c638023 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -1063,6 +1063,26 @@ function TaskCardComponent({ const isCompleteColumn = isCompleteColumnRole(taskColumnFlags, task.column); const isArchivedColumn = isArchivedColumnRole(taskColumnFlags, task.column); + /* + FNXC:WorkflowResolvedColumns 2026-07-31-03:15: + THE TIME-INDICATOR GATE IS A ROLE QUESTION, not membership of a hardcoded id set. + + `TIME_INDICATOR_COLUMNS` is `{in-progress, in-review, done}`, so a card in a renamed WIP, review or + completion lane was rejected before its traits were consulted and rendered no elapsed time at all. + #2996 fixed the SUBSCRIPTION for these cards — the memo that decides whether to join the shared + ticker kept a pre-load answer — which made them eligible and still not visible, because this gate + rejects them first. Both halves are needed; that PR's claim covered only one. + + The census could not point here: it counts COMPARISONS against legacy ids, and this is a Set + literal, which is a DEFINITION. Same blind spot that hid `BLOCKER_ESCALATION_COLUMNS`. + + DELIBERATE-LITERAL — the legacy set stays as the no-flags fallback, so a card whose traits have not + resolved (first paint, or a lane its workflow no longer declares) behaves exactly as before. + */ + const showsTimeIndicator = taskColumnFlags + ? isWipColumn || isReviewColumn || isCompleteColumn + : TIME_INDICATOR_COLUMNS.has(task.column); + const [isSaving, setIsSaving] = useState(false); const [showSteps, setShowSteps] = useState( isWipColumn || @@ -1754,7 +1774,7 @@ function TaskCardComponent({ const timeIndicatorNowMs = useLiveTimeTicker(wantsLiveTimeIndicator); const timeIndicator = useMemo(() => { - if (!TIME_INDICATOR_COLUMNS.has(task.column)) { + if (!showsTimeIndicator) { return null; } @@ -3129,7 +3149,7 @@ function TaskCardComponent({ return null; })(); - const chipFarRight = TIME_INDICATOR_COLUMNS.has(task.column) + const chipFarRight = showsTimeIndicator && filesChangedButton == null && showTrackingIndicator && Boolean(githubTrackedIssue); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.time-indicator-renamed-lanes.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.time-indicator-renamed-lanes.test.tsx new file mode 100644 index 0000000000..0a80d47432 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/TaskCard.time-indicator-renamed-lanes.test.tsx @@ -0,0 +1,90 @@ +/* +FNXC:WorkflowResolvedColumns 2026-07-31-03:10: +THE TIME INDICATOR IS GATED ON A HARDCODED LEGACY LANE SET, so it never renders on a renamed board. + +`TIME_INDICATOR_COLUMNS` is `{in-progress, in-review, done}`. Both the `timeIndicator` memo and the +`chipFarRight` layout test `task.column` against it directly, so a card in a renamed WIP, review or +completion lane returns `null` no matter what its resolved traits say. + +WHY THIS IS A SEPARATE DEFECT FROM #2996, and a correction to that PR's claim: #2996 fixed the +SUBSCRIPTION — `wantsLiveTimeIndicator` kept a pre-load answer and the card never joined the shared +ticker. That was real, and it was not sufficient. The card now subscribes and still renders nothing, +because this gate rejects it first. I described that PR as making renamed-lane cards "show their live +elapsed-time indicator"; it made them eligible to. + +WHY NO CHECK SAW IT. The census counts COMPARISONS against legacy ids. This is a Set literal — a +DEFINITION — so nothing in the backlog ever pointed here, the same blind spot that hid +`BLOCKER_ESCALATION_COLUMNS` until it was found by hand. +*/ + +import { describe, it, expect, vi } from "vitest"; +import { render } from "@testing-library/react"; +import type { Task } from "@fusion/core"; +import { TaskCard } from "../TaskCard"; + +vi.mock("../../hooks/useLiveTimeTicker", () => ({ + useLiveTimeTicker: () => Date.parse("2026-06-01T01:00:00.000Z"), +})); + +const noop = () => {}; + +function runningTaskIn(column: string): Task { + return { + id: "KB-1", + title: "a running card", + description: "t", + column, + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt: "2026-06-01T00:00:00.000Z", + executionStartedAt: "2026-06-01T00:00:00.000Z", + cumulativeActiveMs: 120_000, + timedExecutionMs: 120_000, + steps: [], + } as unknown as Task; +} + +/** The indicator renders an elapsed duration; any of these means it is present. */ +const hasDuration = (root: HTMLElement) => /\d+\s*(s|m|h)\b|\d+:\d\d/.test(root.textContent ?? ""); + +describe("the card time indicator under a renamed board vocabulary", () => { + /* Control: the legacy vocabulary renders it, with no flags at all. */ + it("default vocabulary: a card in `in-progress` shows an elapsed time", () => { + const { container } = render( + , + ); + + expect(hasDuration(container as unknown as HTMLElement)).toBe(true); + }); + + /* The defect: the renamed WIP lane is not in the hardcoded set, so nothing renders. */ + it("renamed vocabulary: a card whose traits say WIP shows an elapsed time", () => { + const { container } = render( + , + ); + + expect(hasDuration(container as unknown as HTMLElement)).toBe(true); + }); + + /* + The paired negative: resolving traits must not put a live timer on every lane. A card in the + renamed INTAKE lane has not started, so it must stay out — otherwise the fix trades a missing + indicator for a running clock on work that has not begun. + */ + it("renamed vocabulary: a card in the intake lane shows no elapsed time", () => { + const { container } = render( + , + ); + + expect(hasDuration(container as unknown as HTMLElement)).toBe(false); + }); +});