diff --git a/libs/playlist/shared/ui/src/lib/source-row/source-clock.ts b/libs/playlist/shared/ui/src/lib/source-row/source-clock.ts new file mode 100644 index 000000000..8838d7c3d --- /dev/null +++ b/libs/playlist/shared/ui/src/lib/source-row/source-clock.ts @@ -0,0 +1,22 @@ +import { DestroyRef, Injectable, inject, signal } from '@angular/core'; + +/** Often enough for durations shown in whole hours and days. */ +export const SOURCE_CLOCK_TICK_MS = 60_000; + +/** + * One shared minute clock for source rows, so "Not responding ยท 3 days" + * moves on while a list stays open, without a timer per row. + */ +@Injectable({ providedIn: 'root' }) +export class SourceClock { + private readonly state = signal(Date.now()); + readonly now = this.state.asReadonly(); + + constructor() { + const timer = setInterval( + () => this.state.set(Date.now()), + SOURCE_CLOCK_TICK_MS + ); + inject(DestroyRef).onDestroy(() => clearInterval(timer)); + } +} diff --git a/libs/playlist/shared/ui/src/lib/source-row/source-row.component.spec.ts b/libs/playlist/shared/ui/src/lib/source-row/source-row.component.spec.ts index b784566db..2073b9fac 100644 --- a/libs/playlist/shared/ui/src/lib/source-row/source-row.component.spec.ts +++ b/libs/playlist/shared/ui/src/lib/source-row/source-row.component.spec.ts @@ -1,8 +1,10 @@ +import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; import { SourceStatusService } from '@iptvnator/portal/shared/data-access'; import { PlaylistMeta, SourceStatus } from '@iptvnator/shared/interfaces'; +import { SourceClock } from './source-clock'; import { SourceRowComponent } from './source-row.component'; const M3U: PlaylistMeta = { @@ -132,6 +134,24 @@ describe('SourceRowComponent', () => { ).toBe('SOURCE_HEALTH.REASON.network'); }); + it("keeps counting a failure's duration while the list stays open", () => { + const start = Date.UTC(2026, 9, 13, 12); + const now = signal(start); + TestBed.overrideProvider(SourceClock, { useValue: { now } }); + statuses.set('m3u', { + reachability: 'bad', + failingSince: start - 10 * 60_000, + }); + const element = render(M3U); + const tag = () => + element.querySelector('.source-row__tag')?.textContent?.trim(); + expect(tag()).toBe('WORKSPACE.SOURCES.ROW.UNREACHABLE'); + + now.set(start + 2 * 60 * 60_000); + fixture.detectChanges(); + expect(tag()).toBe('WORKSPACE.SOURCES.ROW.UNREACHABLE_FOR'); + }); + it('opens on click and on Enter, but not while busy', () => { const element = render(M3U); const opened = jest.fn(); diff --git a/libs/playlist/shared/ui/src/lib/source-row/source-row.component.ts b/libs/playlist/shared/ui/src/lib/source-row/source-row.component.ts index 1febf5dbd..a6b11f453 100644 --- a/libs/playlist/shared/ui/src/lib/source-row/source-row.component.ts +++ b/libs/playlist/shared/ui/src/lib/source-row/source-row.component.ts @@ -35,6 +35,7 @@ import { sourceMetaParts, sourceStatusTag, } from '@iptvnator/playlist/shared/util'; +import { SourceClock } from './source-clock'; import { SourceTileComponent } from './source-tile.component'; /** @@ -68,6 +69,7 @@ import { SourceTileComponent } from './source-tile.component'; }) export class SourceRowComponent { private readonly statusService = inject(SourceStatusService); + private readonly clock = inject(SourceClock); private readonly translate = inject(TranslateService); private readonly languageTick = injectTranslationTick(); private readonly clipboard = inject(Clipboard); @@ -137,8 +139,13 @@ export class SourceRowComponent { readonly meta = computed(() => sourceMetaParts(this.playlist(), this.formatContext(), this.compact()) ); + /** Reads the shared clock, so a failure's duration keeps counting. */ readonly tag = computed(() => - sourceStatusTag(this.status(), this.formatContext(), this.compact()) + sourceStatusTag( + this.status(), + { ...this.formatContext(), now: this.clock.now() }, + this.compact() + ) ); constructor() {