Deep-review (6 dimensions, 3-verifier adversarial pass) surfaced 7 findings, all
verified; fixed here.
- NavigationProvider + history.ts (medium, correctness): the popstate handlers
restored the open channel / Plex sub-view from history.state ONLY, but a fresh
?chan / ?v bookmark lives in the URL with no marker in state — so any overlay's
programmatic history.back() (closing a modal/player) silently closed the
bookmarked channel or collapsed the sub-view to the grid. Give onPop the same
URL fallback the initializers already use (history.ts extracts a shared readView
so seed and popstate can't diverge again).
- plexCards SeasonCard (medium, a11y): the "In progress" season badge was a
reveal-idle swap-badge → display:none on touch, and a season has no progress bar,
so an in-progress season was indistinguishable from unwatched on touch. Moved it
to the top-left (the toggle owns top-right) and made it always visible.
- plexCards watched toggle (low, a11y / WCAG 1.4.1): watched state was color-only
on touch (green vs white CheckCircle2). New WatchedIcon distinguishes by SHAPE —
CheckCircle2 (watched) vs an empty Circle (not) — used by all three card toggles.
- Modal.tsx (low, a11y): the focus trap leaked on Shift+Tab when focus rested on
the panel container itself (Node.contains(self) is true, so the pull-back branch
never ran). Guard now also checks `active === panel`.
- index.css (low, a11y): .reveal-idle faded only on .group:hover though .reveal is
revealed on hover AND focus-within → the badge overlapped the control on keyboard
focus. Added the focus-within fade.
- PlexBrowse.tsx (low, correctness): the ?v=playlist decoder accepted a non-integer
id (Number.isFinite); tightened to Number.isInteger(n) && n > 0, matching the ?pl
guard.
Gate green (tsc, knip, lint 0-err, 247 vitest). Self-E2E (localdev): ?v=show and
?chan bookmarks survive a modal open+close (were collapsing/closing before); Modal
Shift+Tab from the panel stays inside; in-progress season badge visible on touch;
watched toggle shows an empty-circle-vs-check shape difference; 0 console errors.
The query string is now the source of truth for the current view, so every URL is
bookmarkable/shareable. Reverses the early minimalist-URL rule (user's call).
- urlState: urlWithParams (read-modify-write delta on the live query, each provider
owns its keys) + clearParams (strip only transient auth params) replace the whole-
query stripUrlParams; shareUrl deleted; filterParamUpdates helper.
- NavigationProvider: page + ?chan mirrored to the address bar (pushState URL arg;
history.state mechanics untouched); loadInitialPage resolves deep links
(?v->plex, ?pl->playlists, filter/?chan->feed, bare->stored landing).
- FeedFiltersProvider: feed filters mirrored to the URL, gated on page===feed.
- useHistorySubview: optional SubviewUrl mapping; PlexBrowse mirrors the sub-view to
?v=<kind>:<args> (show/season/info/person/playlist; grid+player stay ephemeral).
- PlaylistsProvider: selected playlist mirrored to ?pl on the Playlists page.
- App: the 5 stripUrlParams calls become targeted clearParams (link/oauth/admin/
access/verify/reset); the share-link hydrate effect removed.
- Retire the "Share view" feature: Sidebar + SavedViewsWidget buttons + i18n keys.
Browser tab title stays the short module/channel name (document.title untouched) —
the query never leaks into it, per the user's request.
Gate green: typecheck, knip, eslint (0 err), prettier, 247 vitest (urlState.dom.test
+9). No browser-E2E yet (most visual sprint — flagged for the epic-end review).
Addresses the 2 findings from /code-review high on 726c382:
- NavigationProvider: openChannel/enterYtSearch no longer markDirty. A sub-view is an
overlay, not a page change — the page re-hydrate's replaceState already spreads the
_chan/_yt marker, so the overlay survives on top of the RESTORED page. Removing the
markDirty lets a fresh tab still restore the stored landing page under a pre-pin overlay
(previously it stuck on the default). setPage keeps markDirty (a real navigation).
- useFilterState.dom.test.ts: model a mutable pinned account and add 3 integration tests —
null->id re-hydrate adopts the stored map, the migrate seed carries a legacy value forward
on re-hydrate when the key is empty, and a pre-pin edit is preserved (markDirty guard).
gate-green: vite build + vitest 120 + eslint-0 + knip + prettier. Fresh-tab restore re-E2E'd
(lands on Plex with full state, zero console errors).
Fixes the 5 findings from /code-review high on the account-pin work:
- Extract the reactive-account primitives (ACTIVE_ACCOUNT_KEY, activeAccountId,
notifyActiveAccountChange, useActiveAccountId) into a new dependency-free
lib/activeAccount.ts, breaking the storage.ts <-> useAccountRehydrate.ts import cycle.
storage.ts / api/core.ts / useAccountRehydrate.ts now import from it.
- NavigationProvider: the page re-hydrate now PRESERVES existing history.state markers
(spread, not overwrite), and openChannel/enterYtSearch markDirty so a sub-view opened
before the account pins isn't clobbered by the page override.
- useAccountRehydrate: reset the dirty flag after each account transition, so a future
non-reloading account switch can't skip the re-hydrate and leak the prior account's state.
- Doc-link useAccountRehydrate to the pre-existing useEffect([me?.id]) server-adopt pattern.
- Tests: model a mutable pinned account and add useSortState integration tests for the
null->id re-hydrate + the markDirty no-clobber guard (vitest 115->117).
gate-green: vite build + vitest 117 + eslint-0 + knip + prettier. Self-E2E'd on a fresh
tab: still lands on Plex with Movies/In-progress/Year restored, zero console errors.
The fresh-tab restore bug wasn't Channels-specific: any provider that mounts above
<App> and reads per-account state at useState-init hit it, since <App> pins the tab's
account only after /api/me. Audited all of them — FeedFilters/FeedView/Prefs/App already
self-correct via their own me?.id server-prefs adopt effects, so the still-broken ones
were the pure-localStorage providers with no server adopt: NavigationProvider (landing
page), PlexProvider (scope/show/sort/filters/q), PlaylistsProvider (selected playlist).
Each now adopts the shared useAccountRehydrate hook (markDirty in the setters; page uses
replaceState so the restore doesn't add a phantom Back step). Also baked re-hydrate into
useAccountPersistedState/Object so their consumers (persisted tabs, player prefs) are
robust even if one ever mounts before the pin. Self-E2E'd on a genuinely fresh tab: it
lands on Plex with Movies/In-progress/Year/search all restored on the FIRST load.
Prettier 3 (pinned), printWidth 100, double quotes — chosen to match the existing
hand-formatting and minimise reflow. This commit is ONLY the mechanical format pass
(`prettier --write`) plus the config/ignore/scripts, isolated so it never pollutes a
feature diff. Verified behaviour-neutral: typecheck, eslint (0 errors), and vitest
all green before and after. See .git-blame-ignore-revs — `git blame` skips this sha.
Move page/channelView/ytSearch and the history/popstate/title effects out of the
App god component into a new NavigationProvider (split state/actions contexts so
the memo'd VideoCard doesn't re-render on a page/channel switch). App consumes it
via useNavigation/useNavigationActions but still passes the props down for now;
the consumer migration that removes the prop-drilling follows.