fix(sources): checks behind filters, keyboard Undo in the switcher, check causes

Local review round 1 (Codex + Greptile) on the redesign:

- A saved Responding / Not responding filter hid every source without a
  result, and the hidden rows were what started the checks, so the list
  stayed empty. keepSourcesChecked (portal shared data-access) now keeps
  every library source checked while the Sources page lives, on desktop
  where a check is one shared job that rows join.
- Removing from the switcher left its focus trap open around the search,
  outside of which the Undo toast sits: Tab could not reach Undo. The
  popover now closes first; Undo takes the focus and returns it to the
  trigger (SourceRemovalOptions.returnFocusTo). Every switcher action
  closes through one path, so a closing row menu no longer pulls the focus
  back into the popover.
- A running desktop Xtream delete brought its source back into the
  switcher as an ordinary option that could be opened or removed again; the
  switcher now leaves it out until the delete ends (the Sources page shows
  its progress and Cancel).
- Undo re-activated the removed source over a source the user had chosen
  since, when nothing had replaced it (it was the only row a filter
  showed). Undo now restores only while no other source is active.
- The switcher highlight stays on its option, or in range, when the list
  changes under the same search, so Enter still opens something.
- The status dot named every failure "Could not connect". SourceStatus
  now carries the check's cause (access denied, account disabled, timeout…,
  kept with the health history and validated on read), and M3U dots again
  say the check covers the playlist URL, not its channels.

Tests: new specs for keepSourcesChecked, the saved-filter check, the Undo
guard and focus return, the switcher close/highlight/deleting cases and
the dot's cause; the filter and Undo specs fail without their fixes. The
switcher keyboard E2E now deletes and undoes from the keyboard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-13 09:51:33 +02:00
1 parent 845d295474
commit 366745f16d
23 files changed
+616 -67

No files matched your search

@@ -16,6 +16,27 @@ export type SourceHealthReason =
| 'invalid'
| 'unknown'
| 'cancelled';
const SOURCE_HEALTH_REASONS: ReadonlySet<string> = new Set<SourceHealthReason>([
'available',
'disabled',
'expired',
'auth',
'http',
'timeout',
'network',
'paused',
'invalid',
'unknown',
'cancelled',
]);
/** Whether stored data names a reason this build can describe. */
export function isSourceHealthReason(
value: unknown
): value is SourceHealthReason {
return typeof value === 'string' && SOURCE_HEALTH_REASONS.has(value);
}
export interface SourceHealthResult {
state: SourceHealthState;
reason: SourceHealthReason;
@@ -72,6 +72,14 @@ describe('nextSourceHealthHistory', () => {
});
});
it('keeps the cause of a failure, never of a success', () => {
const failed = nextSourceHealthHistory(undefined, 'bad', 100, 'auth');
expect(failed?.reason).toBe('auth');
expect(nextSourceHealthHistory(failed, 'ok', 200, 'available')).toEqual(
{ state: 'ok', checkedAt: 200, lastSuccessAt: 200 }
);
});
it('ends the run when the source answers again', () => {
expect(
nextSourceHealthHistory(
@@ -1,4 +1,4 @@
import type { SourceHealthSnapshot } from './source-health';
import type { SourceHealthReason, SourceHealthSnapshot } from './source-health';
/**
* What a source list shows about one network source, folded from the six
@@ -26,6 +26,11 @@ export interface SourceStatus {
checkedAt?: number;
/** A check is running; `reachability` is the last known result. */
pending?: boolean;
/**
* Why the check concluded what it did (`auth`, `disabled`, `timeout`…),
* when known; a list otherwise describes the reachability alone.
*/
reason?: SourceHealthReason;
}
/**
@@ -99,6 +104,8 @@ export interface SourceHealthHistoryEntry {
/** When the current run of failed checks began; absent while `ok`. */
failingSince?: number;
lastSuccessAt?: number;
/** The cause of the last failed check; absent while `ok`. */
reason?: SourceHealthReason;
}
/**
@@ -110,7 +117,8 @@ export interface SourceHealthHistoryEntry {
export function nextSourceHealthHistory(
previous: SourceHealthHistoryEntry | undefined,
reachability: SourceReachability,
checkedAt: number
checkedAt: number,
reason?: SourceHealthReason
): SourceHealthHistoryEntry | undefined {
if (!isConclusiveReachability(reachability)) {
return previous;
@@ -128,5 +136,6 @@ export function nextSourceHealthHistory(
...(previous?.lastSuccessAt !== undefined
? { lastSuccessAt: previous.lastSuccessAt }
: {}),
...(reason ? { reason } : {}),
};
}