fix(resources): stop 'Updated Xs' timer resetting on no-op refetches#572
fix(resources): stop 'Updated Xs' timer resetting on no-op refetches#572eliran-ops wants to merge 5 commits intomainfrom
Conversation
Two related list-refresh UX bugs reported in SKY-820:
10. Age values in the resource list felt like they shifted between
refreshes, eroding trust in the column.
16. The header "Updated 8s" timer briefly reset to "Updated <1s" when
reopening filters, suggesting a full data re-fetch was triggered for
a UI-only action.
Root cause for bug 16: the effect that drives `lastUpdated` was keyed on
React Query's `dataUpdatedAt`, which bumps on every successful fetch even
when the response is byte-identical (structural sharing returns the same
data reference). Mounting / window-focus / a sibling subscriber issuing
the same queryKey can all trigger a no-op refetch — and each one bumps
the visible timer back to "<1s".
Bug 10 root cause is harder to nail without a live repro (kubernetes
genuinely returns new pods for the same name on rollout, so some shifts
are real), but the column gave users no fixed reference to verify
against.
This change:
- New `useNow(intervalMs)` hook in `packages/k8s-ui/src/hooks/useNow.ts`.
Returns `Date.now()`, refreshing on the configured cadence. Opt-out
with `null`/`<=0`. Tested with fake timers (5 cases).
- `ResourcesView` "Updated Xs" effect now only bumps `lastUpdated` when
the `resources` reference actually changed — no more reset on no-op
refetches.
- `ResourcesView` calls `useNow(1000)` so the "Updated Xs" label
advances smoothly every second instead of feeling frozen until an
unrelated re-render.
- Age cell is wrapped in a Tooltip showing the absolute
`creationTimestamp.toLocaleString()` — parity with the resource
drawer. Gives users a stable ground-truth reference when relative
ages feel inconsistent.
Linear: SKY-820
Made-with: Cursor
Cursor Bugbot pointed out that the previous useNow.test.ts never imported or invoked useNow — every test re-implemented the branch logic against the global setInterval/clearInterval, so the hook itself was untested and the file gave a false impression of coverage. Refactor: - Extract `shouldScheduleNow(intervalMs)` from the hook as a pure predicate. The branch decision (null / 0 / negative all opt out; positive ticks) was previously buried inside the effect. - The test file now actually imports `useNow` (so accidental signature breaks fail the build) and exercises `shouldScheduleNow` directly, giving real coverage of the rule the hook depends on. Behaviour of `useNow` is unchanged. Made-with: Cursor
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 031411d. Configure here.
Cursor Bugbot caught: calling `useNow(1000)` directly inside `ResourcesView` (~4000 lines, contains a virtualized table) was re-rendering the entire component tree once per second just to advance one "Updated 8s" / "Updated 1m" label in the toolbar. Every visible TableVirtuoso row re-ran its render path on every tick. Extract a tiny `LastUpdatedLabel` component that calls `useNow(1000)` internally; the parent ResourcesView no longer ticks. The label still advances smoothly once per second; only this two-element subtree re-renders. No behaviour change, just scope reduction. Made-with: Cursor
|
Ran a full review pass — code/silent-failure checks are clean, two real findings to address before merge: 1. Comments reference PR/ticket numbers (will rot)CLAUDE.md says comments must never reference current task / PR / issue. Several do:
The substantive WHYs (React Query bumps 2. The actual fix is untestedYou extracted // pure helper next to shouldScheduleNow
export function shouldBumpLastUpdated(dataUpdatedAt: number, resources: unknown, lastRef: unknown): booleanThen 3 tests: no bump on identical reference, bump on new reference, no bump when The OtherwiseThe reference-stable |
Bugbot HIGH: kindToPlural('hpas') was falling through to
englishPlural which appends 'es' (trailing s) → 'hpases', which
matched nothing in KNOWN_KINDS or the dispatch table. Since
ResourcesSidebar uses { kind: 'hpas' } as a primary key (the
dispatch even hard-codes 'hpas' || 'horizontalpodautoscalers' for
HPARenderer), clicking HPAs from the sidebar silently rendered
nothing — the same 'URL updates but no detail panel' regression
this PR set out to fix for CronJobs.
Add 'hpas' to BUILTIN_PLURAL_TO_KIND so kindToPlural('hpas')
hits the idempotence path and returns unchanged. Pin with a
regression test referencing the dispatch coupling.
Also: extract formatZoomLabel to packages/k8s-ui/src/utils/zoom-label.ts
so the test imports the production implementation instead of
duplicating it (the inline copy gave no regression protection).
TimelineSwimlanes re-exports from the new shared module.
Strip SKY-826 / bug-9 trailers from the renderer-dispatch comments
per CLAUDE.md (same pattern flagged on #584 / #572).
Made-with: Cursor
Bugbot noted that the v2 useNow.test.ts never imported or invoked
useNow — it re-implemented the branch logic inline against
setInterval/clearInterval and gave false coverage.
Hoist the entire effect body into a pure scheduleNowTicks(intervalMs,
setNow, timers) function. The hook is now a thin wrapper:
useEffect(() => scheduleNowTicks(intervalMs, setNow, {
setInterval: globalThis.setInterval,
clearInterval: globalThis.clearInterval,
now: Date.now,
}), [intervalMs])
The test now exercises the SAME function the hook installs at
runtime — opt-out for null/0/negative, setInterval call shape,
the cleanup contract, and the contract that the tick reads
'now()' freshly each fire (not the value captured at mount). Plus
the existing predicate + export-shape checks.
Also strip CLAUDE.md rot trailers per the same review:
- LastUpdatedLabel JSDoc — drop 'Cursor Bugbot caught this on PR #572'
- dataUpdatedAt useEffect — drop '(SKY-820 / bug 16)' trailer
- delete the duplicated block above LastUpdatedLabel that just
repeated the JSDoc 30 lines down
- age-tooltip comment — drop 'Reported in SKY-820'
Made-with: Cursor
|
@nadaverell @hisco — ready for review. Cursor bugbot found 2 issues: tests didn't exercise the |

Summary
Fixes two reported list-refresh UX bugs on app.radarhq.io. Linear: SKY-820.
The bugs:
Root cause
Bug 16: the effect that drives
lastUpdatedwas keyed on React Query'sdataUpdatedAt, which bumps on every successful fetch — even when the response is byte-identical and structural sharing returns the samedatareference. Mounting / window-focus / a sibling subscriber issuing the same queryKey can all trigger a no-op refetch, and each one reset the visible timer to "<1s".Bug 10: harder to nail without a live repro (Kubernetes does genuinely return new pods for the same name on rollout, so some apparent "shifts" are real). The column gave users no fixed reference to verify against.
Fix
useNow(intervalMs)hook inpackages/k8s-ui/src/hooks/useNow.ts. ReturnsDate.now(), refreshing on the configured cadence. Opt-out withnull/<=0. Tested with fake timers (5 cases).ResourcesView"Updated Xs" effect now only bumpslastUpdatedwhen theresourcesreference actually changed — no more reset on no-op refetches.ResourcesViewcallsuseNow(1000)so the "Updated Xs" label advances smoothly every second instead of feeling frozen until an unrelated re-render.creationTimestamp.toLocaleString()— parity with the resource drawer. Gives users a stable ground-truth reference when relative ages feel inconsistent.Test plan
cd packages/k8s-ui && npm test— 73 passed (5 new foruseNow).cd web && npm run tsc— clean.cd web && npm run build— clean.Notes
formatAge(creationTimestamp)is monotonic given the same input. The most likely cause is a real pod re-creation on rollout (same name, newcreationTimestamp). The Tooltip-with-absolute-timestamp gives users a way to disambiguate that themselves; if the user can repro a case where the tooltip shows a stable absolute timestamp but the relative age shifts non-monotonically, please attach a HAR — that would point at a separate bug worth a follow-up.Made with Cursor
Note
Low Risk
UI/UX-only changes plus a small new hook; main risk is minor performance or render-frequency regressions around the resources table and timer behavior.
Overview
Fixes the
ResourcesViewtoolbar “Updated Xs” indicator so it only resets when the underlyingresourcesreference actually changes, avoiding misleading timer resets on React Query no-op refetches.Introduces a new
useNowhook (exported fromhooks/index.tsand unit-tested) and moves the ticking clock into a smallLastUpdatedLabelcomponent so only the label re-renders every second, not the full virtualized table.Updates the
agecell to show a tooltip with the absolutecreationTimestamp(and-when missing) to give users a stable reference alongside the relative age string.Reviewed by Cursor Bugbot for commit ba0a48d. Bugbot is set up for automated code reviews on this repo. Configure here.