chore: Phase 1 hygiene — drop unused imports/exports/types (behavior-neutral)

Machine-baseline harvest (ruff + knip), all tsc/parse-green, no runtime change:
- backend: remove 3 unused imports (channels/playlists/youtube) via ruff; drop the
  unused `job` binding in unshare_download (keep the _own_job ownership guard call).
- frontend: remove `export` from 23 internally-used-only symbols (knip "unused
  exports") to shrink the public surface; delete 2 genuinely-dead declarations
  (PlexBrowseResult — leftover from the removed /browse route; WIDGET_TITLES —
  hardcoded English titles superseded by i18n).

Held back for a decision (unused here = possibly-unwired, NOT dead — flagged, not
removed): e2ee.lock()/clearDevice() (security primitives never wired to logout / a
"forget device" feature) and loadDefaultViewFilters (App reimplements it inline — a
DRY issue). See siftlode-ops/CODE-HYGIENE.md.
This commit is contained in:
npeter83 2026-07-11 04:47:08 +02:00
parent bf8d4b94a0
commit c2a2c98f16
19 changed files with 26 additions and 42 deletions

View file

@ -5,7 +5,7 @@ import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import and_, func, select, update from sqlalchemy import func, select, update
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app import quota, sysconfig from app import quota, sysconfig

View file

@ -675,7 +675,7 @@ def unshare_download(
user: User = Depends(require_human), user: User = Depends(require_human),
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> dict: ) -> dict:
job = _own_job(db, user, job_id) _own_job(db, user, job_id) # ownership guard (404s if not the caller's job)
recipient = db.execute( recipient = db.execute(
select(User).where(func.lower(User.email) == email.strip().lower()) select(User).where(func.lower(User.email) == email.strip().lower())
).scalar_one_or_none() ).scalar_one_or_none()

View file

@ -13,7 +13,7 @@ from sqlalchemy import delete, select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app import progress, quota from app import progress, quota
from app.auth import has_read_scope, has_write_scope from app.auth import has_read_scope
from app.models import Channel, OAuthToken, Playlist, PlaylistItem, User, Video from app.models import Channel, OAuthToken, Playlist, PlaylistItem, User, Video
from app.sync.videos import apply_video_details, parse_dt from app.sync.videos import apply_video_details, parse_dt
from app.youtube.client import YouTubeClient, YouTubeError from app.youtube.client import YouTubeClient, YouTubeError

View file

@ -12,7 +12,6 @@ from datetime import datetime, timedelta, timezone
import httpx import httpx
from app import quota, sysconfig from app import quota, sysconfig
from app.config import settings
from app.models import User from app.models import User
from app.security import decrypt from app.security import decrypt

View file

@ -9,7 +9,7 @@ import { useDismiss } from "../lib/useDismiss";
// other modules (e.g. the playlist manager) can reuse it. Below `md` it falls back to a card // other modules (e.g. the playlist manager) can reuse it. Below `md` it falls back to a card
// list built from the same column definitions. // list built from the same column definitions.
export type ColumnFilter<T> = type ColumnFilter<T> =
| { kind: "text"; get: (row: T) => string } | { kind: "text"; get: (row: T) => string }
| { kind: "select"; options: { value: string; label: string }[]; test: (row: T, value: string) => boolean } | { kind: "select"; options: { value: string; label: string }[]; test: (row: T, value: string) => boolean }
| { kind: "multi"; options: { value: string; label: string }[]; test: (row: T, values: string[]) => boolean }; | { kind: "multi"; options: { value: string; label: string }[]; test: (row: T, values: string[]) => boolean };

View file

@ -4,7 +4,7 @@
// with no react-query provider: the operator contact is fetched with a plain fetch below. // with no react-query provider: the operator contact is fetched with a plain fetch below.
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export const LAST_UPDATED = "June 14, 2026"; const LAST_UPDATED = "June 14, 2026";
// Contact shown on the legal pages: the instance operator's configured admin email, from the // Contact shown on the legal pages: the instance operator's configured admin email, from the
// public /auth/config. One shared fetch across the page; null when the operator set none. // public /auth/config. One shared fetch across the page; null when the operator set none.
@ -19,7 +19,7 @@ function fetchOperatorContact(): Promise<string | null> {
return contactPromise; return contactPromise;
} }
export function useOperatorContact(): string | null { function useOperatorContact(): string | null {
const [contact, setContact] = useState<string | null>(null); const [contact, setContact] = useState<string | null>(null);
useEffect(() => { useEffect(() => {
let alive = true; let alive = true;

View file

@ -59,7 +59,7 @@ export function Section({
/** A label that reveals an explanatory caption on hover (dotted underline) when `hint` is set. /** A label that reveals an explanatory caption on hover (dotted underline) when `hint` is set.
* No-op styling when there's no hint, so it's safe to use unconditionally. */ * No-op styling when there's no hint, so it's safe to use unconditionally. */
export function HintLabel({ hint, children }: { hint?: string; children: ReactNode }) { function HintLabel({ hint, children }: { hint?: string; children: ReactNode }) {
return ( return (
<Tooltip hint={hint ?? ""}> <Tooltip hint={hint ?? ""}>
<span <span

View file

@ -10,7 +10,7 @@ export const LANGUAGES = [
] as const; ] as const;
export type LangCode = (typeof LANGUAGES)[number]["code"]; export type LangCode = (typeof LANGUAGES)[number]["code"];
const SUPPORTED = LANGUAGES.map((l) => l.code) as readonly string[]; const SUPPORTED = LANGUAGES.map((l) => l.code) as readonly string[];
export const LANG_KEY = LS.lang; const LANG_KEY = LS.lang;
// Auto-load every locale file (locales/<lang>/<area>.json) and merge each <area> under one // Auto-load every locale file (locales/<lang>/<area>.json) and merge each <area> under one
// `translation` namespace, so components use t("area.key") and adding an area needs no wiring. // `translation` namespace, so components use t("area.key") and adding an area needs no wiring.
@ -33,7 +33,7 @@ export function isSupported(code: string | null | undefined): code is LangCode {
// Initial language before the server preference is known: stored choice, else the browser // Initial language before the server preference is known: stored choice, else the browser
// language, else English. (After login, App adopts the server-persisted preference.) // language, else English. (After login, App adopts the server-persisted preference.)
export function detectInitialLang(): LangCode { function detectInitialLang(): LangCode {
const stored = localStorage.getItem(LANG_KEY); const stored = localStorage.getItem(LANG_KEY);
if (isSupported(stored)) return stored; if (isSupported(stored)) return stored;
const nav = (navigator.language || "en").slice(0, 2).toLowerCase(); const nav = (navigator.language || "en").slice(0, 2).toLowerCase();

View file

@ -8,7 +8,7 @@ import { LS, setAccountRaw } from "./storage";
// WITHOUT statically importing the — now lazy-loaded — admin page, which would otherwise pull // WITHOUT statically importing the — now lazy-loaded — admin page, which would otherwise pull
// it back into the main bundle and defeat the code-split. // it back into the main bundle and defeat the code-split.
export const ADMIN_USERS_TAB_KEY = LS.adminUsersTab; export const ADMIN_USERS_TAB_KEY = LS.adminUsersTab;
export const ADMIN_USERS_ACCESS_TAB = "access"; const ADMIN_USERS_ACCESS_TAB = "access";
export function focusAccessRequestsTab(): void { export function focusAccessRequestsTab(): void {
setAccountRaw(ADMIN_USERS_TAB_KEY, ADMIN_USERS_ACCESS_TAB); setAccountRaw(ADMIN_USERS_TAB_KEY, ADMIN_USERS_ACCESS_TAB);

View file

@ -78,7 +78,7 @@ export interface VideoDetail {
duration_seconds: number | null; duration_seconds: number | null;
} }
export interface ChannelLink { interface ChannelLink {
title: string | null; title: string | null;
url: string; url: string;
} }
@ -611,7 +611,7 @@ export interface SystemConfigData {
} }
// Admin: result of testing the Plex server connection (also drives the library-picker). // Admin: result of testing the Plex server connection (also drives the library-picker).
export interface PlexSection { interface PlexSection {
key: string; key: string;
title: string; title: string;
type: string; // "movie" | "show" type: string; // "movie" | "show"
@ -656,13 +656,6 @@ export interface PlexCard {
show_id?: string | null; // episode: the show's rating_key (for grouping a playlist by show/season) show_id?: string | null; // episode: the show's rating_key (for grouping a playlist by show/season)
summary?: string | null; // episode summary?: string | null; // episode
} }
export interface PlexBrowseResult {
kind: "movie" | "show";
total: number;
offset: number;
limit: number;
items: PlexCard[];
}
// Unified cross-library browse (movies + shows mixed). `episodes` is populated only on a search that // Unified cross-library browse (movies + shows mixed). `episodes` is populated only on a search that
// also matches episodes (the grouped "Episodes" section). // also matches episodes (the grouped "Episodes" section).
export interface PlexUnifiedResult { export interface PlexUnifiedResult {
@ -823,7 +816,7 @@ export function plexFilterCount(f: PlexFilters): number {
// Serialize the shared PlexFilters metadata fields onto a query string. Used by both the library grid // Serialize the shared PlexFilters metadata fields onto a query string. Used by both the library grid
// and the facets request so the two endpoints always agree on the filter set (paging/sort/sort_dir are // and the facets request so the two endpoints always agree on the filter set (paging/sort/sort_dir are
// each caller's own concern and stay out of here). // each caller's own concern and stay out of here).
export function appendPlexFilters(u: URLSearchParams, f: PlexFilters): void { function appendPlexFilters(u: URLSearchParams, f: PlexFilters): void {
if (f.genres?.length) u.set("genres", f.genres.join(",")); if (f.genres?.length) u.set("genres", f.genres.join(","));
if (f.genreMode === "all") u.set("genre_mode", "all"); if (f.genreMode === "all") u.set("genre_mode", "all");
if (f.contentRatings?.length) u.set("content_ratings", f.contentRatings.join(",")); if (f.contentRatings?.length) u.set("content_ratings", f.contentRatings.join(","));
@ -903,7 +896,7 @@ export interface DownloadProfile {
sort_order: number; sort_order: number;
} }
export interface DownloadAsset { interface DownloadAsset {
id: number; id: number;
status: string; status: string;
title: string | null; title: string | null;

View file

@ -5,7 +5,7 @@ import { createStore } from "./store";
// server can't carry out an action (a definitive 4xx/5xx response). Distinct from toasts // server can't carry out an action (a definitive 4xx/5xx response). Distinct from toasts
// (transient, e.g. connection blips) and from silent handling. Module-level so the non-React // (transient, e.g. connection blips) and from silent handling. Module-level so the non-React
// api layer can raise it; an <ErrorDialog> mounted at the app root renders the current one. // api layer can raise it; an <ErrorDialog> mounted at the app root renders the current one.
export interface AppError { interface AppError {
title: string; title: string;
message: string; message: string;
} }

View file

@ -5,7 +5,7 @@
import { getActiveAccount, type Message, type MessageUser } from "./api"; import { getActiveAccount, type Message, type MessageUser } from "./api";
// A pushed message plus both parties, so the dock can open/flash the right window. // A pushed message plus both parties, so the dock can open/flash the right window.
export interface IncomingMessage { interface IncomingMessage {
message: Message; message: Message;
from?: MessageUser; from?: MessageUser;
to?: MessageUser; to?: MessageUser;

View file

@ -17,7 +17,7 @@ export async function partnerPub(partnerId: number): Promise<string> {
} }
// Live "is secure messaging unlocked on this device" — shared so the page and the dock agree. // Live "is secure messaging unlocked on this device" — shared so the page and the dock agree.
export function useUnlocked(): boolean { function useUnlocked(): boolean {
return useSyncExternalStore(subscribeUnlock, isUnlocked, isUnlocked); return useSyncExternalStore(subscribeUnlock, isUnlocked, isUnlocked);
} }

View file

@ -6,7 +6,7 @@ import { LS, readAccount, readAccountMerged, writeAccount } from "./storage";
export type NotifLevel = "info" | "success" | "warning" | "error" | "fatal"; export type NotifLevel = "info" | "success" | "warning" | "error" | "fatal";
export interface NotifAction { interface NotifAction {
label: string; label: string;
onClick: () => void; onClick: () => void;
} }
@ -34,7 +34,7 @@ export type ChannelSubscribedMeta = {
}; };
// Admin nudge that pending access requests are waiting — carries a durable inbox link to the // Admin nudge that pending access requests are waiting — carries a durable inbox link to the
// Users page (where Access requests live). No payload; the panel provides the navigation. // Users page (where Access requests live). No payload; the panel provides the navigation.
export type AccessRequestsMeta = { type AccessRequestsMeta = {
kind: "access-requests"; kind: "access-requests";
}; };
export type NotifMeta = export type NotifMeta =
@ -224,7 +224,7 @@ export function notify(input: NotifyInput): number {
} }
/** Back-compat: a simple info toast with an optional inline action (e.g. Undo). */ /** Back-compat: a simple info toast with an optional inline action (e.g. Undo). */
export function toast(message: string, action?: NotifAction): number { function toast(message: string, action?: NotifAction): number {
return notify({ message, action }); return notify({ message, action });
} }

View file

@ -14,10 +14,10 @@
import { getAccountRaw, LS, setAccountRaw } from "./storage"; import { getAccountRaw, LS, setAccountRaw } from "./storage";
export const ONBOARD_ACTIVE = "siftlode.onboarding.active"; // sessionStorage (not in LS, which is localStorage) const ONBOARD_ACTIVE = "siftlode.onboarding.active"; // sessionStorage (not in LS, which is localStorage)
// Per-account (see accountKey): a fresh account should still get the onboarding nudge even if a // Per-account (see accountKey): a fresh account should still get the onboarding nudge even if a
// different account in the same browser dismissed it. // different account in the same browser dismissed it.
export const ONBOARD_DISMISSED = LS.onboardingDismissed; const ONBOARD_DISMISSED = LS.onboardingDismissed;
export function shouldAutoOpenOnboarding(me: { export function shouldAutoOpenOnboarding(me: {
can_read: boolean; can_read: boolean;

View file

@ -8,15 +8,7 @@ import { LS, readAccount, writeAccount } from "./storage";
export type WidgetId = "savedviews" | "date" | "language" | "topic" | "tags"; export type WidgetId = "savedviews" | "date" | "language" | "topic" | "tags";
export const ALL_WIDGETS: WidgetId[] = ["savedviews", "date", "tags", "language", "topic"]; const ALL_WIDGETS: WidgetId[] = ["savedviews", "date", "tags", "language", "topic"];
export const WIDGET_TITLES: Record<WidgetId, string> = {
savedviews: "Saved views",
date: "Upload date",
language: "Language",
topic: "Topic",
tags: "Tags",
};
export interface SidebarLayout { export interface SidebarLayout {
order: WidgetId[]; order: WidgetId[];

View file

@ -129,7 +129,7 @@ export function writeJSON(key: string, value: unknown): void {
* Validation is deferred to the caller (clamp at render) so it can be used before an * Validation is deferred to the caller (clamp at render) so it can be used before an
* async-loaded option list is known, keeping hook order stable. Generalizes the per-component * async-loaded option list is known, keeping hook order stable. Generalizes the per-component
* "persisted active tab" pattern (Settings, Stats, admin pages, the channel filter). */ * "persisted active tab" pattern (Settings, Stats, admin pages, the channel filter). */
export function usePersistedState( function usePersistedState(
key: string, key: string,
fallback = "", fallback = "",
): [string, (value: string) => void] { ): [string, (value: string) => void] {

View file

@ -1,6 +1,6 @@
import { LS, readAccountMerged, writeAccount } from "./storage"; import { LS, readAccountMerged, writeAccount } from "./storage";
export type Mode = "dark" | "light"; type Mode = "dark" | "light";
export type Scheme = "midnight" | "forest" | "slate" | "youtube"; export type Scheme = "midnight" | "forest" | "slate" | "youtube";
export const SCHEMES: { id: Scheme; name: string; swatch: string }[] = [ export const SCHEMES: { id: Scheme; name: string; swatch: string }[] = [

View file

@ -89,7 +89,7 @@ export function hasFilterParams(params: URLSearchParams): boolean {
// The single source of truth for valid page ids; `Page` and the runtime validator both // The single source of truth for valid page ids; `Page` and the runtime validator both
// derive from it, so adding a page is a one-line change (no parallel allowlists to keep in // derive from it, so adding a page is a one-line change (no parallel allowlists to keep in
// sync). "feed" is the default/fallback. // sync). "feed" is the default/fallback.
export const PAGES = [ const PAGES = [
"feed", "feed",
"channels", "channels",
"stats", "stats",