From f0875a79c63e1e3b61cec2dd793fc3a9f2aa6080 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 00:47:14 -0700 Subject: [PATCH] fix(dashboard): finished cards on a renamed board never showed a completion date (#3007) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The **fifth** instance of the async-memo shape #2998 documents, and the one that survived #3001's sweep. `lifecycleDates` gates its `completed` value on `isCompleteColumn || isArchivedColumn` — both derived from the async `taskColumnFlags` prop — while listing neither: ```js }, [task.createdAt, task.executionCompletedAt, task.archivedAt, task.column, locale, lifecycleNowMs]); ``` First paint runs with the flags undefined, the role helpers fall back to the legacy ids, and on a board whose complete lane is named anything but `done` that answers false. The flags arrive, `task.column` has not changed, nothing recomputes, and the card renders **no "Completed " line at all**. ## Why #3001's sweep called this covered That PR recorded `mergeSignature` as *"the last live site … nine persistent candidates, seven covered transitively or by a dependency that already carries the flags."* This memo was presumably in the covered pile, and the reasoning is nearly right: it **does** list a dependency that changes — `lifecycleNowMs`. But that value is driven by a timer scheduled with `millisecondsUntilNextLocalMidnight` (FN-8561, so compact date labels turn over at the viewer's midnight). **A dependency that changes once a day is not coverage for a value that must be correct on first paint.** The card shows no completion date for the rest of the session. That distinction is worth adding to the doc's property 2: *does a listed dependency change* is the wrong question — *does it change when the resolved value arrives* is the right one. ## Verification | state | result | |---|---| | clean | 2/2 pass | | revert the dep fix | **1 failed / 1 passed** | The control case (a `done` board) passes either way by design, so a failure in the renamed case means "renamed board", not "nothing renders". **One trap worth recording**, since it nearly cost me the finding: my first `completedLine()` used `time[datetime]:last-of-type`. When only the *Created* line renders, that selector returns **that** element — so the pre-resolution absence assertion silently passed against the wrong node. The test now matches on the element's own `Completed` label. A positional selector cannot express "this specific line is missing". `tsc -p tsconfig.app.json` 0 errors, lint clean, 8/8 across all three renamed-lane TaskCard suites. ## Note `main` is currently red on the FNXC gate for an unrelated reason (#2994's impossible-hour stamps landing after #2995); fixed in #3006. Co-authored-by: Claude Opus 5 (1M context) --- .../taskcard-lifecycle-dates-renamed-lanes.md | 7 ++ .../dashboard/app/components/TaskCard.tsx | 20 +++- ...ard.lifecycle-dates-renamed-lanes.test.tsx | 105 ++++++++++++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 .changeset/taskcard-lifecycle-dates-renamed-lanes.md create mode 100644 packages/dashboard/app/components/__tests__/TaskCard.lifecycle-dates-renamed-lanes.test.tsx diff --git a/.changeset/taskcard-lifecycle-dates-renamed-lanes.md b/.changeset/taskcard-lifecycle-dates-renamed-lanes.md new file mode 100644 index 0000000000..373d1347fd --- /dev/null +++ b/.changeset/taskcard-lifecycle-dates-renamed-lanes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Finished cards on a renamed board now show their completion date. +category: fix +dev: `lifecycleDates` in TaskCard omitted `isCompleteColumn`/`isArchivedColumn` from its dependency list; both derive from the async `taskColumnFlags` prop. diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index 9e1c638023..f0f47b2162 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -1863,7 +1863,25 @@ function TaskCardComponent({ ? formatCompactLifecycleDate(completionSource, locale, new Date(lifecycleNowMs)) : null; return { created, completed }; - }, [task.createdAt, task.executionCompletedAt, task.archivedAt, task.column, locale, lifecycleNowMs]); + /* + FNXC:WorkflowResolvedColumns 2026-07-31-08:20: + `isCompleteColumn` AND `isArchivedColumn` BELONG IN THIS LIST — both derive from the async + `taskColumnFlags` prop, and the completion date is gated on them. + + The board resolves workflow traits after first paint, so the first computation runs with the flags + undefined and the role helpers fall back to the legacy ids. On a renamed board that answers false, + `completed` is null, and the "Completed " line never renders. When the flags arrive nothing in + the old list had changed — every entry was a `task.*` field, `locale`, or `lifecycleNowMs`. + + WHY THE SWEEP IN #3001 CALLED THIS COVERED: `lifecycleNowMs` does change, so the memo does + eventually recompute — but its timer fires at the viewer's LOCAL MIDNIGHT (see the + `millisecondsUntilNextLocalMidnight` effect). A dependency that turns over once a day is not + coverage for a value that must be right on first paint; the card shows no completion date for the + rest of the session. + + A default board hides it: `column === "done"` is already true before the flags land. + */ + }, [task.createdAt, task.executionCompletedAt, task.archivedAt, task.column, locale, lifecycleNowMs, isCompleteColumn, isArchivedColumn]); const liveBadgeData = badgeUpdates.get(`${projectId ?? "default"}:${task.id}`); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.lifecycle-dates-renamed-lanes.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.lifecycle-dates-renamed-lanes.test.tsx new file mode 100644 index 0000000000..0fa344f1ed --- /dev/null +++ b/packages/dashboard/app/components/__tests__/TaskCard.lifecycle-dates-renamed-lanes.test.tsx @@ -0,0 +1,105 @@ +/* +FNXC:WorkflowResolvedColumns 2026-07-31-08:25: +THE "Completed " LINE NEVER APPEARED ON A RENAMED BOARD. + +`lifecycleDates` gates its `completed` value on `isCompleteColumn || isArchivedColumn`, both derived +from the `taskColumnFlags` PROP. The board resolves workflow traits asynchronously, so the first +computation runs with the flags undefined, the role helpers fall back to the legacy ids, and on a +board whose complete lane is named anything but `done` the answer is false. + +The dependency list held only `task.*` fields, `locale` and `lifecycleNowMs`, so when the flags +arrived nothing invalidated and the card kept rendering no completion date. + +WHY THIS SURVIVED THE #3001 SWEEP, which recorded `mergeSignature` as the last live site: this memo +DOES list a dependency that changes — `lifecycleNowMs`. But its timer fires at the viewer's local +midnight, so "eventually recomputes" means once a day. A value that must be correct on first paint is +not covered by a dependency that turns over daily. + +Asserted through the rendered `