From a989eca4bf9f64e22e1d0e23f5dc646eda8ca5e8 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 28 Sep 2026 22:20:51 +0200 Subject: [PATCH] fix(dashboard): refresh expiry badges when the sources rail is re-enabled A boundary can pass while the rail is off, and an expired source has no next boundary to wait for, so re-enabling the rail now reads the clock at once instead of keeping the badge cached while it was disabled. Co-Authored-By: Claude Opus 5.5 --- .../dashboard-source-expiry-clock.spec.ts | 17 +++++++++++++++++ .../rails/dashboard-source-expiry-clock.ts | 19 ++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-source-expiry-clock.spec.ts b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-source-expiry-clock.spec.ts index 1fef554da..8b8c72843 100644 --- a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-source-expiry-clock.spec.ts +++ b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-source-expiry-clock.spec.ts @@ -126,4 +126,21 @@ describe('createSourceExpiryClock', () => { TestBed.tick(); expect(jest.getTimerCount()).toBe(1); }); + + it('reads the clock at once when the sources rail is re-enabled', () => { + active.set(false); + TestBed.tick(); + // Past the expiry while the rail is off: no boundary is left to wait + // for, so only the re-enable can refresh the cached badge. + facts.set(new Map([['xtream', expiringIn(60_000)]])); + TestBed.tick(); + jest.advanceTimersByTime(3 * 60_000); + const before = clock(); + + active.set(true); + TestBed.tick(); + + expect(clock()).toBe(Date.now()); + expect(clock()).toBeGreaterThan(before); + }); }); diff --git a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-source-expiry-clock.ts b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-source-expiry-clock.ts index 5a0c3a19d..b552e2393 100644 --- a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-source-expiry-clock.ts +++ b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-source-expiry-clock.ts @@ -1,4 +1,11 @@ -import { DestroyRef, effect, inject, signal, type Signal } from '@angular/core'; +import { + DestroyRef, + effect, + inject, + signal, + untracked, + type Signal, +} from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { nextSourceExpiryChangeMs, @@ -54,6 +61,16 @@ export function createSourceExpiryClock( onCleanup(() => clearTimeout(timer)); }); + // Re-enabling the rail must not show a badge cached while it was off: a + // boundary may have passed (an expired source has no next boundary to + // wait for), so read the clock at once, as becoming visible does. + let wasActive = untracked(active); + effect(() => { + const isActive = active(); + if (isActive && !wasActive) untracked(() => now.set(Date.now())); + wasActive = isActive; + }); + const onVisibilityChange = () => { visible.set(!document.hidden); if (!document.hidden) now.set(Date.now());