Commit Graph
8 Commits
Author SHA1 Message Date
peter 23ac884b42 fix(e4): address deep-review findings — bookmark popstate, focus trap, touch a11y
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.
2026-08-20 03:26:19 +02:00
peter 8bfc01684f feat(nav): full bookmarkable URL model — retire minimalist-URL + Share-view (E4 S2)
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).
2026-08-19 04:47:36 +02:00
peter e6688edbcc refactor(nav): drop redundant sub-view markDirty; add useFilterState re-hydrate tests
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).
2026-08-14 03:26:30 +02:00
peter 726c382524 refactor(account): address account-pin re-hydrate review — extract module, harden guards
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.
2026-08-14 03:12:50 +02:00
peter 9917d908eb fix(nav,plex,playlists): extend account-pin re-hydrate to every affected provider
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.
2026-08-14 02:56:55 +02:00
peter e257e2aca0 style(frontend): Prettier config + one-time repo-wide format pass
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.
2026-07-21 02:26:40 +02:00
peter 7819c2e3a0 docs(nav): note the render-time ref is the intentional latest-value pattern 2026-07-18 02:24:19 +02:00
peter 41bb1b2ab3 refactor(nav): lift navigation + history into NavigationProvider
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.
2026-07-18 02:06:07 +02:00