chore(hygiene): Phase 4 guardrails — noUnusedLocals/Parameters + dead-code sweep

Enable tsconfig noUnusedLocals + noUnusedParameters (permanent guardrail: every tsc/
build now flags re-accumulated dead code). Fixed the 8 violations that surfaced, all
genuine dead code:
- Sidebar: deleted unused SORT_IDS/SHOW_IDS/rollSeed consts + the dead Toggle component
  (+ its now-unused Switch import).
- PlexBrowse: dropped the unused onClearSearch prop (Props + destructure + App call site).
- VideoEditor: dropped the unused map index; notifications: deleted the dead back-compat
  toast(); storage: deleted the unused non-account usePersistedState (+ readJSON import).
- SavedViewsWidget: deleted the dead loadDefaultViewFilters export (App loads the default
  view inline; resolves the last knip unused-export → knip now clean).

Backend: added backend/ruff.toml (ignore E402 — intentional pre-import setup in main.py)
and fixed the 6 E702/E741 style items (_vtt_s_to_ts semicolons, ambiguous `l` loop vars)
so ruff is green. localdev boots; Feed/Plex/sidebar render; 0 console errors.
This commit is contained in:
npeter83 2026-07-12 02:15:52 +02:00
parent b481de0e48
commit 4dd1327b93
12 changed files with 27 additions and 85 deletions

View file

@ -752,7 +752,7 @@ def list_links(
.scalars() .scalars()
.all() .all()
) )
return [linksmod.owner_view(l) for l in rows] return [linksmod.owner_view(row) for row in rows]
@router.post("/{job_id}/links") @router.post("/{job_id}/links")

View file

@ -1346,11 +1346,15 @@ def _vtt_ts_to_s(ts: str) -> float:
def _vtt_s_to_ts(x: float) -> str: def _vtt_s_to_ts(x: float) -> str:
x = max(0.0, x) x = max(0.0, x)
h = int(x // 3600); x -= h * 3600 h = int(x // 3600)
m = int(x // 60); x -= m * 60 x -= h * 3600
s = int(x); ms = int(round((x - s) * 1000)) m = int(x // 60)
x -= m * 60
s = int(x)
ms = int(round((x - s) * 1000))
if ms >= 1000: if ms >= 1000:
s += 1; ms -= 1000 s += 1
ms -= 1000
return f"{h:02d}:{m:02d}:{s:02d}.{ms:03d}" return f"{h:02d}:{m:02d}:{s:02d}.{ms:03d}"

View file

@ -34,9 +34,9 @@ def _external_links(branding: dict) -> list | None:
channel = branding.get("channel", {}) channel = branding.get("channel", {})
links = channel.get("links") or [] links = channel.get("links") or []
out = [ out = [
{"title": l.get("title"), "url": l.get("url")} {"title": item.get("title"), "url": item.get("url")}
for l in links for item in links
if isinstance(l, dict) and l.get("url") if isinstance(item, dict) and item.get("url")
] ]
return out or None return out or None

6
backend/ruff.toml Normal file
View file

@ -0,0 +1,6 @@
# Ruff config for the Siftlode backend. Keeps ruff's default rule set (E4/E7/E9 + F = the
# high-signal pyflakes/pycodestyle checks that catch real dead code + bugs), but ignores E402:
# a handful of modules (e.g. main.py) deliberately run setup — logging config, sys.path — before
# importing route modules, so "module import not at top of file" is intentional there, not cruft.
[lint]
ignore = ["E402"]

View file

@ -831,7 +831,6 @@ export default function App() {
) : page === "plex" && meQuery.data!.plex_enabled ? ( ) : page === "plex" && meQuery.data!.plex_enabled ? (
<PlexBrowse <PlexBrowse
q={plexQ} q={plexQ}
onClearSearch={() => setPlexQ("")}
scope={plexScope} scope={plexScope}
setScope={setPlexScope} setScope={setPlexScope}
show={plexShowFilter} show={plexShowFilter}

View file

@ -54,7 +54,6 @@ type Sub =
type Props = { type Props = {
q: string; q: string;
onClearSearch: () => void;
scope: string; // movie | show | both (unified cross-library scope) scope: string; // movie | show | both (unified cross-library scope)
setScope: (v: string) => void; setScope: (v: string) => void;
show: string; show: string;
@ -70,7 +69,6 @@ const PAGE = 40;
export default function PlexBrowse({ export default function PlexBrowse({
q, q,
onClearSearch,
scope, scope,
setScope, setScope,
show, show,

View file

@ -20,7 +20,7 @@ import { CSS } from "@dnd-kit/utilities";
import { api, getActiveAccount, type FeedFilters, type SavedView } from "../lib/api"; import { api, getActiveAccount, type FeedFilters, type SavedView } from "../lib/api";
import { filtersToParams, shareUrl } from "../lib/urlState"; import { filtersToParams, shareUrl } from "../lib/urlState";
import { notify } from "../lib/notifications"; import { notify } from "../lib/notifications";
import { accountKey, LS, readJSON, writeJSON } from "../lib/storage"; import { accountKey, LS, writeJSON } from "../lib/storage";
import { useConfirm } from "./ConfirmProvider"; import { useConfirm } from "./ConfirmProvider";
// Compact, order-independent signature of a filter set (reuses the share serializer, which // Compact, order-independent signature of a filter set (reuses the share serializer, which
@ -28,14 +28,6 @@ import { useConfirm } from "./ConfirmProvider";
// saved view that matches the feed's current filters. // saved view that matches the feed's current filters.
const keyOf = (f: FeedFilters) => filtersToParams(f).toString(); const keyOf = (f: FeedFilters) => filtersToParams(f).toString();
/** The default view's filters, or null. Read synchronously on load (App.loadAccountFilters) from a
* per-account localStorage mirror the widget keeps in sync avoids a flash before the query loads.
* Per-account so one account's default view never seeds another account's feed. */
export function loadDefaultViewFilters(): FeedFilters | null {
const key = accountKey(LS.defaultViewFilters, getActiveAccount());
return key ? readJSON<FeedFilters | null>(key, null) : null;
}
function syncDefaultMirror(views: SavedView[]): void { function syncDefaultMirror(views: SavedView[]): void {
const key = accountKey(LS.defaultViewFilters, getActiveAccount()); const key = accountKey(LS.defaultViewFilters, getActiveAccount());
if (!key) return; if (!key) return;

View file

@ -39,29 +39,11 @@ import {
import { shareUrl } from "../lib/urlState"; import { shareUrl } from "../lib/urlState";
import { notify } from "../lib/notifications"; import { notify } from "../lib/notifications";
import { useDebounced } from "../lib/useDebounced"; import { useDebounced } from "../lib/useDebounced";
import { Switch } from "./ui/form";
import TagManager from "./TagManager"; import TagManager from "./TagManager";
import SavedViewsWidget from "./SavedViewsWidget"; import SavedViewsWidget from "./SavedViewsWidget";
import CollapsedFilterRail from "./CollapsedFilterRail"; import CollapsedFilterRail from "./CollapsedFilterRail";
// Filter ids; display labels resolved at render time via t("sidebar.sort.<id>") etc. // Filter ids; display labels resolved at render time via t("sidebar.sort.<id>") etc.
const SORT_IDS = [
"newest",
"oldest",
"views",
"duration_desc",
"duration_asc",
"title",
"subscribers",
"priority",
"shuffle",
];
const SHOW_IDS = ["unwatched", "in_progress", "all", "watched", "hidden"];
// Fresh shuffle token for the "surprise me" sort.
const rollSeed = () => Math.floor(Math.random() * 1_000_000_000);
const DATE_PRESETS: { days: number; key: string }[] = [ const DATE_PRESETS: { days: number; key: string }[] = [
{ days: 1, key: "24h" }, { days: 1, key: "24h" },
{ days: 7, key: "1week" }, { days: 7, key: "1week" },
@ -697,19 +679,3 @@ function WidgetCard({
); );
} }
function Toggle({
label,
checked,
onChange,
}: {
label: string;
checked: boolean;
onChange: (v: boolean) => void;
}) {
return (
<label className="flex items-center justify-between py-1 cursor-pointer text-sm">
<span>{label}</span>
<Switch label={label} checked={checked} onChange={onChange} />
</label>
);
}

View file

@ -366,7 +366,7 @@ export default function VideoEditor({ job, onClose }: { job: DownloadJob; onClos
</div> </div>
)} )}
{/* segment tint: dropped = dimmed + hatched */} {/* segment tint: dropped = dimmed + hatched */}
{segments.map((s, i) => ( {segments.map((s) => (
<div <div
key={s.id} key={s.id}
className={clsx( className={clsx(

View file

@ -223,11 +223,6 @@ export function notify(input: NotifyInput): number {
return id; return id;
} }
/** Back-compat: a simple info toast with an optional inline action (e.g. Undo). */
function toast(message: string, action?: NotifAction): number {
return notify({ message, action });
}
/** Close the transient toast surface; the entry stays in the center's history. */ /** Close the transient toast surface; the entry stays in the center's history. */
export function dismiss(id: number): void { export function dismiss(id: number): void {
items = items.map((n) => (n.id === id ? { ...n, dismissed: true } : n)); items = items.map((n) => (n.id === id ? { ...n, dismissed: true } : n));

View file

@ -129,29 +129,11 @@ export function writeJSON(key: string, value: unknown): void {
// --- reactive persisted string state ---------------------------------------------------- // --- reactive persisted string state ----------------------------------------------------
/** A `useState` whose string value is mirrored to localStorage, so it survives a reload (F5). /** A `useState` whose string value is mirrored to localStorage, scoped to the current account (see
* Validation is deferred to the caller (clamp at render) so it can be used before an * accountKey), so one account's persisted tab/view position doesn't carry over to another in the
* async-loaded option list is known, keeping hook order stable. Generalizes the per-component * same browser. Survives reload (F5); validation is deferred to the caller (clamp at render) so it
* "persisted active tab" pattern (Settings, Stats, admin pages, the channel filter). */ * can be used before an async-loaded option list is known, keeping hook order stable. These hooks
function usePersistedState( * run in components that only mount once signed in, so the account is known. */
key: string,
fallback = "",
): [string, (value: string) => void] {
const [value, setValue] = useState<string>(() => localStorage.getItem(key) ?? fallback);
const set = (next: string) => {
setValue(next);
try {
localStorage.setItem(key, next);
} catch {
/* ignore */
}
};
return [value, set];
}
/** Like usePersistedState but scoped to the current account (see accountKey), so one account's
* persisted tab/view position doesn't carry over to another in the same browser. These hooks run
* in components that only mount once signed in, so the account is known. */
export function useAccountPersistedState( export function useAccountPersistedState(
base: string, base: string,
fallback = "", fallback = "",

View file

@ -12,8 +12,8 @@
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"strict": true, "strict": true,
"noUnusedLocals": false, "noUnusedLocals": true,
"noUnusedParameters": false "noUnusedParameters": true
}, },
"include": ["src"] "include": ["src"]
} }