refactor(fe): eliminate every explicit any; enforce no-explicit-any (R7 S3a·4)

Removes all 13 remaining `any`s and flips the ESLint rule from off to error so
new ones can't creep back:
- api.ts prefs/data JSON bags: Record<string, any> -> Record<string, unknown>,
  narrowed at consumers (App language guard; NotificationsPanel shapes the
  notification `data` per type behind its existing typeof guards)
- catch (e: any) -> catch (e) + `e instanceof HttpError` narrowing
  (SettingsPanel, SetupWizard, PlexPlayer)
- the YouTube IFrame API boundary: new lib/youtube.ts types (YTPlayer/
  YTPlayerEvent/YTNamespace + a Window augmentation) replace the 6 player anys;
  tsc surfaced the exact method surface to declare
- NotificationsPanel `t` props: any-options -> i18next TFunction

Gate green: typecheck (app+node+e2e), eslint 0 errors (rule now enforced),
prettier, 74 vitest.
This commit is contained in:
2026-07-27 22:57:33 +02:00
parent 2d4ecdb6c5
commit 90749a9e45
9 changed files with 102 additions and 32 deletions
+3 -3
View File
@@ -17,7 +17,7 @@ export interface Me {
can_read: boolean;
can_write: boolean;
pending_invites: number;
preferences: Record<string, any>;
preferences: Record<string, unknown>;
}
export interface DemoWhitelistEntry {
@@ -622,7 +622,7 @@ export interface AppNotification {
type: string;
title: string;
body: string | null;
data: Record<string, any> | null;
data: Record<string, unknown> | null;
read: boolean;
dismissed: boolean;
created_at: string | null;
@@ -1189,7 +1189,7 @@ export const api = {
pauseSync: () => req("/api/sync/pause", { method: "POST" }),
resumeSync: () => req("/api/sync/resume", { method: "POST" }),
// Overwriting preferences is idempotent, so let it retry on a transient gateway blip.
savePrefs: (p: Record<string, any>) =>
savePrefs: (p: Record<string, unknown>) =>
req("/api/me/preferences", { method: "PUT", body: JSON.stringify(p) }, { idempotent: true }),
// --- channel manager ---
+59
View File
@@ -0,0 +1,59 @@
// Minimal typings for the YouTube IFrame Player API surface this app actually uses. The API is
// loaded at runtime from youtube.com/iframe_api and ships no official npm types; rather than pull in
// @types/youtube we declare just the methods/events PlayerModal calls, so tsc flags any new call
// that isn't covered here. See [[reference-yt-iframe-api-remote-control-map]] for what's driveable.
export interface YTVideoData {
title: string;
author: string;
video_id?: string;
}
export interface YTPlayer {
getPlayerState(): number;
getCurrentTime(): number;
getDuration(): number;
getVideoData(): YTVideoData;
getIframe(): HTMLIFrameElement;
getVolume(): number;
isMuted(): boolean;
setVolume(volume: number): void;
mute(): void;
unMute(): void;
playVideo(): void;
pauseVideo(): void;
loadVideoById(opts: { videoId: string; startSeconds?: number }): void;
seekTo(seconds: number, allowSeekAhead: boolean): void;
destroy(): void;
}
// The IFrame API fires events carrying a numeric `data` — a player-state code (onStateChange) or an
// error code (onError).
export interface YTPlayerEvent {
data: number;
target: YTPlayer;
}
export interface YTPlayerOptions {
width?: string | number;
height?: string | number;
videoId?: string;
playerVars?: Record<string, string | number | undefined>;
events?: {
onReady?: (e: YTPlayerEvent) => void;
onStateChange?: (e: YTPlayerEvent) => void;
onError?: (e: YTPlayerEvent) => void;
};
}
export interface YTNamespace {
Player: new (el: HTMLElement, opts: YTPlayerOptions) => YTPlayer;
}
// The globals the IFrame API script installs on `window` once loaded.
declare global {
interface Window {
YT?: YTNamespace;
onYouTubeIframeAPIReady?: () => void;
}
}