diff --git a/frontend/e2e/tsconfig.json b/frontend/e2e/tsconfig.json
index 05fda24d..246d917f 100644
--- a/frontend/e2e/tsconfig.json
+++ b/frontend/e2e/tsconfig.json
@@ -8,6 +8,9 @@
"strict": true,
"skipLibCheck": true,
"noEmit": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"resolveJsonModule": true
},
diff --git a/frontend/src/components/ConfigPanel.tsx b/frontend/src/components/ConfigPanel.tsx
index ce03d2d5..74c5b32e 100644
--- a/frontend/src/components/ConfigPanel.tsx
+++ b/frontend/src/components/ConfigPanel.tsx
@@ -81,6 +81,7 @@ export default function ConfigPanel() {
try {
for (const key of dirtyKeys) {
const it = byKey[key];
+ if (!it) continue; // dirtyKeys are all config-item keys; skip if somehow not found
const raw = draft[key] ?? "";
if (it.secret) {
if (secretReset[key] && !raw.trim()) await api.resetConfig(key);
@@ -170,14 +171,14 @@ export default function ConfigPanel() {
{activeGroup && (
- {data.groups[activeGroup].map((item) => (
+ {(data.groups[activeGroup] ?? []).map((item) => (
list
- .map((v) => (overrides[v.id] ? { ...v, status: overrides[v.id] } : v))
- .map((v) => (savedOverrides[v.id] !== undefined ? { ...v, saved: savedOverrides[v.id] } : v));
+ .map((v) => {
+ const status = overrides[v.id]; // read once so the narrowing sticks
+ return status ? { ...v, status } : v;
+ })
+ .map((v) => {
+ const saved = savedOverrides[v.id];
+ return saved !== undefined ? { ...v, saved } : v;
+ });
const merged: Video[] = withOverrides(loaded);
const items: Video[] = merged.filter((v) => matchesView(v.status, filters.show));
// The in-app player's queue (auto-advance / loop / prev-next). It's the filtered feed, but the
diff --git a/frontend/src/components/GlassTuner.tsx b/frontend/src/components/GlassTuner.tsx
index 0f84fd03..62ca5add 100644
--- a/frontend/src/components/GlassTuner.tsx
+++ b/frontend/src/components/GlassTuner.tsx
@@ -339,7 +339,12 @@ export default function GlassTuner() {
{tab === "glass" && (
{SLIDERS.map((s) => (
- setVar(s.k, v)} />
+ setVar(s.k, v)}
+ />
))}
)}
diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx
index 5394d459..e90e0716 100644
--- a/frontend/src/components/PlayerModal.tsx
+++ b/frontend/src/components/PlayerModal.tsx
@@ -124,7 +124,7 @@ export default function PlayerModal({
// where a just-watched item could drop out and snap playback to queue[0]).
let index = queue ? queue.findIndex((v) => v.id === playingId) : 0;
if (index < 0) index = 0;
- const active = queue && queue[index] ? queue[index] : video;
+ const active = queue?.[index] ?? video; // the queue item at index, else the opened video (never undefined)
const hasQueue = !!queue && queue.length > 1;
// startAt only applies to the item we opened with; advanced items resume their own pos.
const resumeAt =
@@ -242,11 +242,11 @@ export default function PlayerModal({
navRef.current = { queue, index };
const goPrev = () => {
const { queue: q, index: i } = navRef.current;
- if (q && i > 0) setPlayingId(q[i - 1].id);
+ if (q && i > 0) setPlayingId(q[i - 1]!.id);
};
const goNext = () => {
const { queue: q, index: i } = navRef.current;
- if (q && i < q.length - 1) setPlayingId(q[i + 1].id);
+ if (q && i < q.length - 1) setPlayingId(q[i + 1]!.id);
};
// Relative seek within the current video (plain Arrow keys), matching YouTube's ±5s.
const nudgeSeek = (delta: number) => {
@@ -277,12 +277,12 @@ export default function PlayerModal({
);
};
const cycleAuto = () => {
- const next = AUTO_MODES[(AUTO_MODES.indexOf(autoMode) + 1) % AUTO_MODES.length];
+ const next = AUTO_MODES[(AUTO_MODES.indexOf(autoMode) + 1) % AUTO_MODES.length]!;
setAutoMode(next);
persistPref({ playerAutoAdvance: next });
};
const cycleLoop = () => {
- const next = LOOP_MODES[(LOOP_MODES.indexOf(loopMode) + 1) % LOOP_MODES.length];
+ const next = LOOP_MODES[(LOOP_MODES.indexOf(loopMode) + 1) % LOOP_MODES.length]!;
setLoopMode(next);
persistPref({ playerLoop: next });
};
@@ -303,21 +303,21 @@ export default function PlayerModal({
if (!q || q.length === 0) return;
const wrap = l === "all";
if (a === "next") {
- if (i < q.length - 1) setPlayingId(q[i + 1].id);
+ if (i < q.length - 1) setPlayingId(q[i + 1]!.id);
else if (wrap) {
if (q.length === 1) replayCurrent();
- else setPlayingId(q[0].id);
+ else setPlayingId(q[0]!.id);
}
} else if (a === "prev") {
- if (i > 0) setPlayingId(q[i - 1].id);
+ if (i > 0) setPlayingId(q[i - 1]!.id);
else if (wrap) {
if (q.length === 1) replayCurrent();
- else setPlayingId(q[q.length - 1].id);
+ else setPlayingId(q[q.length - 1]!.id);
}
} else if (a === "random" && q.length > 1) {
let r = i;
while (r === i) r = Math.floor(Math.random() * q.length);
- setPlayingId(q[r].id);
+ setPlayingId(q[r]!.id);
} else if (a === "random" && wrap) {
replayCurrent();
}
diff --git a/frontend/src/components/PlaylistsRail.tsx b/frontend/src/components/PlaylistsRail.tsx
index ffa6ea06..d705d2f2 100644
--- a/frontend/src/components/PlaylistsRail.tsx
+++ b/frontend/src/components/PlaylistsRail.tsx
@@ -69,7 +69,7 @@ export default function PlaylistsRail({
return;
}
if (selectedId == null || !playlists.some((p) => p.id === selectedId)) {
- setSelectedId(playlists[0].id);
+ setSelectedId(playlists[0]!.id); // playlists is non-empty (the length guard above returns)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [playlists, selectedId, listQuery.data]);
diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx
index cea29757..8b46ff1d 100644
--- a/frontend/src/components/PlexBrowse.tsx
+++ b/frontend/src/components/PlexBrowse.tsx
@@ -183,7 +183,7 @@ export default function PlexBrowse() {
if (!sentinelEl) return;
const io = new IntersectionObserver(
(entries) => {
- if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) fetchNextPage();
+ if (entries[0]?.isIntersecting && hasNextPage && !isFetchingNextPage) fetchNextPage();
},
{ rootMargin: "600px" },
);
diff --git a/frontend/src/components/PlexPlayer.tsx b/frontend/src/components/PlexPlayer.tsx
index 9d7215d9..8e9fccd4 100644
--- a/frontend/src/components/PlexPlayer.tsx
+++ b/frontend/src/components/PlexPlayer.tsx
@@ -54,7 +54,7 @@ function audioOrdForLang(streams: AudioStream[], lang: string): number | null {
// absent (-1) or already the FIRST track (0, which the backend plays by default) → no override.
// (Match by index, NOT the `default` flag — some files mark a non-first track default, which made
// the restored language silently fall back to track 0.)
- return i <= 0 ? null : streams[i].ord;
+ return i <= 0 ? null : streams[i]!.ord; // i > 0 here, so streams[i] exists
}
function subOrdForLang(subs: SubStream[], lang: string): number | null {
if (!lang) return null; // "" = subtitles off
@@ -666,14 +666,14 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
const streams = detail?.audio_streams ?? [];
if (streams.length < 2) return;
const idx = streams.findIndex((s) => s.ord === (audioRef.current ?? 0));
- const next = streams[(idx + 1) % streams.length];
+ const next = streams[(idx + 1) % streams.length]!; // length >= 2 (guarded), so in-bounds
changeAudio(next.ord === 0 ? null : next.ord);
}, [detail, changeAudio]);
const cycleSubtitle = useCallback(() => {
const subs = (detail?.subtitle_streams ?? []).filter((s) => s.text);
const order: (number | null)[] = [null, ...subs.map((s) => s.ord)];
const i = order.findIndex((o) => o === subOrd);
- changeSubtitle(order[(i + 1) % order.length]);
+ changeSubtitle(order[(i + 1) % order.length]!); // order always has [null, ...] → non-empty
}, [detail, subOrd, changeSubtitle]);
// Audio A/V-sync offset: persist immediately (for the slider) but debounce the session restart so
// dragging doesn't respawn ffmpeg on every step.
diff --git a/frontend/src/components/PlexPlaylistView.tsx b/frontend/src/components/PlexPlaylistView.tsx
index 69571b16..1b525b57 100644
--- a/frontend/src/components/PlexPlaylistView.tsx
+++ b/frontend/src/components/PlexPlaylistView.tsx
@@ -406,7 +406,7 @@ export default function PlexPlaylistView({
const set = new Set(keys);
setItems((prev) => prev.filter((i) => !set.has(i.id)));
try {
- if (keys.length === 1) await api.plexPlaylistRemoveItem(playlistId, keys[0]);
+ if (keys.length === 1) await api.plexPlaylistRemoveItem(playlistId, keys[0]!);
else await api.plexPlaylistRemoveBulk(playlistId, keys);
invalidate();
} finally {
@@ -560,7 +560,7 @@ export default function PlexPlaylistView({
{items.length > 0 && (