fix(plex): resolve deep-review findings on the Discover/Collections batch
Round of USER-triggered deep-review fixes (11 CONFIRMED + 1 PLAUSIBLE, all verified) on the 22-commit branch: - collections: split the mutate-then-resync error surface. A resync READ that fails after a successful Plex mutation no longer reports a false 422 "Plex write failed" + rollback; it returns 502 "sync pending" (the next full sync reconciles the mirror). New _write_or_pending_error helper; delete is exempt (no Plex read). Covered by new happy-path / 422-rollback / 502 tests. - New-collection dup-name guard is now advisory only: existingNames is the search-scoped, all-library union, but the backend enforces uniqueness PER library — so it no longer blocks a valid same-name collection in another library, and the backend 409 is localized to nameTaken. - member-remove now confirms before the write-through (matches delete-collection). - add-title / new-collection library search render an error state on request failure instead of a misleading "No matches". - history: seed navCounter past the current entry's persisted sfScroll on load so post-reload ids can't collide with back-stack entries and mis-restore scroll. - PageScroller: LRU-cap the navId-keyed savedScroll map (was unbounded). - share isIntrinsicReadOnly between the browse tile and the info page so the lock/edit affordance can't drift.
This commit is contained in:
@@ -56,6 +56,7 @@ export {
|
||||
plexFilterCount,
|
||||
coercePlexFilters,
|
||||
plexFiltersSignature,
|
||||
isIntrinsicReadOnly,
|
||||
} from "./api/plex";
|
||||
import { messagingApi } from "./api/messaging";
|
||||
export type * from "./api/messaging";
|
||||
|
||||
@@ -187,6 +187,12 @@ export interface PlexCollection {
|
||||
art?: string[]; // up to 2 poster rating_keys for the browse-tile montage (browse union only)
|
||||
total_runtime_s?: number; // total runtime of all items, for the tile sub-line (browse union only)
|
||||
}
|
||||
/** A collection Plex regenerates outside a user's control — a smart collection or an external auto-list
|
||||
* (imdb/tmdb/tvdb/trakt/…) — can never be edited. The single source of truth for the lock badge shown
|
||||
* on both the browse tile and the info page, so the two surfaces can't drift. */
|
||||
export function isIntrinsicReadOnly(c: Pick<PlexCollection, "smart" | "source">): boolean {
|
||||
return !!c.smart || (!!c.source && c.source !== "collection");
|
||||
}
|
||||
// The Collection info page: the card fields + hero aggregates + the ordered members.
|
||||
export interface PlexCollectionDetail extends PlexCollection {
|
||||
content_rating?: string | null;
|
||||
|
||||
@@ -21,6 +21,15 @@ if (typeof window !== "undefined" && "scrollRestoration" in window.history) {
|
||||
window.history.scrollRestoration = "manual";
|
||||
}
|
||||
let navCounter = 0;
|
||||
// sfScroll ids are persisted into window.history.state and survive a full page reload, but this
|
||||
// module-level counter resets to 0 on every load. Seed it past the current entry's id so freshly
|
||||
// minted ids can't collide with ids already carried by pre-reload back-stack entries (ids are
|
||||
// assigned in forward order, so the current entry's id is ≥ every ancestor's) — a collision would
|
||||
// otherwise cross-contaminate PageScroller's per-entry scroll offsets after a reload + navigation.
|
||||
if (typeof window !== "undefined") {
|
||||
const seed = (window.history.state as { sfScroll?: number } | null)?.sfScroll;
|
||||
if (typeof seed === "number") navCounter = seed;
|
||||
}
|
||||
/** A fresh entry id — stamp it into the state of every pushState that opens a new main-content view. */
|
||||
export function nextNavId(): number {
|
||||
return ++navCounter;
|
||||
@@ -29,7 +38,12 @@ export function nextNavId(): number {
|
||||
* initial load, or an entry pushed without one — so the id survives Back/Forward. */
|
||||
export function ensureNavId(): number {
|
||||
const st = (window.history.state ?? {}) as { sfScroll?: number };
|
||||
if (typeof st.sfScroll === "number") return st.sfScroll;
|
||||
if (typeof st.sfScroll === "number") {
|
||||
// Keep the counter ahead of any id we observe (e.g. a Back landed on a higher-id entry after a
|
||||
// reload seeded us low) so the next mint never duplicates a live entry's id.
|
||||
if (st.sfScroll > navCounter) navCounter = st.sfScroll;
|
||||
return st.sfScroll;
|
||||
}
|
||||
const id = nextNavId();
|
||||
window.history.replaceState({ ...st, sfScroll: id }, "");
|
||||
return id;
|
||||
|
||||
Reference in New Issue
Block a user