Merge chore/code-hygiene: Phase 4 guardrails (noUnusedLocals + dead-code sweep + ruff.toml)
tsconfig noUnusedLocals/Parameters=true (permanent re-accumulation guardrail) + fixed 8 dead-code violations; backend/ruff.toml (ignore intentional E402) + 6 style fixes. knip/tsc/ruff all green.
This commit is contained in:
commit
b2c9897001
12 changed files with 27 additions and 85 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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}"
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
6
backend/ruff.toml
Normal 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"]
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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 = "",
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue