From b7a4eaff8da139cc04443bab17640ad6c4171ada Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sun, 23 Aug 2026 01:39:29 +0200 Subject: [PATCH] fix(plex): resolve deep-review findings on the filter-bar overhaul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review of the branch diff (feature/plex-filter-bar-overhaul) against main: - Surprise rail query key omitted `lang` while the backend /surprise now filters against the language-specific genre column → a HU/EN switch under an active genre filter left the rail stale. Key it by scope+lang+filters, matching plexDiscover. - Chip-removing a collection left its entry in `collectionTitles` (hashed into the plex query keys), splitting the cache from the pristine no-collection state and growing the titles map unboundedly. Prune the title on remove, like CollectionPicker. - The saved-views default-mirror effect gained `cfg` in its deps (re-memoized on every live filter edit) → a synchronous localStorage write per keystroke. Depend on the module-stable mirror fn instead. - Collections-browse search input had no accessible name (placeholder only). Add aria-label + type="search". - Extract the tile hue-hash + gradient duplicated between PlexDiscover and plexBrowseLists into a shared lib/hueTile so the two surfaces can't drift. - Document migration 0073 downgrade() as lossy and de-dup non-feed rows before recreating the narrow (user_id, name) constraint (verified by the drift test). - Add unit tests for coercePlexFilters (legacy single-collection migration) and plexFiltersSignature (order-independence, title-ignoring). Gate: tsc/lint/knip/prettier clean, 410 vitest; backend ruff + migration drift + saved-views tests green. --- .../versions/0073_saved_view_source.py | 5 ++ frontend/src/components/PlexDiscover.tsx | 19 ++---- .../src/components/SavedViewsProvider.tsx | 9 ++- frontend/src/components/plexBrowseLists.tsx | 14 ++-- .../src/components/usePlexActiveFilters.ts | 10 ++- frontend/src/lib/api/plex.dom.test.ts | 66 +++++++++++++++++++ frontend/src/lib/hueTile.ts | 13 ++++ frontend/src/lib/queryKeys.ts | 3 +- 8 files changed, 110 insertions(+), 29 deletions(-) create mode 100644 frontend/src/lib/api/plex.dom.test.ts create mode 100644 frontend/src/lib/hueTile.ts diff --git a/backend/alembic/versions/0073_saved_view_source.py b/backend/alembic/versions/0073_saved_view_source.py index c2aa8843..6b2a9061 100644 --- a/backend/alembic/versions/0073_saved_view_source.py +++ b/backend/alembic/versions/0073_saved_view_source.py @@ -29,6 +29,11 @@ def upgrade() -> None: def downgrade() -> None: + # LOSSY / effectively forward-only: upgrade lets a user hold a feed view and a plex view of the same + # name, so re-creating the narrow (user_id, name) constraint can raise IntegrityError on such a pair. + # De-dup by dropping the non-feed rows first (they can't exist in the pre-0073 schema anyway) so the + # narrow constraint re-applies cleanly for the drift test's downgrade→re-upgrade cycle. + op.execute("DELETE FROM saved_views WHERE source <> 'feed'") op.drop_constraint("uq_user_source_saved_view", "saved_views", type_="unique") op.create_unique_constraint("uq_user_saved_view", "saved_views", ["user_id", "name"]) op.drop_column("saved_views", "source") diff --git a/frontend/src/components/PlexDiscover.tsx b/frontend/src/components/PlexDiscover.tsx index 4137cddf..e1c1d6a1 100644 --- a/frontend/src/components/PlexDiscover.tsx +++ b/frontend/src/components/PlexDiscover.tsx @@ -4,6 +4,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { RotateCcw } from "lucide-react"; import { qk } from "../lib/queryKeys"; import { api, type PlexCard, type PlexDiscover as PlexDiscoverData } from "../lib/api"; +import { hueOf, hueTileGradient } from "../lib/hueTile"; import { plexLang } from "../i18n"; import { toggleWatchState } from "../lib/plexWatch"; import { usePlex } from "./PlexProvider"; @@ -18,14 +19,6 @@ import { LoadingState, StateMessage } from "./QueryState"; // info / watched-toggle handlers come from the shell (PlexBrowse) so a rail card behaves exactly like // a grid card; the watched-toggle optimistically flips THIS surface's cache too. -// A stable, pleasant hue per genre string (no art to collage yet — colored tiles stand in). Hashing -// keeps a genre the same color across renders/sessions without storing anything. -function genreHue(name: string): number { - let h = 0; - for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) % 360; - return h; -} - function CardRail({ title, ariaLabel, @@ -99,7 +92,7 @@ export default function PlexDiscover({ }); // Surprise-me is its own query so the ↻ button can redraw it without refetching the whole surface. const surpriseQ = useQuery({ - queryKey: qk.plexSurprise(scope, filters), + queryKey: qk.plexSurprise(scope, lang, filters), queryFn: () => api.plexSurprise(scope, undefined, filters, lang), staleTime: 0, }); @@ -124,7 +117,7 @@ export default function PlexDiscover({ : old, ); qc.setQueryData<{ scope: string; items: PlexCard[] }>( - qk.plexSurprise(scope, filters), + qk.plexSurprise(scope, lang, filters), (old) => (old ? { ...old, items: flip(old.items) } : old), ); }, @@ -207,16 +200,14 @@ export default function PlexDiscover({

{t("plex.discover.byGenre")}

{d.genres.map((g) => { - const hue = genreHue(g.value); + const hue = hueOf(g.value); return (