Files
iptvnator/libs/portal/catalog/feature
4grayandClaude Fable 5 78587f95ba fix(portals): return from a portal handoff without losing the collection view (#1435)
* fix(portals): return from a portal handoff without losing the collection view

"View in portal" left `stalkerReturnTo` pointing at the collection URL, and
the portal detail's back affordance re-navigated there with
`navigateByUrl()`. That starts a stateless history entry, but the
collection's active tab, scope and open inline detail live only in
`window.history.state` — so back landed on the default `live` tab with the
title closed, and the portal page stayed one browser Back away.

The handoff now also sets `stalkerReturnByHistory`, and both Stalker back
handlers step back a single history entry instead. That entry is the one the
handoff itself pushed, so it restores the collection exactly as the user left
it and adds nothing to the history stack. The flag is set only by this
builder and only alongside `returnTo`, so the dashboard handoff and every
other `stalkerReturnTo` caller keeps re-navigating.

Reported by Codex on #1422 after it merged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): scope the history-return marker to its own handoff

`openStalkerItem` is consumed on arrival, but `stalkerReturnTo` and the new
`stalkerReturnByHistory` stay on the history entry, and a Stalker detail
opens in place without pushing one. So after Back + browser Forward the same
entry can host a different title, whose back affordance would follow the
leftover marker out to the collection instead of just closing it.

The marker now carries the handed-off item's identity instead of a bare
`true`, and a marker that does not match the open title is treated as stale:
it suppresses the whole return contract, so back simply closes the detail.

Reported by Codex on #1435.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(portals): share the Stalker back-navigation decision

Both back handlers duplicated the marker/`returnTo` precedence verbatim, and
the catalog view kept its own copy of the identity normalization the marker
binding mirrors — two places for one rule to drift.

`resolveStalkerBackNavigation()` now owns the decision and both handlers just
apply it, while `stalkerItemIdentity()` delegates to the shared
`normalizeStalkerHandoffIdentity()`. Behaviour is unchanged; the precedence
gains direct unit coverage instead of only being exercised through the two
components.

Follow-up to Greptile's review notes on #1435.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): make the return marker one-shot and id-shape agnostic

Two defects in the marker binding, both reported by Codex on #1435:

The comparison identity read only `item.id`, but the marker is built from
`extractStalkerItemId()`, which also accepts `stream_id`/`series_id`/
`movie_id`. A collection row carrying only `movie_id` therefore compared
against an empty identity, and since a marker was present the handler
returned without stepping back or re-navigating — the back affordance simply
stopped working. It now follows the same field order.

Binding also only fixed a *different* title reopened on the entry: selecting
the original title again still matched. Honouring the marker now retires both
return keys from the entry, so a browser Forward cannot replay the handoff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): bind the return marker to the id the detail can report

The previous attempt widened the comparison to `extractStalkerItemId()`'s
field set, but `buildStalkerSelectedVodItem()` — which every opened detail
goes through — derives `id` from `id ?? stream_id` and drops
`series_id`/`movie_id`. The wider lookup therefore ran after the lossy
normalization and still could not match, leaving the back affordance doing
nothing for those rows.

The marker is now bound to the identity the detail will actually report, and
a row whose id cannot survive normalization gets no marker at all: the
handoff falls back to re-navigating via `stalkerReturnTo`, which works.

The earlier regression tests hid this by injecting a `movie_id`-shaped
selection directly, which normalization can never produce; they now build the
selection through `buildStalkerSelectedVodItem()`.

Reported by Codex on #1435.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): retire the return contract after a native browser Back

Leaving the handed-off portal entry with the browser's own Back runs no back
affordance, so nothing consumed the marker. A Forward replay then reopened
the catalog with the contract intact, and opening the same title again
matched the identity — its Back exited to the collection instead of closing
the freshly opened detail.

`CategoryContentViewComponent` now retires the contract whenever it lands on
the entry with no handoff item and no detail open: the handoff is over, so
anything opened from the list afterwards is a fresh selection. The guard on
an open detail keeps arrival itself from retiring a contract it still needs.

Also documents, per Greptile, that a `none` decision still closes the detail
and only suppresses the navigation.

Reported by Codex on #1435.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): keep alternate-id rows on the history return

The previous revision let a row carrying only `movie_id`/`series_id` fall
back to re-navigating, which reproduced the exact defect this PR exists to
fix: the new entry has no `collectionViewState`, so the collection reopens on
its default tab with the title closed.

The builder now pins the resolved id onto the handoff state item when the raw
row carries neither `id` nor `stream_id`, so `buildStalkerSelectedVodItem()`
reports an identity the marker can bind to and those rows get the same
history return as every other one. An existing id is never overwritten.

Also scopes the arrival-side retirement to handoffs that actually set the
marker, so a plain `stalkerReturnTo` caller such as the dashboard keeps its
behaviour — I had made that unconditional, which contradicted the scope this
PR claims.

Reported by Codex on #1435.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(portals): drop a redundant guard in the identity normalizer

`split(':')` always yields at least one element, and the workspace does not
enable `noUncheckedIndexedAccess`, so the optional chain and `?? ''` fallback
were unreachable rather than type-required. Behaviour is unchanged.

Spotted by Greptile on #1435.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 18:45:50 +02:00
..